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
This commit is contained in:
Luna 2026-07-24 09:59:09 -07:00
parent 1d1dee7f6e
commit d835386381
No known key found for this signature in database
43 changed files with 3419 additions and 72 deletions

View file

@ -15,7 +15,10 @@ import (
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/baseline"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/detectors"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/fim"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/pkgdb"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/rootcheck"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/schema"
)
@ -37,6 +40,18 @@ type Agent struct {
// baseline manager so startup grace and durable first-seen state match the
// Python oracle without contaminating parity inputs.
Lifecycle *baseline.Manager
// FIM is optional even when the sidecar is stateful. It owns slow hashing
// and package verification and hands completed alerts back to Sweep, which
// keeps the existing cooldown and snapshot pipeline authoritative.
FIM *fim.Manager
// PackageDB watches the mutable package checksum database itself. It is kept
// separate from FIM because a package-manager database edit can otherwise
// make pacman-based file verification appear clean.
PackageDB *pkgdb.Manager
// Rootcheck is likewise a sidecar-only asynchronous integrity engine. It
// uses the captured socket state but collects independent proc/sys/tool views
// outside this sweep's detector and event-output locks.
Rootcheck *rootcheck.Manager
cooldownMu sync.Mutex
cooldowns map[string]time.Time
@ -76,6 +91,21 @@ func (a *Agent) Sweep() ([]map[string]any, error) {
return nil, err
}
}
if a.FIM != nil {
// Scheduling is deliberately before detector evaluation, but FIM itself
// runs asynchronously so a large hash tree never delays this sweep.
a.FIM.MaybeStart(now)
}
if a.PackageDB != nil {
// Like FIM, DB fingerprinting happens away from detector evaluation. A
// completed tamper result still returns through the shared alert pipeline.
a.PackageDB.MaybeStart(now)
}
if a.Rootcheck != nil {
// Capture provides the socket view rootcheck compares with procfs and ps;
// pass this immutable sweep snapshot to avoid a second full capture.
a.Rootcheck.MaybeStart(now, state)
}
host, err := a.Host()
if err != nil {
return nil, err
@ -131,6 +161,17 @@ func (a *Agent) Sweep() ([]map[string]any, error) {
return nil, err
}
}
if a.FIM != nil {
// Background engines return results here instead of emitting directly, so
// they use this Agent's shared cooldown, snapshot, and event ordering.
alerts = append(alerts, a.FIM.Drain()...)
}
if a.PackageDB != nil {
alerts = append(alerts, a.PackageDB.Drain()...)
}
if a.Rootcheck != nil {
alerts = append(alerts, a.Rootcheck.Drain()...)
}
events, err := a.alertEvents(alerts, now, host, timestamp)
if err != nil {
return nil, err
@ -223,6 +264,22 @@ func (a *Agent) Initialize() error {
// otherwise fixed-clock tests could arm against wall time by accident.
a.Lifecycle.Now = a.Now
a.initializeErr = a.Lifecycle.Initialize(baseline.CaptureFunc(a.Capture))
if a.initializeErr != nil {
return a.initializeErr
}
}
if a.FIM != nil {
// FIM must establish its immutable baseline before its first scheduled
// scan. The manager writes only the caller-selected sidecar LogDir.
a.initializeErr = a.FIM.Initialize()
if a.initializeErr != nil {
return a.initializeErr
}
}
if a.PackageDB != nil {
// Establishing the anchor precedes monitoring, just as the Python daemon
// does. Later unexplained changes deliberately do not refresh it.
a.initializeErr = a.PackageDB.Initialize()
}
return a.initializeErr
}