mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 11:15:36 +00:00
Enforce data handling in AI context
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user