Add baseline reconciliation: accept audited drift with a reason

Implements the v0.9 roadmap item from the approved design spec. Operators
accept a specific FIM/package/listener/SUID drift item with a mandatory
reason; the ack suppresses that one alert only while the live state still
matches the recorded fingerprint. Content kinds (fim/pkgfile) re-alert on
further change; identity kinds (listener/suid) retire on TTL or revoke.

- reconcile.py: ReconcileStore (mtime-cached, fails closed on missing/corrupt
  store), fingerprint builders, and the filter_alerts chokepoint.
- CLI: baseline accept/revoke/list with --reason/--expires/--force/--stale/--json.
- Wired at the daemon sweep + eBPF chokepoint, fim-check, and /api/integrity.
- RECONCILE_V1 schema, config knob, COMMAND_REFERENCE/SCHEMAS/OPERATIONS/ROADMAP
  docs, and reconcile unit/CLI/integration tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-27 10:33:55 -07:00
parent 3d2047fde2
commit dbbe35b3fc
18 changed files with 1155 additions and 28 deletions

View file

@ -372,12 +372,14 @@ def integrity_report(cfg: Config, status: dict | None = None,
),
}
keyring_present = pkgdb.keyring_present()
reconciliation = _reconciliation_summary(cfg)
checks = {
"watchdog": "ok" if watchdog_ok else "review",
"fim_baseline": fim_state["status"],
"pkgdb_anchor": pkg_state["status"],
"pacman_siglevel": "review" if sig_alert else "ok",
"pacman_keyring": "ok" if keyring_present else "missing",
"reconciliation": "review" if reconciliation["stale"] else "ok",
}
overall = "ok"
if any(v in ("review", "missing", "unreadable") for v in checks.values()):
@ -413,10 +415,30 @@ def integrity_report(cfg: Config, status: dict | None = None,
"missing": len(watched) - present,
"paths": watched,
},
"reconciliation": reconciliation,
"read_only": True,
}
def _reconciliation_summary(cfg: Config) -> dict:
"""Acknowledged-drift counts for the dashboard. Read-only: this evaluates
TTL expiry and persisted status, but does not run live FIM/package scans
from the web request path (consistent with the rest of integrity_report)."""
from . import reconcile
records = reconcile.ReconcileStore.load(cfg).list()
stale = [r for r in records if r.status == "stale"]
return {
"total": len(records),
"ok": len(records) - len(stale),
"stale": len(stale),
"stale_items": [
{"kind": r.kind, "target": r.target, "reason": r.reason,
"accepted_at": r.accepted_at, "expires_at": r.expires_at}
for r in stale
],
}
# --- HTTP layer ------------------------------------------------------------
class _Handler(BaseHTTPRequestHandler):