bastion 0.54.0
ci / personal-data (push) Successful in 4s
ci / fuzz (push) Successful in 2m44s
ci / test (push) Successful in 2m11s

This commit is contained in:
Privatae
2026-10-21 22:29:10 +00:00
parent e4c9b69b46
commit d62da8adb4
58 changed files with 6944 additions and 124 deletions
+83
View File
@@ -0,0 +1,83 @@
//go:build linux
package main
import (
"os"
"path/filepath"
"testing"
)
// The Linux reader end to end over an invented machine: a /proc with two
// processes and an /etc and /run that say who they belong to. Nothing here is
// read from the box the test runs on.
func TestLinuxOwnerFactsReaderOverAFixtureMachine(t *testing.T) {
root := t.TempDir()
write := func(rel, body string) {
t.Helper()
p := filepath.Join(root, rel)
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
write("etc/passwd", "root:x:0:0::/root:/bin/sh\npat:x:1001:1001::/home/pat:/bin/bash\n"+
"kim:x:1002:1002::/home/kim:/bin/bash\nsvcdemo:x:120:120::/nonexistent:/usr/sbin/nologin\n")
write("etc/group", "root:x:0:\nsudo:x:27:pat\npat:x:1001:\nkim:x:1002:\n")
write("etc/sudoers", "root ALL=(ALL:ALL) ALL\n%sudo ALL=(ALL:ALL) ALL\n")
write("etc/nsswitch.conf", "passwd: files\n")
write("run/systemd/users/1001", "UID=1001\nSTATE=active\n")
write("proc/400/status", "Name:\tdemo\nUid:\t1001\t1001\t1001\t1001\nGroups:\t27 1001\n")
write("proc/401/status", "Name:\tdemo\nUid:\t1002\t1002\t1002\t1002\nGroups:\t1002\n")
write("proc/402/status", "Name:\tdemo\nUid:\t120\t120\t120\t120\nGroups:\t\n")
write("proc/403/status", "Name:\tdemo\nUid:\t1002\t0\t1002\t1002\nGroups:\t1002\n")
oldID, oldProc := identityRoot, procRoot
identityRoot, procRoot = root, filepath.Join(root, "proc")
t.Cleanup(func() { identityRoot, procRoot = oldID, oldProc })
read := ownerFactsReader()
for _, c := range []struct {
pid int
user string
want ownerFacts
}{
{400, "pat", ownerFacts{ownerClassLocal, ownerYes, ownerYes}},
{401, "kim", ownerFacts{ownerClassLocal, ownerNo, ownerNo}},
{402, "svcdemo", ownerFacts{ownerClassService, ownerNo, ownerNo}},
// Effective root under kim's account: administrator, class unchanged.
{403, "kim", ownerFacts{ownerClassLocal, ownerYes, ownerNo}},
// A pid with no status file: everything unknown, nothing guessed.
{404, "ghost", unknownOwner},
} {
if got := read(c.pid, c.user); got != c.want {
t.Errorf("pid %d (%s): got %+v, want %+v", c.pid, c.user, got, c.want)
}
}
}
// Every observation scanAgents returns carries the owner facts: the wiring,
// not only the classifier. The one process is this test itself, so the facts
// come from the real /proc and are only checked for being filled in.
func TestScanAgentsCarriesOwnerFacts(t *testing.T) {
prev := scanProcessesFn
t.Cleanup(func() { scanProcessesFn = prev })
self := os.Getpid()
scanProcessesFn = func() (map[int]agentObs, error) {
return map[int]agentObs{self: {PID: self, User: procUser(self), Signature: "claude-code",
Cmdline: "claude", raw: "claude"}}, nil
}
got, err := scanAgents()
if err != nil || len(got) == 0 {
t.Fatalf("scan: %d observations, %v", len(got), err)
}
// The network signal may add this box's own model clients; every
// observation, however it was caught, must carry the facts.
for _, a := range got {
if a.OwnerClass == "" || a.OwnerAdmin == "" || a.OwnerInteractive == "" {
t.Fatalf("owner facts missing from pid %d", a.PID)
}
}
}