package main // delta_blind_class_test.go — a class one pass could not see. // // A failed process walk already leaves every clock and the machine store alone // (the walkErr guard in deltaPass). The listener and identity classes can go // blind one at a time: `ss` exits non-zero or reads short, an account file // cannot be opened. Every consumer that reads "absent from this pass" as a // fact about the machine must then leave that class alone too, or a flaky // reader turns into findings that close and reopen, and absences that never // mature. These drive the real deltaPass and the real store, with only the two // class readers replaced. import ( "strings" "testing" "time" ) // blindableListeners installs a listener reader that sees `socks` when // *blind is false and reports a failed read when it is true. func blindableListeners(t *testing.T, blind *bool, socks []listenSocket) { t.Helper() oI, oL := deltaIdentityReader, deltaListenerReader deltaIdentityReader = func() identityScanReport { return identityScanReport{Supported: true} } deltaListenerReader = func() ([]listenSocket, bool, string) { if *blind { return nil, false, "ss exited 1" } return socks, true, "" } t.Cleanup(func() { deltaIdentityReader, deltaListenerReader = oI, oL }) } func saveFourClassAccounting(t *testing.T, entries []baselineEntry) { t.Helper() if err := baselineSave(baselineFile{ Version: baselineVersion, TakenAt: stamp(time.Now()), Coverage: baselineCoverage(true, true, true, true), Entries: entries, }); err != nil { t.Fatal(err) } } func findingOfKind(r deltaReport, kind string) *deltaEvent { for i := range r.Findings { if r.Findings[i].Kind == kind { return &r.Findings[i] } } return nil } // A listener finding on a box whose `ss` fails every other pass stays OPEN in // the machine store. It closed and reopened on every flap: twelve times in 25 // passes for a listener on 0.0.0.0:4444. func TestABlindListenerPassClosesNoListenerFinding(t *testing.T) { isolateConfigDir(t) if !processIsRoot() { t.Skip("the pass refuses without machine authority, which is its own test") } blind := false blindableListeners(t, &blind, []listenSocket{sock("tcp", "0.0.0.0", 4444, "nc", 1)}) saveFourClassAccounting(t, nil) now := time.Date(2026, 9, 23, 3, 0, 0, 0, time.UTC) var fp string for i := 0; i < 6; i++ { blind = i%2 == 1 at := now.Add(time.Duration(i) * time.Minute) r := deltaPass(&digestCache{}, at, deltaRoleOperator) if blind && (len(r.Skipped) != 1 || r.Skipped[0] != "listener") { t.Fatalf("pass %d could not read the socket table and did not say so: skipped=%v", i, r.Skipped) } // The heartbeat carries it too: the host reads the summary, and zero // counts on a blind pass must not reach it as a clean machine. if sum := r.summary(); blind && (len(sum.Skipped) != 1 || sum.Skipped[0] != "listener") { t.Fatalf("pass %d: the summary the heartbeat carries does not name the skipped class: %v", i, sum.Skipped) } if i == 0 { ev := findingOfKind(r, deltaListenerNew) if ev == nil { t.Fatalf("the listener on 0.0.0.0:4444 was not a finding: %v", kindsOf(r.Findings)) } fp = ev.FP // The pass itself records the finding's class, which is what the // prune reads on a later blind pass. if st := deltaStateLoad(r.BaselineDigest); st.SeenKind[fp] != "listener" { t.Fatalf("the pass did not record the finding's class: seen_kind=%q", st.SeenKind[fp]) } } if _, err := storeRecordPass(r, at); err != nil { t.Fatal(err) } } var transitions []string for _, l := range storeReadJournal(t, now) { if l.Event != nil && l.Event.FP == fp { transitions = append(transitions, l.Transition) } } if len(transitions) != 1 || transitions[0] != "opened" { t.Fatalf("a finding in a class the pass could not see must stay open; journal for it: %v", transitions) } // And a pass that DID look and found it gone still closes it. blind = false deltaListenerReader = func() ([]listenSocket, bool, string) { return nil, true, "" } at := now.Add(10 * time.Minute) if _, err := storeRecordPass(deltaPass(&digestCache{}, at, deltaRoleOperator), at); err != nil { t.Fatal(err) } transitions = nil for _, l := range storeReadJournal(t, now) { if l.Event != nil && l.Event.FP == fp { transitions = append(transitions, l.Transition) } } if len(transitions) != 2 || transitions[1] != "closed" { t.Fatalf("a pass that looked and found it gone must close it: %v", transitions) } } // A listener that disappears is reported once its absence has lasted the grace // period, even when every other pass cannot read the socket table. A blind // pass reset the clock, so it was never reported at all. func TestABlindPassDoesNotResetTheGoneClock(t *testing.T) { isolateConfigDir(t) if !processIsRoot() { t.Skip("the pass refuses without machine authority, which is its own test") } ssh, _ := baselineFromListeners([]listenSocket{sock("tcp", "0.0.0.0", 22, "sshd", 1)}) blind := false blindableListeners(t, &blind, nil) // :22 is gone whenever the table is read saveFourClassAccounting(t, ssh) now := time.Date(2026, 9, 23, 3, 0, 0, 0, time.UTC) reportedAt := -1 for i := 0; i < 25 && reportedAt < 0; i++ { blind = i%2 == 1 r := deltaPass(&digestCache{}, now.Add(time.Duration(i)*time.Minute), deltaRoleOperator) if findingOfKind(r, deltaListenerGone) != nil { reportedAt = i } } if reportedAt < 0 { t.Fatal("listener_gone for :22 was never reported across 25 passes with every other pass blind") } if reportedAt < int(deltaGoneGrace/time.Minute) { t.Fatalf("reported at pass %d, before the %v grace had passed", reportedAt, deltaGoneGrace) } } // The class check runs BEFORE the comparison. A socket table that can never be // read must not turn the accounting's listeners into listener_gone findings, // however long it stays unreadable. Run after the comparison instead, the // events were already made and matured on the grace clock. func TestAClassBlindOnEveryPassProducesNoGoneFindings(t *testing.T) { isolateConfigDir(t) if !processIsRoot() { t.Skip("the pass refuses without machine authority, which is its own test") } ssh, _ := baselineFromListeners([]listenSocket{sock("tcp", "0.0.0.0", 22, "sshd", 1)}) blind := true blindableListeners(t, &blind, nil) saveFourClassAccounting(t, ssh) now := time.Date(2026, 9, 23, 3, 0, 0, 0, time.UTC) for _, after := range []time.Duration{0, deltaGoneGrace + time.Minute, 2*deltaGoneGrace + time.Minute} { r := deltaPass(&digestCache{}, now.Add(after), deltaRoleOperator) for _, ev := range append(append([]deltaEvent{}, r.Findings...), r.Filtered...) { if ev.Entry.Kind == "listener" { t.Fatalf("a class nobody could read produced %s at +%v", ev.Kind, after) } } } } // The prune forgets a first-seen stamp a week after its last sighting, unless // its class was not looked at on this pass. func TestThePruneKeepsAClassItCouldNotSee(t *testing.T) { now := time.Date(2026, 9, 30, 0, 0, 0, 0, time.UTC) old := stamp(now.Add(-deltaStateForget - time.Hour)) mk := func() *deltaState { return &deltaState{ FirstSeen: map[string]string{"lst": old, "prc": old}, LastSeen: map[string]string{"lst": old, "prc": old}, SeenKind: map[string]string{"lst": "listener", "prc": "process"}, } } s := mk() s.setBlind([]string{"listener"}, nil) s.prune(now) if _, kept := s.FirstSeen["lst"]; !kept { t.Fatal("a listener's first-seen was forgotten on a pass that could not read listeners") } if _, kept := s.FirstSeen["prc"]; kept { t.Fatal("a process the pass could see was not pruned after a week") } s = mk() s.setBlind(nil, nil) s.prune(now) if _, kept := s.FirstSeen["lst"]; kept { t.Fatal("a listener seen by nobody for a week was kept on a pass that could see listeners") } } // A listener hold is not settled as short-lived by a pass that could not read // the socket table. A process hold still is. func TestAListenerHoldIsNotSettledByABlindPass(t *testing.T) { lst := deltaListenerHoldPrefix + "tcp\x00x" s := &deltaState{Held: map[string]string{lst: stamp(time.Now()), "prc": stamp(time.Now())}} s.setBlind([]string{"listener"}, nil) if n := s.settleHeld(map[string]bool{}); n != 1 { t.Fatalf("settled %d, want only the process hold", n) } if _, kept := s.Held[lst]; !kept { t.Fatal("a listener hold was counted short-lived on a pass that could not see listeners") } s.setBlind(nil, nil) if n := s.settleHeld(map[string]bool{}); n != 1 { t.Fatalf("a pass that could see listeners must settle the hold: settled %d", n) } } // The class maps are bookkeeping for what is still open or still remembered; // neither may outlive the entry it describes. func TestTheClassMapsDoNotOutliveTheirEntries(t *testing.T) { now := time.Date(2026, 9, 30, 0, 0, 0, 0, time.UTC) old := stamp(now.Add(-deltaStateForget - time.Hour)) s := &deltaState{ FirstSeen: map[string]string{"prc": old}, LastSeen: map[string]string{"prc": old}, SeenKind: map[string]string{"prc": "process"}, } s.setBlind(nil, nil) s.prune(now) if _, left := s.SeenKind["prc"]; left { t.Fatal("the prune forgot a fingerprint but kept its class") } isolateConfigDir(t) at := time.Date(2026, 9, 23, 3, 0, 0, 0, time.UTC) ev := storeFinding("aaa", deltaHigh) if _, err := storeRecordPass(storeReport("base-a", ev), at); err != nil { t.Fatal(err) } if k := storeStateLoadRaw().OpenKind["aaa"]; k != "process" { t.Fatalf("an open finding's class was not recorded: %q", k) } if _, err := storeRecordPass(storeReport("base-a"), at.Add(time.Minute)); err != nil { t.Fatal(err) } st := storeStateLoadRaw() if _, open := st.Open["aaa"]; open { t.Fatal("a pass that looked and did not find it left it open") } if _, left := st.OpenKind["aaa"]; left { t.Fatal("a closed finding's class stayed in the open-class map") } } // An accounting taken while a reader failed on a platform that CAN read the // class says `unread`, not `unsupported`: the second means a platform that // never compares the class, and whoever reads the line (the host, a person) // would be told a retake changes nothing when a retake is exactly the fix. func TestAnAccountingTakenWithAFailedReaderSaysUnread(t *testing.T) { isolateConfigDir(t) if !processIsRoot() { t.Skip("the accounting refuses without machine authority, which is its own test") } oI, oL := deltaIdentityReader, deltaListenerReader t.Cleanup(func() { deltaIdentityReader, deltaListenerReader = oI, oL }) deltaListenerReader = func() ([]listenSocket, bool, string) { return nil, false, "ss exited 1" } deltaIdentityReader = func() identityScanReport { return identityScanReport{Reason: "the account database could not be read"} } f, _, _, _, err := collectBaseline() if err != nil { t.Fatal(err) } want := map[bool]string{true: "unread", false: "unsupported"} for class, readable := range map[string]bool{"listeners": listenersReadableHere, "identities": identitiesReadableHere} { if !strings.Contains(f.Coverage, class+"="+want[readable]) { t.Fatalf("%s: a failed reader on a platform that %s read it wrote %q", class, map[bool]string{true: "can", false: "cannot"}[readable], f.Coverage) } if baselineHasClass(f.Coverage, class) { t.Fatalf("%s is counted as held by an accounting that could not read it: %q", class, f.Coverage) } } // The same readers working: the classes are held. deltaListenerReader = func() ([]listenSocket, bool, string) { return nil, true, "" } deltaIdentityReader = func() identityScanReport { return identityScanReport{Supported: true} } f, _, _, _, err = collectBaseline() if err != nil { t.Fatal(err) } if listenersReadableHere && (!baselineHasClass(f.Coverage, "listeners") || strings.Contains(f.Coverage, "unread")) { t.Fatalf("working readers must hold the classes: %q", f.Coverage) } } // The noise filter's "started by something the accounting holds" evidence comes // from accounted PROCESSES only. An accounted listener held by a replaced // program must not vouch for that program's children. func TestAnAccountedListenerIsNotABaselineParent(t *testing.T) { sshd := baselineEntry{FP: "sshd-real", Kind: "process", User: "root", Unit: "ssh.service", ArgvShape: "sshd -D", ExeDigest: "real"} port, _ := baselineFromListeners([]listenSocket{sock("tcp", "0.0.0.0", 22, "sshd", 1)}) trojan := sshd trojan.FP, trojan.ExeDigest = "sshd-trojan", "trojan" holder, _ := baselineFromListeners([]listenSocket{sock("tcp", "0.0.0.0", 22, "sshd", 4242)}) cron := baselineEntry{FP: "cron", Kind: "process", User: "root", Unit: "cron.service", ArgvShape: "cron -f", ExeDigest: "c"} base := &baselineFile{Entries: append([]baselineEntry{sshd, cron}, port...)} live := []deltaLiveEntry{ {Entry: trojan, PIDs: []int{4242}}, {Entry: holder[0], PIDs: []int{4242}}, {Entry: cron, PIDs: []int{77}}, } n := deltaNoiseFor(deltaCompare(base.Entries, live), base, live) if n.BaselinePIDs[4242] { t.Fatal("an accounted listener made the replaced program holding it a baseline parent") } if !n.BaselinePIDs[77] { t.Fatal("an accounted process running now must still be a baseline parent") } }