# SPDX-License-Identifier: GPL-3.0-or-later """Read-only incident correlation rules. Correlation does not hide the raw alerts or mutate the host. It adds a small ``correlations`` list to an incident when the incident's existing evidence matches a higher-confidence story. """ from __future__ import annotations from dataclasses import dataclass from .alert import Severity SID_MULTI_STAGE_INTRUSION = 100080 @dataclass(frozen=True) class CorrelationRule: sid: int signature: str classtype: str severity: Severity summary: str required_any: tuple[set[str], ...] max_window: int DEFAULT_RULES: tuple[CorrelationRule, ...] = ( CorrelationRule( sid=SID_MULTI_STAGE_INTRUSION, signature="correlation.multi_stage_intrusion", classtype="multi-stage-intrusion", severity=Severity.CRITICAL, summary=( "Web/database service spawned a shell and the same incident showed " "suspicious egress or an unusual listener within the correlation window" ), required_any=( {"exec_rule.web-rce"}, {"host_rule.suspicious-egress", "host_rule.suspicious-listener"}, ), max_window=600, ), ) def correlate(incident: dict, rules: tuple[CorrelationRule, ...] = DEFAULT_RULES) -> list[dict]: """Return correlation records matched by an incident index entry.""" signatures = set(incident.get("signatures") or []) duration = float(incident.get("last_ts", 0.0)) - float(incident.get("first_ts", 0.0)) out: list[dict] = [] for rule in rules: if duration > rule.max_window: continue if not all(signatures & choices for choices in rule.required_any): continue out.append({ "sid": rule.sid, "signature": rule.signature, "classtype": rule.classtype, "severity": str(rule.severity), "summary": rule.summary, "window_seconds": rule.max_window, "matched_signatures": sorted(signatures), }) return out