package capability import ( "os" "regexp" "strings" "testing" ) // sudoersPath is the in-repo allowlist, relative to this test file (internal/capability/). const sudoersPath = "../../configs/felhom-agent.sudoers" // parseSudoersEntries returns every command pattern from the Cmnd_Alias blocks, with the sudoers // escapes (`\,` `\:`) unescaped. It joins continuation lines and splits the alias RHS on commas // that are NOT backslash-escaped (escaped commas are literal arg chars, e.g. the lvs `-o` list). func parseSudoersEntries(t *testing.T, text string) []string { t.Helper() // 1. Collapse line continuations, keeping only Cmnd_Alias RHS text. var rhs strings.Builder lines := strings.Split(text, "\n") inAlias := false for _, ln := range lines { trimmed := strings.TrimSpace(ln) if strings.HasPrefix(trimmed, "#") { continue } if strings.HasPrefix(trimmed, "Cmnd_Alias ") { inAlias = true if eq := strings.IndexByte(trimmed, '='); eq >= 0 { trimmed = trimmed[eq+1:] } } else if !inAlias { continue } // The final NOPASSWD line ("felhom-agent ALL=...") ends the alias region. if strings.Contains(trimmed, "ALL=(") { inAlias = false continue } cont := strings.HasSuffix(trimmed, "\\") rhs.WriteString(strings.TrimSuffix(trimmed, "\\")) rhs.WriteString(" ") if !cont { // A non-continued line is the last entry of this alias. Emit a comma so it does not // merge with the next alias's first entry when all RHS text is concatenated. rhs.WriteString(", ") inAlias = false } } // 2. Split on unescaped commas → individual command entries. raw := rhs.String() var entries []string var cur strings.Builder for i := 0; i < len(raw); i++ { c := raw[i] if c == '\\' && i+1 < len(raw) { cur.WriteByte(raw[i+1]) // unescape: keep the next char literally (\, → , ; \: → :) i++ continue } if c == ',' { entries = appendTrimmed(entries, cur.String()) cur.Reset() continue } cur.WriteByte(c) } entries = appendTrimmed(entries, cur.String()) return entries } func appendTrimmed(entries []string, s string) []string { if t := strings.Join(strings.Fields(s), " "); t != "" { return append(entries, t) } return entries } // globToRegex translates a sudoers fnmatch pattern to an anchored regex. It is NOT a perfect sudo // emulator — it only needs to catch a removed/renamed grant (the real failure mode). `*` → `.*`, // `[...]` char classes pass through (valid regex), regex metachars are escaped. func globToRegex(pat string) *regexp.Regexp { var b strings.Builder b.WriteString("^") for i := 0; i < len(pat); i++ { c := pat[i] switch { case c == '*': b.WriteString(".*") case c == '[': // copy the char class verbatim (valid in regex too) if j := strings.IndexByte(pat[i:], ']'); j > 0 { b.WriteString(pat[i : i+j+1]) i += j continue } b.WriteString("\\[") case strings.IndexByte(`.+()|{}^$\?`, c) >= 0: b.WriteByte('\\') b.WriteByte(c) default: b.WriteByte(c) } } b.WriteString("$") return regexp.MustCompile(b.String()) } // matchesAny reports whether cmdline matches at least one sudoers entry pattern. func matchesAny(cmdline string, entries []string) bool { for _, e := range entries { if globToRegex(e).MatchString(cmdline) { return true } } return false } // TestManifestCoveredBySudoers is the headline build-time gate: EVERY manifest capability's // representative command line must be permitted by at least one sudoers pattern. This is exactly // the check that would have caught the lxc-info / make-private grants being dropped at the // 2026-06-28 cutover — in CI, before shipping. func TestManifestCoveredBySudoers(t *testing.T) { data, err := os.ReadFile(sudoersPath) if err != nil { t.Fatalf("read sudoers %s: %v", sudoersPath, err) } entries := parseSudoersEntries(t, string(data)) if len(entries) < 20 { t.Fatalf("parsed only %d sudoers entries — parser likely broke", len(entries)) } for _, c := range Manifest() { cmdline := strings.TrimSpace(c.Binary + " " + strings.Join(c.ReprArgs, " ")) if !matchesAny(cmdline, entries) { t.Errorf("capability %q (%s) NOT covered by any sudoers grant:\n %s", c.Name, c.Feature, cmdline) } } } // TestRedProof_DroppedGrantFailsCheck is the companion red-proof: with the lxc-info line removed // from an in-memory copy of the sudoers, the coverage check for guest-init-pid MUST fail. Proves // the build gate actually catches the regression (a green test that can never go red is hollow). func TestRedProof_DroppedGrantFailsCheck(t *testing.T) { data, err := os.ReadFile(sudoersPath) if err != nil { t.Fatalf("read sudoers: %v", err) } // Drop the lxc-info grant line. var kept []string for _, ln := range strings.Split(string(data), "\n") { if strings.Contains(ln, "lxc-info") { continue } kept = append(kept, ln) } mutated := strings.Join(kept, "\n") if strings.Contains(mutated, "lxc-info") { t.Fatal("setup: lxc-info line not removed") } entries := parseSudoersEntries(t, mutated) var guestInit Capability for _, c := range Manifest() { if c.Name == "guest-init-pid" { guestInit = c } } if guestInit.Name == "" { t.Fatal("manifest missing guest-init-pid") } cmdline := guestInit.Binary + " " + strings.Join(guestInit.ReprArgs, " ") if matchesAny(cmdline, entries) { t.Errorf("red-proof FAILED: guest-init-pid still matches after dropping the lxc-info grant — the build gate would NOT catch the regression") } // Sanity: the UNMUTATED file MUST cover it (so the failure above is specific to the drop). full := parseSudoersEntries(t, string(data)) if !matchesAny(cmdline, full) { t.Errorf("guest-init-pid should be covered by the real sudoers") } } // TestRedProof_DroppedControllerSwapTeeFailsCheck is the companion red-proof for the v0.45.0 // FELHOM_CONTROLLERSWAP grants: with the `tee /etc/felhom-controller-image` line removed, the // controllerswap-write capability MUST be reported uncovered. Proves the build gate watches the new // swap write grant (so dropping it can't ship a non-root agent that silently can't auto-update). func TestRedProof_DroppedControllerSwapTeeFailsCheck(t *testing.T) { data, err := os.ReadFile(sudoersPath) if err != nil { t.Fatalf("read sudoers: %v", err) } var kept []string for _, ln := range strings.Split(string(data), "\n") { if strings.Contains(ln, "tee /etc/felhom-controller-image") { continue } kept = append(kept, ln) } mutated := strings.Join(kept, "\n") entries := parseSudoersEntries(t, mutated) var write Capability for _, c := range Manifest() { if c.Name == "controllerswap-write" { write = c } } if write.Name == "" { t.Fatal("manifest missing controllerswap-write") } cmdline := write.Binary + " " + strings.Join(write.ReprArgs, " ") if matchesAny(cmdline, entries) { t.Errorf("red-proof FAILED: controllerswap-write still matches after dropping the tee grant") } if full := parseSudoersEntries(t, string(data)); !matchesAny(cmdline, full) { t.Errorf("controllerswap-write should be covered by the real sudoers") } } // TestWGCapabilityCriticality pins the exact S4 (v0.66.0) Critical set for the FELHOM_WG entries: // the backup path (conf install, unit enable/restart, handshake read) is operator-alert-worthy now // that offsite backups ride the tunnel; the one-time apt install and the deliberate disable // (revocation) are NOT. Red-proof: flip any one entry's Critical in manifest.go and this fails. func TestWGCapabilityCriticality(t *testing.T) { wantCritical := map[string]bool{ "wg-tools-install": false, "wg-conf-install": true, "wg-enable": true, "wg-restart": true, "wg-disable": false, "wg-handshake-read": true, } seen := map[string]bool{} for _, c := range Manifest() { want, ok := wantCritical[c.Name] if !ok { continue } seen[c.Name] = true if c.Critical != want { t.Errorf("%s: Critical = %v, want %v", c.Name, c.Critical, want) } } for name := range wantCritical { if !seen[name] { t.Errorf("manifest missing wg capability %q", name) } } }