mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
Repair scoped Patrol finding identity safely
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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}},
|
||||
|
||||
Reference in New Issue
Block a user