From d5b2430051f93ff4174ef1abcbf17f8d2c273f95 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Wed, 1 Apr 2026 09:51:15 +0200 Subject: [PATCH] feat: add policyEvaluationsCount and policyViolationsCount to attestation predicate Add summary count fields to the attestation predicate so external consumers can know the total number of policy evaluations and violations without downloading the full policy evaluations bundle from CAS. Also consolidates the gated-violation detection loop into groupEvaluations, eliminating a redundant iteration over policy evaluations in predicate(). Closes #2975 Signed-off-by: Miguel Martinez Trivino --- .../controlplane/v1/response_messages.pb.go | 31 ++++- .../controlplane/v1/response_messages.proto | 4 + .../controlplane/v1/response_messages.ts | 40 +++++- ...tem.PolicyEvaluationStatus.jsonschema.json | 24 ++++ ...ionItem.PolicyEvaluationStatus.schema.json | 24 ++++ .../internal/service/attestation.go | 2 + .../renderer/chainloop/chainloop.go | 4 + .../testdata/attestation.output-2.v0.2.json | 2 + .../testdata/attestation.output-2.v2.json | 2 + .../testdata/attestation.output.v0.2.json | 2 + .../testdata/attestation.output.v2.json | 2 + pkg/attestation/renderer/chainloop/v02.go | 63 ++++++---- .../renderer/chainloop/v02_test.go | 118 ++++++++++++++++++ 13 files changed, 288 insertions(+), 30 deletions(-) diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index 939318e2d..0fff67449 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -2292,8 +2292,12 @@ type AttestationItem_PolicyEvaluationStatus struct { Blocked bool `protobuf:"varint,3,opt,name=blocked,proto3" json:"blocked,omitempty"` HasViolations bool `protobuf:"varint,4,opt,name=has_violations,json=hasViolations,proto3" json:"has_violations,omitempty"` HasGatedViolations bool `protobuf:"varint,5,opt,name=has_gated_violations,json=hasGatedViolations,proto3" json:"has_gated_violations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Total number of policy evaluations + EvaluationsCount int32 `protobuf:"varint,6,opt,name=evaluations_count,json=evaluationsCount,proto3" json:"evaluations_count,omitempty"` + // Total number of policy violations across all evaluations + ViolationsCount int32 `protobuf:"varint,7,opt,name=violations_count,json=violationsCount,proto3" json:"violations_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttestationItem_PolicyEvaluationStatus) Reset() { @@ -2361,6 +2365,20 @@ func (x *AttestationItem_PolicyEvaluationStatus) GetHasGatedViolations() bool { return false } +func (x *AttestationItem_PolicyEvaluationStatus) GetEvaluationsCount() int32 { + if x != nil { + return x.EvaluationsCount + } + return 0 +} + +func (x *AttestationItem_PolicyEvaluationStatus) GetViolationsCount() int32 { + if x != nil { + return x.ViolationsCount + } + return 0 +} + type AttestationItem_EnvVariable struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -2675,8 +2693,7 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\n" + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12;\n" + "\vreleased_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "releasedAt\"\xe3\n" + - "\n" + + "releasedAt\"\xbb\v\n" + "\x0fAttestationItem\x12\x1e\n" + "\benvelope\x18\x03 \x01(\fB\x02\x18\x01R\benvelope\x12\x16\n" + "\x06bundle\x18\n" + @@ -2692,13 +2709,15 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + "\x16PolicyEvaluationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x128\n" + - "\x05value\x18\x02 \x01(\v2\".controlplane.v1.PolicyEvaluationsR\x05value:\x028\x01\x1a\xc3\x01\n" + + "\x05value\x18\x02 \x01(\v2\".controlplane.v1.PolicyEvaluationsR\x05value:\x028\x01\x1a\x9b\x02\n" + "\x16PolicyEvaluationStatus\x12\x1a\n" + "\bstrategy\x18\x01 \x01(\tR\bstrategy\x12\x1a\n" + "\bbypassed\x18\x02 \x01(\bR\bbypassed\x12\x18\n" + "\ablocked\x18\x03 \x01(\bR\ablocked\x12%\n" + "\x0ehas_violations\x18\x04 \x01(\bR\rhasViolations\x120\n" + - "\x14has_gated_violations\x18\x05 \x01(\bR\x12hasGatedViolations\x1a7\n" + + "\x14has_gated_violations\x18\x05 \x01(\bR\x12hasGatedViolations\x12+\n" + + "\x11evaluations_count\x18\x06 \x01(\x05R\x10evaluationsCount\x12)\n" + + "\x10violations_count\x18\a \x01(\x05R\x0fviolationsCount\x1a7\n" + "\vEnvVariable\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value\x1a\xf9\x02\n" + diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index d31b62472..dc3e71779 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -117,6 +117,10 @@ message AttestationItem { bool blocked = 3; bool has_violations = 4; bool has_gated_violations = 5; + // Total number of policy evaluations + int32 evaluations_count = 6; + // Total number of policy violations across all evaluations + int32 violations_count = 7; } message EnvVariable { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index f906d87fb..51e86aa40 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -392,6 +392,10 @@ export interface AttestationItem_PolicyEvaluationStatus { blocked: boolean; hasViolations: boolean; hasGatedViolations: boolean; + /** Total number of policy evaluations */ + evaluationsCount: number; + /** Total number of policy violations across all evaluations */ + violationsCount: number; } export interface AttestationItem_EnvVariable { @@ -1698,7 +1702,15 @@ export const AttestationItem_PolicyEvaluationsEntry = { }; function createBaseAttestationItem_PolicyEvaluationStatus(): AttestationItem_PolicyEvaluationStatus { - return { strategy: "", bypassed: false, blocked: false, hasViolations: false, hasGatedViolations: false }; + return { + strategy: "", + bypassed: false, + blocked: false, + hasViolations: false, + hasGatedViolations: false, + evaluationsCount: 0, + violationsCount: 0, + }; } export const AttestationItem_PolicyEvaluationStatus = { @@ -1718,6 +1730,12 @@ export const AttestationItem_PolicyEvaluationStatus = { if (message.hasGatedViolations === true) { writer.uint32(40).bool(message.hasGatedViolations); } + if (message.evaluationsCount !== 0) { + writer.uint32(48).int32(message.evaluationsCount); + } + if (message.violationsCount !== 0) { + writer.uint32(56).int32(message.violationsCount); + } return writer; }, @@ -1763,6 +1781,20 @@ export const AttestationItem_PolicyEvaluationStatus = { message.hasGatedViolations = reader.bool(); continue; + case 6: + if (tag !== 48) { + break; + } + + message.evaluationsCount = reader.int32(); + continue; + case 7: + if (tag !== 56) { + break; + } + + message.violationsCount = reader.int32(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1779,6 +1811,8 @@ export const AttestationItem_PolicyEvaluationStatus = { blocked: isSet(object.blocked) ? Boolean(object.blocked) : false, hasViolations: isSet(object.hasViolations) ? Boolean(object.hasViolations) : false, hasGatedViolations: isSet(object.hasGatedViolations) ? Boolean(object.hasGatedViolations) : false, + evaluationsCount: isSet(object.evaluationsCount) ? Number(object.evaluationsCount) : 0, + violationsCount: isSet(object.violationsCount) ? Number(object.violationsCount) : 0, }; }, @@ -1789,6 +1823,8 @@ export const AttestationItem_PolicyEvaluationStatus = { message.blocked !== undefined && (obj.blocked = message.blocked); message.hasViolations !== undefined && (obj.hasViolations = message.hasViolations); message.hasGatedViolations !== undefined && (obj.hasGatedViolations = message.hasGatedViolations); + message.evaluationsCount !== undefined && (obj.evaluationsCount = Math.round(message.evaluationsCount)); + message.violationsCount !== undefined && (obj.violationsCount = Math.round(message.violationsCount)); return obj; }, @@ -1807,6 +1843,8 @@ export const AttestationItem_PolicyEvaluationStatus = { message.blocked = object.blocked ?? false; message.hasViolations = object.hasViolations ?? false; message.hasGatedViolations = object.hasGatedViolations ?? false; + message.evaluationsCount = object.evaluationsCount ?? 0; + message.violationsCount = object.violationsCount ?? 0; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.jsonschema.json index 4d330a558..1f40e6028 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.jsonschema.json @@ -3,11 +3,23 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(evaluations_count)$": { + "description": "Total number of policy evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, "^(has_gated_violations)$": { "type": "boolean" }, "^(has_violations)$": { "type": "boolean" + }, + "^(violations_count)$": { + "description": "Total number of policy violations across all evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" } }, "properties": { @@ -17,6 +29,12 @@ "bypassed": { "type": "boolean" }, + "evaluationsCount": { + "description": "Total number of policy evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, "hasGatedViolations": { "type": "boolean" }, @@ -25,6 +43,12 @@ }, "strategy": { "type": "string" + }, + "violationsCount": { + "description": "Total number of policy violations across all evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" } }, "title": "Policy Evaluation Status", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.schema.json index 5ad5d5e91..bf75df7fa 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.PolicyEvaluationStatus.schema.json @@ -3,11 +3,23 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "patternProperties": { + "^(evaluationsCount)$": { + "description": "Total number of policy evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, "^(hasGatedViolations)$": { "type": "boolean" }, "^(hasViolations)$": { "type": "boolean" + }, + "^(violationsCount)$": { + "description": "Total number of policy violations across all evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" } }, "properties": { @@ -17,6 +29,12 @@ "bypassed": { "type": "boolean" }, + "evaluations_count": { + "description": "Total number of policy evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, "has_gated_violations": { "type": "boolean" }, @@ -25,6 +43,12 @@ }, "strategy": { "type": "string" + }, + "violations_count": { + "description": "Total number of policy violations across all evaluations", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" } }, "title": "Policy Evaluation Status", diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 0d81bbd9e..ece70348e 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -563,6 +563,8 @@ func bizAttestationToPb(att *biz.Attestation, predicate chainloop.NormalizablePr Blocked: policyEvaluationStatus.Blocked, HasViolations: policyEvaluationStatus.HasViolations, HasGatedViolations: policyEvaluationStatus.HasGatedViolations, + EvaluationsCount: int32(policyEvaluationStatus.EvaluationsCount), + ViolationsCount: int32(policyEvaluationStatus.ViolationsCount), }, Bundle: att.Bundle, }, nil diff --git a/pkg/attestation/renderer/chainloop/chainloop.go b/pkg/attestation/renderer/chainloop/chainloop.go index a5bebe019..ae46fe1a1 100644 --- a/pkg/attestation/renderer/chainloop/chainloop.go +++ b/pkg/attestation/renderer/chainloop/chainloop.go @@ -57,6 +57,10 @@ type PolicyEvaluationStatus struct { HasViolations bool // Whether the attestation has gated policy violations HasGatedViolations bool + // Total number of policy evaluations + EvaluationsCount int + // Total number of policy violations across all evaluations + ViolationsCount int } type NormalizedMaterial struct { diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v0.2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v0.2.json index 1523398b1..9cf0762ad 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v0.2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v0.2.json @@ -121,7 +121,9 @@ "policyAttBlocked": false, "policyBlockBypassEnabled": false, "policyCheckBlockingStrategy": "ADVISORY", + "policyEvaluationsCount": 0, "policyHasViolations": false, + "policyViolationsCount": 0, "runnerType": "RUNNER_TYPE_UNSPECIFIED" } } \ No newline at end of file diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v2.json index 1523398b1..9cf0762ad 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output-2.v2.json @@ -121,7 +121,9 @@ "policyAttBlocked": false, "policyBlockBypassEnabled": false, "policyCheckBlockingStrategy": "ADVISORY", + "policyEvaluationsCount": 0, "policyHasViolations": false, + "policyViolationsCount": 0, "runnerType": "RUNNER_TYPE_UNSPECIFIED" } } \ No newline at end of file diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json index 29a93cba3..25cc5c0e5 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v0.2.json @@ -117,7 +117,9 @@ } ] }, + "policyEvaluationsCount": 1, "policyHasViolations": true, + "policyViolationsCount": 1, "runnerType": "GITHUB_ACTION" } } \ No newline at end of file diff --git a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json index 29a93cba3..25cc5c0e5 100644 --- a/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json +++ b/pkg/attestation/renderer/chainloop/testdata/attestation.output.v2.json @@ -117,7 +117,9 @@ } ] }, + "policyEvaluationsCount": 1, "policyHasViolations": true, + "policyViolationsCount": 1, "runnerType": "GITHUB_ACTION" } } \ No newline at end of file diff --git a/pkg/attestation/renderer/chainloop/v02.go b/pkg/attestation/renderer/chainloop/v02.go index 56628c508..3d6b5a5b4 100644 --- a/pkg/attestation/renderer/chainloop/v02.go +++ b/pkg/attestation/renderer/chainloop/v02.go @@ -52,6 +52,10 @@ type ProvenancePredicateV02 struct { // Whether the attestation has policy violations PolicyHasViolations bool `json:"policyHasViolations"` + // Total number of policy evaluations + PolicyEvaluationsCount int `json:"policyEvaluationsCount"` + // Total number of policy violations across all evaluations + PolicyViolationsCount int `json:"policyViolationsCount"` // Whether the attestation has policy violations in gated policies PolicyHasGatedViolations bool `json:"policyHasGatedViolations,omitempty"` // Whether we want to block the attestation on policy violations @@ -234,7 +238,7 @@ func (r *RendererV02) predicate() (*structpb.Struct, error) { return nil, fmt.Errorf("error normalizing materials: %w", err) } - policies, hasViolations, err := mappedPolicyEvaluations(r.att) + evalResult, err := mappedPolicyEvaluations(r.att) if err != nil { return nil, fmt.Errorf("error rendering policy evaluations: %w", err) } @@ -244,27 +248,21 @@ func (r *RendererV02) predicate() (*structpb.Struct, error) { policyCheckBlockingStrategy = PolicyViolationBlockingStrategyEnforced } - // Determine if the attestation is blocked // An attestation is blocked when: // - any of the policies marked as gate has violations // - or if there are any policy violations and the attestation is configured to be blocked on violations // In all cases, if the bypass flag is set, the attestation is not blocked - gated := false - for _, eval := range r.att.PolicyEvaluations { - if len(eval.Violations) > 0 && eval.Gate { - gated = true - break - } - } - blocked := !r.att.GetBypassPolicyCheck() && (gated || hasViolations && r.att.GetBlockOnPolicyViolation()) + blocked := !r.att.GetBypassPolicyCheck() && (evalResult.hasGatedViolations || (evalResult.hasViolations && r.att.GetBlockOnPolicyViolation())) p := ProvenancePredicateV02{ ProvenancePredicateCommon: predicateCommon(r.builder, r.att), Materials: normalizedMaterials, - PolicyEvaluations: policies, + PolicyEvaluations: evalResult.evaluations, PolicyEvaluationsRef: r.policyEvaluationsRef, - PolicyHasViolations: hasViolations, - PolicyHasGatedViolations: gated, + PolicyHasViolations: evalResult.hasViolations, + PolicyEvaluationsCount: evalResult.evaluationsCount, + PolicyViolationsCount: evalResult.violationsCount, + PolicyHasGatedViolations: evalResult.hasGatedViolations, PolicyCheckBlockingStrategy: policyCheckBlockingStrategy, PolicyBlockBypassEnabled: r.att.GetBypassPolicyCheck(), PolicyAttBlocked: blocked, @@ -288,7 +286,7 @@ func (r *RendererV02) predicate() (*structpb.Struct, error) { return predicate, nil } -func mappedPolicyEvaluations(att *v1.Attestation) (map[string][]*PolicyEvaluation, bool, error) { +func mappedPolicyEvaluations(att *v1.Attestation) (*evaluationsResult, error) { return groupEvaluations(att.GetPolicyEvaluations()) } @@ -300,13 +298,25 @@ func PolicyEvaluationsFromBundle(data []byte) (map[string][]*PolicyEvaluation, e return nil, fmt.Errorf("unmarshaling policy evaluation bundle: %w", err) } - evals, _, err := groupEvaluations(bundle.GetEvaluations()) - return evals, err + res, err := groupEvaluations(bundle.GetEvaluations()) + if err != nil { + return nil, err + } + return res.evaluations, nil } -func groupEvaluations(evals []*v1.PolicyEvaluation) (map[string][]*PolicyEvaluation, bool, error) { - var hasViolations bool - result := make(map[string][]*PolicyEvaluation) +type evaluationsResult struct { + evaluations map[string][]*PolicyEvaluation + hasViolations bool + hasGatedViolations bool + evaluationsCount int + violationsCount int +} + +func groupEvaluations(evals []*v1.PolicyEvaluation) (*evaluationsResult, error) { + res := &evaluationsResult{ + evaluations: make(map[string][]*PolicyEvaluation), + } for _, p := range evals { keyName := p.MaterialName @@ -316,17 +326,22 @@ func groupEvaluations(evals []*v1.PolicyEvaluation) (map[string][]*PolicyEvaluat ev, err := renderEvaluation(p) if err != nil { - return nil, false, err + return nil, err } if len(ev.Violations) > 0 { - hasViolations = true + res.hasViolations = true + res.violationsCount += len(ev.Violations) + if ev.Gate { + res.hasGatedViolations = true + } } - result[keyName] = append(result[keyName], ev) + res.evaluations[keyName] = append(res.evaluations[keyName], ev) } - return result, hasViolations, nil + res.evaluationsCount = len(evals) + return res, nil } func renderEvaluation(ev *v1.PolicyEvaluation) (*PolicyEvaluation, error) { @@ -454,6 +469,8 @@ func (p *ProvenancePredicateV02) GetPolicyEvaluationStatus() *PolicyEvaluationSt Blocked: p.PolicyAttBlocked, HasViolations: p.PolicyHasViolations, HasGatedViolations: p.PolicyHasGatedViolations, + EvaluationsCount: p.PolicyEvaluationsCount, + ViolationsCount: p.PolicyViolationsCount, } } diff --git a/pkg/attestation/renderer/chainloop/v02_test.go b/pkg/attestation/renderer/chainloop/v02_test.go index 16195ef1f..744687155 100644 --- a/pkg/attestation/renderer/chainloop/v02_test.go +++ b/pkg/attestation/renderer/chainloop/v02_test.go @@ -492,6 +492,124 @@ func TestPolicyEvaluationsFromBundle(t *testing.T) { } } +func TestGroupEvaluationsCounts(t *testing.T) { + tests := []struct { + name string + evals []*api.PolicyEvaluation + wantEvalCount int + wantViolCount int + wantViolations bool + wantGatedViolations bool + }{ + { + name: "no evaluations", + evals: nil, + wantEvalCount: 0, + wantViolCount: 0, + }, + { + name: "evaluations without violations", + evals: []*api.PolicyEvaluation{ + {Name: "check-1", MaterialName: "sbom"}, + {Name: "check-2", MaterialName: "sarif"}, + }, + wantEvalCount: 2, + wantViolCount: 0, + }, + { + name: "evaluations with violations", + evals: []*api.PolicyEvaluation{ + { + Name: "check-1", + MaterialName: "sbom", + Violations: []*api.PolicyEvaluation_Violation{ + {Subject: "s1", Message: "m1"}, + {Subject: "s2", Message: "m2"}, + }, + }, + {Name: "check-2", MaterialName: "sbom"}, + { + Name: "check-3", + MaterialName: "sarif", + Violations: []*api.PolicyEvaluation_Violation{ + {Subject: "s3", Message: "m3"}, + }, + }, + }, + wantEvalCount: 3, + wantViolCount: 3, + wantViolations: true, + }, + { + name: "single evaluation with multiple violations", + evals: []*api.PolicyEvaluation{ + { + Name: "check-1", + MaterialName: "image", + Violations: []*api.PolicyEvaluation_Violation{ + {Subject: "CVE-1", Message: "critical"}, + {Subject: "CVE-2", Message: "high"}, + {Subject: "CVE-3", Message: "medium"}, + }, + }, + }, + wantEvalCount: 1, + wantViolCount: 3, + wantViolations: true, + }, + { + name: "gated evaluation with violations", + evals: []*api.PolicyEvaluation{ + { + Name: "gated-check", + MaterialName: "sbom", + Gate: true, + Violations: []*api.PolicyEvaluation_Violation{ + {Subject: "s1", Message: "m1"}, + }, + }, + {Name: "check-2", MaterialName: "sbom"}, + }, + wantEvalCount: 2, + wantViolCount: 1, + wantViolations: true, + wantGatedViolations: true, + }, + { + name: "gated evaluation without violations", + evals: []*api.PolicyEvaluation{ + {Name: "gated-check", MaterialName: "sbom", Gate: true}, + }, + wantEvalCount: 1, + wantViolCount: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := groupEvaluations(tc.evals) + require.NoError(t, err) + assert.Equal(t, tc.wantEvalCount, res.evaluationsCount) + assert.Equal(t, tc.wantViolCount, res.violationsCount) + assert.Equal(t, tc.wantViolations, res.hasViolations) + assert.Equal(t, tc.wantGatedViolations, res.hasGatedViolations) + }) + } +} + +func TestPolicyEvaluationStatusCounts(t *testing.T) { + p := &ProvenancePredicateV02{ + PolicyEvaluationsCount: 5, + PolicyViolationsCount: 2, + PolicyHasViolations: true, + } + + status := p.GetPolicyEvaluationStatus() + assert.Equal(t, 5, status.EvaluationsCount) + assert.Equal(t, 2, status.ViolationsCount) + assert.True(t, status.HasViolations) +} + func TestPolicyEvaluationsField(t *testing.T) { raw, err := os.ReadFile("testdata/attestation-pe-snake.json") require.NoError(t, err)