feat: proactive host_silence scan for stale agent.heartbeat (0.8.3)

Background scan every 5 min opens rule:host_silence and notifies without
waiting for the next ingest event. CLI job + optional systemd timer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-10 09:07:32 +10:00
parent 2a0eb6e90c
commit aafb80fa31
13 changed files with 411 additions and 10 deletions
+24 -8
View File
@@ -122,18 +122,26 @@ def evaluate_privilege_spike(db: Session, event: Event) -> RuleMatch | None:
)
def evaluate_host_silence(db: Session, event: Event) -> RuleMatch | None:
if event.type == HEARTBEAT_TYPE:
return None
def host_silence_match_for_host(
db: Session,
host_id: int,
*,
hostname: str | None = None,
now: datetime | None = None,
) -> RuleMatch | None:
"""Сформировать RuleMatch host_silence, если последний heartbeat устарел."""
settings = get_settings()
hb = last_heartbeat_at(db, event.host_id)
ref = now or _now()
hb = last_heartbeat_at(db, host_id)
if hb is None:
return None
if agent_status(hb, stale_minutes=settings.sac_heartbeat_stale_minutes, now=_now()) != "stale":
if agent_status(hb, stale_minutes=settings.sac_heartbeat_stale_minutes, now=ref) != "stale":
return None
host = db.get(Host, event.host_id)
hostname = host.hostname if host else f"host#{event.host_id}"
age_min = int((_now() - hb.replace(tzinfo=timezone.utc if hb.tzinfo is None else hb.tzinfo)).total_seconds() // 60)
if hostname is None:
host = db.get(Host, host_id)
hostname = host.hostname if host else f"host#{host_id}"
hb_utc = hb.replace(tzinfo=timezone.utc) if hb.tzinfo is None else hb.astimezone(timezone.utc)
age_min = int((ref - hb_utc).total_seconds() // 60)
return RuleMatch(
rule_id=RULE_HOST_SILENCE,
correlation_type="host.silence",
@@ -146,6 +154,14 @@ def evaluate_host_silence(db: Session, event: Event) -> RuleMatch | None:
)
def evaluate_host_silence(db: Session, event: Event) -> RuleMatch | None:
if event.type == HEARTBEAT_TYPE:
return None
host = db.get(Host, event.host_id)
hostname = host.hostname if host else None
return host_silence_match_for_host(db, event.host_id, hostname=hostname)
def evaluate_immediate_high(event: Event) -> RuleMatch | None:
"""Одиночные high/critical и известные типы (ban, mass bruteforce)."""
problem_types = frozenset({"ssh.ip.banned", "ssh.bruteforce.mass"})