Enforce data handling in AI context

This commit is contained in:
rcourtman
2026-04-25 20:41:13 +01:00
parent fc5325ab80
commit dc8208cc00
10 changed files with 408 additions and 16 deletions
@@ -562,6 +562,7 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) {
},
filepath.Join("..", "ai", "resource_context.go"): {
"unifiedresources.ResourcePolicyLabel(",
"unifiedresources.ResourcePolicyRedactedText(",
"unifiedresources.ResourceDisplayName(",
"unifiedresources.ResourceClusterName(",
"unifiedresources.ResourceIPSummary(",
@@ -577,6 +578,7 @@ func TestResourcePolicyLabelHelpersUsedByAIConsumers(t *testing.T) {
"return ResourcePolicyRequiresGovernedSummary(policy)",
"Policy: sensitivity=%s, routing=%s",
"func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ...ResourceRedactionHint) string",
"func ResourcePolicyRedactedText(value string, resource Resource) string",
"const ResourcePolicyRedactedLabel = \"redacted by policy\"",
"func ResourceRedactionLabelsFromHints(hints []ResourceRedactionHint) []string",
"func ResourceClusterName(resource Resource) string",
@@ -416,6 +416,53 @@ func TestResourcePolicyLabelHelpers(t *testing.T) {
}
}
func TestResourcePolicyRedactedText(t *testing.T) {
resource := Resource{
ID: "vm:prod-west:101",
Name: "finance-db",
Type: ResourceTypeVM,
Identity: ResourceIdentity{
Hostnames: []string{"finance-db.internal"},
IPAddresses: []string{"10.10.0.5"},
ClusterName: "prod-west",
},
Canonical: &CanonicalIdentity{
DisplayName: "finance-db",
PlatformID: "prod-west:101",
Aliases: []string{"finance-db-primary"},
},
Storage: &StorageMeta{
Path: "/mnt/pve/finance-db",
},
Policy: &ResourcePolicy{
Sensitivity: ResourceSensitivitySensitive,
Routing: ResourceRoutingPolicy{
Scope: ResourceRoutingScopeLocalFirst,
Redact: []ResourceRedactionHint{
ResourceRedactionHostname,
ResourceRedactionIPAddress,
ResourceRedactionPlatformID,
ResourceRedactionAlias,
ResourceRedactionPath,
},
},
},
}
got := ResourcePolicyRedactedText(
"finance-db at finance-db.internal / 10.10.0.5 moved from prod-west:101 alias finance-db-primary path /mnt/pve/finance-db",
resource,
)
for _, raw := range []string{"finance-db", "finance-db.internal", "10.10.0.5", "prod-west:101", "finance-db-primary", "/mnt/pve/finance-db"} {
if strings.Contains(got, raw) {
t.Fatalf("expected %q to be redacted from %q", raw, got)
}
}
if count := strings.Count(got, ResourcePolicyRedactedLabel); count < 5 {
t.Fatalf("expected redaction markers in %q", got)
}
}
func TestResourceRedactionLabelsFromHints(t *testing.T) {
got := ResourceRedactionLabelsFromHints([]ResourceRedactionHint{
ResourceRedactionPath,
@@ -264,6 +264,47 @@ func ResourcePolicyRedactedValue(value string, policy *ResourcePolicy, hints ...
return value
}
// ResourcePolicyRedactedText replaces exact resource identifiers that the
// canonical resource policy marks for redaction. It is intentionally
// value-based so alert, prompt, and export summaries cannot leak the same raw
// hostname, IP, alias, platform ID, or path through nearby free-form text.
func ResourcePolicyRedactedText(value string, resource Resource) string {
value = strings.TrimSpace(value)
if value == "" || resource.Policy == nil {
return value
}
candidates := resourcePolicyRedactionCandidates(resource)
if len(candidates) == 0 {
return value
}
sort.Slice(candidates, func(i, j int) bool {
if len(candidates[i].value) == len(candidates[j].value) {
return candidates[i].value < candidates[j].value
}
return len(candidates[i].value) > len(candidates[j].value)
})
redacted := value
seen := make(map[string]struct{}, len(candidates))
for _, candidate := range candidates {
if _, ok := seen[candidate.value]; ok {
continue
}
seen[candidate.value] = struct{}{}
if !resourcePolicyShouldRedactCandidate(resource.Policy, candidate.hint) {
continue
}
if len([]rune(candidate.value)) < 3 {
continue
}
redacted = strings.ReplaceAll(redacted, candidate.value, ResourcePolicyRedactedLabel)
}
return redacted
}
// ResourceDisplayName returns the canonical resource display name fallback.
func ResourceDisplayName(resource Resource) string {
if resource.Canonical != nil {
@@ -332,3 +373,56 @@ func ResourcePolicyRequiresGovernedSummary(policy *ResourcePolicy) bool {
}
return len(policy.Routing.Redact) > 0
}
type resourcePolicyRedactionCandidate struct {
hint ResourceRedactionHint
value string
}
func resourcePolicyShouldRedactCandidate(policy *ResourcePolicy, hint ResourceRedactionHint) bool {
if policy == nil {
return false
}
if policy.Routing.Scope == ResourceRoutingScopeLocalOnly {
return true
}
return ResourcePolicyRedacts(policy, hint)
}
func resourcePolicyRedactionCandidates(resource Resource) []resourcePolicyRedactionCandidate {
var candidates []resourcePolicyRedactionCandidate
add := func(hint ResourceRedactionHint, values ...string) {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
candidates = append(candidates, resourcePolicyRedactionCandidate{hint: hint, value: value})
}
}
}
add(ResourceRedactionHostname, resource.Name, canonicalHostname(resource))
add(ResourceRedactionHostname, resource.Identity.Hostnames...)
add(ResourceRedactionIPAddress, resource.Identity.IPAddresses...)
if resource.Canonical != nil {
add(ResourceRedactionHostname, resource.Canonical.DisplayName)
add(ResourceRedactionPlatformID, resource.Canonical.PlatformID, resource.Canonical.PrimaryID)
add(ResourceRedactionAlias, resource.Canonical.Aliases...)
}
add(ResourceRedactionPlatformID, canonicalPlatformID(resource), resource.ID)
add(ResourceRedactionAlias, resource.Identity.ClusterName)
if resource.Proxmox != nil {
add(ResourceRedactionHostname, resource.Proxmox.NodeName)
add(ResourceRedactionAlias, resource.Proxmox.ClusterName)
}
if resource.Kubernetes != nil {
add(ResourceRedactionPlatformID, resource.Kubernetes.ClusterID)
add(ResourceRedactionAlias, resource.Kubernetes.ClusterName, resource.Kubernetes.SourceName)
}
if resource.Storage != nil {
add(ResourceRedactionPath, resource.Storage.Path)
}
return candidates
}