Files
Anthropic-Cybersecurity-Skills/skills/implementing-ics-firewall-with-tofino/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

214 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""Agent for auditing and configuring Tofino ICS firewall rules."""
import json
import argparse
from datetime import datetime
from collections import Counter
OT_PROTOCOLS = {
"modbus": {"port": 502, "layer": "TCP", "dpi": True},
"enip": {"port": 44818, "layer": "TCP/UDP", "dpi": True},
"dnp3": {"port": 20000, "layer": "TCP", "dpi": True},
"opc_ua": {"port": 4840, "layer": "TCP", "dpi": True},
"s7comm": {"port": 102, "layer": "TCP", "dpi": True},
"bacnet": {"port": 47808, "layer": "UDP", "dpi": True},
"iec61850_mms": {"port": 102, "layer": "TCP", "dpi": True},
"profinet": {"port": 34964, "layer": "UDP", "dpi": False},
}
RISKY_MODBUS_FUNCTIONS = {
5: "Write Single Coil",
6: "Write Single Register",
15: "Write Multiple Coils",
16: "Write Multiple Registers",
22: "Mask Write Register",
23: "Read/Write Multiple Registers",
}
def audit_firewall_rules(rules_path):
"""Audit Tofino firewall rules for security issues."""
with open(rules_path) as f:
rules = json.load(f)
rule_list = rules if isinstance(rules, list) else rules.get("rules", [])
findings = []
for rule in rule_list:
action = rule.get("action", "").lower()
src = rule.get("source", rule.get("src", "any"))
dst = rule.get("destination", rule.get("dst", "any"))
protocol = rule.get("protocol", "").lower()
port = rule.get("port", rule.get("dst_port", "any"))
if src == "any" and dst == "any" and action == "allow":
findings.append({
"rule": rule.get("id", rule.get("name", "")),
"issue": "Allow-any-any rule detected",
"severity": "CRITICAL",
"recommendation": "Restrict to specific source/destination",
})
if protocol in ("modbus", "enip", "s7comm") and not rule.get("dpi_enabled", False):
findings.append({
"rule": rule.get("id", ""),
"issue": f"DPI not enabled for {protocol}",
"severity": "HIGH",
"recommendation": f"Enable deep packet inspection for {protocol}",
})
if protocol == "modbus":
allowed_funcs = rule.get("allowed_functions", [])
risky = [f for f in allowed_funcs if f in RISKY_MODBUS_FUNCTIONS]
if risky:
findings.append({
"rule": rule.get("id", ""),
"issue": f"Write functions allowed: {risky}",
"severity": "HIGH",
"detail": {fc: RISKY_MODBUS_FUNCTIONS[fc] for fc in risky},
})
if action == "allow" and not rule.get("logging", False):
findings.append({
"rule": rule.get("id", ""),
"issue": "Allow rule without logging",
"severity": "MEDIUM",
})
has_deny_all = any(r.get("action", "").lower() == "deny" and
r.get("source", "any") == "any" and
r.get("destination", "any") == "any" for r in rule_list)
if not has_deny_all:
findings.append({
"issue": "No default deny-all rule found",
"severity": "CRITICAL",
"recommendation": "Add deny-all as last rule",
})
return findings
def analyze_ot_traffic_log(log_path):
"""Analyze OT network traffic log for anomalies."""
with open(log_path) as f:
entries = json.load(f)
items = entries if isinstance(entries, list) else entries.get("flows", [])
by_protocol = Counter()
by_src = Counter()
anomalies = []
for entry in items:
proto = entry.get("protocol", entry.get("app_protocol", "unknown")).lower()
by_protocol[proto] += 1
by_src[entry.get("src_ip", "unknown")] += 1
port = entry.get("dst_port", 0)
if proto == "modbus" and port != 502:
anomalies.append({"type": "non_standard_port", "protocol": proto,
"port": port, "severity": "HIGH"})
if entry.get("action", "").lower() == "denied":
anomalies.append({
"type": "denied_connection",
"src": entry.get("src_ip", ""),
"dst": entry.get("dst_ip", ""),
"protocol": proto,
"severity": "MEDIUM",
})
return {
"total_flows": len(items),
"by_protocol": dict(by_protocol),
"unique_sources": len(by_src),
"anomalies": anomalies[:50],
"anomaly_count": len(anomalies),
}
def generate_zone_rules(zone_config):
"""Generate Tofino firewall rules from zone configuration."""
rules = []
zones = zone_config.get("zones", [])
for zone in zones:
zone_name = zone.get("name", "")
allowed_protocols = zone.get("allowed_protocols", [])
plc_ips = zone.get("plc_ips", [])
hmi_ips = zone.get("hmi_ips", [])
eng_ips = zone.get("engineering_ips", [])
for proto in allowed_protocols:
proto_info = OT_PROTOCOLS.get(proto, {})
port = proto_info.get("port", "any")
for hmi in hmi_ips:
for plc in plc_ips:
rules.append({
"zone": zone_name,
"action": "allow",
"source": hmi,
"destination": plc,
"protocol": proto,
"port": port,
"dpi_enabled": proto_info.get("dpi", False),
"logging": True,
})
if proto == "modbus":
for eng in eng_ips:
for plc in plc_ips:
rules.append({
"zone": zone_name,
"action": "allow",
"source": eng,
"destination": plc,
"protocol": "modbus",
"port": 502,
"dpi_enabled": True,
"allowed_functions": [1, 2, 3, 4],
"logging": True,
"comment": "Read-only Modbus for engineering",
})
rules.append({"action": "deny", "source": "any", "destination": "any",
"protocol": "any", "logging": True, "comment": "Default deny-all"})
return rules
def main():
parser = argparse.ArgumentParser(description="Tofino ICS Firewall Agent")
parser.add_argument("--rules", help="Firewall rules JSON to audit")
parser.add_argument("--traffic-log", help="OT traffic log JSON")
parser.add_argument("--zone-config", help="Zone config JSON for rule generation")
parser.add_argument("--action", choices=["audit", "traffic", "generate", "full"],
default="full")
parser.add_argument("--output", default="tofino_ics_firewall_report.json")
args = parser.parse_args()
report = {"generated_at": datetime.utcnow().isoformat(), "results": {}}
if args.action in ("audit", "full") and args.rules:
findings = audit_firewall_rules(args.rules)
report["results"]["audit"] = findings
critical = sum(1 for f in findings if f.get("severity") == "CRITICAL")
print(f"[+] Audit: {len(findings)} findings, {critical} critical")
if args.action in ("traffic", "full") and args.traffic_log:
result = analyze_ot_traffic_log(args.traffic_log)
report["results"]["traffic"] = result
print(f"[+] Traffic: {result['total_flows']} flows, {result['anomaly_count']} anomalies")
if args.action in ("generate", "full") and args.zone_config:
with open(args.zone_config) as f:
zc = json.load(f)
rules = generate_zone_rules(zc)
report["results"]["generated_rules"] = rules
print(f"[+] Generated {len(rules)} firewall rules")
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()