mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Reject incoherent Patrol finding identities
This commit is contained in:
@@ -7499,6 +7499,16 @@ target. Deleted-resource reconciliation still uses the full current global
|
||||
state, and the synthetic Patrol runtime finding retains its existing special
|
||||
handling.
|
||||
|
||||
Finding creation also validates the provider-authored resource ID and display
|
||||
name as one identity pair against the current runtime snapshot. When both
|
||||
values resolve uniquely but identify different same-type resources, core
|
||||
rejects the report and lets the bounded finding repair turn resubmit a coherent
|
||||
canonical pair; it never guesses which value was intended. This prevents one
|
||||
incident from being persisted twice when a report copies the ID of an in-scope
|
||||
sibling while describing the correct affected resource by name. The existing
|
||||
exact-name repair remains limited to unknown ID transcription errors and does
|
||||
not redirect a real known resource ID across the finding scope boundary.
|
||||
|
||||
A Watch provider turn that stops because its output-token allowance was
|
||||
exhausted is not a completed finding decision. Core preserves the partial turn
|
||||
only as model context, excludes its unfinished prose from the durable Patrol
|
||||
|
||||
@@ -550,6 +550,9 @@ func newPatrolFindingCreatorAdapterState(p *PatrolService, snap patrolRuntimeSta
|
||||
}
|
||||
|
||||
func (a *patrolFindingCreatorAdapter) CreateFinding(input tools.PatrolFindingInput) (string, bool, error) {
|
||||
if err := a.validateFindingResourceIdentity(input); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if canonical, repaired := a.canonicalizeUnknownFindingResource(input); repaired {
|
||||
log.Info().
|
||||
Str("submitted_resource_id", input.ResourceID).
|
||||
@@ -673,6 +676,57 @@ func (a *patrolFindingCreatorAdapter) CreateFinding(input tools.PatrolFindingInp
|
||||
return id, isNew, nil
|
||||
}
|
||||
|
||||
// validateFindingResourceIdentity rejects a model-authored resource ID/name
|
||||
// pair when each value resolves uniquely to a different same-type runtime
|
||||
// resource. Either value can be copied incorrectly, so core must not guess
|
||||
// which one the provider intended. Returning a validation error lets the
|
||||
// bounded finding repair turn submit one coherent canonical identity instead
|
||||
// of persisting duplicate incidents against sibling resources.
|
||||
func (a *patrolFindingCreatorAdapter) validateFindingResourceIdentity(input tools.PatrolFindingInput) error {
|
||||
if a == nil || strings.TrimSpace(input.ResourceID) == "" || strings.TrimSpace(input.ResourceName) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
resourceIDToken := canonicalPatrolScopeToken(input.ResourceID)
|
||||
resourceNameToken := canonicalPatrolScopeToken(input.ResourceName)
|
||||
resourceTypeToken := canonicalPatrolScopeToken(input.ResourceType)
|
||||
idMatches := make([]int, 0, 1)
|
||||
nameMatches := make([]int, 0, 1)
|
||||
recordIndex := 0
|
||||
|
||||
patrolVisitRuntimeResources(a.snap, func(record patrolRuntimeResourceRecord) bool {
|
||||
currentIndex := recordIndex
|
||||
recordIndex++
|
||||
if resourceTypeToken != "" && canonicalPatrolScopeToken(record.resourceType) != resourceTypeToken {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, value := range append(append([]string(nil), record.ids...), record.aliases...) {
|
||||
if canonicalPatrolScopeToken(value) == resourceIDToken {
|
||||
idMatches = append(idMatches, currentIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, alias := range record.aliases {
|
||||
if canonicalPatrolScopeToken(alias) == resourceNameToken {
|
||||
nameMatches = append(nameMatches, currentIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(idMatches) == 1 && len(nameMatches) == 1 && idMatches[0] != nameMatches[0] {
|
||||
return fmt.Errorf(
|
||||
"resource identity mismatch: resource_id %q and resource_name %q identify different %s resources; submit one canonical ID/name pair",
|
||||
input.ResourceID,
|
||||
input.ResourceName,
|
||||
input.ResourceType,
|
||||
)
|
||||
}
|
||||
return 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
|
||||
|
||||
@@ -1658,6 +1658,40 @@ func TestPatrolFindingAdapterRepairsOnlyUnknownIDsFromUniqueScopedIdentity(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatrolFindingAdapterRejectsKnownSiblingIDAndNameMismatch(t *testing.T) {
|
||||
state := newPatrolRuntimeState(models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{{
|
||||
ID: "docker-host-1",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "app-container-healthy", Name: "control", State: "running", Health: "healthy"},
|
||||
{ID: "app-container-unhealthy", Name: "existing", State: "running", Health: "unhealthy"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
ps := NewPatrolService(nil, nil)
|
||||
adapter := newPatrolFindingCreatorAdapterState(ps, state)
|
||||
adapter.setFindingScope([]string{"app-container-healthy", "app-container-unhealthy"})
|
||||
|
||||
input := tools.PatrolFindingInput{
|
||||
ResourceID: "app-container-healthy", ResourceName: "existing", ResourceType: "app-container",
|
||||
Key: "health-check-failed", Severity: "warning", Category: "reliability",
|
||||
Title: "Container health check is unhealthy", Description: "The existing container is unhealthy.",
|
||||
Impact: "Requests may fail.", Recommendation: "Inspect the health check.",
|
||||
Evidence: "Current provider health for existing is unhealthy.",
|
||||
}
|
||||
if _, isNew, err := adapter.CreateFinding(input); err == nil || isNew || !strings.Contains(err.Error(), "resource identity mismatch") {
|
||||
t.Fatalf("mismatched sibling report = (%t, %v), want identity validation error", isNew, err)
|
||||
}
|
||||
if active := ps.findings.GetActive(FindingSeverityInfo); len(active) != 0 {
|
||||
t.Fatalf("mismatched sibling report persisted findings: %+v", active)
|
||||
}
|
||||
|
||||
input.ResourceID = "app-container-unhealthy"
|
||||
if findingID, isNew, err := adapter.CreateFinding(input); err != nil || !isNew || findingID == "" {
|
||||
t.Fatalf("coherent canonical report = (%q, %t, %v), want new finding", findingID, isNew, err)
|
||||
}
|
||||
}
|
||||
|
||||
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