npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
When to Use
- When conducting an initial security baseline of an OT/ICS environment for a new client
- When evaluating the security posture of a facility after an IT/OT convergence initiative
- When preparing for IEC 62443 or NERC CIP compliance audits
- When assessing risk following a merger or acquisition involving industrial facilities
- When investigating whether an OT network has been compromised or has unmonitored pathways to corporate IT
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.
Prerequisites
- Written authorization from the asset owner and operations management for all assessment activities
- Understanding of the Purdue Reference Model and IEC 62443 zone/conduit architecture
- Passive network monitoring tools (Nozomi Guardian, Dragos Platform, or Wireshark with industrial protocol dissectors)
- Access to network diagrams, firewall rule sets, and asset inventories (or the ability to perform passive discovery)
- Safety briefing on the physical processes controlled by the OT systems under assessment
Workflow
Step 1: Establish Assessment Scope and Safety Boundaries
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.
# 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"Step 2: Perform Passive Network Discovery and Asset Inventory
Deploy passive monitoring to map all devices, communication flows, and protocols on the OT network without sending any traffic that could disrupt operations.
#!/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}")Step 3: Evaluate Firewall Rules Between Purdue Zones
Analyze firewall configurations between OT zones to identify overly permissive rules, missing deny defaults, and unauthorized conduits that violate the IEC 62443 zone model.
#!/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']}")Step 4: Assess Industrial Protocol Security
Evaluate the security configuration of industrial protocols in use, checking for authentication, encryption, and access controls.
# 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]}')
"Step 5: Generate Assessment Report
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)Key Concepts
| 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 |
Tools & Systems
- Nozomi Networks Guardian: Passive OT network monitoring platform providing asset discovery, vulnerability assessment, and anomaly detection for industrial environments
- Dragos Platform: OT cybersecurity platform with asset visibility, threat detection, and vulnerability management designed for critical infrastructure
- Claroty xDome: Cyber-physical systems protection platform providing comprehensive asset inventory and risk scoring across OT, IoT, and IIoT
- Wireshark/tshark: Network protocol analyzer with industrial protocol dissectors for Modbus, DNP3, S7comm, EtherNet/IP, OPC UA, and BACnet
- Nmap with OT scripts: Network scanner with NSE scripts for OT protocol enumeration (use only on Level 2+ with authorization)
- Grassmarlin: NSA-developed passive OT network mapping tool for identifying SCADA/ICS network topology
Common Scenarios
Scenario: Flat OT Network with No Segmentation
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:
- Deploy passive monitoring on the SPAN port to capture a complete communication baseline over 2-4 weeks
- Map all device-to-device communication flows with protocols and data volumes
- Classify assets into Purdue levels based on their function and communication patterns
- Design zone architecture with VLANs and inter-zone firewalls per IEC 62443-3-2
- Prioritize DMZ creation between Level 3 and Level 4 as the highest-impact segmentation
- Present segmentation plan with migration phases that avoid production disruption
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.
Output Format
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]References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md1.4 KB
API Reference — Performing OT Network Security Assessment
Libraries Used
- csv: Parse asset inventories and firewall rule exports
- subprocess: Execute nmap for OT protocol scanning
- xml.etree.ElementTree: Parse nmap XML output
CLI Interface
python agent.py assets --csv ot_inventory.csv
python agent.py segmentation --csv firewall_rules.csv
python agent.py protocols --subnet 192.168.1.0/24
python agent.py report --assets inventory.csv [--firewall fw_rules.csv]Core Functions
assess_asset_inventory(csv_file) — Purdue model zone analysis
Groups assets by Purdue level. Flags end-of-life and unknown firmware.
assess_network_segmentation(csv_file) — Firewall rule audit
Detects: direct IT-to-OT access (CRITICAL), allow-any-protocol rules (HIGH).
scan_ot_protocols(target_subnet) — OT protocol discovery
Scans ports: 102 (S7), 502 (Modbus), 4840 (OPC-UA), 44818 (EtherNet/IP), 47808 (BACnet), 20000 (DNP3).
generate_assessment_report(...) — Comprehensive report
OT Protocol Ports
| Port | Protocol | Usage |
|---|---|---|
| 102 | S7Comm | Siemens S7 PLCs |
| 502 | Modbus TCP | Industrial automation |
| 4840 | OPC-UA | Industrial data exchange |
| 44818 | EtherNet/IP | Allen-Bradley PLCs |
| 47808 | BACnet | Building automation |
| 20000 | DNP3 | SCADA/utility |
Dependencies
System: nmap (optional, for protocol scanning) No Python packages required.
standards.md4.0 KB
Standards Reference - OT Network Security Assessment
IEC 62443 (ISA/IEC 62443)
IEC 62443-1-1: Terminology, Concepts, and Models
- Defines foundational concepts including zones, conduits, security levels, and the IACS reference architecture
- Establishes the vocabulary used across all parts of the standard
IEC 62443-2-1: Security Management System Requirements
- Requirements for an IACS security management system (SMS)
- Covers risk assessment, security policies, organization, staff competence, and awareness
IEC 62443-3-2: Security Risk Assessment for System Design
- Defines the process for partitioning an IACS into zones and conduits
- Establishes Security Level targets (SL-T) for each zone based on risk assessment
- Zone characteristics: assets, access points, communication channels, data flows
- Conduit characteristics: connected zones, protocols, security countermeasures
IEC 62443-3-3: System Security Requirements and Security Levels
- Foundational requirements (FR) mapped to security levels (SL 1-4):
- FR 1: Identification and Authentication Control (IAC)
- FR 2: Use Control (UC)
- FR 3: System Integrity (SI)
- FR 4: Data Confidentiality (DC)
- FR 5: Restricted Data Flow (RDF)
- FR 6: Timely Response to Events (TRE)
- FR 7: Resource Availability (RA)
Security Levels
| Level | Description |
|---|---|
| SL 1 | Protection against casual or coincidental violation |
| SL 2 | Protection against intentional violation using simple means |
| SL 3 | Protection against sophisticated attack with moderate resources |
| SL 4 | Protection against state-sponsored attack with extensive resources |
NIST SP 800-82 Revision 3
Guide to Operational Technology (OT) Security
- Published September 2023, replacing the ICS-focused Rev. 2
- Expanded scope covers: ICS/SCADA, building automation, transportation, physical access control, physical environment monitoring
- Provides OT overlay for NIST SP 800-53r5 security controls
- Tailored security control baselines for low/moderate/high-impact OT systems
Key Sections for Network Assessment
- Section 3: OT Overview and Architectures (Purdue Model reference)
- Section 4: OT Risk Management
- Section 5: OT Security Architecture (network segmentation, DMZ design)
- Section 6: Applying Security Controls to OT (overlay guidance)
OT-Specific Control Tailoring
- AC-4 (Information Flow Enforcement): Implement data diodes for unidirectional flows from Level 3 to Level 3.5
- SC-7 (Boundary Protection): Industrial firewalls with protocol-aware deep packet inspection
- AU-6 (Audit Record Review): Correlate OT network events with process alarms
NERC CIP Standards (Power Sector)
CIP-002-5.1a: BES Cyber System Categorization
- Identify and categorize Bulk Electric System (BES) cyber systems as high, medium, or low impact
CIP-005-7: Electronic Security Perimeters
- Define Electronic Security Perimeters (ESP) around BES cyber systems
- Require Electronic Access Points (EAP) at all ESP boundaries
- 2025 update requires MFA for remote access to medium and high-impact systems
CIP-007-6: System Security Management
- Port and service management, security patch management
- Malicious code prevention, security event monitoring
- System access controls and authentication
CIP-010-4: Configuration Change Management and Vulnerability Assessments
- Baseline configurations for BES cyber systems
- Transient cyber asset and removable media management
- Active vulnerability assessments at least every 15 months
Purdue Reference Model (ISA-95)
Level Architecture
| Level | Name | Examples |
|---|---|---|
| Level 0 | Physical Process | Sensors, actuators, analyzers |
| Level 1 | Basic Control | PLCs, RTUs, safety controllers |
| Level 2 | Area Supervisory Control | HMI, engineering workstations, local historian |
| Level 3 | Site Operations | OPC servers, site historian, MES |
| Level 3.5 | DMZ | Data diodes, jump servers, patch servers |
| Level 4 | Enterprise | ERP, email, corporate IT |
| Level 5 | Internet/Cloud | Remote access, cloud services |
workflows.md4.7 KB
Workflows - OT Network Security Assessment
Assessment Lifecycle
Phase 1: Scoping Phase 2: Discovery Phase 3: Analysis
+-----------------+ +-----------------+ +-----------------+
| Define scope | | Passive capture | | Protocol review |
| Safety limits | ----> | Asset inventory | ---> | Zone evaluation |
| Authorization | | Traffic mapping | | Firewall audit |
+-----------------+ +-----------------+ +-----------------+
|
Phase 6: Verify Phase 5: Remediate Phase 4: Report
+-----------------+ +-----------------+ +-----------------+
| Validate fixes | | Segmentation | | Risk scoring |
| Re-assessment | <---- | FW rule changes | <--- | Finding detail |
| Compliance map | | Protocol harden | | Prioritization |
+-----------------+ +-----------------+ +-----------------+Phase 1: Scoping and Authorization
Inputs
- Facility type and industry vertical
- Regulatory requirements (NERC CIP, IEC 62443, NIST CSF)
- Existing network diagrams and asset inventories
- Maintenance window schedules
Activities
- Meet with operations, engineering, and IT security teams
- Define Purdue levels in scope and safety-critical exclusions
- Obtain written authorization specifying permitted assessment activities
- Identify SPAN/TAP points for passive monitoring deployment
- Review prior assessment reports and known issues
Outputs
- Signed Rules of Engagement document
- Assessment scope matrix (Purdue levels vs. activity types)
- SPAN/TAP deployment plan
- Emergency contact list and escalation procedures
Phase 2: Passive Network Discovery
Inputs
- SPAN port access on OT network switches
- Assessment scope document
Activities
- Deploy passive monitoring sensors on SPAN ports at each Purdue level boundary
- Capture network traffic for minimum 2 weeks to observe full operational cycle
- Build asset inventory from observed traffic (MAC, IP, protocols, firmware versions)
- Map all communication flows with source, destination, protocol, and frequency
- Identify industrial protocols in use (Modbus, DNP3, OPC UA, EtherNet/IP, S7comm)
- Detect unauthorized devices and rogue connections
Activities - Wireless Assessment
- Scan for wireless access points in OT areas using spectrum analyzer
- Identify wireless industrial protocols (WirelessHART, ISA100.11a, Zigbee)
- Check for unauthorized Wi-Fi networks bridging IT and OT
Outputs
- Complete asset inventory with Purdue level classification
- Network communication flow map
- Protocol distribution analysis
- Unauthorized device/connection list
Phase 3: Analysis and Evaluation
Inputs
- Asset inventory and traffic capture data
- Firewall rule exports
- Network architecture diagrams
Activities
- Evaluate zone architecture against IEC 62443-3-2 requirements
- Analyze firewall rules for overly permissive or prohibited conduits
- Assess industrial protocol security (authentication, encryption, access controls)
- Review remote access architecture and authentication mechanisms
- Evaluate patch levels of HMI, engineering workstations, and servers
- Check for known vulnerabilities in discovered OT firmware versions
- Assess physical security of network equipment in field locations
Outputs
- Finding list with severity ratings
- Gap analysis against applicable standards
- Risk matrix mapping findings to operational/safety impact
Phase 4: Reporting
Report Structure
- Executive Summary (1 page)
- Scope and Methodology
- Asset Inventory Summary
- Network Architecture Assessment
- Detailed Findings (Critical/High/Medium/Low)
- Compliance Gap Analysis
- Remediation Roadmap with Prioritization
- Appendices (asset inventory, network diagrams, tool output)
Phase 5: Remediation Support
Priority Order
- Immediate: Block unauthorized cross-zone paths (enterprise to field devices)
- 30-day: Implement DMZ between corporate IT and OT operations
- 60-day: Deploy industrial protocol-aware firewalls between zones
- 90-day: Harden remote access with MFA and jump servers
- 6-month: Full zone/conduit segmentation per IEC 62443 design
Risk Scoring for OT Environments
OT risk scoring must account for safety impact beyond traditional CIA triad:
| Factor | Weight | Description |
|---|---|---|
| Safety Impact | 30% | Potential for physical harm to personnel or public |
| Operational Impact | 25% | Production disruption or equipment damage |
| Environmental Impact | 15% | Release of hazardous materials |
| Financial Impact | 15% | Direct costs and regulatory penalties |
| Reputational Impact | 15% | Public trust and regulatory scrutiny |
Scripts 2
agent.py6.4 KB
#!/usr/bin/env python3
"""Agent for performing OT network security assessment based on Purdue model."""
import json
import argparse
import csv
import subprocess
from datetime import datetime
from collections import Counter
PURDUE_LEVELS = {
0: "Physical Process (sensors, actuators)",
1: "Basic Control (PLC, RTU, SIS)",
2: "Area Supervisory (HMI, SCADA, DCS)",
3: "Site Operations (Historian, Patch Mgmt)",
3.5: "IT/OT DMZ (jump servers, data diode)",
4: "Business Planning (ERP, Email)",
5: "Enterprise Network (Internet, Cloud)",
}
def assess_asset_inventory(csv_file):
"""Assess OT asset inventory against Purdue model zones."""
with open(csv_file, "r", encoding="utf-8", errors="replace") as f:
reader = csv.DictReader(f)
assets = list(reader)
by_level = {}
by_vendor = Counter()
by_protocol = Counter()
findings = []
for asset in assets:
level = asset.get("purdue_level", asset.get("zone", "unknown"))
by_level.setdefault(str(level), []).append(asset)
by_vendor[asset.get("vendor", "unknown")] += 1
proto = asset.get("protocol", asset.get("protocols", ""))
for p in proto.split(","):
if p.strip():
by_protocol[p.strip()] += 1
firmware = asset.get("firmware_version", "")
if asset.get("end_of_life", "").lower() in ("yes", "true"):
findings.append({"asset": asset.get("name", ""), "issue": "END_OF_LIFE", "severity": "HIGH"})
if not firmware:
findings.append({"asset": asset.get("name", ""), "issue": "UNKNOWN_FIRMWARE", "severity": "MEDIUM"})
return {
"total_assets": len(assets),
"by_purdue_level": {k: len(v) for k, v in by_level.items()},
"by_vendor": dict(by_vendor.most_common(10)),
"protocols": dict(by_protocol.most_common(15)),
"findings": findings[:20],
}
def assess_network_segmentation(csv_file):
"""Check OT network segmentation and firewall rules."""
with open(csv_file, "r", encoding="utf-8", errors="replace") as f:
reader = csv.DictReader(f)
rules = list(reader)
findings = []
for rule in rules:
src_zone = rule.get("src_zone", "")
dst_zone = rule.get("dst_zone", "")
action = rule.get("action", "").lower()
protocol = rule.get("protocol", "")
if action == "allow" and src_zone.startswith("IT") and dst_zone.startswith("OT"):
findings.append({
"rule": rule.get("name", rule.get("id", "")),
"issue": "DIRECT_IT_TO_OT_ACCESS",
"severity": "CRITICAL",
"src_zone": src_zone, "dst_zone": dst_zone,
})
if action == "allow" and protocol.lower() in ("any", "all", "*"):
findings.append({
"rule": rule.get("name", rule.get("id", "")),
"issue": "ALLOW_ANY_PROTOCOL",
"severity": "HIGH",
})
return {
"total_rules": len(rules),
"allow_rules": sum(1 for r in rules if r.get("action", "").lower() == "allow"),
"deny_rules": sum(1 for r in rules if r.get("action", "").lower() in ("deny", "drop")),
"findings": findings[:20],
"dmz_rules": sum(1 for r in rules if "dmz" in r.get("src_zone", "").lower() or "dmz" in r.get("dst_zone", "").lower()),
}
def scan_ot_protocols(target_subnet):
"""Scan for common OT protocols on a subnet using nmap."""
ot_ports = "102,502,2222,4840,20000,44818,47808"
cmd = ["nmap", "-sV", "-p", ot_ports, target_subnet, "-oX", "-", "--open"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
import xml.etree.ElementTree as ET
root = ET.fromstring(result.stdout)
hosts = []
for host in root.findall(".//host"):
addr = host.find("address").get("addr", "") if host.find("address") is not None else ""
ports = []
for port in host.findall(".//port"):
service = port.find("service")
ports.append({
"port": int(port.get("portid", 0)),
"protocol": service.get("name", "") if service is not None else "",
"product": service.get("product", "") if service is not None else "",
})
if ports:
hosts.append({"ip": addr, "ot_services": ports})
protocol_map = {502: "Modbus", 102: "S7Comm", 44818: "EtherNet/IP", 4840: "OPC-UA", 47808: "BACnet", 20000: "DNP3"}
return {"subnet": target_subnet, "hosts_with_ot": len(hosts), "hosts": hosts, "protocol_reference": protocol_map}
except FileNotFoundError:
return {"error": "nmap not installed"}
except Exception as e:
return {"error": str(e)}
def generate_assessment_report(asset_csv, firewall_csv=None):
"""Generate comprehensive OT security assessment."""
report = {
"generated": datetime.utcnow().isoformat(),
"frameworks": ["IEC 62443", "NIST SP 800-82", "NERC CIP"],
"asset_assessment": assess_asset_inventory(asset_csv),
}
if firewall_csv:
report["segmentation"] = assess_network_segmentation(firewall_csv)
return report
def main():
parser = argparse.ArgumentParser(description="OT Network Security Assessment Agent")
sub = parser.add_subparsers(dest="command")
a = sub.add_parser("assets", help="Assess OT asset inventory")
a.add_argument("--csv", required=True)
s = sub.add_parser("segmentation", help="Assess network segmentation")
s.add_argument("--csv", required=True, help="Firewall rules CSV")
p = sub.add_parser("protocols", help="Scan for OT protocols")
p.add_argument("--subnet", required=True)
r = sub.add_parser("report", help="Full assessment report")
r.add_argument("--assets", required=True)
r.add_argument("--firewall", help="Firewall rules CSV")
args = parser.parse_args()
if args.command == "assets":
result = assess_asset_inventory(args.csv)
elif args.command == "segmentation":
result = assess_network_segmentation(args.csv)
elif args.command == "protocols":
result = scan_ot_protocols(args.subnet)
elif args.command == "report":
result = generate_assessment_report(args.assets, args.firewall)
else:
parser.print_help()
return
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()
process.py17.5 KB
#!/usr/bin/env python3
"""OT Network Security Assessment - Automated Discovery and Analysis.
This script performs passive OT network discovery from pcap captures,
classifies assets by Purdue level, maps industrial protocol usage,
and identifies security findings.
Usage:
python process.py --pcap <capture.pcap> [--firewall-rules <rules.csv>]
python process.py --live-capture --interface <iface> --duration <seconds>
"""
import argparse
import csv
import json
import sys
from collections import defaultdict
from dataclasses import dataclass, field, asdict
from datetime import datetime
from ipaddress import ip_address, ip_network
from typing import Optional
try:
from scapy.all import rdpcap, sniff, IP, TCP, UDP, Ether, ARP
except ImportError:
print("[ERROR] scapy is required: pip install scapy")
sys.exit(1)
# ── Industrial Protocol Definitions ──────────────────────────────────
OT_PROTOCOLS = {
502: {"name": "Modbus/TCP", "vendor": "Open", "risk": "high", "auth": False},
102: {"name": "S7comm", "vendor": "Siemens", "risk": "high", "auth": False},
44818: {"name": "EtherNet/IP", "vendor": "ODVA/Rockwell", "risk": "high", "auth": False},
2222: {"name": "EtherNet/IP Implicit", "vendor": "ODVA", "risk": "medium", "auth": False},
4840: {"name": "OPC UA", "vendor": "OPC Foundation", "risk": "low", "auth": True},
20000: {"name": "DNP3", "vendor": "IEEE", "risk": "high", "auth": False},
47808: {"name": "BACnet/IP", "vendor": "ASHRAE", "risk": "high", "auth": False},
1911: {"name": "Niagara Fox", "vendor": "Tridium", "risk": "high", "auth": False},
2404: {"name": "IEC 60870-5-104", "vendor": "IEC", "risk": "high", "auth": False},
18245: {"name": "GE SRTP", "vendor": "GE", "risk": "high", "auth": False},
5094: {"name": "HART-IP", "vendor": "FieldComm", "risk": "medium", "auth": False},
789: {"name": "Crimson v3", "vendor": "Red Lion", "risk": "high", "auth": False},
1089: {"name": "FF HSE", "vendor": "Fieldbus Foundation", "risk": "medium", "auth": False},
9600: {"name": "OMRON FINS", "vendor": "OMRON", "risk": "high", "auth": False},
5007: {"name": "Mitsubishi MELSEC", "vendor": "Mitsubishi", "risk": "high", "auth": False},
}
# Modbus function codes with write capability
MODBUS_WRITE_FUNCS = {5, 6, 15, 16, 22, 23}
MODBUS_DIAGNOSTIC_FUNCS = {8, 17, 43}
# ── Purdue Level Classification ─────────────────────────────────────
@dataclass
class PurdueConfig:
"""Configuration for Purdue level subnet mappings."""
level_0_1: list = field(default_factory=lambda: ["10.10.0.0/16", "192.168.10.0/24"])
level_2: list = field(default_factory=lambda: ["10.20.0.0/16", "192.168.20.0/24"])
level_3: list = field(default_factory=lambda: ["10.30.0.0/16", "192.168.30.0/24"])
level_3_5: list = field(default_factory=lambda: ["172.16.0.0/16"])
level_4: list = field(default_factory=lambda: ["10.0.0.0/8"])
def classify_purdue_level(ip_str, config=None):
"""Classify IP to Purdue level based on subnet mappings."""
if config is None:
config = PurdueConfig()
try:
addr = ip_address(ip_str)
except ValueError:
return "Unknown"
level_map = [
("Level 0-1", config.level_0_1),
("Level 2", config.level_2),
("Level 3", config.level_3),
("Level 3.5", config.level_3_5),
("Level 4", config.level_4),
]
for level_name, subnets in level_map:
for subnet in subnets:
if addr in ip_network(subnet, strict=False):
return level_name
return "Unknown"
# ── Asset and Finding Models ─────────────────────────────────────────
@dataclass
class OTAsset:
ip: str
mac: str = ""
purdue_level: str = ""
protocols: list = field(default_factory=list)
roles: list = field(default_factory=list)
vendor_hint: str = ""
first_seen: str = ""
last_seen: str = ""
ports_open: list = field(default_factory=list)
@dataclass
class Finding:
finding_id: str
severity: str # critical, high, medium, low
title: str
description: str
affected_assets: list = field(default_factory=list)
iec_62443_ref: str = ""
nist_800_82_ref: str = ""
remediation: str = ""
# ── Passive Discovery Engine ─────────────────────────────────────────
class OTNetworkDiscovery:
"""Passive OT network discovery and analysis engine."""
def __init__(self, purdue_config=None):
self.config = purdue_config or PurdueConfig()
self.assets = {}
self.connections = defaultdict(lambda: {
"count": 0, "protocols": set(), "ports": set(),
"first_seen": None, "last_seen": None
})
self.protocol_stats = defaultdict(int)
self.findings = []
self.modbus_writes = []
self.cross_zone_flows = []
def process_packet(self, pkt):
"""Process a single packet for OT asset and protocol discovery."""
if not pkt.haslayer(IP):
return
src_ip = pkt[IP].src
dst_ip = pkt[IP].dst
timestamp = str(datetime.fromtimestamp(float(pkt.time)))
# Discover or update assets
for ip_addr in (src_ip, dst_ip):
if ip_addr not in self.assets:
mac = ""
if pkt.haslayer(Ether):
mac = pkt[Ether].src if ip_addr == src_ip else pkt[Ether].dst
self.assets[ip_addr] = OTAsset(
ip=ip_addr,
mac=mac,
purdue_level=classify_purdue_level(ip_addr, self.config),
first_seen=timestamp,
last_seen=timestamp,
)
self.assets[ip_addr].last_seen = timestamp
# Extract port information
dst_port = src_port = None
if pkt.haslayer(TCP):
dst_port = pkt[TCP].dport
src_port = pkt[TCP].sport
elif pkt.haslayer(UDP):
dst_port = pkt[UDP].dport
src_port = pkt[UDP].sport
# Identify OT protocols
if dst_port in OT_PROTOCOLS:
proto_info = OT_PROTOCOLS[dst_port]
proto_name = proto_info["name"]
self.protocol_stats[proto_name] += 1
asset_src = self.assets[src_ip]
asset_dst = self.assets[dst_ip]
if proto_name not in asset_src.protocols:
asset_src.protocols.append(proto_name)
if proto_name not in asset_dst.protocols:
asset_dst.protocols.append(proto_name)
if "master/client" not in asset_src.roles:
asset_src.roles.append("master/client")
if "slave/server" not in asset_dst.roles:
asset_dst.roles.append("slave/server")
if dst_port not in asset_dst.ports_open:
asset_dst.ports_open.append(dst_port)
if proto_info["vendor"] != "Open":
asset_dst.vendor_hint = proto_info["vendor"]
# Track Modbus write operations
if dst_port == 502 and pkt.haslayer(TCP):
payload = bytes(pkt[TCP].payload)
if len(payload) > 7:
func_code = payload[7]
if func_code in MODBUS_WRITE_FUNCS:
self.modbus_writes.append({
"timestamp": timestamp,
"src": src_ip,
"dst": dst_ip,
"function_code": func_code,
})
# Track connections
conn_key = f"{src_ip}->{dst_ip}"
conn = self.connections[conn_key]
conn["count"] += 1
if conn["first_seen"] is None:
conn["first_seen"] = timestamp
conn["last_seen"] = timestamp
if dst_port:
conn["ports"].add(dst_port)
if dst_port in OT_PROTOCOLS:
conn["protocols"].add(OT_PROTOCOLS[dst_port]["name"])
# Detect cross-zone communication
src_level = self.assets[src_ip].purdue_level
dst_level = self.assets[dst_ip].purdue_level
if src_level != dst_level and "Unknown" not in (src_level, dst_level):
self.cross_zone_flows.append({
"src": src_ip, "src_level": src_level,
"dst": dst_ip, "dst_level": dst_level,
"port": dst_port,
"protocol": OT_PROTOCOLS.get(dst_port, {}).get("name", f"port/{dst_port}"),
"timestamp": timestamp,
})
def analyze_pcap(self, pcap_file):
"""Analyze a pcap file for OT network discovery."""
print(f"[*] Loading pcap: {pcap_file}")
packets = rdpcap(pcap_file)
print(f"[*] Processing {len(packets)} packets...")
for pkt in packets:
self.process_packet(pkt)
print(f"[*] Discovery complete: {len(self.assets)} assets found")
def live_capture(self, interface, duration):
"""Perform live passive capture on a network interface."""
print(f"[*] Starting live capture on {interface} for {duration}s...")
sniff(iface=interface, timeout=duration, prn=self.process_packet, store=0)
print(f"[*] Capture complete: {len(self.assets)} assets found")
def generate_findings(self):
"""Analyze discovered data and generate security findings."""
finding_counter = 1
# Finding: Direct enterprise-to-field communication
for flow in self.cross_zone_flows:
if "Level 4" in flow["src_level"] and "Level 0-1" in flow["dst_level"]:
self.findings.append(Finding(
finding_id=f"OT-{finding_counter:03d}",
severity="critical",
title="Direct Enterprise-to-Field Device Communication",
description=(
f"Traffic observed from {flow['src']} ({flow['src_level']}) "
f"to {flow['dst']} ({flow['dst_level']}) via {flow['protocol']}. "
"This bypasses all intermediate security zones."
),
affected_assets=[flow["src"], flow["dst"]],
iec_62443_ref="IEC 62443-3-3 SR 5.1 - Network Segmentation",
nist_800_82_ref="NIST SP 800-82r3 Section 5.3 - Network Architecture",
remediation="Block direct L4-to-L0/1 traffic. Route through DMZ and OT firewall.",
))
finding_counter += 1
# Finding: Enterprise bypassing DMZ to reach control
for flow in self.cross_zone_flows:
if "Level 4" in flow["src_level"] and "Level 2" in flow["dst_level"]:
self.findings.append(Finding(
finding_id=f"OT-{finding_counter:03d}",
severity="critical",
title="Enterprise-to-Control Bypass of DMZ",
description=(
f"Traffic from {flow['src']} ({flow['src_level']}) reaches "
f"{flow['dst']} ({flow['dst_level']}) without traversing DMZ."
),
affected_assets=[flow["src"], flow["dst"]],
iec_62443_ref="IEC 62443-3-3 SR 5.2 - Zone Boundary Protection",
nist_800_82_ref="NIST SP 800-82r3 Section 5.4 - DMZ Architecture",
remediation="Deploy DMZ between enterprise and control zones with bidirectional firewall.",
))
finding_counter += 1
# Finding: Unauthenticated Modbus write operations
if self.modbus_writes:
unique_targets = set(w["dst"] for w in self.modbus_writes)
self.findings.append(Finding(
finding_id=f"OT-{finding_counter:03d}",
severity="critical",
title="Unauthenticated Modbus/TCP Write Commands Detected",
description=(
f"{len(self.modbus_writes)} Modbus write operations observed "
f"targeting {len(unique_targets)} devices. Modbus/TCP has no "
"native authentication; any network-connected device can modify registers."
),
affected_assets=list(unique_targets),
iec_62443_ref="IEC 62443-3-3 SR 1.1 - Human User Identification and Authentication",
nist_800_82_ref="NIST SP 800-82r3 Section 6.2 - Access Control",
remediation=(
"Deploy Modbus-aware firewall (e.g., Tofino, Claroty Edge) to restrict "
"write-capable source addresses. Implement allowlisting for Modbus function codes."
),
))
finding_counter += 1
# Finding: Unauthenticated industrial protocols
for port, info in OT_PROTOCOLS.items():
if not info["auth"] and info["name"] in self.protocol_stats:
exposed_assets = [
ip for ip, asset in self.assets.items()
if port in asset.ports_open
]
if exposed_assets:
self.findings.append(Finding(
finding_id=f"OT-{finding_counter:03d}",
severity="high",
title=f"Unauthenticated {info['name']} Protocol in Use",
description=(
f"{len(exposed_assets)} devices expose {info['name']} (port {port}) "
f"which lacks native authentication. Vendor: {info['vendor']}."
),
affected_assets=exposed_assets,
iec_62443_ref="IEC 62443-3-3 SR 1.2 - Software Process Identification",
nist_800_82_ref="NIST SP 800-82r3 Section 6.2.1 - Protocol Security",
remediation=f"Deploy protocol-aware firewall for {info['name']} traffic inspection.",
))
finding_counter += 1
return self.findings
def export_report(self, output_file):
"""Export full assessment results to JSON."""
report = {
"assessment_date": datetime.now().isoformat(),
"summary": {
"total_assets": len(self.assets),
"total_connections": len(self.connections),
"protocols_detected": dict(self.protocol_stats),
"cross_zone_flows": len(self.cross_zone_flows),
"modbus_write_operations": len(self.modbus_writes),
"findings_critical": sum(1 for f in self.findings if f.severity == "critical"),
"findings_high": sum(1 for f in self.findings if f.severity == "high"),
"findings_medium": sum(1 for f in self.findings if f.severity == "medium"),
"findings_low": sum(1 for f in self.findings if f.severity == "low"),
},
"assets": {ip: asdict(asset) for ip, asset in self.assets.items()},
"findings": [asdict(f) for f in self.findings],
"cross_zone_flows": self.cross_zone_flows[:100],
}
with open(output_file, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[*] Report saved to: {output_file}")
def print_summary(self):
"""Print assessment summary to console."""
print("\n" + "=" * 70)
print("OT NETWORK SECURITY ASSESSMENT - SUMMARY")
print("=" * 70)
print(f"\nAssets Discovered: {len(self.assets)}")
level_counts = defaultdict(int)
for asset in self.assets.values():
level_counts[asset.purdue_level] += 1
for level in sorted(level_counts.keys()):
print(f" {level}: {level_counts[level]} devices")
print(f"\nProtocol Distribution:")
for proto, count in sorted(self.protocol_stats.items(), key=lambda x: -x[1]):
print(f" {proto}: {count} packets")
print(f"\nCross-Zone Flows: {len(self.cross_zone_flows)}")
print(f"Modbus Write Operations: {len(self.modbus_writes)}")
print(f"\nFindings:")
severity_counts = defaultdict(int)
for f in self.findings:
severity_counts[f.severity] += 1
for sev in ["critical", "high", "medium", "low"]:
if severity_counts[sev]:
print(f" {sev.upper()}: {severity_counts[sev]}")
if self.findings:
print(f"\nTop Findings:")
for f in self.findings[:10]:
print(f" [{f.finding_id}] [{f.severity.upper()}] {f.title}")
def main():
parser = argparse.ArgumentParser(description="OT Network Security Assessment Tool")
parser.add_argument("--pcap", help="Path to pcap file for analysis")
parser.add_argument("--live-capture", action="store_true", help="Perform live capture")
parser.add_argument("--interface", help="Network interface for live capture")
parser.add_argument("--duration", type=int, default=300, help="Capture duration in seconds")
parser.add_argument("--output", default="ot_assessment_report.json", help="Output report file")
args = parser.parse_args()
discovery = OTNetworkDiscovery()
if args.pcap:
discovery.analyze_pcap(args.pcap)
elif args.live_capture and args.interface:
discovery.live_capture(args.interface, args.duration)
else:
parser.print_help()
sys.exit(1)
discovery.generate_findings()
discovery.print_summary()
discovery.export_report(args.output)
if __name__ == "__main__":
main()