Expand 38 agent.py stubs, standardize 347 SKILL.md sections, fix 4 verify=False

This commit is contained in:
mukul975
2026-03-19 13:55:45 +01:00
parent 79287253fb
commit 051e7e72ed
11 changed files with 1931 additions and 386 deletions
@@ -1,61 +1,213 @@
#!/usr/bin/env python3
"""Purdue model OT network segmentation audit."""
import argparse, json
"""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
try:
import requests
except ImportError:
requests = None
def audit_config(target, token):
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 = []
if not requests: return [{"error": "requests required"}]
headers = {"Authorization": f"Bearer {token}"}
try:
resp = requests.get(f"{target}/api/v1/status", headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
if not data.get("enabled", True):
findings.append({"check": "Service Status", "status": "DISABLED", "severity": "CRITICAL"})
elif resp.status_code == 401:
findings.append({"check": "Authentication", "status": "UNAUTHORIZED", "severity": "HIGH"})
except requests.RequestException as e:
findings.append({"error": str(e)})
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 check_compliance(target, token):
def audit_ot_protocols(target_ips):
"""Check for exposed OT/ICS protocols on target hosts."""
findings = []
if not requests: return []
headers = {"Authorization": f"Bearer {token}"}
try:
resp = requests.get(f"{target}/api/v1/compliance", headers=headers, timeout=10)
if resp.status_code == 200:
for item in resp.json().get("checks", []):
if item.get("status") != "PASS":
findings.append({"check": item.get("name"), "status": item.get("status"),
"severity": item.get("severity", "MEDIUM")})
except requests.RequestException:
pass
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():
p = argparse.ArgumentParser(description="Purdue model OT network segmentation audit")
p.add_argument("--target", required=True, help="Target URL")
p.add_argument("--token", required=True, help="API token")
p.add_argument("--output", "-o", help="Output JSON report")
p.add_argument("--verbose", "-v", action="store_true")
a = p.parse_args()
print("[*] Purdue model OT network segmentation audit")
report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": []}
report["findings"].extend(audit_config(a.target, a.token))
report["findings"].extend(check_compliance(a.target, a.token))
high = sum(1 for f in report["findings"] if f.get("severity") in ("HIGH", "CRITICAL"))
report["risk_level"] = "HIGH" if high else "MEDIUM" if report["findings"] else "LOW"
print(f"[*] {len(report['findings'])} findings, risk: {report['risk_level']}")
if a.output:
with open(a.output, "w") as f: json.dump(report, f, indent=2)
else:
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()