add timoute och export av fg config

This commit is contained in:
larssand
2026-06-18 22:17:03 +02:00
parent e1ca3c0cea
commit b4ad932c91
5 changed files with 130 additions and 25 deletions

View File

@@ -24,25 +24,24 @@ def analyze_logs(args: argparse.Namespace) -> int:
min_score=args.min_score,
allowlist=parse_allowlist(args.allowlist),
)
_print_json(
{
"summary": summarize_events(events),
"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
],
}
)
analysis = {
"summary": summarize_events(events),
"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
],
}
_print_json(analysis)
if args.llm:
print("\nLLM summary:")
print(ollama_summary([], candidates, args.model))
print(ollama_summary([], candidates, args.model, analysis=analysis, timeout=args.llm_timeout))
return 0
@@ -51,7 +50,7 @@ def audit_policy_file(args: argparse.Namespace) -> int:
_print_json([finding.__dict__ for finding in findings])
if args.llm:
print("\nLLM summary:")
print(ollama_summary(findings, [], args.model))
print(ollama_summary(findings, [], args.model, timeout=args.llm_timeout))
return 0
@@ -117,12 +116,14 @@ def build_parser() -> argparse.ArgumentParser:
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.set_defaults(func=analyze_logs)
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")

View File

@@ -7,28 +7,42 @@ from urllib import request
from .models import BlockCandidate, Finding
def ollama_summary(findings: list[Finding], candidates: list[BlockCandidate], model: str | None = None) -> str:
def ollama_summary(
findings: list[Finding],
candidates: list[BlockCandidate],
model: str | None = None,
*,
analysis: dict[str, object] | None = None,
timeout: int | None = None,
) -> str:
host = os.getenv("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
selected_model = model or os.getenv("OLLAMA_MODEL", "llama3.3")
selected_timeout = timeout or int(os.getenv("OLLAMA_TIMEOUT", "180"))
prompt = {
"analysis": analysis or {},
"findings": [finding.__dict__ for finding in findings],
"block_candidates": [
{"src_ip": candidate.src_ip, "score": candidate.score, "reasons": candidate.reasons}
for candidate in candidates
for candidate in candidates[:20]
],
}
body = json.dumps(
{
"model": selected_model,
"stream": False,
"options": {
"num_predict": 350,
"temperature": 0.2,
},
"prompt": (
"You are a local FortiGate security analyst. Summarize these policy findings "
"and UTM block candidates. Be concise, include risk, likely cause, and next action. "
"and log diagnostics. Be concise. Include risk, likely cause, and next action. "
"Do not recommend blocking private/internal client IPs unless the data explicitly proves compromise. "
f"Data: {json.dumps(prompt)}"
),
}
).encode("utf-8")
req = request.Request(f"{host}/api/generate", data=body, method="POST", headers={"Content-Type": "application/json"})
with request.urlopen(req, timeout=60) as response:
with request.urlopen(req, timeout=selected_timeout) as response:
data = json.loads(response.read().decode("utf-8"))
return str(data.get("response", "")).strip()

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import json
import shlex
from pathlib import Path
@@ -56,8 +57,60 @@ def parse_policy_config(text: str) -> list[PolicyRule]:
return policies
def _normalize_policy_value(value: object) -> list[str]:
if value is None:
return []
if isinstance(value, list):
normalized: list[str] = []
for item in value:
if isinstance(item, dict):
if "name" in item:
normalized.append(str(item["name"]))
elif "q_origin_key" in item:
normalized.append(str(item["q_origin_key"]))
else:
normalized.append(json.dumps(item, sort_keys=True))
else:
normalized.append(str(item))
return normalized
if isinstance(value, dict):
if "name" in value:
return [str(value["name"])]
if "q_origin_key" in value:
return [str(value["q_origin_key"])]
return [json.dumps(value, sort_keys=True)]
return [str(value)]
def parse_policy_json(text: str) -> list[PolicyRule]:
data = json.loads(text)
raw_policies = data.get("results", data) if isinstance(data, dict) else data
if not isinstance(raw_policies, list):
raise ValueError("policy JSON must be a list or contain a results list")
policies: list[PolicyRule] = []
for index, raw_policy in enumerate(raw_policies, start=1):
if not isinstance(raw_policy, dict):
continue
policy_id = str(raw_policy.get("policyid", raw_policy.get("policy_id", raw_policy.get("id", index))))
settings = {str(key): _normalize_policy_value(value) for key, value in raw_policy.items()}
policies.append(PolicyRule(policy_id, settings))
return policies
def read_policies(path: str | Path) -> list[PolicyRule]:
return parse_policy_config(Path(path).read_text(encoding="utf-8", errors="replace"))
policy_path = Path(path)
if not policy_path.exists():
raise FileNotFoundError(
f"{policy_path} does not exist. Fetch policies first with: "
f"fgai fetch-policies --output exports/policies.json"
)
text = policy_path.read_text(encoding="utf-8", errors="replace")
stripped = text.lstrip()
if stripped.startswith("{") or stripped.startswith("["):
return parse_policy_json(text)
return parse_policy_config(text)
def audit_policies(policies: list[PolicyRule]) -> list[Finding]: