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:
parent
1d1dee7f6e
commit
d835386381
43 changed files with 3419 additions and 72 deletions
90
go-agent/internal/pkgdb/pkgdb_test.go
Normal file
90
go-agent/internal/pkgdb/pkgdb_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue