diff --git a/internal/audit/audit.go b/internal/audit/audit.go index be30051..d6055de 100644 --- a/internal/audit/audit.go +++ b/internal/audit/audit.go @@ -88,6 +88,14 @@ func Report(ctx context.Context, pool *db.Pool) ([]Finding, Summary) { AND src.state NOT IN ('destroyed','deprecated') AND tgt.state IN ('destroyed','deprecated')`, }, + { + Finding{Category: "polluted_attrs", Severity: "warning", + Evidence: "routing-critical attributes carrying prose (breaks resolution) — e.g. host='hubris (confirmed via pct…')", + SuggestedRunbook: "knowledge-graph-audit"}, + `SELECT slug || ': host=' || (attributes->>'host') FROM entities + WHERE attributes->>'host' IS NOT NULL + AND (attributes->>'host') ~ '[ (]'`, + }, } findings := make([]Finding, 0, len(specs)) diff --git a/internal/remote/remote.go b/internal/remote/remote.go index 956b805..6271ee1 100644 --- a/internal/remote/remote.go +++ b/internal/remote/remote.go @@ -104,6 +104,14 @@ func ResolveHost(ctx context.Context, pool *db.Pool, hostSlug, fallbackUser stri // directly rather than a slug subquery. func ResolveProxmoxHostSlug(ctx context.Context, pool *db.Pool, entityID uuid.UUID, hostAttr string) string { hostSlug := strings.TrimSpace(hostAttr) + // Only trust a clean token as a host name. The attribute is operator/ + // agent-writable and has been polluted with prose before ("hubris + // (confirmed via pct config…)") — using that verbatim produces a slug that + // never resolves. Treat anything with whitespace or parens as invalid and + // fall back to the canonical `hosts` edge below. + if strings.ContainsAny(hostSlug, " \t()") { + hostSlug = "" + } if hostSlug == "" { var relHostSlug string if err := pool.QueryRow(ctx, `