Install any skill in seconds. Free to start, no credit card required.
Get Started Free →This skill covers conducting comprehensive security assessments of Operational Technology (OT) networks including SCADA systems, DCS architectures, and industrial control system communication paths. It addresses the Purdue Reference Model layers, identifies IT/OT convergence risks, evaluates firewall rules between zones, and maps industrial protocol traffic (Modbus, DNP3, OPC UA, EtherNet/IP) to detect misconfigurations, unauthorized connections, and attack surfaces in critical infrastructure.
.claude/skills/performing-ot-network-security-assessment/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-13 | ✗→✗ | = Same ✗ | — | — |
| case-07 | ✗→✗ | = Same ✗ | — | — |
| case-15 | ✗→✗ | = Same ✗ | — | — |
Do not use for IT-only network assessments without OT components, for application-layer vulnerability scanning of IT web applications (see performing-web-app-penetration-test), or for active exploitation of live OT systems without explicit authorization and safety controls in place.
Define the scope based on the Purdue Reference Model levels and identify safety-critical systems that must not be actively scanned. OT assessments differ fundamentally from IT assessments because active scanning can crash PLCs, disrupt safety instrumented systems (SIS), and cause physical harm.
yaml# OT Assessment Scope Definition assessment: facility: "Chemical Processing Plant - Site Alpha" purdue_levels_in_scope: - level_0: "Physical process sensors and actuators (passive observation only)" - level_1: "PLCs, RTUs, safety controllers (passive only, no active scanning)" - level_2: "HMI stations, engineering workstations, historian (limited active with approval)" - level_3: "Site operations - OPC servers, application servers (active scanning permitted)" - level_3_5: "DMZ - data diodes, jump servers (active scanning permitted)" - level_4: "Enterprise IT connecting to OT (active scanning permitted)" safety_exclusions: - "Safety Instrumented Systems (SIS) - Triconex controllers" - "Emergency Shutdown (ESD) systems" - "Fire and Gas detection systems" - "Any Level 0/1 device during active production" authorized_activities: passive: - "Network traffic capture and analysis via SPAN ports" - "Industrial protocol deep packet inspection" - "Wireless spectrum analysis" - "Physical walkthrough and visual inspection" active_with_approval: - "Targeted Nmap scans of Level 2-4 systems during maintenance windows" - "Authentication testing on HMI and engineering workstations" - "Firewall rule verification between zones" prohibited: - "Active scanning of PLCs, RTUs, or SIS controllers" - "Fuzzing industrial protocols on live systems" - "Modifying PLC logic or firmware"
Deploy passive monitoring to map all devices, communication flows, and protocols on the OT network without sending any traffic that could disrupt operations.
python#!/usr/bin/env python3 """OT Network Passive Discovery and Asset Inventory Builder. Uses pcap captures from SPAN ports to identify OT assets, protocols, and communication patterns without active scanning. """ import json import sys from collections import defaultdict from datetime import datetime try: from scapy.all import rdpcap, IP, TCP, UDP from scapy.contrib.modbus import ModbusADURequest, ModbusADUResponse except ImportError: print("Install scapy: pip install scapy") sys.exit(1) # Industrial protocol port mappings OT_PROTOCOL_PORTS = { 502: "Modbus/TCP", 102: "S7comm (Siemens)", 44818: "EtherNet/IP (CIP)", 2222: "EtherNet/IP (implicit)", 4840: "OPC UA", 20000: "DNP3", 47808: "BACnet", 1911: "Niagara Fox", 789: "Crimson v3 (Red Lion)", 2404: "IEC 60870-5-104", 18245: "GE SRTP", 5094: "HART-IP", } PURDUE_LEVEL_RANGES = { "Level 0-1 (Field Devices)": ["10.10.0.0/16", "192.168.10.0/24"], "Level 2 (Control Systems)": ["10.20.0.0/16", "192.168.20.0/24"], "Level 3 (Site Operations)": ["10.30.0.0/16", "192.168.30.0/24"], "Level 3.5 (DMZ)": ["172.16.0.0/16"], "Level 4 (Enterprise)": ["10.0.0.0/16"], } def classify_purdue_level(ip_addr): """Classify an IP address to its Purdue Reference Model level.""" from ipaddress import ip_address, ip_network addr = ip_address(ip_addr) for level, subnets in PURDUE_LEVEL_RANGES.items(): for subnet in subnets: if addr in ip_network(subnet): return level return "Unknown" def analyze_ot_pcap(pcap_file): """Analyze pcap file to discover OT assets and communication patterns.""" packets = rdpcap(pcap_file) assets = {} connections = defaultdict(lambda: {"count": 0, "protocols": set(), "ports": set()}) protocol_stats = defaultdict(int) cross_zone_flows = [] for pkt in packets: if not pkt.haslayer(IP): continue src_ip = pkt[IP].src dst_ip = pkt[IP].dst # Track assets for ip in (src_ip, dst_ip): if ip not in assets: assets[ip] = { "ip": ip, "purdue_level": classify_purdue_level(ip), "protocols_observed": set(), "roles": set(), "first_seen": str(pkt.time), "last_seen": str(pkt.time), "mac": None, } assets[ip]["last_seen"] = str(pkt.time) # Identify OT protocols by port dst_port = None if pkt.haslayer(TCP): dst_port = pkt[TCP].dport elif pkt.haslayer(UDP): dst_port = pkt[UDP].dport if dst_port and dst_port in OT_PROTOCOL_PORTS: protocol_name = OT_PROTOCOL_PORTS[dst_port] protocol_stats[protocol_name] += 1 assets[src_ip]["protocols_observed"].add(protocol_name) assets[dst_ip]["protocols_observed"].add(protocol_name) # Determine roles assets[src_ip]["roles"].add("client/master") assets[dst_ip]["roles"].add("server/slave") # Track connections conn_key = (src_ip, dst_ip) connections[conn_key]["count"] += 1 if dst_port: connections[conn_key]["ports"].add(dst_port) if dst_port in OT_PROTOCOL_PORTS: connections[conn_key]["protocols"].add(OT_PROTOCOL_PORTS[dst_port]) # Detect cross-zone communication src_level = classify_purdue_level(src_ip) dst_level = classify_purdue_level(dst_ip) if src_level != dst_level and src_level != "Unknown" and dst_level != "Unknown": cross_zone_flows.append({ "src": src_ip, "src_level": src_level, "dst": dst_ip, "dst_level": dst_level, "protocol": OT_PROTOCOL_PORTS.get(dst_port, f"port/{dst_port}"), }) return { "asset_count": len(assets), "assets": {ip: {k: list(v) if isinstance(v, set) else v for k, v in info.items()} for ip, info in assets.items()}, "protocol_distribution": dict(protocol_stats), "total_connections": len(connections), "cross_zone_flows": cross_zone_flows[:50], } def generate_assessment_report(results): """Generate the OT network assessment findings report.""" report = [] report.append("=" * 70) report.append("OT NETWORK PASSIVE DISCOVERY REPORT") report.append(f"Generated: {datetime.now().isoformat()}") report.append("=" * 70) report.append(f"\nTotal Assets Discovered: {results['asset_count']}") report.append(f"Total Unique Connections: {results['total_connections']}") report.append("\n--- INDUSTRIAL PROTOCOL DISTRIBUTION ---") for proto, count in sorted(results["protocol_distribution"].items(), key=lambda x: -x[1]): report.append(f" {proto}: {count} packets") report.append("\n--- CROSS-ZONE COMMUNICATION FLOWS ---") if results["cross_zone_flows"]: for flow in results["cross_zone_flows"][:20]: report.append( f" {flow['src']} ({flow['src_level']}) -> " f"{flow['dst']} ({flow['dst_level']}) via {flow['protocol']}" ) else: report.append(" No cross-zone flows detected (check subnet classifications)") report.append("\n--- FINDINGS ---") # Check for Level 4 to Level 0-1 direct connections (critical finding) for flow in results["cross_zone_flows"]: if "Level 4" in flow["src_level"] and "Level 0-1" in flow["dst_level"]: report.append( f" [CRITICAL] Direct Enterprise-to-Field traffic: " f"{flow['src']} -> {flow['dst']} via {flow['protocol']}" ) elif "Level 4" in flow["src_level"] and "Level 2" in flow["dst_level"]: report.append( f" [HIGH] Enterprise-to-Control traffic bypassing DMZ: " f"{flow['src']} -> {flow['dst']} via {flow['protocol']}" ) return "\n".join(report) if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python ot_network_discovery.py <pcap_file>") sys.exit(1) results = analyze_ot_pcap(sys.argv[1]) print(generate_assessment_report(results)) # Save detailed JSON output_file = sys.argv[1].replace(".pcap", "_inventory.json") with open(output_file, "w") as f: json.dump(results, f, indent=2, default=str) print(f"\nDetailed inventory saved to: {output_file}")
Analyze firewall configurations between OT zones to identify overly permissive rules, missing deny defaults, and unauthorized conduits that violate the IEC 62443 zone model.
python#!/usr/bin/env python3 """OT Zone Firewall Rule Analyzer. Parses firewall rule exports (CSV format) and evaluates them against IEC 62443 zone/conduit model requirements. """ import csv import json import sys from dataclasses import dataclass, field from typing import Optional @dataclass class FirewallRule: rule_id: str source_zone: str source_ip: str dest_zone: str dest_ip: str service: str port: str action: str enabled: bool comment: str = "" # IEC 62443 zone communication policy # Defines which zone pairs are allowed to communicate and through what conduit ALLOWED_CONDUITS = { ("Level 4", "Level 3.5"): { "allowed_ports": [443, 3389, 22], "description": "Enterprise to DMZ - web services, jump hosts", "requires_inspection": True, }, ("Level 3.5", "Level 3"): { "allowed_ports": [443, 1433, 5432, 8080], "description": "DMZ to Site Ops - historian mirror, OPC relay", "requires_inspection": True, }, ("Level 3", "Level 2"): { "allowed_ports": [502, 44818, 4840, 102], "description": "Site Ops to Control - OPC UA, Modbus, S7comm", "requires_inspection": True, }, ("Level 2", "Level 1"): { "allowed_ports": [502, 44818, 102, 2222], "description": "Control to Field - direct industrial protocols", "requires_inspection": False, }, } # Prohibited direct connections PROHIBITED_CONDUITS = [ ("Level 4", "Level 3"), ("Level 4", "Level 2"), ("Level 4", "Level 1"), ("Level 4", "Level 0"), ("Level 3", "Level 1"), ("Level 3", "Level 0"), ("Internet", "Level 3.5"), ("Internet", "Level 3"), ("Internet", "Level 2"), ("Internet", "Level 1"), ] def parse_firewall_rules(csv_file): """Parse firewall rules from CSV export.""" rules = [] with open(csv_file, "r") as f: reader = csv.DictReader(f) for row in reader: rules.append(FirewallRule( rule_id=row.get("rule_id", ""), source_zone=row.get("source_zone", ""), source_ip=row.get("source_ip", ""), dest_zone=row.get("dest_zone", ""), dest_ip=row.get("dest_ip", ""), service=row.get("service", ""), port=row.get("port", ""), action=row.get("action", ""), enabled=row.get("enabled", "true").lower() == "true", comment=row.get("comment", ""), )) return rules def analyze_rules(rules): """Analyze firewall rules against IEC 62443 zone model.""" findings = {"critical": [], "high": [], "medium": [], "low": [], "info": []} for rule in rules: if not rule.enabled or rule.action.lower() != "allow": continue zone_pair = (rule.source_zone, rule.dest_zone) port = int(rule.port) if rule.port.isdigit() else 0 # Check for prohibited conduits if zone_pair in PROHIBITED_CONDUITS: findings["critical"].append({ "rule_id": rule.rule_id, "finding": f"Prohibited direct connection: {rule.source_zone} -> {rule.dest_zone}", "detail": f"Rule allows {rule.source_ip} to reach {rule.dest_ip}:{rule.port} ({rule.service})", "remediation": "Remove rule. Route traffic through DMZ (Level 3.5) with application-layer inspection.", }) # Check for overly broad rules (any/any) elif rule.source_ip in ("any", "0.0.0.0/0") or rule.dest_ip in ("any", "0.0.0.0/0"): findings["high"].append({ "rule_id": rule.rule_id, "finding": f"Overly permissive rule with 'any' address", "detail": f"{rule.source_ip} -> {rule.dest_ip}:{rule.port} in {zone_pair}", "remediation": "Restrict to specific host IPs per IEC 62443 least-privilege conduit policy.", }) # Check allowed conduits for port violations elif zone_pair in ALLOWED_CONDUITS: conduit = ALLOWED_CONDUITS[zone_pair] if port and port not in conduit["allowed_ports"]: findings["medium"].append({ "rule_id": rule.rule_id, "finding": f"Unauthorized port in conduit {zone_pair}", "detail": f"Port {port} ({rule.service}) not in allowed list {conduit['allowed_ports']}", "remediation": f"Remove port {port} from conduit or justify in risk assessment.", }) return findings if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python ot_firewall_analyzer.py <rules.csv>") sys.exit(1) rules = parse_firewall_rules(sys.argv[1]) findings = analyze_rules(rules) print("=" * 70) print("OT ZONE FIREWALL RULE ANALYSIS") print("=" * 70) for severity in ["critical", "high", "medium", "low"]: if findings[severity]: print(f"\n--- {severity.upper()} FINDINGS ({len(findings[severity])}) ---") for f in findings[severity]: print(f" [{f['rule_id']}] {f['finding']}") print(f" Detail: {f['detail']}") print(f" Fix: {f['remediation']}")
Evaluate the security configuration of industrial protocols in use, checking for authentication, encryption, and access controls.
bash# Capture Modbus/TCP traffic for analysis tcpdump -i eth0 -w ot_capture.pcap 'port 502 or port 44818 or port 4840 or port 102 or port 20000' -c 100000 # Use Wireshark with OT protocol dissectors for deep inspection tshark -r ot_capture.pcap -Y "modbus" -T fields \ -e ip.src -e ip.dst -e modbus.func_code -e modbus.reference_num \ -e modbus.word_cnt > modbus_analysis.csv # Check for unauthenticated Modbus write operations (function codes 5,6,15,16) tshark -r ot_capture.pcap -Y "modbus.func_code >= 5 && modbus.func_code <= 16" \ -T fields -e ip.src -e ip.dst -e modbus.func_code -e frame.time # Scan for OPC UA servers and check security policies # Only run against Level 3+ systems with explicit authorization python3 -c " from opcua import Client server_url = 'opc.tcp://10.30.1.50:4840' client = Client(server_url) endpoints = client.connect_and_get_server_endpoints() for ep in endpoints: print(f'Endpoint: {ep.EndpointUrl}') print(f' Security Mode: {ep.SecurityMode}') print(f' Security Policy: {ep.SecurityPolicyUri}') print(f' Auth Tokens: {[t.TokenType for t in ep.UserIdentityTokens]}') "
Compile findings into a structured report aligned with IEC 62443 and NIST SP 800-82 Rev.3.
OT Network Security Assessment Report
=======================================
Facility: Chemical Processing Plant - Site Alpha
Assessment Date: 2026-02-23
Standard: IEC 62443-3-3 / NIST SP 800-82r3
Assessor: [Assessor Name]
EXECUTIVE SUMMARY:
The OT network assessment identified 47 assets across Purdue levels 0-4.
12 critical and 23 high-severity findings were identified, primarily
related to insufficient network segmentation, unauthenticated industrial
protocols, and unauthorized cross-zone communication paths.
ASSET INVENTORY SUMMARY:
Level 0-1 (Field): 18 devices (PLCs, RTUs, I/O modules)
Level 2 (Control): 9 devices (HMIs, engineering workstations)
Level 3 (Operations): 12 devices (historians, OPC servers, app servers)
Level 3.5 (DMZ): 3 devices (data diode, jump server, patch server)
Level 4 (Enterprise): 5 devices (domain controllers, file servers)
CRITICAL FINDINGS:
[OT-001] Direct Enterprise-to-PLC communication detected
Source: 10.0.5.22 (Level 4 - IT workstation)
Dest: 10.10.1.15 (Level 1 - Allen-Bradley PLC)
Protocol: EtherNet/IP (port 44818)
Impact: An attacker on the corporate network could directly modify PLC logic
Remediation: Block direct L4-L1 traffic; route through DMZ proxy
[OT-002] Modbus/TCP write commands without authentication
Affected: 8 PLCs accepting unauthenticated FC6 (Write Single Register)
Impact: Any device on the OT network can modify process setpoints
Remediation: Deploy Modbus-aware firewall; restrict write-capable sources
[OT-003] Flat network - no segmentation between Purdue levels
Detail: All OT devices share VLAN 100 (10.10.0.0/16)
Impact: Compromised HMI has direct access to all PLCs and SIS
Remediation: Implement zone-based segmentation per IEC 62443-3-2
RISK MATRIX:
Critical: 12 findings (immediate remediation required)
High: 23 findings (remediate within 30 days)
Medium: 15 findings (remediate within 90 days)
Low: 8 findings (remediate in next maintenance cycle)| Term | Definition | |------|------------| | Purdue Reference Model | Hierarchical architecture model (Levels 0-5) for organizing industrial control systems, defining security zones from physical process to enterprise IT | | IEC 62443 | International standard series for industrial automation and control systems (IACS) security, defining security levels, zones, conduits, and security requirements | | Zone | A grouping of logical or physical assets that share common security requirements, defined by IEC 62443-3-2 | | Conduit | A logical grouping of communication channels connecting two or more zones, subject to common security policies | | SCADA | Supervisory Control and Data Acquisition - system architecture for high-level process supervisory management of industrial processes | | DCS | Distributed Control System - control system architecture where control elements are distributed throughout the system | | Air Gap | Physical isolation of OT networks from IT/internet, increasingly replaced by managed conduits with firewalls and data diodes | | Safety Instrumented System (SIS) | Independent system designed to bring a process to a safe state when a hazardous condition is detected |
Context: A water utility has all OT devices on a single VLAN. Passive network monitoring reveals HMIs, PLCs, historians, and a domain controller all sharing the same Layer 2 broadcast domain. There is no DMZ between the corporate network and the OT environment.
Approach:
Pitfalls: Active scanning PLCs during production can cause communication timeouts and process disruptions. Implementing segmentation without a complete traffic baseline will break legitimate control system communications. Relying solely on network-layer firewalls without industrial protocol inspection leaves Modbus/TCP and EtherNet/IP write commands unchecked.
OT Network Security Assessment Report
=======================================
Facility: [Facility Name]
Assessment Date: YYYY-MM-DD
Standard: IEC 62443-3-3 / NIST SP 800-82r3
EXECUTIVE SUMMARY:
[2-3 sentence overview of findings and risk level]
ASSET INVENTORY:
Level 0-1: [count] field devices
Level 2: [count] control systems
Level 3: [count] operations systems
Level 3.5: [count] DMZ systems
Level 4: [count] enterprise systems
FINDINGS BY SEVERITY:
Critical: [count] (immediate action required)
High: [count] (30-day remediation)
Medium: [count] (90-day remediation)
Low: [count] (next maintenance window)
DETAILED FINDINGS:
[OT-NNN] Finding Title
Severity: Critical|High|Medium|Low
Affected Assets: [list]
IEC 62443 Reference: [section]
NIST 800-82r3 Reference: [section]
Description: [technical detail]
Impact: [operational and safety impact]
Remediation: [specific technical remediation steps]| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +9 percentage points is the difference between those two pass rates over the 21 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.