Files
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

147 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""OT Network Segmentation Agent - audits IT/OT boundaries, firewall rules, and zone compliance."""
import json
import argparse
import logging
import subprocess
from datetime import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
PURDUE_ZONES = {
"level_0": {"name": "Process", "description": "Field devices, sensors, actuators"},
"level_1": {"name": "Control", "description": "PLCs, RTUs, safety systems"},
"level_2": {"name": "Supervisory", "description": "HMI, SCADA, engineering workstations"},
"level_3": {"name": "Operations", "description": "Historian, OT domain services"},
"level_3.5": {"name": "DMZ", "description": "IT/OT demilitarized zone"},
"level_4": {"name": "Enterprise", "description": "IT network, business systems"},
"level_5": {"name": "External", "description": "Internet, cloud services"},
}
def scan_zone_hosts(subnet):
"""Discover hosts in an OT zone via nmap ping scan."""
cmd = ["nmap", "-sn", subnet, "-oX", "-"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
hosts = []
import re
for match in re.finditer(r'addr="(\d+\.\d+\.\d+\.\d+)"', result.stdout):
hosts.append(match.group(1))
return hosts
def check_firewall_rules(firewall_config_file):
"""Parse firewall rules for IT/OT boundary enforcement."""
findings = []
try:
with open(firewall_config_file) as f:
rules = json.load(f)
for rule in rules:
if rule.get("action") == "permit":
src_zone = rule.get("source_zone", "")
dst_zone = rule.get("destination_zone", "")
if src_zone in ("enterprise", "external") and dst_zone in ("control", "process"):
findings.append({
"rule_id": rule.get("id", ""),
"issue": f"Direct access from {src_zone} to {dst_zone} zone",
"severity": "critical",
"recommendation": "Route through DMZ with application proxy",
})
if rule.get("protocol") == "any" or rule.get("port") == "any":
findings.append({
"rule_id": rule.get("id", ""),
"issue": "Overly permissive rule (any protocol/port)",
"severity": "high",
"recommendation": "Restrict to specific OT protocols (Modbus/TCP 502, EtherNet/IP 44818)",
})
except (FileNotFoundError, json.JSONDecodeError):
findings.append({"issue": "Firewall config not found or invalid", "severity": "critical"})
return findings
def check_ot_protocol_exposure(target_subnet):
"""Check for exposed OT protocols on the network."""
ot_ports = {"502": "Modbus", "102": "S7comm", "44818": "EtherNet/IP",
"20000": "DNP3", "4840": "OPC-UA", "2222": "EtherNet/IP-explicit"}
port_list = ",".join(ot_ports.keys())
cmd = ["nmap", "-sS", "-p", port_list, target_subnet, "--open", "-oX", "-"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
exposed = []
import re
current_host = ""
for line in result.stdout.split("\n"):
host_match = re.search(r'addr="(\d+\.\d+\.\d+\.\d+)"', line)
if host_match:
current_host = host_match.group(1)
port_match = re.search(r'portid="(\d+)".*state="open"', line)
if port_match and current_host:
port = port_match.group(1)
exposed.append({
"host": current_host, "port": int(port),
"protocol": ot_ports.get(port, "unknown"),
"risk": "high" if port in ("502", "102") else "medium",
})
return exposed
def audit_zone_compliance(zone_config):
"""Audit zone assignment compliance against Purdue model."""
findings = []
for zone_id, zone_data in zone_config.items():
if zone_id not in PURDUE_ZONES:
findings.append({"zone": zone_id, "issue": "Non-standard zone", "severity": "medium"})
continue
hosts = zone_data.get("hosts", [])
for host in hosts:
if host.get("type") == "workstation" and zone_id in ("level_0", "level_1"):
findings.append({"zone": zone_id, "host": host.get("ip"), "issue": "Workstation in control/process zone", "severity": "high"})
if host.get("internet_access") and zone_id in ("level_0", "level_1", "level_2"):
findings.append({"zone": zone_id, "host": host.get("ip"), "issue": "Internet access from OT zone", "severity": "critical"})
return findings
def generate_report(fw_findings, ot_exposure, zone_findings, zone_config):
all_findings = fw_findings + zone_findings
critical = sum(1 for f in all_findings if f.get("severity") == "critical")
report = {
"timestamp": datetime.utcnow().isoformat(),
"framework": "IEC 62443 / Purdue Model",
"zones_defined": len(zone_config) if zone_config else 0,
"firewall_findings": fw_findings,
"ot_protocol_exposure": ot_exposure,
"zone_compliance_findings": zone_findings,
"total_findings": len(all_findings),
"critical_findings": critical,
}
return report
def main():
parser = argparse.ArgumentParser(description="OT Network Segmentation Audit Agent")
parser.add_argument("--firewall-config", help="JSON firewall rules config file")
parser.add_argument("--zone-config", help="JSON zone configuration file")
parser.add_argument("--scan-subnet", help="OT subnet to scan for protocol exposure")
parser.add_argument("--output", default="ot_segmentation_report.json")
args = parser.parse_args()
fw_findings = check_firewall_rules(args.firewall_config) if args.firewall_config else []
ot_exposure = check_ot_protocol_exposure(args.scan_subnet) if args.scan_subnet else []
zone_config = {}
zone_findings = []
if args.zone_config:
with open(args.zone_config) as f:
zone_config = json.load(f)
zone_findings = audit_zone_compliance(zone_config)
report = generate_report(fw_findings, ot_exposure, zone_findings, zone_config)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("OT segmentation: %d findings (%d critical), %d exposed OT ports",
report["total_findings"], report["critical_findings"], len(ot_exposure))
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()