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
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, anda25c73a. - On top of that, uncommitted work now adds
go-agent/internal/{fim,pkgdb, rootcheck}/, wires them intoagent.Sweep/Initializebehind--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 --shortcurrently 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, andnew_suid. --state-direnables 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-sentineldirectory.
Event rules and native sources
- Exec rules: built-in SIDs
100001-100004plus configured rules. - Syscall rules: SIDs
100060-100066. - Typed host rules: SIDs
100067-100073and100076-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/ebpfand fail open to polling on load/attach/read failures. - Native typed transports include UID/GID changes, chmod/chown variants,
capset,finit_module, confirmed interpreterwrite/writev/pwrite64/pwritev/pwritev2, and successful TCP connect/bind/listen/accept calls with/procendpoint resolution. - Generated little- and big-endian BPF objects live under
go-agent/internal/ebpfsource/. Runmake generate-goonly after changinggo-agent/internal/ebpfsource/bpf/*.bpf.c.
Production validation path
make build-gocreates a stripped, static, VCS-independent binary.make install-goinstalls the separate binary andenodia-sentinel-go-sidecar.servicewithout replacing or enabling the Python service.- The hardened systemd unit has a current
systemd-analyze securityexposure 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.jsonlplusevents.jsonl.1(64 MiB per active segment boundary). Non-status events are synchronized to disk. /var/lib/enodia-sentinel-go/heartbeatis atomically refreshed after each successful status emission.enodia-sentinel-go --healthreturnsenodia.go.health.v1JSON and a watchdog-friendly exit status.enodia-sentinel-go --events-tail Nreads 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 requiredenodia.alert.snapshot.v1fields. Same-second alerts merge into one pair, process context is collected best-effort from/proc, and sharedmax_snapshots/max_snapshot_age_dayslimits are applied at startup, capture, and status time. enodia.status.v1totals, severity counts, and last-alert time are populated only from parseable retained Go snapshots. Python's unchangedlist_alertsandget_alertdata layer successfully consumed a Go-produced pair.- Snapshot incident IDs now reference an atomic private
incidents.jsonusing the requiredenodia.incident.v1fields. 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
100080is persisted. Python's unchangedlist_incidentsandget_incidentconsumers 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.v1records. 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 -Qkkand 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-listand--incident-show <id>now read the sidecar's isolated state without starting the agent. The latter returnsenodia.incident.view.v1with 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/fimis a sidecar-only engine, gated on--state-dir, that persists a SHA-256 security-path baseline asfim-baseline.jsonand performs non-overlapping background comparisons onfim_scan_interval, emitting the Python-identical SIDs100017-100019.fim_pkg_verify = trueopts into a boundedpacman -Qkkverification pass emitting SID100020.go-agent/internal/pkgdbfingerprints pacman's local checksum database onpkgdb_intervaland alerts (SID100021) only on changes lacking a corresponding transaction-log record; it never refreshes an unexplained changed anchor.go-agent/internal/rootcheckbounds PID cross-view probing withrootcheck_pid_cap, runs one background comparison perrootcheck_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.SweepcallsMaybeStartbefore detector evaluation andDrainafter, so a slow hash tree orpacmaninvocation never delays a sweep) and return alerts through the existing cooldown/snapshot/incident pipeline rather than emitting directly. enodia_sentinel/web.pyadds 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 newenodia.go.sidecar.v1schema lives inenodia_sentinel/schemas.pyand is pinned bytests/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 vetandgo 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 verifyand the three Go sidecar packaging/isolation tests: passed.- Static builds:
linux/amd64,linux/arm64,linux/riscv64, andlinux/s390xpassed. - 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
-racesuites: all packages passed, including the newfim,pkgdb, androotcheckpackages. go vetandgo 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 verifyand 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
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 intoagent.Sweep.Add a read-only management API consumer for isolated Go snapshot/event state while keeping Python authoritative— done:/api/go-sidecar/*inenodia_sentinel/web.pyplus the dashboard Sidecar tab, schema-checked and never controlling the Go process.- 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.
- 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.
- Consider whether
04-integrity.mdand05-rootcheck.mdindocs/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.