package capability import ( "context" "os" ) // Status is one capability's live result — the wire shape the agent attaches to its hub report // (HostReport.Capabilities). The hub mirrors this struct field-for-field and keys its alert on // Critical+degraded. Reason is empty when ok. type Status struct { Name string `json:"name"` Feature string `json:"feature"` Critical bool `json:"critical"` Status string `json:"status"` // "ok" | "degraded" Reason string `json:"reason,omitempty"` } const ( StatusOK = "ok" StatusDegraded = "degraded" ) // Runner is the minimal exec seam the probe needs (satisfied by proxmox.ExecRunner). The probe // runs `sudo -n -l -- ` LITERALLY — a sudo POLICY LIST that never executes the // command — so the Runner MUST be a DIRECT runner (RunnerDirect), not the sudo-prepending one // (else it would double-sudo). exit 0 ⇔ the command is permitted under the NOPASSWD allowlist. type Runner interface { Run(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error) } // Prober checks the manifest against the live host. Exists defaults to an os.Stat check on the // absolute binary path (what `command -v` would resolve for an absolute path) when nil. type Prober struct { Runner Runner Exists func(path string) bool // nil → os.Stat } // Probe lists every manifest capability against the sudo policy and checks its binary exists, // mapping to ok/degraded (§8 of the spec). It NEVER executes a probed command and NEVER returns a // fatal error (serve-degraded): a probe failure is reported, not raised. If sudo itself is // unavailable for the agent (the drop-in is missing / the user has no sudo at all), it collapses // to ONE aggregate degraded signal instead of N identical ones. func (p Prober) Probe(ctx context.Context) []Status { exists := p.Exists if exists == nil { exists = func(path string) bool { _, err := os.Stat(path); return err == nil } } caps := Manifest() // Preflight: a bare `sudo -n -l` lists the user's allowed commands. For our NOPASSWD service // user it exits 0; if it fails, the drop-in isn't installed (or sudo is gone) and EVERY vector // would individually fail — collapse to one aggregate signal so the operator gets one alert. if p.Runner != nil { if _, _, err := p.Runner.Run(ctx, "sudo", "-n", "-l"); err != nil { return []Status{{ Name: "sudo", Feature: "the entire privileged surface (mount/format/pct/dnsmasq/lxc-info)", Critical: true, Status: StatusDegraded, Reason: "sudoers drop-in not installed / sudo unavailable", }} } } out := make([]Status, 0, len(caps)) for _, c := range caps { s := Status{Name: c.Name, Feature: c.Feature, Critical: c.Critical, Status: StatusOK} switch { case !exists(c.Binary): s.Status, s.Reason = StatusDegraded, "binary not found" case p.Runner != nil && !p.granted(ctx, c): s.Status, s.Reason = StatusDegraded, "sudo policy denied" } out = append(out, s) } return out } // granted reports whether `sudo -n -l -- ` is permitted (exit 0). List-mode is // side-effect-free — the command is matched against the policy, never run. func (p Prober) granted(ctx context.Context, c Capability) bool { args := append([]string{"-n", "-l", "--", c.Binary}, c.ReprArgs...) _, _, err := p.Runner.Run(ctx, "sudo", args...) return err == nil } // Summarize returns (okCount, total, degraded) for logging. degraded lists every non-ok status. func Summarize(statuses []Status) (ok, total int, degraded []Status) { total = len(statuses) for _, s := range statuses { if s.Status == StatusOK { ok++ } else { degraded = append(degraded, s) } } return ok, total, degraded }