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
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
// 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")
|
|
}
|
|
}
|