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,75 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package fim
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
)
func TestManagerBuildsBaselineAndReportsDriftWithoutReplacingIt(t *testing.T) {
directory := t.TempDir()
watched := filepath.Join(directory, "watched")
writeFile(t, watched, "trusted")
state := filepath.Join(directory, "state")
cfg := config.Default()
cfg.LogDir = state
cfg.FIMPaths = []string{watched}
manager := New(cfg)
if err := manager.Initialize(); err != nil {
t.Fatal(err)
}
baselineHash := *manager.baseline[watched].SHA256
if _, err := os.Stat(filepath.Join(state, baselineFilename)); err != nil {
t.Fatalf("baseline not persisted: %v", err)
}
writeFile(t, watched, "tampered")
manager.runScan(cloneEntries(manager.baseline), PathList(cfg.FIMPaths))
alerts := manager.Drain()
if len(alerts) != 1 || alerts[0].SID != SIDModified || alerts[0].Severity != "HIGH" {
t.Fatalf("alerts = %+v, want one high modified alert", alerts)
}
if got := *manager.baseline[watched].SHA256; got != baselineHash {
t.Fatalf("scan silently replaced baseline %q with %q", baselineHash, got)
}
}
func TestManagerPackageVerificationIsBoundedAndDrained(t *testing.T) {
cfg := config.Default()
cfg.LogDir = t.TempDir()
cfg.FIMPaths = []string{filepath.Join(t.TempDir(), "absent")}
manager := New(cfg)
manager.Verify = func(ctx context.Context) (string, error) {
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > pacmanTimeout {
t.Fatal("package verification did not receive a bounded context")
}
return "openssh: /usr/bin/ssh (SHA256 checksum mismatch)", nil
}
if err := manager.Initialize(); err != nil {
t.Fatal(err)
}
manager.runVerify()
alerts := manager.Drain()
if len(alerts) != 1 || alerts[0].SID != SIDPackage || alerts[0].Severity != "CRITICAL" {
t.Fatalf("alerts = %+v, want one critical package alert", alerts)
}
}
func TestDisabledManagerDoesNotCreateAStateDirectory(t *testing.T) {
cfg := config.Default()
cfg.FIMEnabled = false
cfg.LogDir = filepath.Join(t.TempDir(), "state")
manager := New(cfg)
if err := manager.Initialize(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(cfg.LogDir); !os.IsNotExist(err) {
t.Fatalf("disabled FIM created state directory: %v", err)
}
}