from __future__ import annotations import argparse import json import sys from pathlib import Path from .anomaly import anomaly_summary, detect_source_anomalies from .dashboard import serve_dashboard from .llm import ollama_summary from .logs import local_in_failures, read_events, summarize_events, top_field_values from .mitigation import FortiGateClient, parse_allowlist, suggest_block_candidates from .policies import audit_policies, read_policies from .recommendations import build_recommendations from .syslog_server import listen_udp_syslog from .monitor import monitor_loop from .threat_intel import enrich_ips, is_public_ip def _print_json(data: object) -> None: print(json.dumps(data, indent=2, sort_keys=True)) def analyze_logs(args: argparse.Namespace) -> int: events = read_events(args.logs) candidates = suggest_block_candidates( events, min_events=args.min_events, min_score=args.min_score, allowlist=parse_allowlist(args.allowlist), ) anomalies = detect_source_anomalies(events, limit=args.anomaly_limit) analysis = { "summary": summarize_events(events), "anomaly_summary": anomaly_summary(anomalies), "diagnostics": { "top_source_ips": top_field_values(events, "srcip", limit=10), "top_services": top_field_values(events, "service", limit=10), "top_actions": top_field_values(events, "action", limit=10), "top_subtypes": top_field_values(events, "subtype", limit=10), "local_in_failures": local_in_failures(events, limit=10), }, "block_candidates": [ {"src_ip": candidate.src_ip, "score": candidate.score, "reasons": candidate.reasons} for candidate in candidates ], "anomalies": [ { "subject": finding.subject, "score": finding.score, "severity": finding.severity, "confidence": finding.confidence, "reasons": finding.reasons, "evidence": finding.evidence, } for finding in anomalies ], } _print_json(analysis) if args.llm: print("\nLLM summary:") print(ollama_summary([], candidates, args.model, analysis=analysis, timeout=args.llm_timeout)) return 0 def audit_policy_file(args: argparse.Namespace) -> int: findings = audit_policies(read_policies(args.config)) _print_json([finding.__dict__ for finding in findings]) if args.llm: print("\nLLM summary:") print(ollama_summary(findings, [], args.model, timeout=args.llm_timeout)) return 0 def suggest_blocks(args: argparse.Namespace) -> int: events = read_events(args.logs) candidates = suggest_block_candidates( events, min_events=args.min_events, min_score=args.min_score, allowlist=parse_allowlist(args.allowlist), ) if not candidates: print("No block candidates met the current thresholds.") return 0 for candidate in candidates: print(f"{candidate.src_ip} score={candidate.score} reasons={', '.join(candidate.reasons)}") if not args.execute: print(" dry-run: not blocked") continue client = FortiGateClient.from_env() reason = f"fgAI UTM mitigation score={candidate.score}" response = client.quarantine_ip(candidate.src_ip, args.expiry_minutes, reason) print(f" blocked: {response}") return 0 def detect_anomalies(args: argparse.Namespace) -> int: events = read_events(args.logs) anomalies = detect_source_anomalies(events, limit=args.limit) output = { "summary": anomaly_summary(anomalies), "anomalies": [ { "subject": finding.subject, "score": finding.score, "severity": finding.severity, "confidence": finding.confidence, "reasons": finding.reasons, "evidence": finding.evidence, } for finding in anomalies if finding.score >= args.min_score ], } _print_json(output) if args.llm: print("\nLLM summary:") print(ollama_summary([], [], args.model, analysis=output, timeout=args.llm_timeout)) return 0 def recommend(args: argparse.Namespace) -> int: events = read_events(args.logs) anomalies = detect_source_anomalies(events, limit=args.limit) intel_ips = sorted( { ip for event in events for ip in (event.src_ip, event.dst_ip) if is_public_ip(ip) } ) reputation = enrich_ips(intel_ips, limit=args.intel_limit) if args.threat_intel else {} recommendations = build_recommendations(events, anomalies, reputation) output = { "recommendations": [ { "subject": item.subject, "score": item.score, "severity": item.severity, "title": item.title, "recommendation": item.recommendation, "reasons": item.reasons, "related_policy_ids": item.related_policy_ids, "related_services": item.related_services, } for item in recommendations if item.score >= args.min_score ], "reputation": reputation, } _print_json(output) if args.llm: print("\nLLM summary:") print(ollama_summary([], [], args.model, analysis=output, timeout=args.llm_timeout)) return 0 def test_connection(args: argparse.Namespace) -> int: client = FortiGateClient.from_env() status = client.system_status() results = status.get("results", status) print("FortiGate API connection OK") _print_json(results) return 0 def fetch_policies(args: argparse.Namespace) -> int: client = FortiGateClient.from_env() policies = client.firewall_policies() if args.output: output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(policies, indent=2, sort_keys=True), encoding="utf-8") print(f"Wrote {output}") else: _print_json(policies) return 0 def listen_syslog(args: argparse.Namespace) -> int: listen_udp_syslog( args.host, args.port, args.output, rotate_bytes=args.rotate_bytes, rotate_count=args.rotate_count, quiet=args.quiet, ) return 0 def run_monitor(args: argparse.Namespace) -> int: monitor_loop( args.logs, args.output, policy_path=args.policies, interval=args.interval, anomaly_limit=args.anomaly_limit, llm=args.llm, llm_interval=args.llm_interval, llm_model=args.model, llm_timeout=args.llm_timeout, ) return 0 def run_dashboard(args: argparse.Namespace) -> int: serve_dashboard(args.host, args.port, args.status_file, image_dir=args.image_dir) return 0 def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Local FortiGate AI/ML inspection tool") subparsers = parser.add_subparsers(required=True) logs = subparsers.add_parser("analyze-logs", help="Analyze local FortiGate logs") logs.add_argument("--logs", required=True, help="Path to syslog JSONL or key/value log file") logs.add_argument("--min-events", type=int, default=3) logs.add_argument("--min-score", type=int, default=7) logs.add_argument("--allowlist", default=None, help="Comma-separated IPs/CIDRs never to block") logs.add_argument("--llm", action="store_true", help="Ask local Ollama to summarize results") logs.add_argument("--model", default=None, help="Ollama model name") logs.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds") logs.add_argument("--anomaly-limit", type=int, default=20, help="Maximum anomaly findings to include") logs.set_defaults(func=analyze_logs) anomalies = subparsers.add_parser("detect-anomalies", help="Score likely traffic anomalies by source IP") anomalies.add_argument("--logs", required=True, help="Path to syslog JSONL or key/value log file") anomalies.add_argument("--limit", type=int, default=20, help="Maximum anomaly findings to include") anomalies.add_argument("--min-score", type=int, default=1, help="Minimum anomaly score to output") anomalies.add_argument("--llm", action="store_true", help="Ask local Ollama to summarize results") anomalies.add_argument("--model", default=None, help="Ollama model name") anomalies.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds") anomalies.set_defaults(func=detect_anomalies) recommendations = subparsers.add_parser("recommend", help="Generate policy and response recommendations from anomalies") recommendations.add_argument("--logs", required=True, help="Path to syslog JSONL or key/value log file") recommendations.add_argument("--limit", type=int, default=20, help="Maximum anomaly findings to evaluate") recommendations.add_argument("--min-score", type=int, default=35, help="Minimum recommendation score to output") recommendations.add_argument("--threat-intel", action="store_true", help="Use enabled external threat intelligence lookups") recommendations.add_argument("--intel-limit", type=int, default=25, help="Maximum public IPs to enrich") recommendations.add_argument("--llm", action="store_true", help="Ask local Ollama to summarize results") recommendations.add_argument("--model", default=None, help="Ollama model name") recommendations.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds") recommendations.set_defaults(func=recommend) policies = subparsers.add_parser("audit-policies", help="Audit FortiOS firewall policy config") policies.add_argument("--config", required=True, help="Path to FortiOS config backup") policies.add_argument("--llm", action="store_true", help="Ask local Ollama to summarize results") policies.add_argument("--model", default=None, help="Ollama model name") policies.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds") policies.set_defaults(func=audit_policy_file) blocks = subparsers.add_parser("suggest-blocks", help="Suggest or execute guarded source IP blocks") blocks.add_argument("--logs", required=True, help="Path to syslog JSONL or key/value log file") blocks.add_argument("--min-events", type=int, default=3) blocks.add_argument("--min-score", type=int, default=7) blocks.add_argument("--allowlist", default=None, help="Comma-separated IPs/CIDRs never to block") blocks.add_argument("--execute", action="store_true", help="Actually call the FortiGate quarantine API") blocks.add_argument("--expiry-minutes", type=int, default=60) blocks.set_defaults(func=suggest_blocks) connection = subparsers.add_parser("test-connection", help="Test FortiGate REST API credentials") connection.set_defaults(func=test_connection) fetch = subparsers.add_parser("fetch-policies", help="Fetch firewall policies through the FortiGate REST API") fetch.add_argument("--output", help="Write JSON response to this file") fetch.set_defaults(func=fetch_policies) listener = subparsers.add_parser("listen-syslog", help="Listen for UDP syslog and append to a local log file") listener.add_argument("--host", default="0.0.0.0", help="Bind address") listener.add_argument("--port", type=int, default=5514, help="UDP port. Use 514 only with sudo/capability.") listener.add_argument("--output", default="logs/fg_syslog.jsonl", help="File to append received logs to") listener.add_argument("--rotate-bytes", type=int, default=25 * 1024 * 1024, help="Rotate active log at this size; 0 disables rotation") listener.add_argument("--rotate-count", type=int, default=14, help="Number of compressed log archives to retain") listener.add_argument("--quiet", action="store_true", help="Do not print each received syslog message") listener.set_defaults(func=listen_syslog) monitor = subparsers.add_parser("monitor", help="Continuously analyze logs and write dashboard status JSON") monitor.add_argument("--logs", required=True, help="Path to syslog JSONL or key/value log file") monitor.add_argument("--output", default="state/fgai-status.json", help="Status JSON written for dashboard") monitor.add_argument("--policies", default=None, help="Optional FortiGate policy JSON/config file to audit continuously") monitor.add_argument("--interval", type=int, default=10, help="Seconds between analysis runs") monitor.add_argument("--anomaly-limit", type=int, default=20, help="Maximum anomaly findings to include") monitor.add_argument("--llm", action="store_true", help="Generate cached Ollama analyst note for dashboard") monitor.add_argument("--llm-interval", type=int, default=300, help="Seconds between Ollama dashboard assessments") monitor.add_argument("--model", default=None, help="Ollama model name") monitor.add_argument("--llm-timeout", type=int, default=None, help="Ollama request timeout in seconds") monitor.set_defaults(func=run_monitor) dashboard = subparsers.add_parser("dashboard", help="Serve local fgAI dashboard") dashboard.add_argument("--host", default="127.0.0.1", help="Dashboard bind address") dashboard.add_argument("--port", type=int, default=8088, help="Dashboard TCP port") dashboard.add_argument("--status-file", default="state/fgai-status.json", help="Status JSON produced by monitor") dashboard.add_argument("--image-dir", default="images", help="Directory containing dashboard images") dashboard.set_defaults(func=run_dashboard) return parser def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) try: return args.func(args) except Exception as exc: print(f"fgai: {exc}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())