enodia-sentinal/go-agent/internal/fim/pkgverify_test.go
Luna d835386381
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
2026-07-24 09:59:09 -07:00

151 lines
4.8 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package fim
import (
"errors"
"reflect"
"testing"
"codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model"
)
func TestParsePacmanVerifyExtractsPackagePathAndReason(t *testing.T) {
output := "coreutils: /usr/bin/ls (Size mismatch)"
got := ParsePacmanVerify(output)
want := []PackageFinding{{Package: "coreutils", Path: "/usr/bin/ls", Reason: "Size mismatch"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("findings = %+v, want %+v", got, want)
}
}
func TestParsePacmanVerifyStripsWarningPrefix(t *testing.T) {
got := ParsePacmanVerify("warning: openssh: /usr/bin/ssh (SHA256 checksum mismatch)")
want := []PackageFinding{{Package: "openssh", Path: "/usr/bin/ssh", Reason: "SHA256 checksum mismatch"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("findings = %+v, want %+v", got, want)
}
}
// A `touch` does not alter content, so a modification-time-only mismatch is
// benign noise that must never reach an operator.
func TestParsePacmanVerifyIgnoresModificationTimeOnlyMismatches(t *testing.T) {
got := ParsePacmanVerify("coreutils: /usr/bin/ls (Modification time mismatch)")
if len(got) != 0 {
t.Errorf("findings = %+v, want none", got)
}
}
func TestParsePacmanVerifyKeepsEveryContentAndOwnershipReason(t *testing.T) {
reasons := []string{
"Size mismatch", "SHA256 checksum mismatch", "Permissions mismatch",
"UID mismatch", "GID mismatch", "Ownership mismatch",
}
for _, reason := range reasons {
got := ParsePacmanVerify("pkg: /usr/bin/x (" + reason + ")")
if len(got) != 1 {
t.Errorf("reason %q: got %d findings, want 1", reason, len(got))
}
}
}
func TestParsePacmanVerifyMatchesReasonCaseInsensitively(t *testing.T) {
if got := ParsePacmanVerify("pkg: /usr/bin/x (size MISMATCH)"); len(got) != 1 {
t.Errorf("findings = %+v, want 1", got)
}
}
func TestParsePacmanVerifyIgnoresSummaryAndUnownedLines(t *testing.T) {
output := `warning: No package owns /usr/local/bin/custom
coreutils: 468 total files, 0 altered files
pacman: /usr/bin/pacman (Size mismatch)`
got := ParsePacmanVerify(output)
want := []PackageFinding{{Package: "pacman", Path: "/usr/bin/pacman", Reason: "Size mismatch"}}
if !reflect.DeepEqual(got, want) {
t.Errorf("findings = %+v, want %+v", got, want)
}
}
func TestParsePacmanVerifyIgnoresLinesWithoutAnAbsolutePath(t *testing.T) {
if got := ParsePacmanVerify("something odd (Size mismatch)"); len(got) != 0 {
t.Errorf("findings = %+v, want none", got)
}
}
func TestParsePacmanVerifyReadsTheLastParenthesisGroup(t *testing.T) {
got := ParsePacmanVerify("pkg: /usr/bin/x (1.0-1) (Size mismatch)")
if len(got) != 1 || got[0].Reason != "Size mismatch" {
t.Errorf("findings = %+v, want reason 'Size mismatch'", got)
}
}
func TestParsePacmanVerifyHandlesEmptyOutput(t *testing.T) {
if got := ParsePacmanVerify(""); got == nil || len(got) != 0 {
t.Errorf("findings = %+v, want empty non-nil slice", got)
}
}
func TestPackageAlertIsCriticalWithDistroReason(t *testing.T) {
got := PackageAlert(PackageFinding{Package: "openssh", Path: "/usr/bin/ssh", Reason: "SHA256 checksum mismatch"})
want := model.Alert{
SID: 100020, Severity: "CRITICAL", Signature: "fim_pkg_modified",
Classtype: "integrity-violation", Key: "fim:pkg:/usr/bin/ssh",
Detail: "package file altered (openssh: SHA256 checksum mismatch): /usr/bin/ssh",
PIDs: []int{},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("alert = %+v, want %+v", got, want)
}
}
func TestVerifyAlertsConvertsEveryFinding(t *testing.T) {
run := func() (string, error) {
return "a: /usr/bin/a (Size mismatch)\nb: /usr/bin/b (UID mismatch)", nil
}
alerts := VerifyAlerts(run)
if len(alerts) != 2 {
t.Fatalf("alerts = %+v, want 2", alerts)
}
if alerts[0].Key != "fim:pkg:/usr/bin/a" || alerts[1].Key != "fim:pkg:/usr/bin/b" {
t.Errorf("unexpected alert keys: %s, %s", alerts[0].Key, alerts[1].Key)
}
}
// A host without pacman, or a verification that times out, must degrade to no
// alerts rather than reporting a false integrity violation.
func TestVerifyAlertsFailsQuietWhenVerificationCannotRun(t *testing.T) {
run := func() (string, error) { return "", errors.New("pacman: not found") }
alerts := VerifyAlerts(run)
if len(alerts) != 0 || alerts == nil {
t.Errorf("alerts = %+v, want empty non-nil slice", alerts)
}
}
// pacman writes diagnostics to stderr, so a non-zero exit still carries
// findings that must be parsed rather than discarded.
func TestVerifyAlertsParsesOutputEvenWithFindingsPresentOnFailureExit(t *testing.T) {
run := func() (string, error) {
return "openssh: /usr/bin/ssh (Size mismatch)", errors.New("exit status 1")
}
alerts := VerifyAlerts(run)
if len(alerts) != 1 {
t.Fatalf("alerts = %+v, want 1", alerts)
}
if alerts[0].Severity != "CRITICAL" {
t.Errorf("severity = %s, want CRITICAL", alerts[0].Severity)
}
}