enodia-sentinal/docs/GO_PORT_HANDOFF.md
Luna d835386381
feat(go): port FIM/pkgdb/rootcheck integrity engines and add go-sidecar dashboard consumer
Adds sidecar-only, --state-dir-gated FIM, package-DB-anchor, and rootcheck
engines to the Go migration sidecar, wired into agent.Sweep/Initialize
behind the existing baseline lifecycle. Adds a read-only, schema-checked
Python dashboard consumer (go_sidecar_state_dir, /api/go-sidecar/*, Sidecar
tab) that never starts, stops, or mutates the Go sidecar's state.

Also fixes drift found while reconciling this work: enodia_sentinel/web.py
had reinvented local schema constants instead of using the canonical
enodia_sentinel/schemas.py catalog (now registers enodia.go.sidecar.v1
there and reuses ALERT_SNAPSHOT_V1/INCIDENT_V1); docs/RULES.md had drifted
from what `rules docs` actually generates for SIDs 100069-100078 (ruleops.py
was missing metadata for four SIDs and had stale drill text for four more),
now back in sync with a regression test pinning them together.

Updates CLAUDE.md, README.md, go-agent/README.md, and docs/{ROADMAP,
GO_PORT_HANDOFF,SURICATA_ASSIMILATION,THREAT_MODEL,OPERATIONS,SCHEMAS,
COMMAND_REFERENCE}.md to reflect the landed work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQivSKBqQJsayz1xcYqWzZ
2026-07-24 09:59:09 -07:00

12 KiB

Go Port Handoff

Saved: 2026-07-22T04:08:40-07:00 Updated: 2026-07-24T04:45:55-07:00 Branch: main Base commit: a25c73a (feat(go): report integrity anchor context); the FIM/ package-DB/rootcheck engines and the Python go-sidecar dashboard consumer described below landed on top of that commit and remain uncommitted. Status: items 1 and 2 of "Resume here" are now implemented and green; see the updated sections below. Desktop GUI work referenced by the original checkpoint has since been committed (457c760, 1d1dee7, and related commits) and is no longer part of this checkout's uncommitted state.

Worktree warning

The checkout is intentionally dirty and contains work from multiple related continuations. Do not reset, clean, or broadly restage it.

  • The validation-sidecar, incident-persistence, bounded-enrichment, local- assurance, asynchronous-enrichment, incident-reader, package-ownership, and integrity-anchor tranches are signed commits f85c2e8, 538d1d9, fd2bbba, 1ab4add, eff31b3, 6355fb4, 88d7a6e, and a25c73a.
  • On top of that, uncommitted work now adds go-agent/internal/{fim,pkgdb, rootcheck}/, wires them into agent.Sweep/Initialize behind --state-dir, and adds Python's read-only /api/go-sidecar/* consumer (enodia_sentinel/web.py, schemas.GO_SIDECAR_V1) plus a Sidecar dashboard tab. git status --short currently shows this as modified files across both trees plus the three new untracked Go package directories.
  • build/ is ignored and contains only local build output.

Implemented Go surface

The Go module requires Go 1.25 or newer and is currently verified with go1.26.5-X:nodwarf5. It uses github.com/cilium/ebpf v0.22.0 and builds as a static binary.

Polling and lifecycle

  • All current Python poll detectors have Go parity: reverse_shell, ld_preload, deleted_exe, input_snooper, credential_access, stealth_network, memory_obfuscation, egress, first_seen, new_listener, persistence, and new_suid.
  • --state-dir enables isolated listener/SUID baselines, startup grace, persistence scans, and atomic first-seen retention.
  • The Go state directory must never be the Python daemon's /var/log/enodia-sentinel directory.

Event rules and native sources

  • Exec rules: built-in SIDs 100001-100004 plus configured rules.
  • Syscall rules: SIDs 100060-100066.
  • Typed host rules: SIDs 100067-100073 and 100076-100078.
  • Mixed JSONL routing, rule catalog operations, correlation SID 100080, and one shared cooldown gate are implemented.
  • Embedded CO-RE exec and filtered raw-syscall probes use cilium/ebpf and fail open to polling on load/attach/read failures.
  • Native typed transports include UID/GID changes, chmod/chown variants, capset, finit_module, confirmed interpreter write/writev/pwrite64/ pwritev/pwritev2, and successful TCP connect/bind/listen/accept calls with /proc endpoint resolution.
  • Generated little- and big-endian BPF objects live under go-agent/internal/ebpfsource/. Run make generate-go only after changing go-agent/internal/ebpfsource/bpf/*.bpf.c.

Production validation path

  • make build-go creates a stripped, static, VCS-independent binary.
  • make install-go installs the separate binary and enodia-sentinel-go-sidecar.service without replacing or enabling the Python service.
  • The hardened systemd unit has a current systemd-analyze security exposure score of 4.3 (OK). It preserves the host /proc, /tmp, and network view because detectors require them, while hiding the Python state tree.
  • The unit uses Type=notify; READY is sent only after baseline setup, event log preflight, and both requested probe-load attempts.
  • JSONL remains visible in journald and is also retained as private bounded /var/lib/enodia-sentinel-go/events.jsonl plus events.jsonl.1 (64 MiB per active segment boundary). Non-status events are synchronized to disk.
  • /var/lib/enodia-sentinel-go/heartbeat is atomically refreshed after each successful status emission.
  • enodia-sentinel-go --health returns enodia.go.health.v1 JSON and a watchdog-friendly exit status.
  • enodia-sentinel-go --events-tail N reads active and rotated retained events chronologically, validates JSON, fails closed on corruption, and deliberately works without loading the agent configuration.
  • The unit now retains private alert-YYYYMMDD-HHMMSS.{json,log} pairs using the required enodia.alert.snapshot.v1 fields. Same-second alerts merge into one pair, process context is collected best-effort from /proc, and shared max_snapshots / max_snapshot_age_days limits are applied at startup, capture, and status time.
  • enodia.status.v1 totals, severity counts, and last-alert time are populated only from parseable retained Go snapshots. Python's unchanged list_alerts and get_alert data layer successfully consumed a Go-produced pair.
  • Snapshot incident IDs now reference an atomic private incidents.json using the required enodia.incident.v1 fields. Grouping mirrors Python's process- lineage-first, time-window fallback behavior, the index is bounded to 1000 recent incidents, and same-second alert batches update one incident without duplicating the snapshot name.
  • Incident severity/signatures/SIDs/PIDs/lineage/timestamps are updated additively and correlation SID 100080 is persisted. Python's unchanged list_incidents and get_incident consumers successfully loaded a live Go-produced index, snapshot, and timeline.
  • A bounded no-I/O enrichment stage now derives Python-shaped process/parent context, lineage, remote-IP classification, and candidate paths entirely from already-captured records. Alert/process/indicator caps bound attacker- controlled work on the emission path.
  • Snapshot log/JSON revisions now append synchronized private enodia.hash_chain.v1 records. Artifact SHA-256 is streamed, previous-hash lookup reads a bounded chain tail, and append completion is synced before the alert event is emitted.
  • A single 16-job asynchronous worker now streams SHA-256 only for regular executables at most 8 MiB and adds lstat file metadata. Slow I/O occurs outside the snapshot lock; a full queue simply leaves optional fields unknown.
  • The asynchronous worker now resolves pacman/dpkg/rpm package-owner strings with a two-second bound and cache; ownership lookup never holds a detector lock or blocks alert capture.
  • The worker reports FIM-baseline, package-DB-anchor, and Go hash-chain presence/age, plus the configured pacman -Qkk and rootcheck cadences now that those engines are ported (see below). This remains metadata only: recording a snapshot never starts integrity work itself.
  • Notification fan-out is not yet ported.
  • --incidents-list and --incident-show <id> now read the sidecar's isolated state without starting the agent. The latter returns enodia.incident.view.v1 with the incident, available snapshots, and time-ordered timeline.
  • No live system service was installed or enabled during development.

FIM, package-DB anchor, and rootcheck engines (new since the base commit)

  • go-agent/internal/fim is a sidecar-only engine, gated on --state-dir, that persists a SHA-256 security-path baseline as fim-baseline.json and performs non-overlapping background comparisons on fim_scan_interval, emitting the Python-identical SIDs 100017-100019. fim_pkg_verify = true opts into a bounded pacman -Qkk verification pass emitting SID 100020.
  • go-agent/internal/pkgdb fingerprints pacman's local checksum database on pkgdb_interval and alerts (SID 100021) only on changes lacking a corresponding transaction-log record; it never refreshes an unexplained changed anchor.
  • go-agent/internal/rootcheck bounds PID cross-view probing with rootcheck_pid_cap, runs one background comparison per rootcheck_interval, and reproduces the Python rootcheck SIDs (hidden process/module/port families, promiscuous mode, module/kernel taint) from procfs, sysfs, ps, and socket views.
  • All three run asynchronously off the sweep loop (agent.Sweep calls MaybeStart before detector evaluation and Drain after, so a slow hash tree or pacman invocation never delays a sweep) and return alerts through the existing cooldown/snapshot/incident pipeline rather than emitting directly.
  • enodia_sentinel/web.py adds a read-only, schema-checked consumer for this evidence: go_sidecar_state_dir (config key) exposes /api/go-sidecar/ {status,alerts,incidents,events} and a dashboard Sidecar tab. It never starts, stops, or writes into the Go sidecar's state directory, and a corrupted retained event log fails the request closed rather than showing a partial stream. The new enodia.go.sidecar.v1 schema lives in enodia_sentinel/schemas.py and is pinned by tests/test_schema_contracts.py::test_go_sidecar_v1_contract.

Last green verification

Run from the repository root:

python3 -m unittest discover -s tests -q
cd go-agent
GOCACHE=/tmp/enodia-go-cache go test ./...
GOCACHE=/tmp/enodia-go-cache go test -race ./...
GOCACHE=/tmp/enodia-go-cache go vet ./...
go mod verify
cd ..
make parity-go
make check-go-service
git diff --check

Results at original save time (a25c73a):

  • Python: 377 tests passed.
  • Go unit and race suites: all packages passed through a25c73a.
  • go vet and go mod verify: passed.
  • Parity: 22 poll alerts, 5 exec alerts, 7 syscall alerts, 10 typed-host alerts, 22 catalog records, 3 mixed-stream alerts, and 2 correlations through 88d7a6e; the later integrity-anchor change is enrichment-only.
  • systemd-analyze verify and the three Go sidecar packaging/isolation tests: passed.
  • Static builds: linux/amd64, linux/arm64, linux/riscv64, and linux/s390x passed.
  • Integration smoke tests passed for static binary identity, JSON emission, READY/STOPPING notification, heartbeat freshness/staleness, retained event equivalence, tail selection, corruption rejection, and evidence reading with malformed TOML. Snapshot smoke tests also passed for same-second batching, severity aggregation, private JSON/text pairs, truthful status counts, startup/quiet-period retention, process context, and Python dashboard-reader compatibility.

Results re-verified 2026-07-24 with the FIM/pkgdb/rootcheck engines and go-sidecar dashboard consumer added on top:

  • Python: 385 tests passed (unittest discover).
  • Go unit and -race suites: all packages passed, including the new fim, pkgdb, and rootcheck packages.
  • go vet and go mod verify: passed.
  • Parity counts are unchanged from the original checkpoint (22/5/7/10/22/3/2), as expected: the new engines are integrity checks outside the poll/exec/ syscall/host-rule parity fixture.
  • systemd-analyze verify and the three Go sidecar packaging/isolation tests: passed.
  • git diff --check: passed (no whitespace errors).
  • Static cross-arch builds were not re-run this pass.

The Python suite prints three expected CLI error lines from negative tests and one existing ResourceWarning for an implicitly cleaned-up HTTP 401 object; the suite still exits successfully.

Resume here

  1. Port the actual FIM/package-verification/rootcheck engines behind explicit, bounded acceptance gates — done: go-agent/internal/{fim,pkgdb, rootcheck}, gated on --state-dir, tested, and wired into agent.Sweep.
  2. Add a read-only management API consumer for isolated Go snapshot/event state while keeping Python authoritative — done: /api/go-sidecar/* in enodia_sentinel/web.py plus the dashboard Sidecar tab, schema-checked and never controlling the Go process.
  3. Continue broader Phase 3 rule metadata/state parity before considering any default-service or package cutover. This is the remaining item from the original checkpoint.
  4. Notification fan-out for the Go sidecar (item noted above) is still unported; the FIM/pkgdb/rootcheck engines currently return alerts only through the shared JSONL/snapshot/incident pipeline, not push backends.
  5. Consider whether 04-integrity.md and 05-rootcheck.md in docs/behavior/ (currently "planned") should be written now that these engines exist on both sides, so future Go-side changes have the same reviewable behavioral contract as the poll detectors and event rules do.

Keep the validation sidecar opt-in, preserve its separate state tree, and keep new attack/event primitives disconnected from destructive response paths.