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,90 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package pkgdb
import (
"os"
"path/filepath"
"testing"
"time"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config"
)
func write(t *testing.T, path, contents string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o640); err != nil {
t.Fatal(err)
}
}
func TestFingerprintIsDeterministicAndChangesWithContent(t *testing.T) {
directory := t.TempDir()
write(t, filepath.Join(directory, "b", "desc"), "second")
write(t, filepath.Join(directory, "a", "desc"), "first")
first, err := Fingerprint(directory)
if err != nil {
t.Fatal(err)
}
again, err := Fingerprint(directory)
if err != nil || first != again {
t.Fatalf("fingerprints %q %q err=%v", first, again, err)
}
write(t, filepath.Join(directory, "a", "desc"), "changed")
changed, err := Fingerprint(directory)
if err != nil || changed == first {
t.Fatalf("changed fingerprint %q original %q err=%v", changed, first, err)
}
}
func TestLastTransactionTimeAcceptsPacmanTimestampLayouts(t *testing.T) {
path := filepath.Join(t.TempDir(), "pacman.log")
write(t, path, "[2026-07-24T10:00:00-0700] [ALPM] transaction started\n[2026-07-24T10:01:00-07:00] [ALPM] transaction completed\n")
got, ok := LastTransactionTime(path)
if !ok || got.Format(time.RFC3339) != "2026-07-24T10:01:00-07:00" {
t.Fatalf("transaction=%v ok=%v", got, ok)
}
}
func TestManagerPreservesUnexplainedChangedAnchorAndAcceptsTransaction(t *testing.T) {
directory := t.TempDir()
database := filepath.Join(directory, "pacman-db")
write(t, filepath.Join(database, "pkg", "desc"), "trusted")
cfg := config.Default()
cfg.LogDir = filepath.Join(directory, "state")
manager := New(cfg)
manager.DBDir = database
now := time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC)
manager.Now = func() time.Time { return now }
manager.LastTransaction = func(string) (time.Time, bool) { return time.Time{}, false }
if err := manager.Initialize(); err != nil {
t.Fatal(err)
}
initial, ok := loadAnchor(manager.anchorPath())
if !ok {
t.Fatal("missing initial anchor")
}
write(t, filepath.Join(database, "pkg", "desc"), "tampered")
if err := manager.evaluate(now.Add(time.Minute)); err != nil {
t.Fatal(err)
}
alerts := manager.Drain()
if len(alerts) != 1 || alerts[0].SID != SIDTamper || alerts[0].Severity != "CRITICAL" {
t.Fatalf("alerts=%+v", alerts)
}
afterTamper, _ := loadAnchor(manager.anchorPath())
if afterTamper.Fingerprint != initial.Fingerprint {
t.Fatal("unexplained change silently replaced anchor")
}
manager.LastTransaction = func(string) (time.Time, bool) { return now.Add(time.Minute), true }
if err := manager.evaluate(now.Add(2 * time.Minute)); err != nil {
t.Fatal(err)
}
afterTransaction, _ := loadAnchor(manager.anchorPath())
if afterTransaction.Fingerprint == initial.Fingerprint {
t.Fatal("transaction-correlated change did not refresh anchor")
}
}