This commit is contained in:
larssand
2026-06-18 22:38:47 +02:00
parent b4ad932c91
commit 5650973e50
9 changed files with 700 additions and 28 deletions

92
src/fgai/monitor.py Normal file
View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import json
import time
from pathlib import Path
from .anomaly import anomaly_summary, detect_source_anomalies
from .logs import local_in_failures, read_events, summarize_events, top_field_values
from .mitigation import parse_allowlist, suggest_block_candidates
from .policies import audit_policies, read_policies
def build_status(
log_path: str,
*,
policy_path: str | None = None,
min_block_events: int = 3,
min_block_score: int = 7,
anomaly_limit: int = 20,
) -> dict[str, object]:
events = read_events(log_path) if Path(log_path).exists() else []
anomalies = detect_source_anomalies(events, limit=anomaly_limit)
block_candidates = suggest_block_candidates(
events,
min_events=min_block_events,
min_score=min_block_score,
allowlist=parse_allowlist(),
)
policy_findings: list[dict[str, str | None]] = []
policy_error: str | None = None
if policy_path and Path(policy_path).exists():
try:
policy_findings = [finding.__dict__ for finding in audit_policies(read_policies(policy_path))]
except Exception as exc:
policy_error = str(exc)
return {
"generated_at": int(time.time()),
"log_path": log_path,
"policy_path": policy_path,
"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),
},
"anomalies": [
{
"subject": finding.subject,
"score": finding.score,
"severity": finding.severity,
"confidence": finding.confidence,
"reasons": finding.reasons,
"evidence": finding.evidence,
}
for finding in anomalies
],
"block_candidates": [
{"src_ip": candidate.src_ip, "score": candidate.score, "reasons": candidate.reasons}
for candidate in block_candidates
],
"policy_findings": policy_findings,
"policy_error": policy_error,
}
def write_status(status: dict[str, object], output: str) -> None:
output_path = Path(output)
output_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = output_path.with_suffix(f"{output_path.suffix}.tmp")
tmp_path.write_text(json.dumps(status, indent=2, sort_keys=True), encoding="utf-8")
tmp_path.replace(output_path)
def monitor_loop(
log_path: str,
output: str,
*,
policy_path: str | None = None,
interval: int = 10,
anomaly_limit: int = 20,
) -> None:
print(f"Monitoring {log_path}")
print(f"Writing status to {output}")
while True:
status = build_status(log_path, policy_path=policy_path, anomaly_limit=anomaly_limit)
write_status(status, output)
time.sleep(interval)