diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index e67aca313..c29abecdc 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -758,6 +758,7 @@ func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findin res := make([]*v12.PolicyEvaluation_Violation, 0) var warnings []string warnedNoFindingType := false + warnedNoStructuredData := false for _, r := range results { for _, v := range r.Violations { @@ -780,7 +781,14 @@ func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findin } case findingType != "" && !hasStructuredData: - return nil, nil, fmt.Errorf("declares finding_type %q but violation is not a structured object", findingType) + // Policy declares a finding type but this violation is a plain string. + // This can happen when some evaluation branches do not support structured output yet. + // Fall back to treating it as a regular string violation without the typed finding. + if !warnedNoStructuredData { + warnings = append(warnings, + fmt.Sprintf("policy declares finding_type %q but some violations are plain strings — structured finding data will not be available for those", findingType)) + warnedNoStructuredData = true + } case findingType != "" && hasStructuredData: finding, err := findings.ValidateFinding(findingType, v.RawFinding) diff --git a/pkg/policies/policies_test.go b/pkg/policies/policies_test.go index 9e4d07471..1b130b50d 100644 --- a/pkg/policies/policies_test.go +++ b/pkg/policies/policies_test.go @@ -1526,12 +1526,47 @@ func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { }, }, { - name: "finding_type + string violations - error", + name: "finding_type + string violations - fallback with warning", findingType: "VULNERABILITY", violations: []*engine.PolicyViolation{ {Subject: "p1", Violation: "plain string"}, }, - wantErr: "declares finding_type", + wantViolations: 1, + wantWarnings: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + // The violation is kept as a plain string, no structured finding + s.Equal("plain string", violations[0].GetMessage()) + s.Nil(violations[0].GetVulnerability()) + }, + }, + { + name: "finding_type + mixed string and structured violations - per-violation handling", + findingType: "VULNERABILITY", + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "plain string fallback"}, + {Subject: "p2", Violation: "vuln found", RawFinding: map[string]any{ + "message": "vuln found", "external_id": "CVE-2024-1234", + "package_purl": "pkg:golang/example.com/lib@v1.0.0", "severity": "HIGH", + }}, + {Subject: "p3", Violation: "another plain string"}, + }, + wantViolations: 3, + wantWarnings: 1, // deduplicated warning + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + // First violation: plain string, no structured finding + s.Equal("plain string fallback", violations[0].GetMessage()) + s.Nil(violations[0].GetVulnerability()) + + // Second violation: structured, validated + f := violations[1].GetVulnerability() + s.Require().NotNil(f) + s.Equal("CVE-2024-1234", f.GetExternalId()) + s.Equal("HIGH", f.GetSeverity()) + + // Third violation: plain string, no structured finding + s.Equal("another plain string", violations[2].GetMessage()) + s.Nil(violations[2].GetVulnerability()) + }, }, { name: "finding_type + valid structured violations - validates and sets oneof",