// 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) } }