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