mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
214 lines
7.5 KiB
Python
214 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Purdue model OT network segmentation audit agent.
|
|
|
|
Audits OT/ICS network segmentation against the Purdue Enterprise
|
|
Reference Architecture by testing connectivity between network zones,
|
|
verifying firewall rules, and mapping discovered hosts to Purdue levels.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
PURDUE_LEVELS = {
|
|
0: {"name": "Process", "description": "Sensors, actuators, field devices"},
|
|
1: {"name": "Basic Control", "description": "PLCs, RTUs, safety systems"},
|
|
2: {"name": "Area Supervisory", "description": "HMIs, SCADA, historian"},
|
|
3: {"name": "Site Operations", "description": "Patch mgmt, AV, file servers"},
|
|
3.5: {"name": "DMZ", "description": "Industrial DMZ between IT and OT"},
|
|
4: {"name": "Site Business", "description": "ERP, email, corporate apps"},
|
|
5: {"name": "Enterprise", "description": "Internet, cloud, remote access"},
|
|
}
|
|
|
|
PROHIBITED_FLOWS = [
|
|
{"from_level": 5, "to_level": 0, "description": "Internet to Process (critical violation)"},
|
|
{"from_level": 5, "to_level": 1, "description": "Internet to Basic Control"},
|
|
{"from_level": 5, "to_level": 2, "description": "Internet to SCADA"},
|
|
{"from_level": 4, "to_level": 0, "description": "Corporate to Process"},
|
|
{"from_level": 4, "to_level": 1, "description": "Corporate to PLC/RTU"},
|
|
]
|
|
|
|
|
|
def test_connectivity(source_ip, target_ip, ports, timeout=3):
|
|
"""Test TCP connectivity between two hosts on specified ports."""
|
|
results = []
|
|
for port in ports:
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(timeout)
|
|
result = sock.connect_ex((target_ip, port))
|
|
sock.close()
|
|
reachable = result == 0
|
|
results.append({
|
|
"target": target_ip,
|
|
"port": port,
|
|
"reachable": reachable,
|
|
})
|
|
except (socket.error, OSError):
|
|
results.append({"target": target_ip, "port": port, "reachable": False})
|
|
return results
|
|
|
|
|
|
def audit_zone_separation(zone_map):
|
|
"""Audit network segmentation between Purdue zones."""
|
|
findings = []
|
|
print("[*] Auditing zone separation...")
|
|
|
|
for flow in PROHIBITED_FLOWS:
|
|
from_level = flow["from_level"]
|
|
to_level = flow["to_level"]
|
|
from_hosts = zone_map.get(str(from_level), [])
|
|
to_hosts = zone_map.get(str(to_level), [])
|
|
|
|
for src in from_hosts[:3]:
|
|
for dst in to_hosts[:3]:
|
|
common_ports = [22, 80, 443, 502, 102, 44818, 47808, 20000]
|
|
results = test_connectivity(src, dst, common_ports)
|
|
open_ports = [r for r in results if r["reachable"]]
|
|
if open_ports:
|
|
findings.append({
|
|
"check": f"Zone {from_level} -> Zone {to_level}",
|
|
"severity": "CRITICAL",
|
|
"source": src,
|
|
"destination": dst,
|
|
"open_ports": [r["port"] for r in open_ports],
|
|
"detail": flow["description"],
|
|
"recommendation": "Block traffic between these zones via firewall",
|
|
})
|
|
|
|
if not findings:
|
|
findings.append({
|
|
"check": "Prohibited zone flows",
|
|
"severity": "INFO",
|
|
"detail": "No prohibited cross-zone connectivity detected",
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def audit_ot_protocols(target_ips):
|
|
"""Check for exposed OT/ICS protocols on target hosts."""
|
|
findings = []
|
|
ot_ports = {
|
|
502: "Modbus TCP",
|
|
102: "S7comm (Siemens)",
|
|
44818: "EtherNet/IP",
|
|
47808: "BACnet",
|
|
20000: "DNP3",
|
|
4840: "OPC UA",
|
|
2222: "EtherCAT",
|
|
1911: "Niagara Fox",
|
|
9600: "OMRON FINS",
|
|
}
|
|
|
|
print(f"[*] Scanning {len(target_ips)} hosts for exposed OT protocols...")
|
|
for ip in target_ips:
|
|
for port, protocol in ot_ports.items():
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(2)
|
|
result = sock.connect_ex((ip, port))
|
|
sock.close()
|
|
if result == 0:
|
|
findings.append({
|
|
"check": f"Exposed OT protocol: {protocol}",
|
|
"severity": "HIGH",
|
|
"host": ip,
|
|
"port": port,
|
|
"protocol": protocol,
|
|
"detail": f"{protocol} on {ip}:{port} is accessible",
|
|
})
|
|
except (socket.error, OSError):
|
|
pass
|
|
|
|
print(f"[+] Found {len(findings)} exposed OT protocols")
|
|
return findings
|
|
|
|
|
|
def load_zone_map(config_path):
|
|
"""Load zone-to-host mapping from config file."""
|
|
with open(config_path, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def format_summary(zone_findings, protocol_findings, zone_map):
|
|
"""Print audit summary."""
|
|
all_findings = zone_findings + protocol_findings
|
|
print(f"\n{'='*60}")
|
|
print(f" Purdue Model Network Segmentation Audit")
|
|
print(f"{'='*60}")
|
|
|
|
for level, info in sorted(PURDUE_LEVELS.items()):
|
|
host_count = len(zone_map.get(str(level), []))
|
|
print(f" Level {level}: {info['name']:20s} ({host_count} hosts) - {info['description']}")
|
|
|
|
print(f"\n Zone Separation Findings : {len(zone_findings)}")
|
|
print(f" Protocol Exposure Findings: {len(protocol_findings)}")
|
|
|
|
severity_counts = {}
|
|
for f in all_findings:
|
|
sev = f.get("severity", "INFO")
|
|
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
|
|
if all_findings:
|
|
print(f"\n Critical/High Issues:")
|
|
for f in all_findings:
|
|
if f["severity"] in ("CRITICAL", "HIGH"):
|
|
print(f" [{f['severity']:8s}] {f['check']}: {f.get('detail', '')[:50]}")
|
|
|
|
return severity_counts
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Purdue model OT network segmentation audit agent"
|
|
)
|
|
parser.add_argument("--zone-map", required=True,
|
|
help="JSON file mapping Purdue levels to host IPs")
|
|
parser.add_argument("--scan-protocols", action="store_true",
|
|
help="Scan for exposed OT protocols")
|
|
parser.add_argument("--output", "-o", help="Output JSON report")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
zone_map = load_zone_map(args.zone_map)
|
|
zone_findings = audit_zone_separation(zone_map)
|
|
|
|
protocol_findings = []
|
|
if args.scan_protocols:
|
|
all_hosts = []
|
|
for level_hosts in zone_map.values():
|
|
all_hosts.extend(level_hosts)
|
|
protocol_findings = audit_ot_protocols(list(set(all_hosts)))
|
|
|
|
severity_counts = format_summary(zone_findings, protocol_findings, zone_map)
|
|
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"tool": "Purdue Model Audit",
|
|
"zone_map": zone_map,
|
|
"zone_findings": zone_findings,
|
|
"protocol_findings": protocol_findings,
|
|
"severity_counts": severity_counts,
|
|
"risk_level": (
|
|
"CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
|
|
else "HIGH" if severity_counts.get("HIGH", 0) > 0
|
|
else "LOW"
|
|
),
|
|
}
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
elif args.verbose:
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|