Files
Anthropic-Cybersecurity-Skills/skills/implementing-iec-62443-security-zones/scripts/agent.py
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

213 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""Agent for designing and auditing IEC 62443 security zones and conduits."""
import json
import argparse
from datetime import datetime
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, engineering workstations"},
3: {"name": "Site Operations", "description": "Historians, OPC servers, MES"},
3.5: {"name": "DMZ", "description": "IT/OT demilitarized zone"},
4: {"name": "Enterprise", "description": "ERP, email, business systems"},
5: {"name": "External", "description": "Internet, cloud, vendors"},
}
SECURITY_LEVELS = {
"SL1": "Protection against casual or coincidental violation",
"SL2": "Protection against intentional violation using simple means",
"SL3": "Protection against sophisticated attack with moderate resources",
"SL4": "Protection against state-sponsored attack with extended resources",
}
def audit_zone_architecture(zones_path):
"""Audit IEC 62443 zone architecture for compliance."""
with open(zones_path) as f:
architecture = json.load(f)
zones = architecture.get("zones", [])
conduits = architecture.get("conduits", [])
findings = []
for zone in zones:
zone_name = zone.get("name", "")
sl_target = zone.get("sl_target", zone.get("security_level", ""))
sl_achieved = zone.get("sl_achieved", zone.get("current_sl", ""))
purdue_level = zone.get("purdue_level", -1)
if not sl_target:
findings.append({"zone": zone_name, "issue": "No SL-T defined",
"severity": "CRITICAL"})
if sl_target and sl_achieved:
t = int(sl_target.replace("SL", ""))
a = int(sl_achieved.replace("SL", ""))
if a < t:
findings.append({
"zone": zone_name,
"issue": f"SL gap: target={sl_target} achieved={sl_achieved}",
"severity": "HIGH",
})
if not zone.get("assets"):
findings.append({"zone": zone_name, "issue": "No assets documented",
"severity": "MEDIUM"})
if purdue_level in (0, 1) and sl_target in ("SL1", ""):
findings.append({
"zone": zone_name,
"issue": f"Low SL for Purdue Level {purdue_level}",
"severity": "HIGH",
"recommendation": "Critical control zones should target SL3+",
})
for conduit in conduits:
src = conduit.get("source_zone", "")
dst = conduit.get("destination_zone", "")
controls = conduit.get("security_controls", [])
if not controls:
findings.append({
"conduit": f"{src} -> {dst}",
"issue": "No security controls on conduit",
"severity": "CRITICAL",
})
if not conduit.get("firewall", False):
findings.append({
"conduit": f"{src} -> {dst}",
"issue": "No firewall on conduit",
"severity": "HIGH",
})
if conduit.get("allows_remote_access", False) and \
not conduit.get("requires_mfa", False):
findings.append({
"conduit": f"{src} -> {dst}",
"issue": "Remote access conduit without MFA",
"severity": "CRITICAL",
})
# Check for direct Level 5 to Level 0/1 conduit
for conduit in conduits:
src_level = conduit.get("source_purdue_level", -1)
dst_level = conduit.get("destination_purdue_level", -1)
if (src_level >= 4 and dst_level <= 1) or (dst_level >= 4 and src_level <= 1):
if not conduit.get("passes_through_dmz", False):
findings.append({
"conduit": f"{conduit.get('source_zone')} -> {conduit.get('destination_zone')}",
"issue": "Direct IT-to-OT conduit bypassing DMZ",
"severity": "CRITICAL",
})
return findings
def generate_zone_template(facility_name, zone_count=5):
"""Generate IEC 62443 zone template based on Purdue model."""
zones = [
{"name": f"{facility_name}_Level0_Field",
"purdue_level": 0, "sl_target": "SL2",
"description": "Field instruments, sensors, actuators",
"assets": ["PLC I/O modules", "Sensors", "Actuators"]},
{"name": f"{facility_name}_Level1_Control",
"purdue_level": 1, "sl_target": "SL3",
"description": "PLCs, RTUs, safety controllers",
"assets": ["PLCs", "Safety Controllers", "RTUs"]},
{"name": f"{facility_name}_Level2_Supervisory",
"purdue_level": 2, "sl_target": "SL3",
"description": "HMI, engineering workstations",
"assets": ["HMI Stations", "Engineering Workstations"]},
{"name": f"{facility_name}_Level3_Operations",
"purdue_level": 3, "sl_target": "SL2",
"description": "Historian, OPC, MES",
"assets": ["Historian Server", "OPC Gateway", "MES"]},
{"name": f"{facility_name}_DMZ",
"purdue_level": 3.5, "sl_target": "SL3",
"description": "IT/OT demilitarized zone",
"assets": ["Data Diode", "Patch Server", "AV Update Server"]},
]
conduits = [
{"source_zone": zones[0]["name"], "destination_zone": zones[1]["name"],
"protocols": ["Modbus", "PROFINET"], "firewall": True,
"security_controls": ["Protocol-aware firewall", "DPI"]},
{"source_zone": zones[1]["name"], "destination_zone": zones[2]["name"],
"protocols": ["EtherNet/IP", "OPC UA"], "firewall": True,
"security_controls": ["Industrial firewall", "Allowlist"]},
{"source_zone": zones[2]["name"], "destination_zone": zones[3]["name"],
"protocols": ["OPC UA", "SQL"], "firewall": True,
"security_controls": ["Firewall", "Network monitoring"]},
{"source_zone": zones[3]["name"], "destination_zone": zones[4]["name"],
"protocols": ["HTTPS", "SFTP"], "firewall": True,
"security_controls": ["Data diode", "Firewall", "IDS"]},
]
return {"zones": zones, "conduits": conduits}
def assess_sl_requirements(risk_assessment_path):
"""Map risk assessment results to IEC 62443 Security Level targets."""
with open(risk_assessment_path) as f:
assessment = json.load(f)
zones = assessment.get("zones", [])
recommendations = []
for zone in zones:
threat_level = zone.get("threat_level", "medium").lower()
impact = zone.get("impact", "medium").lower()
if threat_level == "high" and impact in ("high", "critical"):
sl = "SL4"
elif threat_level == "high" or impact == "high":
sl = "SL3"
elif threat_level == "medium":
sl = "SL2"
else:
sl = "SL1"
recommendations.append({
"zone": zone.get("name", ""),
"threat_level": threat_level,
"impact": impact,
"recommended_sl": sl,
"sl_description": SECURITY_LEVELS[sl],
})
return recommendations
def main():
parser = argparse.ArgumentParser(description="IEC 62443 Security Zones Agent")
parser.add_argument("--zones", help="Zone architecture JSON to audit")
parser.add_argument("--risk-assessment", help="Risk assessment JSON for SL mapping")
parser.add_argument("--facility", help="Facility name for template generation")
parser.add_argument("--action", choices=["audit", "template", "sl-map", "full"],
default="full")
parser.add_argument("--output", default="iec62443_zones_report.json")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "results": {}}
if args.action in ("audit", "full") and args.zones:
findings = audit_zone_architecture(args.zones)
report["results"]["audit"] = findings
critical = sum(1 for f in findings if f.get("severity") == "CRITICAL")
print(f"[+] Zone audit: {len(findings)} findings, {critical} critical")
if args.action in ("template", "full") and args.facility:
template = generate_zone_template(args.facility)
report["results"]["template"] = template
print(f"[+] Template: {len(template['zones'])} zones, {len(template['conduits'])} conduits")
if args.action in ("sl-map", "full") and args.risk_assessment:
recommendations = assess_sl_requirements(args.risk_assessment)
report["results"]["sl_recommendations"] = recommendations
print(f"[+] SL recommendations for {len(recommendations)} zones")
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f"[+] Report saved to {args.output}")
if __name__ == "__main__":
main()