1452 lines
62 KiB
Go
1452 lines
62 KiB
Go
package main
|
|
|
|
// The delta has exactly one job — say what changed — and there are two ways to
|
|
// fail at it that no single test can cover, because a fix for either is the
|
|
// cause of the other:
|
|
//
|
|
// MISSES IT the swapped binary reads as unrelated new-and-gone rows, the
|
|
// new cron line reads as nothing at all, and the product does not
|
|
// do the thing it says on the box.
|
|
// CRIES WOLF a service restart, an apt upgrade, the bastion's own harness
|
|
// run, or a shell command that lived a second each land in the
|
|
// findings, and by the second week nobody opens the list.
|
|
//
|
|
// Every test below is one of those two, named for the behaviour rather than the
|
|
// function, so a mutation that breaks the behaviour fails a test whose name
|
|
// says what was lost.
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
func procEntry(digest, user, unit, shape string) baselineEntry {
|
|
return baselineEntry{
|
|
FP: baselineFP("process", digest, user, unit, shape),
|
|
Kind: "process",
|
|
ExeDigest: digest, Exe: "/usr/bin/thing", User: user, Unit: unit, ArgvShape: shape,
|
|
}
|
|
}
|
|
|
|
func schedBaseEntry(user, unit, shape string) baselineEntry {
|
|
return baselineEntry{
|
|
FP: baselineFP("scheduler", "", user, unit, shape),
|
|
Kind: "scheduler", User: user, Unit: unit, ArgvShape: shape,
|
|
}
|
|
}
|
|
|
|
func liveOf(e baselineEntry, pids ...int) deltaLiveEntry {
|
|
return deltaLiveEntry{Entry: e, PIDs: pids}
|
|
}
|
|
|
|
func eventsOfKind(evs []deltaEvent, kind string) []deltaEvent {
|
|
out := []deltaEvent{}
|
|
for _, e := range evs {
|
|
if e.Kind == kind {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func freshState(t *testing.T) *deltaState {
|
|
t.Helper()
|
|
return &deltaState{Version: deltaStateVersion, Baseline: "b"}
|
|
}
|
|
|
|
// ── missing it ──────────────────────────────────────────────────────────────
|
|
|
|
// THE CENTRAL CLAIM OF THE WHOLE PLANE. A binary replaced at a post the
|
|
// accounting already knows about must be ONE event that says so, carrying both
|
|
// digests. Reported as a `proc_new` plus a `proc_gone` it is two rows nobody
|
|
// connects, and the design's own `proc_changed` — "same fingerprint, new exe
|
|
// digest" — cannot fire at all, because the digest is inside the fingerprint.
|
|
func TestASwappedBinaryAtAKnownPostIsOneChangedEventNotTwoHalves(t *testing.T) {
|
|
was := procEntry("aaaaaaaaaaaa", "root", "web.service", "node server.js")
|
|
now := procEntry("bbbbbbbbbbbb", "root", "web.service", "node server.js")
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 41)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("a replaced binary produced %d proc_changed events, want 1:\n%+v", len(changed), evs)
|
|
}
|
|
if changed[0].Reason != deltaReasonExeDigest {
|
|
t.Errorf("reason = %q, want %q", changed[0].Reason, deltaReasonExeDigest)
|
|
}
|
|
if changed[0].Was == nil || changed[0].Was.ExeDigest != "aaaaaaaaaaaa" {
|
|
t.Error("the event does not carry what was there before, so a reader cannot " +
|
|
"see what the binary was replaced FROM")
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcNew)); n != 0 {
|
|
t.Errorf("%d proc_new events as well — the swap was reported twice, as two "+
|
|
"unrelated halves of one sentence", n)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcGone)); n != 0 {
|
|
t.Errorf("%d proc_gone events as well — same defect from the other side", n)
|
|
}
|
|
}
|
|
|
|
// A program that was never there, in a place nothing was, is NEW. This is the
|
|
// other side of the slot rule: it must not be swallowed as a change to
|
|
// something else.
|
|
func TestAProgramInAPlaceNothingWasReadsAsNew(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "node server.js")
|
|
now := procEntry("cccc", "user", "interactive", "/tmp/<tmp> --x")
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(was, 1), liveOf(now, 99)})
|
|
if n := len(eventsOfKind(evs, deltaProcNew)); n != 1 {
|
|
t.Fatalf("got %d proc_new, want 1:\n%+v", n, evs)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcChanged)); n != 0 {
|
|
t.Errorf("the unrelated program was reported as a change to a known one (%d events)", n)
|
|
}
|
|
}
|
|
|
|
// A PROCESS THE ACCOUNTING ALREADY HOLDS THAT HAS STARTED TALKING TO A MODEL.
|
|
// Endpoints are deliberately not fingerprinted (baseline.go), so this entry's
|
|
// fingerprint is unchanged and this is the only place the fact can be caught.
|
|
func TestAKnownProgramThatStartsTalkingToAModelIsReported(t *testing.T) {
|
|
was := procEntry("aaaa", "user", "interactive", "python3 x.py")
|
|
now := was
|
|
now.Endpoints = []string{"api.anthropic.com"}
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 7)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 || changed[0].Reason != deltaReasonEndpoint {
|
|
t.Fatalf("a known process that began talking to a model produced %+v", evs)
|
|
}
|
|
if len(changed[0].Added) != 1 || changed[0].Added[0] != "api.anthropic.com" {
|
|
t.Errorf("the event does not name the endpoint that is new: %+v", changed[0].Added)
|
|
}
|
|
if got := deltaLevel(changed[0]); got != deltaHigh {
|
|
t.Errorf("level = %q, want high — a model endpoint is high", got)
|
|
}
|
|
}
|
|
|
|
// The same endpoints in a different order are not a finding. A scan returns
|
|
// connections in whatever order the kernel had them.
|
|
func TestTheSameEndpointsInAnotherOrderAreNotAFinding(t *testing.T) {
|
|
was := procEntry("aaaa", "user", "interactive", "python3 x.py")
|
|
was.Endpoints = []string{"api.anthropic.com", "api.openai.com"}
|
|
now := was
|
|
now.Endpoints = []string{"API.OpenAI.com", "api.anthropic.com"}
|
|
if evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 7)}); len(evs) != 0 {
|
|
t.Errorf("re-ordered endpoints produced %d events: %+v", len(evs), evs)
|
|
}
|
|
}
|
|
|
|
// A STANDING JOB IS ITS OWN SLOT. Editing what an existing crontab line runs is
|
|
// the persistence move, and it must read as a change to that job — not as an
|
|
// unrelated job appearing while another vanishes.
|
|
func TestAnEditedCronLineIsAChangeToThatJob(t *testing.T) {
|
|
was := schedBaseEntry("root", "cron:/etc/crontab:4", "/usr/bin/backup.sh")
|
|
now := schedBaseEntry("root", "cron:/etc/crontab:4", "curl <hex> | sh")
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now)})
|
|
changed := eventsOfKind(evs, deltaSchedChanged)
|
|
if len(changed) != 1 || changed[0].Reason != deltaReasonCommand {
|
|
t.Fatalf("an edited cron line produced %+v", evs)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaSchedNew)) + len(eventsOfKind(evs, deltaSchedGone)); n != 0 {
|
|
t.Errorf("it was ALSO reported as %d new/gone events — one edit, two rows", n)
|
|
}
|
|
}
|
|
|
|
// A brand new standing job that would run a recognised agent is the persistence
|
|
// form of "a new process with an agent signature", which the level table puts at high.
|
|
func TestANewScheduledJobRunningAnAgentIsHigh(t *testing.T) {
|
|
plain := deltaEvent{Kind: deltaSchedNew, Entry: schedBaseEntry("root", "cron:x:1", "/usr/bin/backup.sh")}
|
|
agent := deltaEvent{Kind: deltaSchedNew}
|
|
agent.Entry = schedBaseEntry("root", "cron:x:2", "claude -p go")
|
|
agent.Entry.Signature = "claude-code"
|
|
if got := deltaLevel(plain); got != deltaWarn {
|
|
t.Errorf("an ordinary new job is %q, want warn", got)
|
|
}
|
|
if got := deltaLevel(agent); got != deltaHigh {
|
|
t.Errorf("a new job that would start an agent is %q, want high", got)
|
|
}
|
|
}
|
|
|
|
// ── crying wolf ─────────────────────────────────────────────────────────────
|
|
|
|
// AN UNREADABLE EXECUTABLE IS NOT A SWAPPED ONE. Both produce "the digests
|
|
// differ", and one of them is a security finding while the other is this
|
|
// process not being able to look. Getting this wrong fills the list with
|
|
// alarms every time a scan runs with fewer rights than the accounting did.
|
|
func TestLosingSightOfABinaryIsNotReportedAsASwap(t *testing.T) {
|
|
was := procEntry("aaaaaaaaaaaa", "root", "web.service", "node server.js")
|
|
now := procEntry("", "root", "web.service", "node server.js")
|
|
now.DigestNote = "could not be read by this account"
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 3)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("got %+v", evs)
|
|
}
|
|
if changed[0].Reason != deltaReasonUnreadable {
|
|
t.Fatalf("reason = %q, want %q — this is a visibility change, not a machine change",
|
|
changed[0].Reason, deltaReasonUnreadable)
|
|
}
|
|
if got := deltaLevel(changed[0]); got != deltaNotice {
|
|
t.Errorf("level = %q, want notice", got)
|
|
}
|
|
}
|
|
|
|
// A packaged binary is ordinary; a packaged binary talking to a model is not.
|
|
// The tier must lower the first and never the second — a noise filter that can
|
|
// hide the finding is worse than no filter.
|
|
func TestTheTierLowersAnOrdinaryNewProgramAndNeverOneTalkingToAModel(t *testing.T) {
|
|
plain := deltaEvent{Kind: deltaProcNew, Entry: procEntry("aaaa", "root", "svc", "thing")}
|
|
if got := deltaLevel(plain); got != deltaWarn {
|
|
t.Errorf("an unexplained new program is %q, want warn", got)
|
|
}
|
|
plain.Tier = tierKnown
|
|
if got := deltaLevel(plain); got != deltaNotice {
|
|
t.Errorf("an explained new program is %q, want notice", got)
|
|
}
|
|
talking := plain
|
|
talking.Entry.Endpoints = []string{"api.anthropic.com"}
|
|
if got := deltaLevel(talking); got != deltaHigh {
|
|
t.Fatalf("a KNOWN-tier program talking to a model is %q, want high. The package "+
|
|
"database shipping /usr/bin/python3 says nothing about what it is doing.", got)
|
|
}
|
|
signed := plain
|
|
signed.Entry.Signature = "claude-code"
|
|
if got := deltaLevel(signed); got != deltaHigh {
|
|
t.Fatalf("a KNOWN-tier program with an agent signature is %q, want high", got)
|
|
}
|
|
}
|
|
|
|
// THE BASTION'S OWN UPDATE MUST NOT BE A CRITICAL. Every machine on this fleet
|
|
// takes published updates; a critical that fires on each one is a critical
|
|
// nobody reads by the third. A digest that moved WITHOUT the version is the
|
|
// swap, and stays critical.
|
|
func TestABastionUpdateIsHighAndASilentSwapIsCritical(t *testing.T) {
|
|
upd := deltaBinaryEvent("aaaa", "0.27.0", "bbbb", "0.28.0")
|
|
if upd == nil || upd.Level != deltaHigh {
|
|
t.Fatalf("a bastion self-update levelled %+v, want high", upd)
|
|
}
|
|
swap := deltaBinaryEvent("aaaa", "0.27.0", "bbbb", "0.27.0")
|
|
if swap == nil || swap.Level != deltaCritical {
|
|
t.Fatalf("a binary replaced under the SAME version levelled %+v, want critical", swap)
|
|
}
|
|
if same := deltaBinaryEvent("aaaa", "0.27.0", "aaaa", "0.27.0"); same != nil {
|
|
t.Error("an unchanged binary produced an event")
|
|
}
|
|
if blind := deltaBinaryEvent("", "", "aaaa", "0.27.0"); blind != nil {
|
|
t.Error("a baseline that recorded no digest produced a change event — there is " +
|
|
"nothing to compare, and an absent record is not a difference")
|
|
}
|
|
}
|
|
|
|
// AND THE CRITICAL RUNG SURVIVES THE FIRST LEGITIMATE UPDATE. Compared against
|
|
// the ACCOUNTING's version — which nothing retakes when the bastion updates
|
|
// itself — every machine on an auto-updating fleet is permanently "newer than
|
|
// its baseline", so every later digest change reads as another update and comes
|
|
// out high. The rung was disarmed by the very event its own justification was
|
|
// written around.
|
|
func TestASwapAfterALegitimateUpdateIsStillCritical(t *testing.T) {
|
|
// The accounting was taken at 0.27.0/aaaa. The fleet updates.
|
|
upd := deltaBinaryEvent("aaaa", "0.27.0", "bbbb", "0.28.0")
|
|
if upd == nil || upd.Level != deltaHigh {
|
|
t.Fatalf("the update levelled %+v, want high", upd)
|
|
}
|
|
// Days later, still 0.28.0, and the binary moves again. Compared against
|
|
// what the LAST PASS saw, this is the swap.
|
|
swap := deltaBinaryEvent("bbbb", "0.28.0", "cccc", "0.28.0")
|
|
if swap == nil || swap.Level != deltaCritical {
|
|
t.Fatalf("a binary replaced under an unchanged version, after an earlier update, "+
|
|
"levelled %+v — want critical. Comparing against the accounting's frozen "+
|
|
"version is what switches this rung off for good.", swap)
|
|
}
|
|
}
|
|
|
|
// AN ABSENCE IS NOT A FINDING ON THE STRENGTH OF ONE SCAN. A service restart,
|
|
// an upgrade that stops and starts a unit, a reboot: each makes the accounting
|
|
// briefly wrong, and reporting on the first miss reports all three.
|
|
func TestAnAbsenceIsOnlyReportedOnceItHasOutlastedARestart(t *testing.T) {
|
|
gone := deltaEvent{Kind: deltaProcGone, FP: "fp1"}
|
|
st := freshState(t)
|
|
t0 := time.Now()
|
|
if out := deltaApplyGrace([]deltaEvent{gone}, st, t0); len(out) != 0 {
|
|
t.Fatalf("the FIRST miss was reported: %+v", out)
|
|
}
|
|
if out := deltaApplyGrace([]deltaEvent{gone}, st, t0.Add(deltaGoneGrace-time.Minute)); len(out) != 0 {
|
|
t.Fatalf("an absence inside the grace was reported: %+v", out)
|
|
}
|
|
out := deltaApplyGrace([]deltaEvent{gone}, st, t0.Add(deltaGoneGrace+time.Minute))
|
|
if len(out) != 1 {
|
|
t.Fatalf("an absence past the grace was NOT reported: %+v", out)
|
|
}
|
|
if out[0].FirstSeen == "" {
|
|
t.Error("the reported absence does not say when it started")
|
|
}
|
|
}
|
|
|
|
// A service that bounces must not accumulate. Without clearing the clock when
|
|
// it comes back, the SECOND bounce is measured from the first and reports gone
|
|
// immediately — for ever after.
|
|
func TestAServiceThatComesBackResetsItsClock(t *testing.T) {
|
|
gone := deltaEvent{Kind: deltaProcGone, FP: "fp1"}
|
|
st := freshState(t)
|
|
t0 := time.Now()
|
|
deltaApplyGrace([]deltaEvent{gone}, st, t0)
|
|
deltaApplyGrace(nil, st, t0.Add(time.Minute)) // it is back: no gone event this pass
|
|
if out := deltaApplyGrace([]deltaEvent{gone}, st, t0.Add(deltaGoneGrace+time.Minute)); len(out) != 0 {
|
|
t.Fatalf("after coming back and going again it reported immediately, timed from "+
|
|
"the FIRST disappearance: %+v", out)
|
|
}
|
|
}
|
|
|
|
// A baseline entry whose post is now filled by something else is already
|
|
// reported as the change at that post. Reporting it gone as well makes one
|
|
// event into two halves of the same sentence — the exact defect the slot key
|
|
// exists to remove.
|
|
func TestAReplacedEntryIsNotAlsoReportedGone(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "node server.js")
|
|
now := procEntry("bbbb", "root", "web.service", "node server.js")
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 5)})
|
|
if n := len(eventsOfKind(evs, deltaProcGone)); n != 0 {
|
|
t.Errorf("%d gone events beside the change: %+v", n, evs)
|
|
}
|
|
}
|
|
|
|
// ── the noise filter ────────────────────────────────────────────────────────
|
|
|
|
// A PROGRAM REPORTING ITS OWN WORK AS AN INTRUSION IS IGNORED BY WEEK TWO. The
|
|
// bastion runs harnesses, labs and session shells; all of them are below it in
|
|
// the process tree and none of them is a finding.
|
|
func TestWhatThisBastionItselfStartedIsNotAFinding(t *testing.T) {
|
|
ev := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{300},
|
|
Entry: procEntry("aaaa", "root", "svc", "harness run")}
|
|
n := deltaNoise{Self: 100, Parents: map[int]int{300: 200, 200: 100}, Sockets: true}
|
|
findings, filtered := deltaFilter([]deltaEvent{ev}, n, freshState(t), time.Now())
|
|
if len(findings) != 0 || len(filtered) != 1 {
|
|
t.Fatalf("a grandchild of this bastion was reported: findings=%+v", findings)
|
|
}
|
|
if filtered[0].Filtered != deltaFilterBastion {
|
|
t.Errorf("filtered for %q, want %q", filtered[0].Filtered, deltaFilterBastion)
|
|
}
|
|
// And a process that merely SHARES a fingerprint with one of ours is still
|
|
// the user's: `every`, not `any`.
|
|
ev.PIDs = []int{300, 999}
|
|
findings, _ = deltaFilter([]deltaEvent{ev}, n, freshState(t), time.Now())
|
|
if len(findings) != 1 {
|
|
t.Error("an entry with a process that is NOT ours was filtered anyway — a " +
|
|
"binary the bastion runs and the user also runs is still the user's")
|
|
}
|
|
}
|
|
|
|
// Short-lived work is counted, not reported — and only once it has proved it
|
|
// was short-lived by being gone next time.
|
|
func TestSomethingThatCameAndWentIsCountedNotReported(t *testing.T) {
|
|
st := freshState(t)
|
|
young := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{7},
|
|
Entry: procEntry("aaaa", "user", "interactive", "ls")}
|
|
n := deltaNoise{Self: 1, Parents: map[int]int{7: 2}, Sockets: true,
|
|
Age: func(int) (time.Duration, bool) { return time.Second, true }}
|
|
findings, filtered := deltaFilter([]deltaEvent{young}, n, st, time.Now())
|
|
if len(findings) != 0 || len(filtered) != 1 {
|
|
t.Fatalf("a one-second-old process was reported: %+v", findings)
|
|
}
|
|
if st.ShortLived != 0 {
|
|
t.Error("it was counted while still running — it has not proved anything yet")
|
|
}
|
|
// Next pass: it is gone.
|
|
if _, _ = deltaFilter(nil, n, st, time.Now()); st.ShortLived != 1 {
|
|
t.Errorf("short-lived count = %d, want 1 — the storm counter never moved", st.ShortLived)
|
|
}
|
|
// And one that is still there next pass is a finding after all.
|
|
st2 := freshState(t)
|
|
deltaFilter([]deltaEvent{young}, n, st2, time.Now())
|
|
n.Age = func(int) (time.Duration, bool) { return time.Hour, true }
|
|
findings, _ = deltaFilter([]deltaEvent{young}, n, st2, time.Now())
|
|
if len(findings) != 1 {
|
|
t.Error("a process that survived was still being held back")
|
|
}
|
|
if st2.ShortLived != 0 {
|
|
t.Error("something that survived was counted as short-lived")
|
|
}
|
|
}
|
|
|
|
// The short-lived rule's exemptions are not optional: whatever opened a listening socket or
|
|
// reached a model endpoint in its first seconds is the most interesting thing
|
|
// on the box, not the least. And where the socket table could not be read at
|
|
// all, the rule does not fire — an exemption that cannot be checked is not one.
|
|
func TestTheShortLivedRuleNeverSwallowsAListenerOrAModelEndpoint(t *testing.T) {
|
|
base := deltaNoise{Self: 1, Parents: map[int]int{7: 2}, Sockets: true,
|
|
Age: func(int) (time.Duration, bool) { return time.Second, true }}
|
|
young := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{7},
|
|
Entry: procEntry("aaaa", "user", "interactive", "x")}
|
|
|
|
talking := young
|
|
talking.Entry.Endpoints = []string{"api.anthropic.com"}
|
|
if f, _ := deltaFilter([]deltaEvent{talking}, base, freshState(t), time.Now()); len(f) != 1 {
|
|
t.Error("a process talking to a model was filtered for being young")
|
|
}
|
|
listening := base
|
|
listening.Listening = map[int]bool{7: true}
|
|
if f, _ := deltaFilter([]deltaEvent{young}, listening, freshState(t), time.Now()); len(f) != 1 {
|
|
t.Error("a process holding a listening socket was filtered for being young")
|
|
}
|
|
blind := base
|
|
blind.Sockets = false
|
|
if f, _ := deltaFilter([]deltaEvent{young}, blind, freshState(t), time.Now()); len(f) != 1 {
|
|
t.Error("the rule fired on a box that could not read its socket table, so it " +
|
|
"could not know whether the exemption applied")
|
|
}
|
|
ageless := base
|
|
ageless.Age = func(int) (time.Duration, bool) { return 0, false }
|
|
if f, _ := deltaFilter([]deltaEvent{young}, ageless, freshState(t), time.Now()); len(f) != 1 {
|
|
t.Error("the rule fired where the platform cannot read a process age — an " +
|
|
"unknown age was treated as a young one")
|
|
}
|
|
}
|
|
|
|
// BEING STARTED BY SOMETHING IN THE ACCOUNTING CHANGES NOTHING ABOUT A FINDING,
|
|
// and this is the test for a rule that was BUILT, MEASURED AND THEN TAKEN OUT.
|
|
//
|
|
// Measured on a live machine: with the parent rule lowering the
|
|
// tier, a brand-new root-owned binary at /tmp/brandnewthing and a swapped
|
|
// binary at a known post both came out as `notice` — because each had been
|
|
// started from a shell that was running when the accounting was taken. On a
|
|
// real machine nearly everything is a child of something that was already
|
|
// running, so the design's rule minus its declared-children set is not a noise
|
|
// filter, it is a way to lose the two findings that matter most.
|
|
func TestBeingStartedByAKnownProcessDoesNotLowerAFinding(t *testing.T) {
|
|
ev := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{300},
|
|
Entry: procEntry("aaaa", "root", "web.service", "sh -c curl")}
|
|
n := deltaNoise{Self: 1, Parents: map[int]int{300: 200}, BaselinePIDs: map[int]bool{200: true},
|
|
Sockets: true}
|
|
findings, filtered := deltaFilter([]deltaEvent{ev}, n, freshState(t), time.Now())
|
|
if len(findings) != 1 {
|
|
t.Fatalf("a child of a baseline process was REMOVED from the findings (%d filtered). "+
|
|
"Without the declared children set that is the attack being hidden.", len(filtered))
|
|
}
|
|
if findings[0].Tier == tierKnown {
|
|
t.Fatal("a new program was lowered to `known` for having been started by a " +
|
|
"process that was already running. Every interactive program on a machine " +
|
|
"is a child of a shell that was already running.")
|
|
}
|
|
if got := deltaLevel(findings[0]); got != deltaWarn {
|
|
t.Errorf("level = %q, want warn — nothing here explains the program away", got)
|
|
}
|
|
// The fact still travels, because it is real context for whoever reads it.
|
|
var said bool
|
|
for _, e := range findings[0].Evidence {
|
|
if e.Signal == "child-of-baseline" {
|
|
said = true
|
|
if e.Tier != tierAmbiguous {
|
|
t.Errorf("the evidence line is tier %q; a signal that explains nothing "+
|
|
"must annotate and never promote", e.Tier)
|
|
}
|
|
}
|
|
}
|
|
if !said {
|
|
t.Error("the parentage was dropped entirely — it is context a reader wants even " +
|
|
"though it changes no level")
|
|
}
|
|
}
|
|
|
|
// The package database lowers a row and names the package that shipped it.
|
|
func TestAPackagedBinaryIsLoweredAndSaysWhichPackage(t *testing.T) {
|
|
ev := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{300},
|
|
Entry: procEntry("aaaa", "root", "svc", "thing")}
|
|
n := deltaNoise{Self: 1, Parents: map[int]int{300: 2}, Sockets: true,
|
|
Packaged: func(paths []string) map[string]string {
|
|
return map[string]string{paths[0]: "coreutils"}
|
|
}}
|
|
findings, _ := deltaFilter([]deltaEvent{ev}, n, freshState(t), time.Now())
|
|
if len(findings) != 1 || findings[0].Tier != tierKnown {
|
|
t.Fatalf("a packaged binary was not lowered: %+v", findings)
|
|
}
|
|
var said bool
|
|
for _, e := range findings[0].Evidence {
|
|
if strings.Contains(e.Value, "coreutils") {
|
|
said = true
|
|
}
|
|
}
|
|
if !said {
|
|
t.Error("the evidence does not name the package, so the reader cannot check it")
|
|
}
|
|
}
|
|
|
|
// Absences and the bastion's own binary have no process to explain away, and
|
|
// must never be filtered as if they did.
|
|
func TestAnAbsenceIsNeverFilteredAsNoise(t *testing.T) {
|
|
evs := []deltaEvent{
|
|
{Kind: deltaProcGone, FP: "a"},
|
|
{Kind: deltaBinaryChanged, FP: "b", Level: deltaCritical},
|
|
}
|
|
n := deltaNoise{Self: 1, Parents: map[int]int{}, Sockets: true}
|
|
findings, filtered := deltaFilter(evs, n, freshState(t), time.Now())
|
|
if len(findings) != 2 || len(filtered) != 0 {
|
|
t.Fatalf("findings=%d filtered=%d, want 2 and 0", len(findings), len(filtered))
|
|
}
|
|
}
|
|
|
|
// ── the bookkeeping ─────────────────────────────────────────────────────────
|
|
|
|
// RETAKING THE ACCOUNTING REDEFINES NORMAL, so every clock kept against the old
|
|
// one is timing something that is no longer being compared.
|
|
func TestRetakingTheAccountingThrowsAwayTheOldClocks(t *testing.T) {
|
|
isolateConfigDir(t)
|
|
st := &deltaState{Version: deltaStateVersion, Baseline: "digest-one"}
|
|
st.setMissingSince("fp1", time.Now().Add(-time.Hour))
|
|
if err := st.save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again := deltaStateLoad("digest-one"); len(again.MissingSince) != 1 {
|
|
t.Fatalf("the state did not survive a save/load: %+v", again)
|
|
}
|
|
after := deltaStateLoad("digest-two")
|
|
if len(after.MissingSince) != 0 {
|
|
t.Error("a clock from the OLD accounting was carried into the new one — a " +
|
|
"process missing from a baseline nobody is comparing against any more " +
|
|
"would report gone the moment the new one was taken")
|
|
}
|
|
}
|
|
|
|
// The state file is the bastion's own, and nothing else's business.
|
|
func TestTheDeltaStateIsWrittenPrivate(t *testing.T) {
|
|
dir := isolateConfigDir(t)
|
|
st := &deltaState{Version: deltaStateVersion, Baseline: "d", dirty: true}
|
|
if err := st.save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fi, err := os.Stat(filepath.Join(dir, "delta-state.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fi.Mode().Perm() != 0o600 {
|
|
t.Errorf("mode %v, want 0600", fi.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// A corrupt bookkeeping file costs one pass of memory, never the feature.
|
|
func TestACorruptStateFileDoesNotStopTheDelta(t *testing.T) {
|
|
dir := isolateConfigDir(t)
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "delta-state.json"), []byte("{not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := deltaStateLoad("d")
|
|
if st == nil || st.Version != deltaStateVersion {
|
|
t.Fatalf("a truncated bookkeeping file stopped the plane: %+v", st)
|
|
}
|
|
}
|
|
|
|
// ── the wiring ──────────────────────────────────────────────────────────────
|
|
|
|
// A BOX WITH NO ACCOUNTING MUST NOT READ AS A BOX WITH NOTHING TO REPORT.
|
|
// Zero findings and "nobody ever said what normal is here" are opposite facts
|
|
// and they render identically unless the state travels with the counts.
|
|
func TestABoxWithNoAccountingSaysSoRatherThanReportingZero(t *testing.T) {
|
|
isolateConfigDir(t)
|
|
if !processIsRoot() {
|
|
t.Skip("the pass refuses without machine authority, which is its own test")
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
|
|
if r.Baseline != deltaBaselineNone {
|
|
t.Fatalf("baseline = %q, want %q", r.Baseline, deltaBaselineNone)
|
|
}
|
|
s := r.summary()
|
|
if s.Baseline != deltaBaselineNone {
|
|
t.Errorf("the summary that rides the beat says %q", s.Baseline)
|
|
}
|
|
if len(r.Findings) != 0 {
|
|
t.Errorf("it reported %d findings against an accounting that does not exist", len(r.Findings))
|
|
}
|
|
}
|
|
|
|
// AN ACCOUNTING TAKEN UNDER OTHER FINGERPRINT RULES REPORTS THE WHOLE MACHINE.
|
|
// baselineVersion is bumped exactly so old entries cannot match new ones, which
|
|
// makes the first pass after such a bump 188 findings that are all one fact
|
|
// about a software upgrade. One sentence asking for a retake is the answer.
|
|
func TestAnAccountingFromOlderRulesIsNotComparedAtAll(t *testing.T) {
|
|
dir := isolateConfigDir(t)
|
|
if !processIsRoot() {
|
|
t.Skip("the pass refuses without machine authority, which is its own test")
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := baselineFile{Version: "privatae-baseline-v0", TakenAt: "2026-01-01T00:00:00Z",
|
|
Coverage: "processes=all,schedulers=all",
|
|
Entries: []baselineEntry{procEntry("aaaa", "root", "svc", "thing")}}
|
|
if err := baselineSave(old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
|
|
if r.Baseline != deltaBaselineStale {
|
|
t.Fatalf("baseline = %q, want %q", r.Baseline, deltaBaselineStale)
|
|
}
|
|
if len(r.Findings) != 0 {
|
|
t.Fatalf("it reported %d findings against an accounting it cannot compare with", len(r.Findings))
|
|
}
|
|
if !strings.Contains(strings.Join(r.Notes, " "), "taken again") {
|
|
t.Errorf("the notes do not say what to do about it: %v", r.Notes)
|
|
}
|
|
}
|
|
|
|
// The delta refuses to run without machine authority rather than producing a
|
|
// page of false findings from a view of the box the accounting never had.
|
|
func TestWithoutMachineAuthorityThePassSaysSoInsteadOfGuessing(t *testing.T) {
|
|
if processIsRoot() {
|
|
t.Skip("this test is about the unprivileged path; run the suite as a user for it")
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
|
|
if r.Baseline != deltaBaselineNoAuth {
|
|
t.Fatalf("baseline = %q, want %q", r.Baseline, deltaBaselineNoAuth)
|
|
}
|
|
}
|
|
|
|
// THE SUMMARY REACHES THE HOST. A detection plane whose result never leaves the
|
|
// box is a detection plane nobody has. This holds the wiring — that
|
|
// buildHeartbeatBody actually carries what deltaPayload publishes — rather than
|
|
// only that the two functions exist.
|
|
func TestTheFindingsSummaryRidesTheHeartbeat(t *testing.T) {
|
|
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
|
deltaMu.Lock()
|
|
deltaLast = &deltaSummary{Baseline: deltaBaselineOK, High: 2, Critical: 1,
|
|
NewSince: "2026-09-14T22:00:00Z"}
|
|
deltaMu.Unlock()
|
|
t.Cleanup(func() {
|
|
deltaMu.Lock()
|
|
deltaLast = nil
|
|
deltaMu.Unlock()
|
|
})
|
|
body := buildHeartbeatBody("", "box", "linux", "test", "pub", "fp", "", "", false, nil, nil, nil)
|
|
for _, want := range []string{`"findings"`, `"critical":1`, `"high":2`, `"baseline":"ok"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("the heartbeat does not carry %s:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before the first pass completes there is no key at all, the way posture does
|
|
// it — an absent field is a host that keeps what it had, and a zeroed one is a
|
|
// host told this machine is clean.
|
|
func TestNoSummaryTravelsBeforeTheFirstPass(t *testing.T) {
|
|
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
|
deltaMu.Lock()
|
|
deltaLast = nil
|
|
deltaMu.Unlock()
|
|
if body := buildHeartbeatBody("", "box", "linux", "test", "pub", "fp", "", "", false, nil, nil, nil); strings.Contains(body, `"findings"`) {
|
|
t.Fatalf("a findings key travelled before anything had been measured:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// ── /proc's byte layout ─────────────────────────────────────────────────────
|
|
|
|
// The comm field is parenthesised and may contain spaces AND parens, which is
|
|
// why the fields are counted from the last ')'. A process named ") (" is legal
|
|
// and is how a naive split gets the wrong number.
|
|
func TestTheProcessStartTimeIsReadFromTheRightField(t *testing.T) {
|
|
// pid comm state ppid ... starttime is field 22.
|
|
fields := []string{"2", "1", "1", "0", "0", "0", "0", "0", "0", "0",
|
|
"0", "0", "0", "0", "0", "0", "0", "0", "914623"}
|
|
line := "1234 (weird ) name) S " + strings.Join(fields, " ") + " 0 0 0"
|
|
got, ok := parseProcStatStartTicks(line)
|
|
if !ok || got != 914623 {
|
|
t.Fatalf("start ticks = %d (%v), want 914623 — a wrong field here makes every "+
|
|
"process read as the wrong age", got, ok)
|
|
}
|
|
if _, ok := parseProcStatStartTicks("garbage with no paren"); ok {
|
|
t.Error("a line with no comm field read as a valid start time")
|
|
}
|
|
if _, ok := parseProcStatStartTicks("1 (x) S 0 0"); ok {
|
|
t.Error("a truncated line read as a valid start time")
|
|
}
|
|
}
|
|
|
|
// ── what a second look found ────────────────────────────────────────────────
|
|
|
|
// A SECOND PROGRAM BESIDE THE FIRST IS NOT A REPLACEMENT OF IT. The slot rule
|
|
// fired on "the slot is known" alone, so a venv or pyenv interpreter starting
|
|
// the same script beside the system one — same user, same unit, same masked
|
|
// argv — came out as proc_changed at high, carrying `was:` a process that was
|
|
// still running. A doorbell that rings for a benign event is what the noise
|
|
// filter exists to prevent, and the sentence it rang with was not true.
|
|
func TestASecondProgramBesideTheOriginalIsNewNotAReplacement(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "python3 x.py")
|
|
second := procEntry("bbbb", "root", "web.service", "python3 x.py")
|
|
// Both running: the original is still there.
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(was, 1), liveOf(second, 2)})
|
|
if n := len(eventsOfKind(evs, deltaProcChanged)); n != 0 {
|
|
t.Fatalf("%d proc_changed while the original was still running: %+v", n, evs)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcNew)); n != 1 {
|
|
t.Fatalf("got %d proc_new, want 1: %+v", n, evs)
|
|
}
|
|
// And when the original HAS gone, it is a replacement again.
|
|
evs = deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(second, 2)})
|
|
if n := len(eventsOfKind(evs, deltaProcChanged)); n != 1 {
|
|
t.Fatalf("with the original gone this must be one proc_changed, got %+v", evs)
|
|
}
|
|
}
|
|
|
|
// THE OTHER HALF OF THE SAME RULE: an entry that leaves must still be reported
|
|
// gone when something ELSE happens to share its slot. Suppressing on "is
|
|
// anything standing here" lost the departure entirely.
|
|
func TestOneOfTwoEntriesSharingASlotLeavingIsStillReported(t *testing.T) {
|
|
a := procEntry("aaaa", "root", "web.service", "python3 x.py")
|
|
b := procEntry("bbbb", "root", "web.service", "python3 x.py")
|
|
// b stays, a goes. They share a slot; a's absence is its own fact.
|
|
evs := deltaCompare([]baselineEntry{a, b}, []deltaLiveEntry{liveOf(b, 2)})
|
|
gone := eventsOfKind(evs, deltaProcGone)
|
|
if len(gone) != 1 || gone[0].FP != a.FP {
|
|
t.Fatalf("the entry that left was not reported gone: %+v", evs)
|
|
}
|
|
}
|
|
|
|
// THE PACKAGE DATABASE EXPLAINS THE BINARY, NEVER THE ENDPOINT. apt upgrades
|
|
// node, the service that runs it restarts, and it now talks to api.openai.com:
|
|
// the proc_changed branch demoted on the tier alone and landed that at notice,
|
|
// while the identical situation as a proc_new came out high. Two branches
|
|
// asking the same question about the same machine have to answer the same
|
|
// way.
|
|
func TestAPackagedReplacementTalkingToAModelIsStillHigh(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "node server.js")
|
|
now := procEntry("bbbb", "root", "web.service", "node server.js")
|
|
now.Endpoints = []string{"api.openai.com"}
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 9)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("got %+v", evs)
|
|
}
|
|
// The event has to CARRY the endpoint, or no tier could see it.
|
|
if len(changed[0].Added) != 1 || changed[0].Added[0] != "api.openai.com" {
|
|
t.Fatalf("the replacement event does not name the model endpoint it gained: %+v",
|
|
changed[0].Added)
|
|
}
|
|
ev := changed[0]
|
|
ev.Tier = tierKnown // the distribution shipped the new binary
|
|
if got := deltaLevel(ev); got != deltaHigh {
|
|
t.Fatalf("a PACKAGED replacement that also began talking to a model levelled %q, "+
|
|
"want high. The package database explains the binary; it says nothing about "+
|
|
"what the program is doing.", got)
|
|
}
|
|
}
|
|
|
|
// THE TWO SIDES OF THE COMPARISON AGGREGATE THE SAME WAY. The accounting kept
|
|
// the first pid's endpoints and the delta's live side unioned them, so four
|
|
// workers of one service with only the fourth talking to a model recorded no
|
|
// endpoint at baseline and then reported proc_changed[llm_endpoint] at high on
|
|
// an unchanged machine, every pass until the accounting was retaken.
|
|
func TestTheAccountingUnionsEndpointsTheWayTheDeltaDoes(t *testing.T) {
|
|
obs := []agentObs{
|
|
{PID: 1, User: "root", Cmdline: "worker --port 3001", Origin: "service"},
|
|
{PID: 2, User: "root", Cmdline: "worker --port 3002", Origin: "service",
|
|
LLMEndpoints: []string{"api.openai.com"}},
|
|
}
|
|
entries, _ := baselineFromObservations(obs, func(string) (string, string) { return "d", "" })
|
|
if len(entries) != 1 {
|
|
t.Fatalf("two workers of one service made %d entries", len(entries))
|
|
}
|
|
if len(entries[0].Endpoints) != 1 || entries[0].Endpoints[0] != "api.openai.com" {
|
|
t.Fatalf("the accounting recorded %v — the endpoint belonged to the SECOND pid and "+
|
|
"was dropped, so the next delta reports it as newly appeared on an "+
|
|
"unchanged machine", entries[0].Endpoints)
|
|
}
|
|
// Which is exactly what the live side would build from the same input.
|
|
live, _, _ := deltaCollectLiveFrom(obs, func(string) (string, string) { return "d", "" })
|
|
if len(live) != 1 || len(live[0].Entry.Endpoints) != 1 {
|
|
t.Fatalf("the live side built %+v", live)
|
|
}
|
|
if evs := deltaCompare(entries, live); len(evs) != 0 {
|
|
t.Fatalf("the same machine compared against itself produced %d events: %+v", len(evs), evs)
|
|
}
|
|
}
|
|
|
|
// THE MODEL ENDPOINT REACHES THE WALK AT ALL. The unfiltered /proc walk carries
|
|
// pid, user, argv, exe, unit and parent and no endpoints; only the observe
|
|
// plane resolves those. So on Linux — the one platform WITH the full walk —
|
|
// both sides of every comparison held an empty list and the two rules that rest
|
|
// on it could never fire.
|
|
func TestTheWalkCarriesTheModelEndpointsItIsJudgedOn(t *testing.T) {
|
|
obs := []agentObs{{PID: 41, User: "user", Cmdline: "python3 agent.py"}}
|
|
got := baselineAttachEndpoints(obs, map[int][]string{41: {"api.anthropic.com"}})
|
|
if len(got[0].LLMEndpoints) != 1 {
|
|
t.Fatal("the join dropped the endpoint, so every level that depends on one is dead")
|
|
}
|
|
// And on this machine, for real: whatever the observe plane can see, the
|
|
// walk the delta is built from must also see.
|
|
seen := map[int][]string{}
|
|
agentsNow, err := scanAgents()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, a := range agentsNow {
|
|
if len(a.LLMEndpoints) > 0 {
|
|
seen[a.PID] = a.LLMEndpoints
|
|
}
|
|
}
|
|
if len(seen) == 0 {
|
|
t.Skip("nothing on this box is talking to a model right now")
|
|
}
|
|
walked, complete, err := baselineWalk()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !complete {
|
|
t.Skip("this platform has no unfiltered walk, so there is nothing to enrich")
|
|
}
|
|
found := 0
|
|
for _, a := range walked {
|
|
if _, ok := seen[a.PID]; ok && len(a.LLMEndpoints) > 0 {
|
|
found++
|
|
}
|
|
}
|
|
if found == 0 {
|
|
t.Fatalf("the observe plane sees %d processes talking to a model and the walk the "+
|
|
"delta compares carries none of them", len(seen))
|
|
}
|
|
}
|
|
|
|
// THE DAEMON READS ITS BINARY FROM DISK. selfDigest() is a sync.Once frozen at
|
|
// process start (enforcing.go:35-38), so a long-running daemon compared its own
|
|
// startup digest for ever and could never observe its binary being replaced —
|
|
// the one process in a position to notice, unable to.
|
|
func TestTheBastionBinaryIsReadFromDiskAndNotFromAFrozenDigest(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "privatae-bastion")
|
|
if err := os.WriteFile(p, []byte("version one"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := deltaSelfPath
|
|
deltaSelfPath = p
|
|
t.Cleanup(func() { deltaSelfPath = old })
|
|
|
|
dc := &digestCache{}
|
|
before := deltaSelfOnDisk(dc)
|
|
if before == "" {
|
|
t.Fatal("the binary was not digested at all")
|
|
}
|
|
// Replaced the way replaceSelf does it: the old one moved aside, a new one
|
|
// written at the same path (update.go:160-169).
|
|
if err := os.Rename(p, p+".old"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte("version two"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
after := deltaSelfOnDisk(dc)
|
|
if after == before {
|
|
t.Fatal("the bastion read the same digest after its binary was replaced under it. " +
|
|
"A frozen digest, or one taken from /proc/self/exe — which follows the inode " +
|
|
"to the file that was moved ASIDE — both read like this.")
|
|
}
|
|
}
|
|
|
|
// ONE PASS PER BOX, NOT ONE PER PAIRING. run.go starts a refresher beside every
|
|
// heartbeat, so a box serving three hosts ran three full passes a minute and
|
|
// three read-modify-writers of one state file — and whether a 40-second process
|
|
// was counted or reported depended on how many hosts the box served.
|
|
func TestASecondRefresherDoesNotRunItsOwnPass(t *testing.T) {
|
|
deltaMu.Lock()
|
|
oldLast, oldAt, oldRan := deltaLast, deltaAt, deltaRan
|
|
deltaLast, deltaAt, deltaRan = &deltaSummary{Baseline: "sentinel"}, time.Now(), true
|
|
deltaMu.Unlock()
|
|
t.Cleanup(func() {
|
|
deltaMu.Lock()
|
|
deltaLast, deltaAt, deltaRan = oldLast, oldAt, oldRan
|
|
deltaMu.Unlock()
|
|
})
|
|
refreshDelta()
|
|
deltaMu.Lock()
|
|
got := deltaLast.Baseline
|
|
deltaMu.Unlock()
|
|
if got != "sentinel" {
|
|
t.Fatalf("the second refresher ran its own pass (published %q). N pairings would "+
|
|
"mean N passes a minute over one state file.", got)
|
|
}
|
|
}
|
|
|
|
// SOMETHING THAT IS ALWAYS YOUNG IS A PATTERN, NOT SHORT-LIVED WORK. A
|
|
// respawner — a crash-looping unit, a beacon relaunched every few seconds — was
|
|
// young at every pass, so it was re-held at every pass and, being seen each
|
|
// time, never settled into the short-lived count either: no finding, and a
|
|
// storm counter reading zero.
|
|
func TestARespawnerStopsBeingHeldAndBecomesAFinding(t *testing.T) {
|
|
st := freshState(t)
|
|
ev := deltaEvent{Kind: deltaProcNew, FP: "fp1", PIDs: []int{7},
|
|
Entry: procEntry("aaaa", "user", "interactive", "beacon")}
|
|
n := deltaNoise{Self: 1, Parents: map[int]int{7: 2}, Sockets: true,
|
|
Age: func(int) (time.Duration, bool) { return time.Second, true }}
|
|
|
|
start := time.Now()
|
|
for i := 0; i < 4; i++ {
|
|
findings, _ := deltaFilter([]deltaEvent{ev}, n, st, start.Add(time.Duration(i)*time.Minute))
|
|
if len(findings) != 0 {
|
|
t.Fatalf("pass %d reported it before the hold ceiling", i)
|
|
}
|
|
}
|
|
findings, _ := deltaFilter([]deltaEvent{ev}, n, st, start.Add(deltaHoldMax+time.Minute))
|
|
if len(findings) != 1 {
|
|
t.Fatal("a process that has been 'about to go away' for longer than the ceiling " +
|
|
"is still being held. It is never counted either, because it is seen every pass.")
|
|
}
|
|
var said bool
|
|
for _, e := range findings[0].Evidence {
|
|
if e.Signal == "respawning" {
|
|
said = true
|
|
}
|
|
}
|
|
if !said {
|
|
t.Error("the finding does not say WHY it arrived late — that it kept coming back")
|
|
}
|
|
}
|
|
|
|
// THE WIRING OF THE BASTION-BINARY RUNG, DRIVEN THROUGH A REAL PASS.
|
|
//
|
|
// The two unit tests above hold deltaSelfOnDisk and deltaBinaryEvent in
|
|
// isolation, and a mutation that pointed deltaPass back at selfDigest() — or
|
|
// back at the accounting's frozen version — passed both of them. That is the
|
|
// defect shape this whole plane keeps being caught by: the claim asserted at a
|
|
// layer nothing wires to. So this drives the pass itself, three times, and
|
|
// watches the rung move.
|
|
func TestThePassReadsTheBinaryFromDiskAndRemembersWhatItSaw(t *testing.T) {
|
|
if !processIsRoot() {
|
|
t.Skip("a pass needs machine authority, which is its own test")
|
|
}
|
|
isolateConfigDir(t)
|
|
|
|
fixture := filepath.Join(t.TempDir(), "privatae-bastion")
|
|
writeBinary := func(body string) string {
|
|
t.Helper()
|
|
if _, err := os.Stat(fixture); err == nil {
|
|
// Replaced the way replaceSelf does it (update.go:160-169).
|
|
if err := os.Rename(fixture, fixture+".old"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(fixture, []byte(body), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, _ := baselineExeDigest(fixture)
|
|
return d
|
|
}
|
|
oldPath, oldVersion := deltaSelfPath, clientVersion
|
|
deltaSelfPath = fixture
|
|
t.Cleanup(func() { deltaSelfPath, clientVersion = oldPath, oldVersion })
|
|
|
|
// An accounting taken by 0.27.0, with that binary on disk.
|
|
clientVersion = "0.27.0"
|
|
first := writeBinary("version one")
|
|
base, _, _, _, err := collectBaseline()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base.BastionDigest, base.BastionVersion = first, "0.27.0"
|
|
if err := baselineSave(base); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
binaryEvent := func(r deltaReport) *deltaEvent {
|
|
for i := range r.Findings {
|
|
if r.Findings[i].Kind == deltaBinaryChanged {
|
|
return &r.Findings[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
dc := &digestCache{}
|
|
if ev := binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon)); ev != nil {
|
|
t.Fatalf("an unchanged binary produced %+v", ev)
|
|
}
|
|
|
|
// The fleet update: new binary, new version.
|
|
writeBinary("version two")
|
|
clientVersion = "0.28.0"
|
|
ev := binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon))
|
|
if ev == nil {
|
|
t.Fatal("the bastion's binary was replaced on disk and the pass did not notice. " +
|
|
"selfDigest() is frozen at process start, so a pass that asks IT can never see this.")
|
|
}
|
|
if ev.Level != deltaHigh {
|
|
t.Fatalf("the update levelled %q, want high", ev.Level)
|
|
}
|
|
|
|
// And now the swap: new binary, SAME version, after that update.
|
|
writeBinary("version three")
|
|
ev = binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon))
|
|
if ev == nil {
|
|
t.Fatal("the second replacement was not noticed at all")
|
|
}
|
|
if ev.Level != deltaCritical {
|
|
t.Fatalf("a binary replaced under an unchanged version, after an earlier update, "+
|
|
"levelled %q — want critical. Compared against the ACCOUNTING's version this "+
|
|
"reads as one more update for ever, because nothing retakes the accounting "+
|
|
"when the bastion updates itself.", ev.Level)
|
|
}
|
|
|
|
// A FOURTH PASS, WHICH IS WHERE THE DEFECT LIVED. Advancing the remembered
|
|
// pair after reporting a swap makes the swapped binary the new normal, so
|
|
// the critical appears for exactly one beat — into a summary the host does
|
|
// not store — and an operator running observe:delta an hour later is told
|
|
// the machine is clean. A finding this severe is pinned until a human
|
|
// resolves it, which here means retaking the accounting.
|
|
ev = binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon))
|
|
if ev == nil {
|
|
t.Fatal("the swap was reported once and then absorbed. The pass that reports it " +
|
|
"must not record the swapped binary as what the machine last saw.")
|
|
}
|
|
if ev.Level != deltaCritical {
|
|
t.Fatalf("the repeat levelled %q, want critical", ev.Level)
|
|
}
|
|
|
|
// And the UPDATE is not repeated, which is the other half of the same rule:
|
|
// a legitimate new version becomes the new normal exactly once.
|
|
writeBinary("version four")
|
|
clientVersion = "0.29.0"
|
|
if ev = binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon)); ev == nil || ev.Level != deltaHigh {
|
|
t.Fatalf("the second update levelled %+v, want high", ev)
|
|
}
|
|
if ev = binaryEvent(deltaPass(dc, time.Now(), deltaRoleDaemon)); ev != nil {
|
|
t.Fatalf("the update was reported again on the next pass: %+v", ev)
|
|
}
|
|
}
|
|
|
|
// THE SEED FROM THE ACCOUNTING IS WHAT COVERS THE FIRST PASS — on a fresh
|
|
// install, after a retake, after a corrupt state file. Nothing held it: deleting
|
|
// the seed left the whole suite green, and without it
|
|
// the first pass is blind to a swap that happened before it ran.
|
|
func TestTheFirstPassComparesAgainstTheAccounting(t *testing.T) {
|
|
if !processIsRoot() {
|
|
t.Skip("a pass needs machine authority, which is its own test")
|
|
}
|
|
isolateConfigDir(t)
|
|
fixture := filepath.Join(t.TempDir(), "privatae-bastion")
|
|
if err := os.WriteFile(fixture, []byte("the one that is here now"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldPath, oldVersion := deltaSelfPath, clientVersion
|
|
deltaSelfPath, clientVersion = fixture, "0.27.0"
|
|
t.Cleanup(func() { deltaSelfPath, clientVersion = oldPath, oldVersion })
|
|
|
|
// The accounting recorded a DIFFERENT binary under this same version — the
|
|
// swap happened before this process ever ran a pass.
|
|
base, _, _, _, err := collectBaseline()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base.BastionDigest, base.BastionVersion = "a-different-binary-entirely", "0.27.0"
|
|
if err := baselineSave(base); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleDaemon)
|
|
found := false
|
|
for _, ev := range r.Findings {
|
|
if ev.Kind == deltaBinaryChanged {
|
|
found = true
|
|
if ev.Level != deltaCritical {
|
|
t.Errorf("levelled %q, want critical", ev.Level)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("with no prior pass to compare against, the accounting's record is the " +
|
|
"only thing that can catch this, and it was not consulted")
|
|
}
|
|
}
|
|
|
|
// AN OPERATOR'S OWN COMMAND MUST NOT POISON THE DAEMON'S BOOKKEEPING. Running
|
|
// observe:delta from a copy of the binary somewhere else would otherwise record
|
|
// THAT copy as what the machine last saw, and the daemon's next pass reports a
|
|
// swap that never happened.
|
|
func TestAnOperatorPassDoesNotRewriteWhatTheMachineLastSaw(t *testing.T) {
|
|
if !processIsRoot() {
|
|
t.Skip("a pass needs machine authority, which is its own test")
|
|
}
|
|
isolateConfigDir(t)
|
|
fixture := filepath.Join(t.TempDir(), "some-other-copy")
|
|
if err := os.WriteFile(fixture, []byte("a developer's build"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldPath, oldVersion := deltaSelfPath, clientVersion
|
|
deltaSelfPath, clientVersion = fixture, "0.27.0"
|
|
t.Cleanup(func() { deltaSelfPath, clientVersion = oldPath, oldVersion })
|
|
|
|
// The accounting records THIS copy, so the pass finds nothing to report —
|
|
// which is precisely the case where a daemon pass advances the remembered
|
|
// pair. If the role is ignored, this is where the operator's copy gets
|
|
// written down as what the machine last saw.
|
|
mine, _ := baselineExeDigest(fixture)
|
|
base, _, _, _, err := collectBaseline()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base.BastionDigest, base.BastionVersion = mine, "0.27.0"
|
|
if err := baselineSave(base); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// THE KEY THE STATE IS FILED UNDER IS THE SAVED FILE'S DIGEST, not the one
|
|
// on the struct that was handed to baselineSave — that function digests its
|
|
// own copy. Reading it back is the difference between this test checking
|
|
// something and this test always passing.
|
|
saved, _, err := baselineLoad()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st := deltaStateLoad(saved.Digest); st.BastionDigest != "" {
|
|
t.Fatalf("the fixture state was not empty to begin with: %q", st.BastionDigest)
|
|
}
|
|
deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
|
|
if st := deltaStateLoad(saved.Digest); st.BastionDigest != "" {
|
|
t.Fatalf("an operator pass wrote %q into the shared state as what the machine "+
|
|
"last saw. Run from a copy of the binary somewhere else, that is a swap the "+
|
|
"daemon reports on its next pass and nobody made.", st.BastionDigest)
|
|
}
|
|
// And the daemon in the same position DOES advance — otherwise this test
|
|
// would pass against a pass that never writes anything at all.
|
|
deltaPass(&digestCache{}, time.Now(), deltaRoleDaemon)
|
|
if st := deltaStateLoad(saved.Digest); st.BastionDigest != mine {
|
|
t.Fatalf("the daemon's pass recorded %q, want the binary it actually has", st.BastionDigest)
|
|
}
|
|
}
|
|
|
|
// A v1 ACCOUNTING CANNOT BE COMPARED, because its entries mean something
|
|
// different: on Linux it holds no model endpoints for anything, while a walk
|
|
// now does. The fingerprints still match, so nothing else would have caught it
|
|
// — every known process talking to a model would report proc_changed
|
|
// [llm_endpoint] at high until somebody retook it.
|
|
func TestTheAccountingVersionMovedWithItsMeaning(t *testing.T) {
|
|
dir := isolateConfigDir(t)
|
|
if !processIsRoot() {
|
|
t.Skip("a pass needs machine authority, which is its own test")
|
|
}
|
|
if baselineVersion == "privatae-baseline-v1" {
|
|
t.Fatal("the endpoint join changed what an entry MEANS without moving the version, " +
|
|
"so a baseline taken before it compares as a page of model-endpoint findings")
|
|
}
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := baselineFile{Version: "privatae-baseline-v1", TakenAt: "2026-09-14T23:51:04Z",
|
|
Coverage: "processes=all,schedulers=all",
|
|
Entries: []baselineEntry{procEntry("aaaa", "root", "svc", "python3 x.py")}}
|
|
if err := baselineSave(old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleOperator)
|
|
if r.Baseline != deltaBaselineStale {
|
|
t.Fatalf("a v1 accounting read as %q, want %q", r.Baseline, deltaBaselineStale)
|
|
}
|
|
if len(r.Findings) != 0 {
|
|
t.Fatalf("it produced %d findings against an accounting it cannot compare with", len(r.Findings))
|
|
}
|
|
}
|
|
|
|
// TWO BASELINE ENTRIES AT ONE SLOT, AND ONE OF THEM UPGRADES. Keeping only the
|
|
// first entry per slot made the answer depend on which of them sorted first:
|
|
// the upgraded one either read as a replacement or as an unrelated new program
|
|
// with the untrue line "neither this program nor anything in its place was in
|
|
// the accounting", while its predecessor was separately reported gone. A hash
|
|
// order was choosing between two different sentences.
|
|
func TestAnUpgradeAtASharedSlotIsAReplacementWhicheverWaySorted(t *testing.T) {
|
|
a := procEntry("aaaa", "root", "web.service", "python3 x.py") // system python
|
|
b := procEntry("bbbb", "root", "web.service", "python3 x.py") // a second one
|
|
upgraded := procEntry("cccc", "root", "web.service", "python3 x.py")
|
|
|
|
for _, order := range [][]baselineEntry{{a, b}, {b, a}} {
|
|
// a is still running; b has been upgraded to `upgraded`.
|
|
evs := deltaCompare(order, []deltaLiveEntry{liveOf(a, 1), liveOf(upgraded, 2)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("order %v: got %+v", []string{order[0].ExeDigest, order[1].ExeDigest}, evs)
|
|
}
|
|
if changed[0].Was == nil || changed[0].Was.FP != b.FP {
|
|
t.Errorf("the replacement names the wrong predecessor: %+v", changed[0].Was)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcNew)); n != 0 {
|
|
t.Errorf("order %v: %d proc_new as well", []string{order[0].ExeDigest, order[1].ExeDigest}, n)
|
|
}
|
|
if n := len(eventsOfKind(evs, deltaProcGone)); n != 0 {
|
|
t.Errorf("order %v: %d proc_gone as well", []string{order[0].ExeDigest, order[1].ExeDigest}, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A BINARY REPLACED AND THEN UNLINKED IS THE MOST DELIBERATE VERSION OF THIS,
|
|
// and it digested as empty — the exe path reads " (deleted)", which is stripped,
|
|
// and the stat then fails — so it came out as a visibility notice. The
|
|
// unreadable shortcut now sits BELOW the endpoint check.
|
|
func TestAReplacedBinaryThatWasUnlinkedIsNotJustAVisibilityNotice(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "node server.js")
|
|
gone := procEntry("", "root", "web.service", "node server.js")
|
|
gone.DigestNote = "could not be read by this account"
|
|
gone.Endpoints = []string{"api.openai.com"}
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(gone, 3)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("got %+v", evs)
|
|
}
|
|
if got := deltaLevel(changed[0]); got != deltaHigh {
|
|
t.Fatalf("a binary replaced at a known post, unlinked, and talking to a model "+
|
|
"levelled %q — want high. An unreadable digest is a visibility change only "+
|
|
"when there is nothing else on the event.", got)
|
|
}
|
|
// With nothing else on it, it is still a notice.
|
|
plain := changed[0]
|
|
plain.Added, plain.Entry.Endpoints, plain.Entry.Signature = nil, nil, ""
|
|
if got := deltaLevel(plain); got != deltaNotice {
|
|
t.Errorf("an unreadable digest with nothing else on it levelled %q, want notice", got)
|
|
}
|
|
}
|
|
|
|
// A PACKAGED UPGRADE OF A SERVICE THAT ALREADY TALKED TO A MODEL IS AN APT
|
|
// UPGRADE. Levelling on "has endpoints" rather than "gained endpoints" made
|
|
// every one of those high, which is the noise the tier exists to remove.
|
|
func TestAPackagedUpgradeOfAModelTalkingServiceIsNotHighOnItsOwn(t *testing.T) {
|
|
was := procEntry("aaaa", "root", "web.service", "node server.js")
|
|
was.Endpoints = []string{"api.openai.com"}
|
|
now := procEntry("bbbb", "root", "web.service", "node server.js")
|
|
now.Endpoints = []string{"api.openai.com"} // the same one, still
|
|
evs := deltaCompare([]baselineEntry{was}, []deltaLiveEntry{liveOf(now, 4)})
|
|
changed := eventsOfKind(evs, deltaProcChanged)
|
|
if len(changed) != 1 {
|
|
t.Fatalf("got %+v", evs)
|
|
}
|
|
ev := changed[0]
|
|
if len(ev.Added) != 0 {
|
|
t.Fatalf("an unchanged endpoint list was reported as added: %v", ev.Added)
|
|
}
|
|
ev.Tier = tierKnown // the distribution shipped the new binary
|
|
if got := deltaLevel(ev); got != deltaNotice {
|
|
t.Errorf("a packaged upgrade of a service whose model endpoint did not change "+
|
|
"levelled %q, want notice", got)
|
|
}
|
|
// Unpackaged, the same replacement is still high.
|
|
ev.Tier = ""
|
|
if got := deltaLevel(ev); got != deltaHigh {
|
|
t.Errorf("an unexplained replacement levelled %q, want high", got)
|
|
}
|
|
}
|
|
|
|
// THE REFRESHERS ARRIVE TOGETHER, because run.go starts every pairing in one
|
|
// loop and each calls refreshDelta immediately. A freshness check alone lets
|
|
// all of them past — none has finished yet — so a three-host box ran three
|
|
// concurrent walks of the machine and three writers of one state file.
|
|
func TestRefreshersThatStartTogetherRunOnePass(t *testing.T) {
|
|
// Isolated because the mutation this test exists to kill makes it run a REAL
|
|
// pass — which would write this machine's own delta-state.json, as root,
|
|
// from a test. A test that only misbehaves when
|
|
// it is doing its job is still a test that misbehaves.
|
|
isolateConfigDir(t)
|
|
deltaMu.Lock()
|
|
oldLast, oldAt, oldRan, oldRunning := deltaLast, deltaAt, deltaRan, deltaRunning
|
|
deltaLast, deltaAt, deltaRan, deltaRunning = nil, time.Time{}, false, true
|
|
deltaMu.Unlock()
|
|
t.Cleanup(func() {
|
|
deltaMu.Lock()
|
|
deltaLast, deltaAt, deltaRan, deltaRunning = oldLast, oldAt, oldRan, oldRunning
|
|
deltaMu.Unlock()
|
|
})
|
|
// A pass is in flight. Every other refresher must return at once rather
|
|
// than starting its own — the freshness check cannot help, because nothing
|
|
// has been published yet.
|
|
done := make(chan struct{})
|
|
go func() { refreshDelta(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("a second refresher started its own pass while one was already running")
|
|
}
|
|
deltaMu.Lock()
|
|
published := deltaLast
|
|
deltaMu.Unlock()
|
|
if published != nil {
|
|
t.Fatal("the second refresher published a result of its own")
|
|
}
|
|
}
|
|
|
|
// A v1 ACCOUNTING HAS NOT BEEN EDITED, and both places that report on a
|
|
// baseline's integrity said it had. baselineDigestOf prefixes with the CURRENT
|
|
// version constant, so every file taken under older rules recomputes to a
|
|
// different digest — which after the v2 bump would have read as tampering on
|
|
// every machine holding one, in the one place a person goes to check exactly
|
|
// that.
|
|
func TestAnAccountingFromOlderRulesIsNotCalledTamperedWith(t *testing.T) {
|
|
old := &baselineFile{Version: "privatae-baseline-v1", TakenAt: "2026-09-14T23:51:04Z"}
|
|
note := baselineIntegrityNote(old, false)
|
|
if note == "" {
|
|
t.Fatal("a v1 accounting produced no note at all")
|
|
}
|
|
if strings.Contains(note, "has been changed since it was taken") {
|
|
t.Errorf("the note accuses somebody of editing it: %q", note)
|
|
}
|
|
if !strings.Contains(note, "NOT evidence") {
|
|
t.Errorf("the note does not rule out tampering, which is the whole reason it "+
|
|
"exists — a person reading it still has to wonder: %q", note)
|
|
}
|
|
if !strings.Contains(note, "taken again") {
|
|
t.Errorf("the note does not say what to do about it: %q", note)
|
|
}
|
|
// A file under the CURRENT rules whose digest does not match HAS been
|
|
// edited, and must still say so — that is the whole point of the digest.
|
|
cur := &baselineFile{Version: baselineVersion, TakenAt: "2026-09-15T00:00:00Z"}
|
|
if note := baselineIntegrityNote(cur, false); !strings.Contains(note, "changed since it was taken") {
|
|
t.Errorf("a tampered file under the current rules reads %q", note)
|
|
}
|
|
if note := baselineIntegrityNote(cur, true); note != "" {
|
|
t.Errorf("an intact file produced a warning: %q", note)
|
|
}
|
|
}
|
|
|
|
// A BASTION THAT CANNOT READ ITS OWN BINARY MUST NOT READ LIKE ONE THAT
|
|
// CHECKED. An empty digest produces no event, and silence is indistinguishable
|
|
// from "checked, unchanged" — the same quiet-versus-blind rule this plane
|
|
// applies to the accounting.
|
|
func TestABastionThatCannotReadItsOwnBinarySaysSo(t *testing.T) {
|
|
if !processIsRoot() {
|
|
t.Skip("a pass needs machine authority, which is its own test")
|
|
}
|
|
isolateConfigDir(t)
|
|
oldPath := deltaSelfPath
|
|
deltaSelfPath = filepath.Join(t.TempDir(), "not-here")
|
|
t.Cleanup(func() { deltaSelfPath = oldPath })
|
|
|
|
base, _, _, _, err := collectBaseline()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
base.BastionDigest, base.BastionVersion = "something-was-recorded", "0.27.0"
|
|
if err := baselineSave(base); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := deltaPass(&digestCache{}, time.Now(), deltaRoleDaemon)
|
|
for _, ev := range r.Findings {
|
|
if ev.Kind == deltaBinaryChanged {
|
|
t.Fatalf("an unreadable binary produced a change event: %+v", ev)
|
|
}
|
|
}
|
|
if !strings.Contains(strings.Join(r.Notes, " "), "could not read its own binary") {
|
|
t.Fatalf("the pass reported nothing about its own binary and said nothing about "+
|
|
"why. That reads exactly like 'checked, unchanged': %v", r.Notes)
|
|
}
|
|
}
|
|
|
|
// AND BOTH COMMANDS THAT REPORT ON IT ACTUALLY USE THAT SENTENCE. The note
|
|
// being right is worth nothing if the two places a person reads it compose
|
|
// their own — which is how the pair drifted apart in the first place. Rewriting
|
|
// either one to print its own wording leaves this red.
|
|
func TestBothBaselineCommandsSayTheRulesMovedRatherThanTampering(t *testing.T) {
|
|
isolateConfigDir(t)
|
|
writeV1Accounting(t)
|
|
|
|
// `observe:baseline:show` prints it.
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stdout := os.Stdout
|
|
os.Stdout = w
|
|
showErr := cmdObserveBaselineShow()
|
|
os.Stdout = stdout
|
|
_ = w.Close()
|
|
var buf strings.Builder
|
|
if _, err := io.Copy(&buf, r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if showErr != nil {
|
|
t.Fatal(showErr)
|
|
}
|
|
shown := buf.String()
|
|
if strings.Contains(shown, "has been changed since it was taken") {
|
|
t.Errorf("observe:baseline:show tells the person their accounting was tampered "+
|
|
"with when the version constant is what moved:\n%s", shown)
|
|
}
|
|
if !strings.Contains(shown, "taken again") {
|
|
t.Errorf("observe:baseline:show does not say what to do:\n%s", shown)
|
|
}
|
|
|
|
// `observe:baseline` without --force refuses, and names the same state.
|
|
if !processIsRoot() {
|
|
return // the take path needs machine authority; the show half stands alone
|
|
}
|
|
takeErr := cmdObserveBaseline(false)
|
|
if takeErr == nil {
|
|
t.Fatal("taking a second accounting without --force was allowed")
|
|
}
|
|
if strings.Contains(takeErr.Error(), "has been changed since it was taken") {
|
|
t.Errorf("observe:baseline accuses somebody of editing it: %v", takeErr)
|
|
}
|
|
if !strings.Contains(takeErr.Error(), "taken again") {
|
|
t.Errorf("observe:baseline does not say the rules moved: %v", takeErr)
|
|
}
|
|
}
|
|
|
|
// AND IT DOES NOT CRASH ON THE FILE IT EXISTS TO INSPECT. `observe:baseline:show`
|
|
// truncated every digest to twelve characters, which a real sha256 always
|
|
// survives and a hand-edited or truncated file does not — so the one command
|
|
// for looking at an accounting you suspect has been tampered with panicked on
|
|
// exactly that accounting. Found by the test above, written for something else.
|
|
func TestShowingATruncatedAccountingDoesNotCrash(t *testing.T) {
|
|
isolateConfigDir(t)
|
|
f := baselineFile{Version: baselineVersion, TakenAt: "2026-09-15T00:00:00Z",
|
|
Coverage: "processes=all,schedulers=all", Entries: []baselineEntry{
|
|
procEntry("aa", "root", "svc", "short"),
|
|
procEntry("", "root", "svc", "none"),
|
|
procEntry(strings.Repeat("f", 64), "root", "svc", "full"),
|
|
}}
|
|
if err := baselineSave(f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmdObserveBaselineShow(); err != nil {
|
|
t.Fatalf("showing an accounting with a short digest failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// writeV1Accounting writes an accounting the way a v1 BINARY would have: its
|
|
// digest computed under the v1 constant, so it is intact by its own rules and
|
|
// mismatched by ours. baselineSave cannot make this fixture — it re-digests
|
|
// under the current constant, which produces a file no v1 bastion ever wrote
|
|
// and which reads as perfectly intact.
|
|
func writeV1Accounting(t *testing.T) {
|
|
t.Helper()
|
|
const v1 = "privatae-baseline-v1"
|
|
f := baselineFile{Version: v1, TakenAt: "2026-09-14T23:51:04Z",
|
|
Coverage: "processes=all,schedulers=all",
|
|
Entries: []baselineEntry{procEntry("aaaa", "root", "svc", "thing")}}
|
|
b, err := json.Marshal(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(append([]byte(v1+"\x00"), b...))
|
|
f.Digest = hex.EncodeToString(sum[:])
|
|
out, err := json.MarshalIndent(f, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p, err := baselinePath()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, out, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// It must be intact under ITS OWN rules and mismatched under ours, or this
|
|
// fixture is not the situation being tested.
|
|
got, ok, err := baselineLoad()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok {
|
|
t.Fatal("the v1 fixture reads as digest-intact under v2, so nothing below is " +
|
|
"exercising the note at all")
|
|
}
|
|
if got.Version != v1 {
|
|
t.Fatalf("fixture version is %q", got.Version)
|
|
}
|
|
}
|