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

@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package rootcheck
import (
"context"
"sync/atomic"
"testing"
"time"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
)
func TestManagerRunsAtMostOneBoundedCheckAndDrainsResults(t *testing.T) {
cfg := config.Default()
cfg.RootcheckInterval = time.Hour
manager := New(cfg)
started := make(chan struct{})
release := make(chan struct{})
var calls atomic.Int32
var sawDeadline atomic.Bool
manager.Run = func(ctx context.Context, state model.State) []model.Alert {
_, ok := ctx.Deadline()
sawDeadline.Store(ok)
calls.Add(1)
close(started)
<-release
return []model.Alert{{SID: SIDHiddenModule, Key: "rk:test", PIDs: []int{}}}
}
now := time.Date(2026, 7, 22, 12, 0, 0, 0, time.UTC)
manager.MaybeStart(now, model.State{})
<-started
manager.MaybeStart(now.Add(2*time.Hour), model.State{})
if got := calls.Load(); got != 1 {
t.Fatalf("concurrent checks = %d, want 1", got)
}
close(release)
for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); {
if alerts := manager.Drain(); len(alerts) == 1 {
if !sawDeadline.Load() {
t.Fatal("rootcheck did not receive a bounded context")
}
if alerts[0].SID != SIDHiddenModule {
t.Fatalf("alerts = %+v", alerts)
}
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("completed rootcheck result was not drained")
}
func TestDisabledManagerNeverSchedules(t *testing.T) {
cfg := config.Default()
cfg.RootcheckEnabled = false
manager := New(cfg)
var calls atomic.Int32
manager.Run = func(context.Context, model.State) []model.Alert { calls.Add(1); return nil }
manager.MaybeStart(time.Now(), model.State{})
if calls.Load() != 0 {
t.Fatal("disabled rootcheck scheduled work")
}
}