Repair scoped Patrol finding identity safely

This commit is contained in:
rcourtman
2026-08-17 05:01:42 +01:00
parent 39ceeda604
commit 59b0ee188b
3 changed files with 138 additions and 0 deletions
@@ -7609,3 +7609,13 @@ silently ignore the resource selector and return fleet connection counts.
Calls without `resource_id` retain the existing aggregate connection-health
overview for backward compatibility. Provider-native IDs and names remain
lookup aliases; responses preserve the canonical unified-resource ID.
Patrol finding creation keeps scope authoritative while tolerating a bounded
redundant-ID transcription error. When a model submits a `resource_id` that
matches no current runtime identity, core may replace it only when the exact
`resource_name` uniquely identifies a same-type runtime record already admitted
to the requested finding lifecycle scope. The canonical ID from that record
then owns deduplication and persistence. A submitted ID that identifies any
real out-of-scope resource, an ambiguous name, a type mismatch, or a name that
does not resolve inside the lifecycle scope remains rejected. No fuzzy name or
ID matching is permitted at this boundary.
+84
View File
@@ -550,6 +550,14 @@ func newPatrolFindingCreatorAdapterState(p *PatrolService, snap patrolRuntimeSta
}
func (a *patrolFindingCreatorAdapter) CreateFinding(input tools.PatrolFindingInput) (string, bool, error) {
if canonical, repaired := a.canonicalizeUnknownFindingResource(input); repaired {
log.Info().
Str("submitted_resource_id", input.ResourceID).
Str("canonical_resource_id", canonical.ResourceID).
Str("resource_type", canonical.ResourceType).
Msg("AI Patrol: Repaired unknown finding resource ID from exact scoped identity")
input = canonical
}
if !a.findingInCurrentScope(&Finding{ResourceID: input.ResourceID, ResourceName: input.ResourceName}) {
return "", false, fmt.Errorf("resource %s is outside the requested patrol finding scope", input.ResourceID)
}
@@ -665,6 +673,82 @@ func (a *patrolFindingCreatorAdapter) CreateFinding(input tools.PatrolFindingInp
return id, isNew, nil
}
// canonicalizeUnknownFindingResource repairs only a redundant model-authored
// ID transcription error. The submitted ID must match no current resource,
// while the exact submitted name must resolve to one same-type runtime record
// that is already admitted to the finding lifecycle scope. A real out-of-scope
// ID, an ambiguous name, or a type mismatch remains rejected. This preserves
// the scope boundary without making stable finding identity depend on perfect
// copying of a long canonical handle.
func (a *patrolFindingCreatorAdapter) canonicalizeUnknownFindingResource(input tools.PatrolFindingInput) (tools.PatrolFindingInput, bool) {
if a == nil || strings.TrimSpace(input.ResourceID) == "" || strings.TrimSpace(input.ResourceName) == "" {
return input, false
}
if a.findingInCurrentScope(&Finding{ResourceID: input.ResourceID, ResourceName: input.ResourceName}) {
return input, false
}
resourceIDToken := canonicalPatrolScopeToken(input.ResourceID)
resourceNameToken := canonicalPatrolScopeToken(input.ResourceName)
resourceTypeToken := canonicalPatrolScopeToken(input.ResourceType)
type match struct {
canonicalID string
}
matches := make([]match, 0, 1)
submittedIDKnown := false
nameMatchCount := 0
patrolVisitRuntimeResources(a.snap, func(record patrolRuntimeResourceRecord) bool {
for _, value := range append(append([]string(nil), record.ids...), record.aliases...) {
if canonicalPatrolScopeToken(value) == resourceIDToken {
submittedIDKnown = true
}
}
if resourceTypeToken != "" && canonicalPatrolScopeToken(record.resourceType) != resourceTypeToken {
return true
}
nameMatches := false
for _, alias := range record.aliases {
if canonicalPatrolScopeToken(alias) == resourceNameToken {
nameMatches = true
break
}
}
if !nameMatches {
return true
}
nameMatchCount++
recordInScope := len(a.findingScope) == 0
canonicalID := ""
for _, id := range record.ids {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if canonicalID == "" {
canonicalID = id
}
if a.findingScope[id] {
recordInScope = true
canonicalID = id
break
}
}
if recordInScope && canonicalID != "" {
matches = append(matches, match{canonicalID: canonicalID})
}
return true
})
if submittedIDKnown || nameMatchCount != 1 || len(matches) != 1 {
return input, false
}
input.ResourceID = matches[0].canonicalID
return input, true
}
func (a *patrolFindingCreatorAdapter) findingOwnedByAlerts(finding *Finding) bool {
if finding == nil || finding.ResourceType != "app-container" || finding.Category != FindingCategoryReliability {
return false
@@ -1614,6 +1614,50 @@ func TestPatrolFindingAdapterSeparatesRequestedLifecycleScopeFromEvidenceScope(t
}
}
func TestPatrolFindingAdapterRepairsOnlyUnknownIDsFromUniqueScopedIdentity(t *testing.T) {
state := newPatrolRuntimeState(models.StateSnapshot{
Nodes: []models.Node{
{ID: "node-requested", Name: "requested", Status: "online"},
{ID: "node-context", Name: "context-only", Status: "online"},
},
})
adapter := newPatrolFindingCreatorAdapterState(NewPatrolService(nil, nil), state)
adapter.setFindingScope([]string{"node-requested"})
input := tools.PatrolFindingInput{
ResourceID: "node-reques7ed", ResourceName: "requested", ResourceType: "node",
}
repaired, ok := adapter.canonicalizeUnknownFindingResource(input)
if !ok || repaired.ResourceID != "node-requested" {
t.Fatalf("unknown transcription error repair = (%+v, %t), want canonical node-requested", repaired, ok)
}
input.ResourceID = "node-context"
if got, ok := adapter.canonicalizeUnknownFindingResource(input); ok || got.ResourceID != "node-context" {
t.Fatalf("real out-of-scope ID must not be redirected by a scoped name: (%+v, %t)", got, ok)
}
input.ResourceID = "node-reques7ed"
input.ResourceType = "vm"
if _, ok := adapter.canonicalizeUnknownFindingResource(input); ok {
t.Fatal("resource type mismatch must not be repaired")
}
ambiguousState := newPatrolRuntimeState(models.StateSnapshot{
Nodes: []models.Node{
{ID: "node-a", Name: "duplicate", Status: "online"},
{ID: "node-b", Name: "duplicate", Status: "online"},
},
})
ambiguous := newPatrolFindingCreatorAdapterState(NewPatrolService(nil, nil), ambiguousState)
ambiguous.setFindingScope([]string{"node-a"})
if _, ok := ambiguous.canonicalizeUnknownFindingResource(tools.PatrolFindingInput{
ResourceID: "node-typo", ResourceName: "duplicate", ResourceType: "node",
}); ok {
t.Fatal("ambiguous exact names must not be repaired even when only one record is in scope")
}
}
func TestPatrolFindingAdapterCanAssessSeededRuntimeFindingDuringScopedRun(t *testing.T) {
state := newPatrolRuntimeState(models.StateSnapshot{
VMs: []models.VM{{ID: "vm-requested", Name: "requested", VMID: 501}},