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

@ -59,6 +59,39 @@ helpers.
Do not run this to silence unexplained findings.
### `baseline accept` / `revoke` / `list`
```bash
enodia-sentinel baseline accept <kind> <target> --reason "..." [--expires 7d] [--force]
enodia-sentinel baseline revoke <kind> <target>
enodia-sentinel baseline list [--stale] [--json]
```
Baseline *reconciliation*: accept a specific, identified drift item with a
mandatory reason instead of silently folding it into a baseline. `<kind>` is one
of `fim`, `pkgfile`, `listener`, or `suid`; `<target>` is a path (FIM/package/
SUID) or a `port/comm` key (listener).
An acknowledgement suppresses that one alert **only while the live state still
matches the accepted fingerprint**. `fim` and `pkgfile` acks are content-bound:
when the file changes again the ack goes *stale* and the alert returns. `listener`
and `suid` acks are identity-bound, so only `--expires` (TTL) or an explicit
`revoke` retires them. Each record stores who accepted it, when, why, and the
fingerprint, in `log_dir/reconciliation.json` (override with `reconcile_store`).
- `accept` refuses to record drift it cannot observe live (e.g. a listener not
currently bound); pass `--force` to accept anyway — required for accepting a
*removed* file. Omitting `--reason` is a hard error.
- `--expires` accepts `Nd` / `Nh` / `Nm` (days/hours/minutes).
- `revoke` exits `1` if the target isn't present.
- `list` shows every ack; `--stale` filters to stale/expired ones. It exits `1`
whenever any acknowledgement is stale (a scriptable drift check) and re-derives
FIM staleness against live files.
Reconciled drift is dropped at the daemon detection chokepoint, hidden from
`fim-check`, and summarised in the dashboard integrity view, so an acknowledged
item stops alerting everywhere until its fingerprint changes or its TTL lapses.
## Detection and Integrity Commands
### `list-detectors`

View file

@ -145,6 +145,13 @@ Baselines are useful only if they represent a known-good state.
- Use `fim-update` only after confirming changes are legitimate. Package hooks
already run it for normal package transactions.
- Keep a copy of important anchors off-box when possible.
- Prefer `baseline accept <kind> <target> --reason "..."` over rebuilding a whole
baseline when only one item changed legitimately (an edited config, a new
service port, an installed SUID helper). Unlike a rebuild, the acceptance is
per-item, carries a recorded reason, is fingerprint-bound (FIM/package items
re-alert if the file changes *again*), and can be time-boxed with `--expires`.
Review outstanding acceptances with `baseline list`; it exits non-zero and
marks rows `stale` when an accepted item has drifted again or its TTL lapsed.
## Suggested Hardening

View file

@ -71,8 +71,11 @@ Purpose: move from "tell me" to "help me act" without unsafe automation.
semantics are designed for pacman, dpkg, and rpm.
- Require explicit `--apply` for changes; default to read-only plans.
- Extend response audit logs to any future state-changing `--apply` workflow.
- Add baseline reconciliation:
accept legitimate FIM/package/listener/SUID changes with a recorded reason.
- ✅ Add baseline reconciliation: accept legitimate FIM/package/listener/SUID
drift with a mandatory reason via `baseline accept/revoke/list`. Acks are
fingerprint-bound (content kinds re-alert on further change), support an
optional TTL, and are filtered at the daemon chokepoint, `fim-check`, and the
dashboard integrity API.
- Add richer false-positive suppression suggestions that can emit TOML snippets
but never modify config automatically.
- Add recovery checks:

View file

@ -147,3 +147,33 @@ Required fields:
| `apply_supported` | boolean | Whether the plan supports apply. |
| `action_count` | integer | Number of proposed actions. |
| `plan_path` | string | Persisted plan artifact path. |
## `enodia.reconcile.v1`
Acknowledged-drift store written by `baseline accept/revoke/list`
(`log_dir/reconciliation.json`, overridable via `reconcile_store`) and emitted
by `baseline list --json`.
Top-level fields:
| Field | Type | Meaning |
|---|---|---|
| `schema` | string | `enodia.reconcile.v1`. |
| `records` | array | Acknowledgement records (below). |
Each record:
| Field | Type | Meaning |
|---|---|---|
| `kind` | string | `fim`, `pkgfile`, `listener`, or `suid`. |
| `target` | string | Path, or `port/comm` key for listeners. |
| `fingerprint` | object | Kind-specific identity compared each sweep. |
| `reason` | string | Mandatory operator justification. |
| `actor` | string | Who recorded the acceptance. |
| `accepted_at` | string | ISO-8601 UTC acceptance time. |
| `status` | string | `ok` or `stale`. |
| `expires_at` | string | ISO-8601 UTC TTL; absent when no TTL. |
The dashboard `enodia.integrity.v1` payload also carries a `reconciliation`
summary object (`total`, `ok`, `stale`, `stale_items`) and a `reconciliation`
entry under `checks`.