capability: agent privileged-capability self-probe (manifest + build-test + runtime snapshot) v0.44.0
New internal/capability: Manifest of required sudo -n grants + Prober that LISTS each via 'sudo -n -l' (never executes) + binary-exists check → ok/degraded snapshot on the hub report. Build-time test asserts manifest⊆sudoers (red-proof: dropping lxc-info FAILs the gate). Startup logs N/N ok + ERROR per degraded. Serve-degraded; no allowlist change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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 -- <binary> <args…>` 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 -- <binary> <reprArgs…>` 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
|
||||
}
|
||||
Reference in New Issue
Block a user