Files
T
mukul975 c21af3347e Complete folder anatomy for all 649 cybersecurity skills + update LICENSE to Mahipal
- Add scripts/agent.py and references/api-reference.md to all remaining skills
- Update all 648 LICENSE files: copyright now reads 'Mahipal'
- Add implementing-security-monitoring-with-datadog (new skill with full anatomy)
- All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
2026-03-11 00:22:12 +01:00

157 lines
6.4 KiB
Python

#!/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()