diff --git a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts index 66d6287e2..7e635bbf7 100644 --- a/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts +++ b/app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts @@ -313,7 +313,17 @@ export interface PolicyEvaluation_Violation { message: string; vulnerability?: PolicyVulnerabilityFinding | undefined; sast?: PolicySASTFinding | undefined; - licenseViolation?: PolicyLicenseViolationFinding | undefined; + licenseViolation?: + | PolicyLicenseViolationFinding + | undefined; + /** + * True when the policy declared a finding_type and returned structured data, + * but validation failed at runtime. The violation was kept as a plain string + * and the finding oneof is NOT set. Consumers should treat this as a + * data-quality signal: the violation exists but structured finding data is + * missing due to a policy authoring issue. + */ + findingDegraded: boolean; } export interface PolicyEvaluation_Reference { @@ -2873,7 +2883,14 @@ export const PolicyEvaluation_WithEntry = { }; function createBasePolicyEvaluation_Violation(): PolicyEvaluation_Violation { - return { subject: "", message: "", vulnerability: undefined, sast: undefined, licenseViolation: undefined }; + return { + subject: "", + message: "", + vulnerability: undefined, + sast: undefined, + licenseViolation: undefined, + findingDegraded: false, + }; } export const PolicyEvaluation_Violation = { @@ -2893,6 +2910,9 @@ export const PolicyEvaluation_Violation = { if (message.licenseViolation !== undefined) { PolicyLicenseViolationFinding.encode(message.licenseViolation, writer.uint32(42).fork()).ldelim(); } + if (message.findingDegraded === true) { + writer.uint32(48).bool(message.findingDegraded); + } return writer; }, @@ -2938,6 +2958,13 @@ export const PolicyEvaluation_Violation = { message.licenseViolation = PolicyLicenseViolationFinding.decode(reader, reader.uint32()); continue; + case 6: + if (tag !== 48) { + break; + } + + message.findingDegraded = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -2958,6 +2985,7 @@ export const PolicyEvaluation_Violation = { licenseViolation: isSet(object.licenseViolation) ? PolicyLicenseViolationFinding.fromJSON(object.licenseViolation) : undefined, + findingDegraded: isSet(object.findingDegraded) ? Boolean(object.findingDegraded) : false, }; }, @@ -2973,6 +3001,7 @@ export const PolicyEvaluation_Violation = { message.licenseViolation !== undefined && (obj.licenseViolation = message.licenseViolation ? PolicyLicenseViolationFinding.toJSON(message.licenseViolation) : undefined); + message.findingDegraded !== undefined && (obj.findingDegraded = message.findingDegraded); return obj; }, @@ -2993,6 +3022,7 @@ export const PolicyEvaluation_Violation = { message.licenseViolation = (object.licenseViolation !== undefined && object.licenseViolation !== null) ? PolicyLicenseViolationFinding.fromPartial(object.licenseViolation) : undefined; + message.findingDegraded = object.findingDegraded ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json index e7d821106..df9932881 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.jsonschema.json @@ -3,11 +3,19 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(finding_degraded)$": { + "description": "True when the policy declared a finding_type and returned structured data,\n but validation failed at runtime. The violation was kept as a plain string\n and the finding oneof is NOT set. Consumers should treat this as a\n data-quality signal: the violation exists but structured finding data is\n missing due to a policy authoring issue.", + "type": "boolean" + }, "^(license_violation)$": { "$ref": "attestation.v1.PolicyLicenseViolationFinding.jsonschema.json" } }, "properties": { + "findingDegraded": { + "description": "True when the policy declared a finding_type and returned structured data,\n but validation failed at runtime. The violation was kept as a plain string\n and the finding oneof is NOT set. Consumers should treat this as a\n data-quality signal: the violation exists but structured finding data is\n missing due to a policy authoring issue.", + "type": "boolean" + }, "licenseViolation": { "$ref": "attestation.v1.PolicyLicenseViolationFinding.jsonschema.json" }, diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json index dc57a847c..32e7a5aa5 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.Violation.schema.json @@ -3,11 +3,19 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(findingDegraded)$": { + "description": "True when the policy declared a finding_type and returned structured data,\n but validation failed at runtime. The violation was kept as a plain string\n and the finding oneof is NOT set. Consumers should treat this as a\n data-quality signal: the violation exists but structured finding data is\n missing due to a policy authoring issue.", + "type": "boolean" + }, "^(licenseViolation)$": { "$ref": "attestation.v1.PolicyLicenseViolationFinding.schema.json" } }, "properties": { + "finding_degraded": { + "description": "True when the policy declared a finding_type and returned structured data,\n but validation failed at runtime. The violation was kept as a plain string\n and the finding oneof is NOT set. Consumers should treat this as a\n data-quality signal: the violation exists but structured finding data is\n missing due to a policy authoring issue.", + "type": "boolean" + }, "license_violation": { "$ref": "attestation.v1.PolicyLicenseViolationFinding.schema.json" }, diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go index 292493eae..f0143f726 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go @@ -2225,9 +2225,15 @@ type PolicyEvaluation_Violation struct { // *PolicyEvaluation_Violation_Vulnerability // *PolicyEvaluation_Violation_Sast // *PolicyEvaluation_Violation_LicenseViolation - Finding isPolicyEvaluation_Violation_Finding `protobuf_oneof:"finding"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Finding isPolicyEvaluation_Violation_Finding `protobuf_oneof:"finding"` + // True when the policy declared a finding_type and returned structured data, + // but validation failed at runtime. The violation was kept as a plain string + // and the finding oneof is NOT set. Consumers should treat this as a + // data-quality signal: the violation exists but structured finding data is + // missing due to a policy authoring issue. + FindingDegraded bool `protobuf:"varint,6,opt,name=finding_degraded,json=findingDegraded,proto3" json:"finding_degraded,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PolicyEvaluation_Violation) Reset() { @@ -2308,6 +2314,13 @@ func (x *PolicyEvaluation_Violation) GetLicenseViolation() *PolicyLicenseViolati return nil } +func (x *PolicyEvaluation_Violation) GetFindingDegraded() bool { + if x != nil { + return x.FindingDegraded + } + return false +} + type isPolicyEvaluation_Violation_Finding interface { isPolicyEvaluation_Violation_Finding() } @@ -2701,7 +2714,7 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\venvironment\x18\x02 \x01(\tR\venvironment\x12$\n" + "\rauthenticated\x18\x03 \x01(\bR\rauthenticated\x12I\n" + "\x04type\x18\x04 \x01(\x0e25.workflowcontract.v1.CraftingSchema.Runner.RunnerTypeR\x04type\x12\x10\n" + - "\x03url\x18\x05 \x01(\tR\x03url\"\x98\x0e\n" + + "\x03url\x18\x05 \x01(\tR\x03url\"\xc3\x0e\n" + "\x10PolicyEvaluation\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + "\rname.dns-1123\x12:must contain only lowercase letters, numbers, and hyphens.\x1a/this.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')R\x04name\x12#\n" + @@ -2731,13 +2744,14 @@ const file_attestation_v1_crafting_state_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a7\n" + "\tWithEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a\xc5\x02\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a\xf0\x02\n" + "\tViolation\x12 \n" + "\asubject\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\asubject\x12 \n" + "\amessage\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\amessage\x12R\n" + "\rvulnerability\x18\x03 \x01(\v2*.attestation.v1.PolicyVulnerabilityFindingH\x00R\rvulnerability\x127\n" + "\x04sast\x18\x04 \x01(\v2!.attestation.v1.PolicySASTFindingH\x00R\x04sast\x12\\\n" + - "\x11license_violation\x18\x05 \x01(\v2-.attestation.v1.PolicyLicenseViolationFindingH\x00R\x10licenseViolationB\t\n" + + "\x11license_violation\x18\x05 \x01(\v2-.attestation.v1.PolicyLicenseViolationFindingH\x00R\x10licenseViolation\x12)\n" + + "\x10finding_degraded\x18\x06 \x01(\bR\x0ffindingDegradedB\t\n" + "\afinding\x1a\xfc\x01\n" + "\tReference\x12\x97\x01\n" + "\x04name\x18\x01 \x01(\tB\x82\x01\xbaH\x7f\xba\x01|\n" + diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto index 9ede42ad1..55165812d 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.proto @@ -284,6 +284,13 @@ message PolicyEvaluation { PolicySASTFinding sast = 4; PolicyLicenseViolationFinding license_violation = 5; } + + // True when the policy declared a finding_type and returned structured data, + // but validation failed at runtime. The violation was kept as a plain string + // and the finding oneof is NOT set. Consumers should treat this as a + // data-quality signal: the violation exists but structured finding data is + // missing due to a policy authoring issue. + bool finding_degraded = 6; } message Reference { diff --git a/pkg/attestation/crafter/crafter.go b/pkg/attestation/crafter/crafter.go index b55eaca20..0236cfec1 100644 --- a/pkg/attestation/crafter/crafter.go +++ b/pkg/attestation/crafter/crafter.go @@ -706,6 +706,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + policies.WithLenientFindingValidation(), ) policyGroupResults, err := pgv.VerifyMaterial(ctx, mt, value) if err != nil { @@ -723,6 +724,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + policies.WithLenientFindingValidation(), ) policyResults, err := pv.VerifyMaterial(ctx, mt, value) if err != nil { @@ -758,6 +760,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), + policies.WithLenientFindingValidation(), ) policyEvaluations, err := pv.VerifyStatement(ctx, statement) if err != nil { @@ -768,6 +771,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), + policies.WithLenientFindingValidation(), ) policyGroupResults, err := pgv.VerifyStatement(ctx, statement) if err != nil { diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index c29abecdc..0c26dd1dc 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -84,32 +84,34 @@ const ( var defaultMaxConcurrency = max(runtime.NumCPU(), 5) type PolicyVerifier struct { - policies *v1.Policies - logger *zerolog.Logger - client v13.AttestationServiceClient - grpcConn *grpc.ClientConn - allowedHostnames []string - defaultGate bool - includeRawData bool - enablePrint bool - evalPhase EvalPhase - maxConcurrency int - policyCache cache.Cache[*policyWithReference] - groupCache cache.Cache[*groupWithReference] + policies *v1.Policies + logger *zerolog.Logger + client v13.AttestationServiceClient + grpcConn *grpc.ClientConn + allowedHostnames []string + defaultGate bool + includeRawData bool + enablePrint bool + evalPhase EvalPhase + maxConcurrency int + lenientFindingValidation bool + policyCache cache.Cache[*policyWithReference] + groupCache cache.Cache[*groupWithReference] } var _ Verifier = (*PolicyVerifier)(nil) type PolicyVerifierOptions struct { - AllowedHostnames []string - DefaultGate bool - IncludeRawData bool - EnablePrint bool - GRPCConn *grpc.ClientConn - EvalPhase EvalPhase - MaxConcurrency int - PolicyCache cache.Cache[*policyWithReference] - GroupCache cache.Cache[*groupWithReference] + AllowedHostnames []string + DefaultGate bool + IncludeRawData bool + EnablePrint bool + GRPCConn *grpc.ClientConn + EvalPhase EvalPhase + MaxConcurrency int + LenientFindingValidation bool + PolicyCache cache.Cache[*policyWithReference] + GroupCache cache.Cache[*groupWithReference] } type PolicyVerifierOption func(*PolicyVerifierOptions) @@ -168,6 +170,15 @@ func WithGroupCache(c cache.Cache[*groupWithReference]) PolicyVerifierOption { } } +// WithLenientFindingValidation makes structured finding validation errors non-fatal. +// When enabled, validation failures produce warnings and the violation is kept as a +// plain string with FindingDegraded=true instead of returning a hard error. +func WithLenientFindingValidation() PolicyVerifierOption { + return func(o *PolicyVerifierOptions) { + o.LenientFindingValidation = true + } +} + const defaultPolicyCacheTTL = 5 * time.Minute func NewPolicyVerifier(policies *v1.Policies, client v13.AttestationServiceClient, logger *zerolog.Logger, opts ...PolicyVerifierOption) *PolicyVerifier { @@ -190,18 +201,19 @@ func NewPolicyVerifier(policies *v1.Policies, client v13.AttestationServiceClien } return &PolicyVerifier{ - policies: policies, - client: client, - logger: logger, - grpcConn: options.GRPCConn, - allowedHostnames: options.AllowedHostnames, - defaultGate: options.DefaultGate, - includeRawData: options.IncludeRawData, - enablePrint: options.EnablePrint, - evalPhase: options.EvalPhase, - maxConcurrency: maxConcurrency, - policyCache: options.PolicyCache, - groupCache: options.GroupCache, + policies: policies, + client: client, + logger: logger, + grpcConn: options.GRPCConn, + allowedHostnames: options.AllowedHostnames, + defaultGate: options.DefaultGate, + includeRawData: options.IncludeRawData, + enablePrint: options.EnablePrint, + evalPhase: options.EvalPhase, + maxConcurrency: maxConcurrency, + lenientFindingValidation: options.LenientFindingValidation, + policyCache: options.PolicyCache, + groupCache: options.GroupCache, } } @@ -389,7 +401,7 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme } findingType := policy.GetMetadata().GetFindingType() - apiViolations, warnings, err := engineEvaluationsToAPIViolations(evalResults, findingType) + apiViolations, warnings, err := engineEvaluationsToAPIViolations(evalResults, findingType, pv.lenientFindingValidation) if err != nil { return nil, NewPolicyError(fmt.Errorf("policy %q: %w", policy.GetMetadata().GetName(), err)) } @@ -754,11 +766,35 @@ func splitArgs(s string) []string { return result } -func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findingType string) ([]*v12.PolicyEvaluation_Violation, []string, error) { +// validateAndSetFinding validates a structured finding and sets it on the violation. +// When lenient is true and validation fails, it returns degraded=true with a descriptive +// error for use as a warning. When lenient is false, it returns degraded=false with the +// error for the caller to treat as fatal. +func validateAndSetFinding(apiV *v12.PolicyEvaluation_Violation, findingType string, rawFinding map[string]any, lenient bool) (degraded bool, err error) { + finding, err := findings.ValidateFinding(findingType, rawFinding) + if err != nil { + if lenient { + return true, fmt.Errorf("structured finding validation failed for finding_type %q: %w — violations will be treated as plain strings", findingType, err) + } + return false, fmt.Errorf("structured violation validation: %w", err) + } + + if err := findings.SetViolationFinding(apiV, findingType, finding); err != nil { + if lenient { + return true, fmt.Errorf("failed to set structured finding for finding_type %q: %w — violations will be treated as plain strings", findingType, err) + } + return false, fmt.Errorf("setting violation finding: %w", err) + } + + return false, nil +} + +func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findingType string, lenientValidation bool) ([]*v12.PolicyEvaluation_Violation, []string, error) { res := make([]*v12.PolicyEvaluation_Violation, 0) var warnings []string warnedNoFindingType := false warnedNoStructuredData := false + warnedValidationFailed := false for _, r := range results { for _, v := range r.Violations { @@ -771,7 +807,7 @@ func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findin switch { case findingType == "" && !hasStructuredData: - // No finding_type, string violation — current behavior + // no-op: plain string violation without finding_type case findingType == "" && hasStructuredData: if !warnedNoFindingType { @@ -791,13 +827,14 @@ func engineEvaluationsToAPIViolations(results []*engine.EvaluationResult, findin } case findingType != "" && hasStructuredData: - finding, err := findings.ValidateFinding(findingType, v.RawFinding) - if err != nil { - return nil, nil, fmt.Errorf("structured violation validation: %w", err) - } - - if err := findings.SetViolationFinding(apiV, findingType, finding); err != nil { - return nil, nil, fmt.Errorf("setting violation finding: %w", err) + if degraded, err := validateAndSetFinding(apiV, findingType, v.RawFinding, lenientValidation); degraded { + apiV.FindingDegraded = true + if !warnedValidationFailed && err != nil { + warnings = append(warnings, err.Error()) + warnedValidationFailed = true + } + } else if err != nil { + return nil, nil, err } } diff --git a/pkg/policies/policies_test.go b/pkg/policies/policies_test.go index 1b130b50d..1b23d28ae 100644 --- a/pkg/policies/policies_test.go +++ b/pkg/policies/policies_test.go @@ -1492,13 +1492,14 @@ func (s *testSuite) TestPolicyAttachmentGate() { func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { cases := []struct { - name string - findingType string - violations []*engine.PolicyViolation - wantErr string - wantWarnings int - wantViolations int - checkFn func(violations []*v1.PolicyEvaluation_Violation) + name string + findingType string + lenientValidation bool + violations []*engine.PolicyViolation + wantErr string + wantWarnings int + wantViolations int + checkFn func(violations []*v1.PolicyEvaluation_Violation) }{ { name: "no finding_type + string violations - current behavior", @@ -1602,7 +1603,7 @@ func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { }, }, { - name: "finding_type + invalid structured violations - validation error", + name: "finding_type + invalid structured violations - strict validation error", findingType: "VULNERABILITY", violations: []*engine.PolicyViolation{ {Subject: "p1", Violation: "vuln found", RawFinding: map[string]any{ @@ -1613,7 +1614,7 @@ func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { wantErr: "validation failed", }, { - name: "unknown finding_type - error", + name: "unknown finding_type - strict error", findingType: "UNKNOWN", violations: []*engine.PolicyViolation{ {Subject: "p1", Violation: "something", RawFinding: map[string]any{ @@ -1628,6 +1629,81 @@ func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { violations: []*engine.PolicyViolation{}, wantViolations: 0, }, + // --- Lenient validation mode --- + { + name: "lenient - invalid structured violations - fallback with warning and degraded flag", + findingType: "VULNERABILITY", + lenientValidation: true, + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "vuln found", RawFinding: map[string]any{ + "message": "vuln found", "external_id": "CVE-1", + // missing package_purl and severity (required) + }}, + }, + wantViolations: 1, + wantWarnings: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + s.Equal("vuln found", violations[0].GetMessage()) + s.Nil(violations[0].GetVulnerability()) + s.True(violations[0].GetFindingDegraded()) + }, + }, + { + name: "lenient - unknown finding_type - fallback with warning and degraded flag", + findingType: "UNKNOWN", + lenientValidation: true, + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "something", RawFinding: map[string]any{ + "message": "something", + }}, + }, + wantViolations: 1, + wantWarnings: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + s.Equal("something", violations[0].GetMessage()) + s.Nil(violations[0].GetVulnerability()) + s.True(violations[0].GetFindingDegraded()) + }, + }, + { + name: "lenient - valid structured violations - still validates and sets oneof", + findingType: "VULNERABILITY", + lenientValidation: true, + violations: []*engine.PolicyViolation{ + {Subject: "p1", 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", + }}, + }, + wantViolations: 1, + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + f := violations[0].GetVulnerability() + s.Require().NotNil(f) + s.Equal("CVE-2024-1234", f.GetExternalId()) + s.False(violations[0].GetFindingDegraded()) + }, + }, + { + name: "lenient - multiple invalid violations - warning deduplicated, all degraded", + findingType: "VULNERABILITY", + lenientValidation: true, + violations: []*engine.PolicyViolation{ + {Subject: "p1", Violation: "vuln 1", RawFinding: map[string]any{ + "message": "vuln 1", "external_id": "CVE-1", + }}, + {Subject: "p2", Violation: "vuln 2", RawFinding: map[string]any{ + "message": "vuln 2", "external_id": "CVE-2", + }}, + }, + wantViolations: 2, + wantWarnings: 1, // deduplicated + checkFn: func(violations []*v1.PolicyEvaluation_Violation) { + s.True(violations[0].GetFindingDegraded()) + s.True(violations[1].GetFindingDegraded()) + s.Nil(violations[0].GetVulnerability()) + s.Nil(violations[1].GetVulnerability()) + }, + }, } for _, tc := range cases { @@ -1636,7 +1712,7 @@ func (s *testSuite) TestEngineEvaluationsToAPIViolationsBehaviorMatrix() { {Violations: tc.violations}, } - violations, warnings, err := engineEvaluationsToAPIViolations(results, tc.findingType) + violations, warnings, err := engineEvaluationsToAPIViolations(results, tc.findingType, tc.lenientValidation) if tc.wantErr != "" { s.Require().Error(err) s.Contains(err.Error(), tc.wantErr)