Add false-positive triage via package-ownership provenance

A noisy IDS gets ignored. Adds explicit FP-handling built on provenance — a
binary owned by the package manager is almost certainly legitimate (the OSSEC
rootcheck / AIDE principle).

- provenance.py: package_owner()/is_package_owned() via pacman/dpkg/rpm, cached;
  framed as confidence-raising, never proof-of-safety
- triage.py: triage_alert() labels each detection likely-FP vs review with a
  reason (package-owned binary, loopback-only listener, allowlisted comm, …);
  reverse_shell/egress/exec rules are ALWAYS review (provenance can't clear a
  network shell); unattributable listeners are reviewed, not cleared
- cli: `enodia-sentinel triage` summarizes captured alerts and suggests
  allowlist entries
- new_listener: listener_allow_comms + optional suppress_package_owned_listeners
  gate (the best single knob for a desktop/seedbox running P2P apps)
- tests: +10 (provenance injected); README + config documented. 65/65 pass

Verified on a live seedbox: 12 detections, 11 auto-cleared as FP (qbittorrent /
nicotine / kdeconnectd / the dashboard itself), 1 correctly held for review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-31 22:05:00 -07:00
parent c00fff224c
commit 07f5261d59
8 changed files with 332 additions and 0 deletions

View file

@ -11,6 +11,7 @@ from collections.abc import Iterator
from ..alert import Alert, Severity
from ..config import Config
from ..provenance import is_package_owned
from ..system import SystemState
@ -25,6 +26,14 @@ def detect(state: SystemState, cfg: Config) -> Iterator[Alert]:
continue
if port in cfg.listener_allow_ports:
continue
if comm in cfg.listener_allow_comms:
continue
# Optional provenance gate: a listener whose binary ships with an
# installed package is very likely legitimate (P2P clients, servers).
if cfg.suppress_package_owned_listeners and s.pid:
proc = state.process(s.pid)
if proc and proc.exe and is_package_owned(proc.exe):
continue
yield Alert(
severity=Severity.HIGH,
signature="new_listener",