bastion 0.54.0
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user