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)