"""Auto-create Problems from ingested events (MVP rules + correlation).""" from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.models import Event, Problem, ProblemEvent PROBLEM_TYPES = frozenset( { "ssh.ip.banned", "ssh.bruteforce.mass", } ) HIGH_SEVERITIES = frozenset({"high", "critical"}) def problem_rule_id(event: Event) -> str | None: if event.severity in HIGH_SEVERITIES: return "high_severity" if event.type in PROBLEM_TYPES: return f"type:{event.type}" return None def problem_fingerprint(host_id: int, event_type: str, rule_id: str) -> str: return f"h{host_id}:t{event_type}:r{rule_id}" def _correlation_cutoff(now: datetime) -> datetime: minutes = get_settings().sac_problem_correlation_window_minutes return now - timedelta(minutes=minutes) def _append_event(db: Session, problem: Problem, event: Event) -> None: linked = db.scalar( select(ProblemEvent).where( ProblemEvent.problem_id == problem.id, ProblemEvent.event_id == event.id, ) ) if linked is not None: return db.add(ProblemEvent(problem_id=problem.id, event_id=event.id)) problem.event_count = (problem.event_count or 0) + 1 problem.last_seen_at = event.occurred_at problem.updated_at = datetime.now(timezone.utc) if event.severity in HIGH_SEVERITIES and problem.severity not in HIGH_SEVERITIES: problem.severity = event.severity db.flush() def maybe_create_problem(db: Session, event: Event) -> tuple[Problem | None, bool]: rule_id = problem_rule_id(event) if rule_id is None: return None, False fingerprint = problem_fingerprint(event.host_id, event.type, rule_id) now = datetime.now(timezone.utc) cutoff = _correlation_cutoff(now) open_problem = db.scalar( select(Problem) .where( Problem.status == "open", Problem.fingerprint == fingerprint, Problem.host_id == event.host_id, Problem.last_seen_at >= cutoff, ) .order_by(Problem.last_seen_at.desc()) .limit(1) ) if open_problem: _append_event(db, open_problem, event) return open_problem, False problem = Problem( host_id=event.host_id, title=event.title, summary=event.summary, severity=event.severity, status="open", rule_id=rule_id, fingerprint=fingerprint, event_count=1, last_seen_at=event.occurred_at, ) db.add(problem) db.flush() db.add(ProblemEvent(problem_id=problem.id, event_id=event.id)) db.flush() return problem, True