From c710e6114a7f63106ea9f13a706cc82404c241e0 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 7 Sep 2026 11:55:00 +0200 Subject: [PATCH 1/2] fix(controlplane): cap the policy evaluations inlined by the workflow-run View API A single attestation can carry a six-figure number of policy violations. Inlining one in a View response held the payload in memory several times over -- CAS download buffer, decoded bundle, regrouped evaluations and response protos -- which was enough to exhaust the control plane. The View API now asks the CAS for the bundle size before downloading it. Bundles above a configurable cap, defaulting to 10MiB, are returned as a reference carrying the digest, size and reason instead of the evaluations themselves, and are never downloaded. The same reference is returned when the bundle cannot be resolved or decoded, so no failure path falls back to an unbounded download. Status and counters continue to be served from the predicate, so gating, bypass and violation counts stay complete regardless. The CLI renders the counters plus the artifact download command in place of the policy table, and exposes the reference in its JSON output. Attestation crafting and push are unchanged. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: d400245f-b8d2-4805-998e-b23f9be4a7fb --- app/cli/cmd/workflow_workflow_run_describe.go | 73 ++- .../workflow_workflow_run_describe_test.go | 147 ++++- app/cli/pkg/action/workflow_run_describe.go | 74 ++- .../controlplane/v1/response_messages.pb.go | 500 ++++++++++++------ .../controlplane/v1/response_messages.proto | 27 + .../controlplane/v1/response_messages.ts | 181 +++++++ ...olplane.v1.AttestationItem.jsonschema.json | 8 + ...ontrolplane.v1.AttestationItem.schema.json | 8 + ...ne.v1.PolicyEvaluationsRef.jsonschema.json | 71 +++ ...lplane.v1.PolicyEvaluationsRef.schema.json | 71 +++ app/controlplane/cmd/wire_gen.go | 1 + .../conf/controlplane/config/v1/conf.pb.go | 25 +- .../conf/controlplane/config/v1/conf.proto | 8 + .../internal/service/workflowrun.go | 129 ++++- .../internal/service/workflowrun_test.go | 260 +++++++++ app/controlplane/pkg/biz/.mockery.yml | 1 + app/controlplane/pkg/biz/casclient.go | 23 + app/controlplane/pkg/biz/casclient_test.go | 73 ++- app/controlplane/pkg/biz/mocks/CASClient.go | 279 +++++++++- pkg/casclient/.mockery.yml | 18 + pkg/casclient/casclient.go | 5 +- pkg/casclient/mocks/Downloader.go | 221 +++++++- pkg/casclient/mocks/DownloaderUploader.go | 364 +++++++++++-- 23 files changed, 2256 insertions(+), 311 deletions(-) create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.jsonschema.json create mode 100644 app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.schema.json create mode 100644 app/controlplane/internal/service/workflowrun_test.go create mode 100644 pkg/casclient/.mockery.yml diff --git a/app/cli/cmd/workflow_workflow_run_describe.go b/app/cli/cmd/workflow_workflow_run_describe.go index 38bd4ca3e..f6bb4f35c 100644 --- a/app/cli/cmd/workflow_workflow_run_describe.go +++ b/app/cli/cmd/workflow_workflow_run_describe.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "code.cloudfoundry.org/bytefmt" "github.com/chainloop-dev/chainloop/app/cli/cmd/output" "github.com/chainloop-dev/chainloop/app/cli/pkg/action" attv1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" @@ -174,11 +175,7 @@ func workflowRunDescribeTableOutput(run *action.WorkflowRunItemFull) error { gt.AppendRow(table.Row{"Policy enforcement bypassed", att.PolicyEvaluationStatus.Bypassed}) } - evs := att.PolicyEvaluations[chainloop.AttPolicyEvaluation] - if len(evs) > 0 { - gt.AppendRow(table.Row{"Policies", "------"}) - policiesTable(evs, gt, flagDebug) - } + appendPolicySection(att, gt, flagDebug) if run.Attestation.AttestationViewURL != "" { gt.AppendRow(table.Row{"Attestation View URL", run.Attestation.AttestationViewURL}) @@ -301,6 +298,72 @@ func policiesTable(evs []*action.PolicyEvaluation, mt table.Writer, debugMode bo } } +// appendPolicySection renders the attestation-level policies, either as the +// usual per-policy rows or, when the server declined to inline the +// evaluations, as a notice pointing at the bundle in the CAS backend. +func appendPolicySection(att *action.WorkflowRunAttestationItem, gt table.Writer, debugMode bool) { + if notice := policyEvaluationsRefNotice(att.PolicyEvaluationsRef, att.PolicyEvaluationStatus); notice != nil { + gt.AppendRow(table.Row{"Policies", "------"}) + for _, line := range notice { + gt.AppendRow(table.Row{"", line}) + } + + return + } + + evs := att.PolicyEvaluations[chainloop.AttPolicyEvaluation] + if len(evs) == 0 { + return + } + + gt.AppendRow(table.Row{"Policies", "------"}) + policiesTable(evs, gt, debugMode) +} + +// policyEvaluationsRefNotice renders the lines shown in place of the policy +// table when the server returned a reference instead of the evaluations. It +// leads with the counters, which stay accurate no matter how large the bundle +// is, and only offers a download when the bundle is known to be there. +func policyEvaluationsRefNotice(ref *action.PolicyEvaluationsRef, status *action.PolicyEvaluationStatus) []string { + if ref == nil { + return nil + } + + reason := "could not be retrieved from the CAS backend" + if ref.Reason == action.PolicyEvaluationsRefReasonTooLarge { + reason = "too large to include inline" + if ref.SizeBytes > 0 { + reason = fmt.Sprintf("%s (%s)", reason, bytefmt.ByteSize(uint64(ref.SizeBytes))) + } + } + + // Without counters there is nothing to summarize, so name the subject instead. + if status == nil { + lines := []string{fmt.Sprintf("policy evaluations %s", reason)} + if ref.Reason == action.PolicyEvaluationsRefReasonTooLarge { + lines = append(lines, downloadPolicyEvaluationsHint(ref)) + } + + return lines + } + + counters := fmt.Sprintf("%d evaluations, %d violations", status.Total, status.Violated) + if status.Suppressed > 0 { + counters = fmt.Sprintf("%s (%d suppressed)", counters, status.Suppressed) + } + + lines := []string{fmt.Sprintf("%s - %s", counters, reason)} + if ref.Reason == action.PolicyEvaluationsRefReasonTooLarge { + lines = append(lines, downloadPolicyEvaluationsHint(ref)) + } + + return lines +} + +func downloadPolicyEvaluationsHint(ref *action.PolicyEvaluationsRef) string { + return fmt.Sprintf("inspect with: chainloop artifact download --digest %s", ref.Digest) +} + // violationSummary builds a single-line description of a violation using the // structured finding when present (CVE id + severity + package + fix info, // or SAST rule + location, or license + component). Falls back to the first diff --git a/app/cli/cmd/workflow_workflow_run_describe_test.go b/app/cli/cmd/workflow_workflow_run_describe_test.go index fb76e11fe..04c886cfb 100644 --- a/app/cli/cmd/workflow_workflow_run_describe_test.go +++ b/app/cli/cmd/workflow_workflow_run_describe_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import ( "github.com/chainloop-dev/chainloop/app/cli/pkg/action" attv1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" + "github.com/jedib0t/go-pretty/v6/table" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -208,3 +210,146 @@ func (s *workflowRunDescribeSuite) TestOutputTypePayload() { s.Require().NoError(err) s.Equal(expected, buf.String()) } + +const ( + testRefDigest = "sha256:abc123" + testRefDownloadHint = "inspect with: chainloop artifact download --digest " + testRefDigest + policiesRowLabel = "Policies" +) + +func TestPolicyEvaluationsRefNotice(t *testing.T) { + tests := []struct { + name string + ref *action.PolicyEvaluationsRef + status *action.PolicyEvaluationStatus + want []string + }{ + { + name: "no reference produces no notice", + }, + { + name: "oversized bundle reports counters, size and how to fetch it", + ref: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + SizeBytes: 64 * 1024 * 1024, + Reason: action.PolicyEvaluationsRefReasonTooLarge, + }, + status: &action.PolicyEvaluationStatus{Total: 12, Violated: 134112, Suppressed: 86321}, + want: []string{ + "12 evaluations, 134112 violations (86321 suppressed) - too large to include inline (64M)", + testRefDownloadHint, + }, + }, + { + name: "oversized bundle with nothing suppressed omits the suppressed count", + ref: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + SizeBytes: 3 * 1024 * 1024, + Reason: action.PolicyEvaluationsRefReasonTooLarge, + }, + status: &action.PolicyEvaluationStatus{Total: 2, Violated: 40}, + want: []string{ + "2 evaluations, 40 violations - too large to include inline (3M)", + testRefDownloadHint, + }, + }, + { + name: "unknown size omits the size", + ref: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + Reason: action.PolicyEvaluationsRefReasonTooLarge, + }, + status: &action.PolicyEvaluationStatus{Total: 2, Violated: 40}, + want: []string{ + "2 evaluations, 40 violations - too large to include inline", + testRefDownloadHint, + }, + }, + { + name: "unavailable bundle does not suggest a download", + ref: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + Reason: action.PolicyEvaluationsRefReasonUnavailable, + }, + status: &action.PolicyEvaluationStatus{Total: 3, Violated: 7}, + want: []string{ + "3 evaluations, 7 violations - could not be retrieved from the CAS backend", + }, + }, + { + name: "missing status still reports the reason", + ref: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + SizeBytes: 1024, + Reason: action.PolicyEvaluationsRefReasonTooLarge, + }, + want: []string{ + "policy evaluations too large to include inline (1K)", + testRefDownloadHint, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, policyEvaluationsRefNotice(tc.ref, tc.status)) + }) + } +} + +func TestAppendPolicySection(t *testing.T) { + tests := []struct { + name string + attestation *action.WorkflowRunAttestationItem + wantContain []string + wantAbsent []string + }{ + { + name: "inlined evaluations are rendered as policy rows", + attestation: &action.WorkflowRunAttestationItem{ + PolicyEvaluations: map[string][]*action.PolicyEvaluation{ + chainloop.AttPolicyEvaluation: { + {Name: "strong-acl", Violations: []*action.PolicyViolation{{Message: "weak ACL"}}}, + }, + }, + PolicyEvaluationStatus: &action.PolicyEvaluationStatus{Total: 1, Violated: 1}, + }, + wantContain: []string{policiesRowLabel, "strong-acl", "weak ACL"}, + wantAbsent: []string{"artifact download"}, + }, + { + name: "an oversized bundle is rendered as a notice instead", + attestation: &action.WorkflowRunAttestationItem{ + PolicyEvaluationStatus: &action.PolicyEvaluationStatus{Total: 12, Violated: 134112}, + PolicyEvaluationsRef: &action.PolicyEvaluationsRef{ + Digest: testRefDigest, + SizeBytes: 64 * 1024 * 1024, + Reason: action.PolicyEvaluationsRefReasonTooLarge, + }, + }, + wantContain: []string{policiesRowLabel, "134112 violations", "too large", "artifact download --digest " + testRefDigest}, + }, + { + name: "no policies and no reference renders nothing", + attestation: &action.WorkflowRunAttestationItem{ + PolicyEvaluationStatus: &action.PolicyEvaluationStatus{}, + }, + wantAbsent: []string{"Policies"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tw := table.NewWriter() + appendPolicySection(tc.attestation, tw, false) + got := tw.Render() + + for _, want := range tc.wantContain { + assert.Contains(t, got, want) + } + for _, absent := range tc.wantAbsent { + assert.NotContains(t, got, absent) + } + }) + } +} diff --git a/app/cli/pkg/action/workflow_run_describe.go b/app/cli/pkg/action/workflow_run_describe.go index aae6448bb..8d36c55cb 100644 --- a/app/cli/pkg/action/workflow_run_describe.go +++ b/app/cli/pkg/action/workflow_run_describe.go @@ -63,6 +63,10 @@ type WorkflowRunAttestationItem struct { PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"` // Policy evaluation status PolicyEvaluationStatus *PolicyEvaluationStatus `json:"policy_evaluation_status,omitempty"` + // Set when the evaluations were not inlined above and must be fetched from + // the CAS backend instead. PolicyEvaluations is empty in that case, while + // PolicyEvaluationStatus stays complete. + PolicyEvaluationsRef *PolicyEvaluationsRef `json:"policy_evaluations_ref,omitempty"` // URL to view the attestation in the UI AttestationViewURL string `json:"attestation_view_url"` } @@ -73,6 +77,15 @@ type PolicyEvaluationStatus struct { Blocked bool `json:"blocked"` HasViolations bool `json:"has_violations"` HasGatedViolations bool `json:"has_gated_violations"` + // Canonical, server-computed status and counters. Status is empty for + // runs that predate the server materializing the summary. + Status string `json:"status,omitempty"` + Total int `json:"total"` + Passed int `json:"passed"` + Skipped int `json:"skipped"` + Violated int `json:"violated"` + Suppressed int `json:"suppressed"` + HasGates bool `json:"has_gates"` } type Material struct { @@ -98,6 +111,27 @@ type Annotation struct { Value string `json:"value"` } +// PolicyEvaluationsRefReason explains why the evaluations were not included +// in the response. +type PolicyEvaluationsRefReason string + +const ( + // The bundle is larger than the server is willing to inline + PolicyEvaluationsRefReasonTooLarge PolicyEvaluationsRefReason = "TOO_LARGE" + // The bundle could not be resolved from the CAS backend + PolicyEvaluationsRefReasonUnavailable PolicyEvaluationsRefReason = "UNAVAILABLE" +) + +// PolicyEvaluationsRef points at a policy-evaluation bundle stored in a CAS +// backend, returned in place of the evaluations themselves. +type PolicyEvaluationsRef struct { + Digest string `json:"digest"` + // Size of the bundle in bytes, zero when it could not be determined + SizeBytes int64 `json:"size_bytes,omitempty"` + MediaType string `json:"media_type,omitempty"` + Reason PolicyEvaluationsRefReason `json:"reason"` +} + type PolicyEvaluation struct { Name string `json:"name"` MaterialName string `json:"material_name,omitempty"` @@ -237,6 +271,14 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes } policyEvaluationStatus := att.GetPolicyEvaluationStatus() + summary := policyEvaluationStatus.GetSummary() + + // Left empty for runs that predate the server materializing the summary, + // so consumers can tell "no policies" apart from "not reported". + var summaryStatus string + if summary != nil { + summaryStatus = summary.GetStatus().String() + } var attestationViewURL string baseUIDashboardURL := fetchUIDashboardURL(ctx, action.cfg.CPConnection) @@ -259,8 +301,16 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes Blocked: policyEvaluationStatus.Blocked, HasViolations: policyEvaluationStatus.HasViolations, HasGatedViolations: policyEvaluationStatus.HasGatedViolations, + Status: summaryStatus, + Total: int(summary.GetTotal()), + Passed: int(summary.GetPassed()), + Skipped: int(summary.GetSkipped()), + Violated: int(summary.GetViolated()), + Suppressed: int(summary.GetSuppressed()), + HasGates: summary.GetHasGates(), }, - AttestationViewURL: attestationViewURL, + PolicyEvaluationsRef: pbPolicyEvaluationsRefToAction(att.GetPolicyEvaluationsRef()), + AttestationViewURL: attestationViewURL, } return item, nil @@ -289,6 +339,28 @@ func trustedRootPbToVerifier(resp *pb.GetTrustedRootResponse) (*verifier.Trusted return tr, nil } +// pbPolicyEvaluationsRefToAction maps the reference the server returns when it +// declines to inline the evaluations. An unspecified reason is treated as +// unavailable, which is the more conservative rendering: it does not promise +// the caller that a download would succeed. +func pbPolicyEvaluationsRefToAction(in *pb.PolicyEvaluationsRef) *PolicyEvaluationsRef { + if in == nil { + return nil + } + + reason := PolicyEvaluationsRefReasonUnavailable + if in.GetReason() == pb.PolicyEvaluationsRef_REASON_TOO_LARGE { + reason = PolicyEvaluationsRefReasonTooLarge + } + + return &PolicyEvaluationsRef{ + Digest: in.GetDigest(), + SizeBytes: in.GetSizeBytes(), + MediaType: in.GetMediaType(), + Reason: reason, + } +} + func policyEvaluationPBToAction(in *pb.PolicyEvaluation) *PolicyEvaluation { var pr *PolicyReference if in.PolicyReference != nil { diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index 1b236dac0..4bc706ee7 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -579,6 +579,57 @@ func (UserNotMemberOfOrgError) EnumDescriptor() ([]byte, []int) { return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{9} } +type PolicyEvaluationsRef_Reason int32 + +const ( + PolicyEvaluationsRef_REASON_UNSPECIFIED PolicyEvaluationsRef_Reason = 0 + // The bundle exceeds the maximum size the server inlines in a response + PolicyEvaluationsRef_REASON_TOO_LARGE PolicyEvaluationsRef_Reason = 1 + // The bundle could not be resolved from CAS + PolicyEvaluationsRef_REASON_UNAVAILABLE PolicyEvaluationsRef_Reason = 2 +) + +// Enum value maps for PolicyEvaluationsRef_Reason. +var ( + PolicyEvaluationsRef_Reason_name = map[int32]string{ + 0: "REASON_UNSPECIFIED", + 1: "REASON_TOO_LARGE", + 2: "REASON_UNAVAILABLE", + } + PolicyEvaluationsRef_Reason_value = map[string]int32{ + "REASON_UNSPECIFIED": 0, + "REASON_TOO_LARGE": 1, + "REASON_UNAVAILABLE": 2, + } +) + +func (x PolicyEvaluationsRef_Reason) Enum() *PolicyEvaluationsRef_Reason { + p := new(PolicyEvaluationsRef_Reason) + *p = x + return p +} + +func (x PolicyEvaluationsRef_Reason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PolicyEvaluationsRef_Reason) Descriptor() protoreflect.EnumDescriptor { + return file_controlplane_v1_response_messages_proto_enumTypes[10].Descriptor() +} + +func (PolicyEvaluationsRef_Reason) Type() protoreflect.EnumType { + return &file_controlplane_v1_response_messages_proto_enumTypes[10] +} + +func (x PolicyEvaluationsRef_Reason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PolicyEvaluationsRef_Reason.Descriptor instead. +func (PolicyEvaluationsRef_Reason) EnumDescriptor() ([]byte, []int) { + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{5, 0} +} + type WorkflowContractVersionItem_RawBody_Format int32 const ( @@ -615,11 +666,11 @@ func (x WorkflowContractVersionItem_RawBody_Format) String() string { } func (WorkflowContractVersionItem_RawBody_Format) Descriptor() protoreflect.EnumDescriptor { - return file_controlplane_v1_response_messages_proto_enumTypes[10].Descriptor() + return file_controlplane_v1_response_messages_proto_enumTypes[11].Descriptor() } func (WorkflowContractVersionItem_RawBody_Format) Type() protoreflect.EnumType { - return &file_controlplane_v1_response_messages_proto_enumTypes[10] + return &file_controlplane_v1_response_messages_proto_enumTypes[11] } func (x WorkflowContractVersionItem_RawBody_Format) Number() protoreflect.EnumNumber { @@ -628,7 +679,7 @@ func (x WorkflowContractVersionItem_RawBody_Format) Number() protoreflect.EnumNu // Deprecated: Use WorkflowContractVersionItem_RawBody_Format.Descriptor instead. func (WorkflowContractVersionItem_RawBody_Format) EnumDescriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{12, 0, 0} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{13, 0, 0} } type OrgItem_PolicyViolationBlockingStrategy int32 @@ -664,11 +715,11 @@ func (x OrgItem_PolicyViolationBlockingStrategy) String() string { } func (OrgItem_PolicyViolationBlockingStrategy) Descriptor() protoreflect.EnumDescriptor { - return file_controlplane_v1_response_messages_proto_enumTypes[11].Descriptor() + return file_controlplane_v1_response_messages_proto_enumTypes[12].Descriptor() } func (OrgItem_PolicyViolationBlockingStrategy) Type() protoreflect.EnumType { - return &file_controlplane_v1_response_messages_proto_enumTypes[11] + return &file_controlplane_v1_response_messages_proto_enumTypes[12] } func (x OrgItem_PolicyViolationBlockingStrategy) Number() protoreflect.EnumNumber { @@ -677,7 +728,7 @@ func (x OrgItem_PolicyViolationBlockingStrategy) Number() protoreflect.EnumNumbe // Deprecated: Use OrgItem_PolicyViolationBlockingStrategy.Descriptor instead. func (OrgItem_PolicyViolationBlockingStrategy) EnumDescriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{15, 0} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16, 0} } type CASBackendItem_ValidationStatus int32 @@ -713,11 +764,11 @@ func (x CASBackendItem_ValidationStatus) String() string { } func (CASBackendItem_ValidationStatus) Descriptor() protoreflect.EnumDescriptor { - return file_controlplane_v1_response_messages_proto_enumTypes[12].Descriptor() + return file_controlplane_v1_response_messages_proto_enumTypes[13].Descriptor() } func (CASBackendItem_ValidationStatus) Type() protoreflect.EnumType { - return &file_controlplane_v1_response_messages_proto_enumTypes[12] + return &file_controlplane_v1_response_messages_proto_enumTypes[13] } func (x CASBackendItem_ValidationStatus) Number() protoreflect.EnumNumber { @@ -726,7 +777,7 @@ func (x CASBackendItem_ValidationStatus) Number() protoreflect.EnumNumber { // Deprecated: Use CASBackendItem_ValidationStatus.Descriptor instead. func (CASBackendItem_ValidationStatus) EnumDescriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16, 0} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{17, 0} } type WorkflowItem struct { @@ -1259,8 +1310,14 @@ type AttestationItem struct { Annotations map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PolicyEvaluations map[string]*PolicyEvaluations `protobuf:"bytes,8,rep,name=policy_evaluations,json=policyEvaluations,proto3" json:"policy_evaluations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PolicyEvaluationStatus *AttestationItem_PolicyEvaluationStatus `protobuf:"bytes,9,opt,name=policy_evaluation_status,json=policyEvaluationStatus,proto3" json:"policy_evaluation_status,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Reference to the policy-evaluation bundle in the CAS backend. Populated + // only when the evaluations are NOT inlined in policy_evaluations, either + // because the bundle is larger than the server inlines or because it could + // not be resolved. Fetch the bundle by digest to inspect the violations. + // Counters and status in policy_evaluation_status remain complete either way. + PolicyEvaluationsRef *PolicyEvaluationsRef `protobuf:"bytes,11,opt,name=policy_evaluations_ref,json=policyEvaluationsRef,proto3" json:"policy_evaluations_ref,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttestationItem) Reset() { @@ -1350,6 +1407,87 @@ func (x *AttestationItem) GetPolicyEvaluationStatus() *AttestationItem_PolicyEva return nil } +func (x *AttestationItem) GetPolicyEvaluationsRef() *PolicyEvaluationsRef { + if x != nil { + return x.PolicyEvaluationsRef + } + return nil +} + +// Pointer to a policy-evaluation bundle held in a CAS backend, returned in +// place of the evaluations themselves. +type PolicyEvaluationsRef struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Digest of the bundle as stored in CAS, in "sha256:" form + Digest string `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"` + // Size of the bundle in bytes. Zero when the size could not be determined. + SizeBytes int64 `protobuf:"varint,2,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + // Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json + MediaType string `protobuf:"bytes,3,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"` + // Why the evaluations were not inlined + Reason PolicyEvaluationsRef_Reason `protobuf:"varint,4,opt,name=reason,proto3,enum=controlplane.v1.PolicyEvaluationsRef_Reason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyEvaluationsRef) Reset() { + *x = PolicyEvaluationsRef{} + mi := &file_controlplane_v1_response_messages_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyEvaluationsRef) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyEvaluationsRef) ProtoMessage() {} + +func (x *PolicyEvaluationsRef) ProtoReflect() protoreflect.Message { + mi := &file_controlplane_v1_response_messages_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyEvaluationsRef.ProtoReflect.Descriptor instead. +func (*PolicyEvaluationsRef) Descriptor() ([]byte, []int) { + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{5} +} + +func (x *PolicyEvaluationsRef) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + +func (x *PolicyEvaluationsRef) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *PolicyEvaluationsRef) GetMediaType() string { + if x != nil { + return x.MediaType + } + return "" +} + +func (x *PolicyEvaluationsRef) GetReason() PolicyEvaluationsRef_Reason { + if x != nil { + return x.Reason + } + return PolicyEvaluationsRef_REASON_UNSPECIFIED +} + type PolicyEvaluations struct { state protoimpl.MessageState `protogen:"open.v1"` Evaluations []*PolicyEvaluation `protobuf:"bytes,1,rep,name=evaluations,proto3" json:"evaluations,omitempty"` @@ -1359,7 +1497,7 @@ type PolicyEvaluations struct { func (x *PolicyEvaluations) Reset() { *x = PolicyEvaluations{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[5] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1371,7 +1509,7 @@ func (x *PolicyEvaluations) String() string { func (*PolicyEvaluations) ProtoMessage() {} func (x *PolicyEvaluations) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[5] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1384,7 +1522,7 @@ func (x *PolicyEvaluations) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyEvaluations.ProtoReflect.Descriptor instead. func (*PolicyEvaluations) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{5} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{6} } func (x *PolicyEvaluations) GetEvaluations() []*PolicyEvaluation { @@ -1418,7 +1556,7 @@ type PolicyEvaluation struct { func (x *PolicyEvaluation) Reset() { *x = PolicyEvaluation{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[6] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1430,7 +1568,7 @@ func (x *PolicyEvaluation) String() string { func (*PolicyEvaluation) ProtoMessage() {} func (x *PolicyEvaluation) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[6] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1443,7 +1581,7 @@ func (x *PolicyEvaluation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyEvaluation.ProtoReflect.Descriptor instead. func (*PolicyEvaluation) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{6} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{7} } func (x *PolicyEvaluation) GetName() string { @@ -1574,7 +1712,7 @@ type PolicyViolation struct { func (x *PolicyViolation) Reset() { *x = PolicyViolation{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[7] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1586,7 +1724,7 @@ func (x *PolicyViolation) String() string { func (*PolicyViolation) ProtoMessage() {} func (x *PolicyViolation) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[7] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1599,7 +1737,7 @@ func (x *PolicyViolation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyViolation.ProtoReflect.Descriptor instead. func (*PolicyViolation) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{7} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{8} } func (x *PolicyViolation) GetSubject() string { @@ -1691,7 +1829,7 @@ type PolicyReference struct { func (x *PolicyReference) Reset() { *x = PolicyReference{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[8] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1703,7 +1841,7 @@ func (x *PolicyReference) String() string { func (*PolicyReference) ProtoMessage() {} func (x *PolicyReference) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[8] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1716,7 +1854,7 @@ func (x *PolicyReference) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyReference.ProtoReflect.Descriptor instead. func (*PolicyReference) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{8} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{9} } func (x *PolicyReference) GetName() string { @@ -1772,7 +1910,7 @@ type WorkflowContractItem struct { func (x *WorkflowContractItem) Reset() { *x = WorkflowContractItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[9] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1784,7 +1922,7 @@ func (x *WorkflowContractItem) String() string { func (*WorkflowContractItem) ProtoMessage() {} func (x *WorkflowContractItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[9] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1797,7 +1935,7 @@ func (x *WorkflowContractItem) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkflowContractItem.ProtoReflect.Descriptor instead. func (*WorkflowContractItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{9} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{10} } func (x *WorkflowContractItem) GetId() string { @@ -1886,7 +2024,7 @@ type ScopedEntity struct { func (x *ScopedEntity) Reset() { *x = ScopedEntity{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[10] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1898,7 +2036,7 @@ func (x *ScopedEntity) String() string { func (*ScopedEntity) ProtoMessage() {} func (x *ScopedEntity) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[10] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1911,7 +2049,7 @@ func (x *ScopedEntity) ProtoReflect() protoreflect.Message { // Deprecated: Use ScopedEntity.ProtoReflect.Descriptor instead. func (*ScopedEntity) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{10} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{11} } func (x *ScopedEntity) GetType() string { @@ -1946,7 +2084,7 @@ type WorkflowRef struct { func (x *WorkflowRef) Reset() { *x = WorkflowRef{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[11] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1958,7 +2096,7 @@ func (x *WorkflowRef) String() string { func (*WorkflowRef) ProtoMessage() {} func (x *WorkflowRef) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[11] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1971,7 +2109,7 @@ func (x *WorkflowRef) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkflowRef.ProtoReflect.Descriptor instead. func (*WorkflowRef) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{11} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{12} } func (x *WorkflowRef) GetId() string { @@ -2015,7 +2153,7 @@ type WorkflowContractVersionItem struct { func (x *WorkflowContractVersionItem) Reset() { *x = WorkflowContractVersionItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[12] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2027,7 +2165,7 @@ func (x *WorkflowContractVersionItem) String() string { func (*WorkflowContractVersionItem) ProtoMessage() {} func (x *WorkflowContractVersionItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[12] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2040,7 +2178,7 @@ func (x *WorkflowContractVersionItem) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkflowContractVersionItem.ProtoReflect.Descriptor instead. func (*WorkflowContractVersionItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{12} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{13} } func (x *WorkflowContractVersionItem) GetId() string { @@ -2130,7 +2268,7 @@ type User struct { func (x *User) Reset() { *x = User{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[13] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2142,7 +2280,7 @@ func (x *User) String() string { func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[13] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2155,7 +2293,7 @@ func (x *User) ProtoReflect() protoreflect.Message { // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{13} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{14} } func (x *User) GetId() string { @@ -2222,7 +2360,7 @@ type OrgMembershipItem struct { func (x *OrgMembershipItem) Reset() { *x = OrgMembershipItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[14] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2234,7 +2372,7 @@ func (x *OrgMembershipItem) String() string { func (*OrgMembershipItem) ProtoMessage() {} func (x *OrgMembershipItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[14] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2247,7 +2385,7 @@ func (x *OrgMembershipItem) ProtoReflect() protoreflect.Message { // Deprecated: Use OrgMembershipItem.ProtoReflect.Descriptor instead. func (*OrgMembershipItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{14} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{15} } func (x *OrgMembershipItem) GetId() string { @@ -2325,7 +2463,7 @@ type OrgItem struct { func (x *OrgItem) Reset() { *x = OrgItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2337,7 +2475,7 @@ func (x *OrgItem) String() string { func (*OrgItem) ProtoMessage() {} func (x *OrgItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[15] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2350,7 +2488,7 @@ func (x *OrgItem) ProtoReflect() protoreflect.Message { // Deprecated: Use OrgItem.ProtoReflect.Descriptor instead. func (*OrgItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{15} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16} } func (x *OrgItem) GetId() string { @@ -2469,7 +2607,7 @@ type CASBackendItem struct { func (x *CASBackendItem) Reset() { *x = CASBackendItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2481,7 +2619,7 @@ func (x *CASBackendItem) String() string { func (*CASBackendItem) ProtoMessage() {} func (x *CASBackendItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[16] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2494,7 +2632,7 @@ func (x *CASBackendItem) ProtoReflect() protoreflect.Message { // Deprecated: Use CASBackendItem.ProtoReflect.Descriptor instead. func (*CASBackendItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{17} } func (x *CASBackendItem) GetId() string { @@ -2621,7 +2759,7 @@ type APITokenItem struct { func (x *APITokenItem) Reset() { *x = APITokenItem{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[17] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2633,7 +2771,7 @@ func (x *APITokenItem) String() string { func (*APITokenItem) ProtoMessage() {} func (x *APITokenItem) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[17] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2646,7 +2784,7 @@ func (x *APITokenItem) ProtoReflect() protoreflect.Message { // Deprecated: Use APITokenItem.ProtoReflect.Descriptor instead. func (*APITokenItem) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{17} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{18} } func (x *APITokenItem) GetId() string { @@ -2744,7 +2882,7 @@ type AttestationItem_PolicyEvaluationStatus struct { func (x *AttestationItem_PolicyEvaluationStatus) Reset() { *x = AttestationItem_PolicyEvaluationStatus{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2756,7 +2894,7 @@ func (x *AttestationItem_PolicyEvaluationStatus) String() string { func (*AttestationItem_PolicyEvaluationStatus) ProtoMessage() {} func (x *AttestationItem_PolicyEvaluationStatus) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[20] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2840,7 +2978,7 @@ type AttestationItem_EnvVariable struct { func (x *AttestationItem_EnvVariable) Reset() { *x = AttestationItem_EnvVariable{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2852,7 +2990,7 @@ func (x *AttestationItem_EnvVariable) String() string { func (*AttestationItem_EnvVariable) ProtoMessage() {} func (x *AttestationItem_EnvVariable) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[21] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2913,7 +3051,7 @@ type AttestationItem_Material struct { func (x *AttestationItem_Material) Reset() { *x = AttestationItem_Material{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[22] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2925,7 +3063,7 @@ func (x *AttestationItem_Material) String() string { func (*AttestationItem_Material) ProtoMessage() {} func (x *AttestationItem_Material) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[22] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3022,7 +3160,7 @@ type WorkflowContractVersionItem_RawBody struct { func (x *WorkflowContractVersionItem_RawBody) Reset() { *x = WorkflowContractVersionItem_RawBody{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[27] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3034,7 +3172,7 @@ func (x *WorkflowContractVersionItem_RawBody) String() string { func (*WorkflowContractVersionItem_RawBody) ProtoMessage() {} func (x *WorkflowContractVersionItem_RawBody) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[27] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3047,7 +3185,7 @@ func (x *WorkflowContractVersionItem_RawBody) ProtoReflect() protoreflect.Messag // Deprecated: Use WorkflowContractVersionItem_RawBody.ProtoReflect.Descriptor instead. func (*WorkflowContractVersionItem_RawBody) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{12, 0} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{13, 0} } func (x *WorkflowContractVersionItem_RawBody) GetBody() []byte { @@ -3074,7 +3212,7 @@ type CASBackendItem_Limits struct { func (x *CASBackendItem_Limits) Reset() { *x = CASBackendItem_Limits{} - mi := &file_controlplane_v1_response_messages_proto_msgTypes[28] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3086,7 +3224,7 @@ func (x *CASBackendItem_Limits) String() string { func (*CASBackendItem_Limits) ProtoMessage() {} func (x *CASBackendItem_Limits) ProtoReflect() protoreflect.Message { - mi := &file_controlplane_v1_response_messages_proto_msgTypes[28] + mi := &file_controlplane_v1_response_messages_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3099,7 +3237,7 @@ func (x *CASBackendItem_Limits) ProtoReflect() protoreflect.Message { // Deprecated: Use CASBackendItem_Limits.ProtoReflect.Descriptor instead. func (*CASBackendItem_Limits) Descriptor() ([]byte, []int) { - return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{16, 0} + return file_controlplane_v1_response_messages_proto_rawDescGZIP(), []int{17, 0} } func (x *CASBackendItem_Limits) GetMaxBytes() int64 { @@ -3173,7 +3311,7 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\thas_gates\x18\x06 \x01(\bR\bhasGates\x12\x1e\n" + "\n" + "suppressed\x18\a \x01(\x05R\n" + - "suppressed\"\xa4\f\n" + + "suppressed\"\x81\r\n" + "\x0fAttestationItem\x12\x1e\n" + "\benvelope\x18\x03 \x01(\fB\x02\x18\x01R\benvelope\x12\x16\n" + "\x06bundle\x18\n" + @@ -3183,7 +3321,8 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\tmaterials\x18\x05 \x03(\v2).controlplane.v1.AttestationItem.MaterialR\tmaterials\x12S\n" + "\vannotations\x18\x06 \x03(\v21.controlplane.v1.AttestationItem.AnnotationsEntryR\vannotations\x12f\n" + "\x12policy_evaluations\x18\b \x03(\v27.controlplane.v1.AttestationItem.PolicyEvaluationsEntryR\x11policyEvaluations\x12q\n" + - "\x18policy_evaluation_status\x18\t \x01(\v27.controlplane.v1.AttestationItem.PolicyEvaluationStatusR\x16policyEvaluationStatus\x1a>\n" + + "\x18policy_evaluation_status\x18\t \x01(\v27.controlplane.v1.AttestationItem.PolicyEvaluationStatusR\x16policyEvaluationStatus\x12[\n" + + "\x16policy_evaluations_ref\x18\v \x01(\v2%.controlplane.v1.PolicyEvaluationsRefR\x14policyEvaluationsRef\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1ah\n" + @@ -3216,7 +3355,18 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + " \x01(\fR\brawValue\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"X\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x82\x02\n" + + "\x14PolicyEvaluationsRef\x12\x16\n" + + "\x06digest\x18\x01 \x01(\tR\x06digest\x12\x1d\n" + + "\n" + + "size_bytes\x18\x02 \x01(\x03R\tsizeBytes\x12\x1d\n" + + "\n" + + "media_type\x18\x03 \x01(\tR\tmediaType\x12D\n" + + "\x06reason\x18\x04 \x01(\x0e2,.controlplane.v1.PolicyEvaluationsRef.ReasonR\x06reason\"N\n" + + "\x06Reason\x12\x16\n" + + "\x12REASON_UNSPECIFIED\x10\x00\x12\x14\n" + + "\x10REASON_TOO_LARGE\x10\x01\x12\x16\n" + + "\x12REASON_UNAVAILABLE\x10\x02\"X\n" + "\x11PolicyEvaluations\x12C\n" + "\vevaluations\x18\x01 \x03(\v2!.controlplane.v1.PolicyEvaluationR\vevaluations\"\x92\x06\n" + "\x10PolicyEvaluation\x12\x12\n" + @@ -3451,8 +3601,8 @@ func file_controlplane_v1_response_messages_proto_rawDescGZIP() []byte { return file_controlplane_v1_response_messages_proto_rawDescData } -var file_controlplane_v1_response_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 13) -var file_controlplane_v1_response_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_controlplane_v1_response_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 14) +var file_controlplane_v1_response_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_controlplane_v1_response_messages_proto_goTypes = []any{ (RunStatus)(0), // 0: controlplane.v1.RunStatus (PolicyViolationsFilter)(0), // 1: controlplane.v1.PolicyViolationsFilter @@ -3464,111 +3614,115 @@ var file_controlplane_v1_response_messages_proto_goTypes = []any{ (FederatedAuthError)(0), // 7: controlplane.v1.FederatedAuthError (UserWithNoMembershipError)(0), // 8: controlplane.v1.UserWithNoMembershipError (UserNotMemberOfOrgError)(0), // 9: controlplane.v1.UserNotMemberOfOrgError - (WorkflowContractVersionItem_RawBody_Format)(0), // 10: controlplane.v1.WorkflowContractVersionItem.RawBody.Format - (OrgItem_PolicyViolationBlockingStrategy)(0), // 11: controlplane.v1.OrgItem.PolicyViolationBlockingStrategy - (CASBackendItem_ValidationStatus)(0), // 12: controlplane.v1.CASBackendItem.ValidationStatus - (*WorkflowItem)(nil), // 13: controlplane.v1.WorkflowItem - (*WorkflowRunItem)(nil), // 14: controlplane.v1.WorkflowRunItem - (*ProjectVersion)(nil), // 15: controlplane.v1.ProjectVersion - (*PolicyStatusSummary)(nil), // 16: controlplane.v1.PolicyStatusSummary - (*AttestationItem)(nil), // 17: controlplane.v1.AttestationItem - (*PolicyEvaluations)(nil), // 18: controlplane.v1.PolicyEvaluations - (*PolicyEvaluation)(nil), // 19: controlplane.v1.PolicyEvaluation - (*PolicyViolation)(nil), // 20: controlplane.v1.PolicyViolation - (*PolicyReference)(nil), // 21: controlplane.v1.PolicyReference - (*WorkflowContractItem)(nil), // 22: controlplane.v1.WorkflowContractItem - (*ScopedEntity)(nil), // 23: controlplane.v1.ScopedEntity - (*WorkflowRef)(nil), // 24: controlplane.v1.WorkflowRef - (*WorkflowContractVersionItem)(nil), // 25: controlplane.v1.WorkflowContractVersionItem - (*User)(nil), // 26: controlplane.v1.User - (*OrgMembershipItem)(nil), // 27: controlplane.v1.OrgMembershipItem - (*OrgItem)(nil), // 28: controlplane.v1.OrgItem - (*CASBackendItem)(nil), // 29: controlplane.v1.CASBackendItem - (*APITokenItem)(nil), // 30: controlplane.v1.APITokenItem - nil, // 31: controlplane.v1.AttestationItem.AnnotationsEntry - nil, // 32: controlplane.v1.AttestationItem.PolicyEvaluationsEntry - (*AttestationItem_PolicyEvaluationStatus)(nil), // 33: controlplane.v1.AttestationItem.PolicyEvaluationStatus - (*AttestationItem_EnvVariable)(nil), // 34: controlplane.v1.AttestationItem.EnvVariable - (*AttestationItem_Material)(nil), // 35: controlplane.v1.AttestationItem.Material - nil, // 36: controlplane.v1.AttestationItem.Material.AnnotationsEntry - nil, // 37: controlplane.v1.PolicyEvaluation.AnnotationsEntry - nil, // 38: controlplane.v1.PolicyEvaluation.WithEntry - nil, // 39: controlplane.v1.PolicyReference.DigestEntry - (*WorkflowContractVersionItem_RawBody)(nil), // 40: controlplane.v1.WorkflowContractVersionItem.RawBody - (*CASBackendItem_Limits)(nil), // 41: controlplane.v1.CASBackendItem.Limits - (*timestamppb.Timestamp)(nil), // 42: google.protobuf.Timestamp - (v1.CraftingSchema_Runner_RunnerType)(0), // 43: workflowcontract.v1.CraftingSchema.Runner.RunnerType - (*v11.PolicyVulnerabilityFinding)(nil), // 44: attestation.v1.PolicyVulnerabilityFinding - (*v11.PolicySASTFinding)(nil), // 45: attestation.v1.PolicySASTFinding - (*v11.PolicyLicenseViolationFinding)(nil), // 46: attestation.v1.PolicyLicenseViolationFinding - (*v1.CraftingSchema)(nil), // 47: workflowcontract.v1.CraftingSchema + (PolicyEvaluationsRef_Reason)(0), // 10: controlplane.v1.PolicyEvaluationsRef.Reason + (WorkflowContractVersionItem_RawBody_Format)(0), // 11: controlplane.v1.WorkflowContractVersionItem.RawBody.Format + (OrgItem_PolicyViolationBlockingStrategy)(0), // 12: controlplane.v1.OrgItem.PolicyViolationBlockingStrategy + (CASBackendItem_ValidationStatus)(0), // 13: controlplane.v1.CASBackendItem.ValidationStatus + (*WorkflowItem)(nil), // 14: controlplane.v1.WorkflowItem + (*WorkflowRunItem)(nil), // 15: controlplane.v1.WorkflowRunItem + (*ProjectVersion)(nil), // 16: controlplane.v1.ProjectVersion + (*PolicyStatusSummary)(nil), // 17: controlplane.v1.PolicyStatusSummary + (*AttestationItem)(nil), // 18: controlplane.v1.AttestationItem + (*PolicyEvaluationsRef)(nil), // 19: controlplane.v1.PolicyEvaluationsRef + (*PolicyEvaluations)(nil), // 20: controlplane.v1.PolicyEvaluations + (*PolicyEvaluation)(nil), // 21: controlplane.v1.PolicyEvaluation + (*PolicyViolation)(nil), // 22: controlplane.v1.PolicyViolation + (*PolicyReference)(nil), // 23: controlplane.v1.PolicyReference + (*WorkflowContractItem)(nil), // 24: controlplane.v1.WorkflowContractItem + (*ScopedEntity)(nil), // 25: controlplane.v1.ScopedEntity + (*WorkflowRef)(nil), // 26: controlplane.v1.WorkflowRef + (*WorkflowContractVersionItem)(nil), // 27: controlplane.v1.WorkflowContractVersionItem + (*User)(nil), // 28: controlplane.v1.User + (*OrgMembershipItem)(nil), // 29: controlplane.v1.OrgMembershipItem + (*OrgItem)(nil), // 30: controlplane.v1.OrgItem + (*CASBackendItem)(nil), // 31: controlplane.v1.CASBackendItem + (*APITokenItem)(nil), // 32: controlplane.v1.APITokenItem + nil, // 33: controlplane.v1.AttestationItem.AnnotationsEntry + nil, // 34: controlplane.v1.AttestationItem.PolicyEvaluationsEntry + (*AttestationItem_PolicyEvaluationStatus)(nil), // 35: controlplane.v1.AttestationItem.PolicyEvaluationStatus + (*AttestationItem_EnvVariable)(nil), // 36: controlplane.v1.AttestationItem.EnvVariable + (*AttestationItem_Material)(nil), // 37: controlplane.v1.AttestationItem.Material + nil, // 38: controlplane.v1.AttestationItem.Material.AnnotationsEntry + nil, // 39: controlplane.v1.PolicyEvaluation.AnnotationsEntry + nil, // 40: controlplane.v1.PolicyEvaluation.WithEntry + nil, // 41: controlplane.v1.PolicyReference.DigestEntry + (*WorkflowContractVersionItem_RawBody)(nil), // 42: controlplane.v1.WorkflowContractVersionItem.RawBody + (*CASBackendItem_Limits)(nil), // 43: controlplane.v1.CASBackendItem.Limits + (*timestamppb.Timestamp)(nil), // 44: google.protobuf.Timestamp + (v1.CraftingSchema_Runner_RunnerType)(0), // 45: workflowcontract.v1.CraftingSchema.Runner.RunnerType + (*v11.PolicyVulnerabilityFinding)(nil), // 46: attestation.v1.PolicyVulnerabilityFinding + (*v11.PolicySASTFinding)(nil), // 47: attestation.v1.PolicySASTFinding + (*v11.PolicyLicenseViolationFinding)(nil), // 48: attestation.v1.PolicyLicenseViolationFinding + (*v1.CraftingSchema)(nil), // 49: workflowcontract.v1.CraftingSchema } var file_controlplane_v1_response_messages_proto_depIdxs = []int32{ - 42, // 0: controlplane.v1.WorkflowItem.created_at:type_name -> google.protobuf.Timestamp - 14, // 1: controlplane.v1.WorkflowItem.last_run:type_name -> controlplane.v1.WorkflowRunItem - 42, // 2: controlplane.v1.WorkflowRunItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 3: controlplane.v1.WorkflowRunItem.finished_at:type_name -> google.protobuf.Timestamp + 44, // 0: controlplane.v1.WorkflowItem.created_at:type_name -> google.protobuf.Timestamp + 15, // 1: controlplane.v1.WorkflowItem.last_run:type_name -> controlplane.v1.WorkflowRunItem + 44, // 2: controlplane.v1.WorkflowRunItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 3: controlplane.v1.WorkflowRunItem.finished_at:type_name -> google.protobuf.Timestamp 0, // 4: controlplane.v1.WorkflowRunItem.status:type_name -> controlplane.v1.RunStatus - 13, // 5: controlplane.v1.WorkflowRunItem.workflow:type_name -> controlplane.v1.WorkflowItem - 43, // 6: controlplane.v1.WorkflowRunItem.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType - 25, // 7: controlplane.v1.WorkflowRunItem.contract_version:type_name -> controlplane.v1.WorkflowContractVersionItem - 15, // 8: controlplane.v1.WorkflowRunItem.version:type_name -> controlplane.v1.ProjectVersion - 16, // 9: controlplane.v1.WorkflowRunItem.policy_summary:type_name -> controlplane.v1.PolicyStatusSummary - 42, // 10: controlplane.v1.ProjectVersion.created_at:type_name -> google.protobuf.Timestamp - 42, // 11: controlplane.v1.ProjectVersion.released_at:type_name -> google.protobuf.Timestamp + 14, // 5: controlplane.v1.WorkflowRunItem.workflow:type_name -> controlplane.v1.WorkflowItem + 45, // 6: controlplane.v1.WorkflowRunItem.runner_type:type_name -> workflowcontract.v1.CraftingSchema.Runner.RunnerType + 27, // 7: controlplane.v1.WorkflowRunItem.contract_version:type_name -> controlplane.v1.WorkflowContractVersionItem + 16, // 8: controlplane.v1.WorkflowRunItem.version:type_name -> controlplane.v1.ProjectVersion + 17, // 9: controlplane.v1.WorkflowRunItem.policy_summary:type_name -> controlplane.v1.PolicyStatusSummary + 44, // 10: controlplane.v1.ProjectVersion.created_at:type_name -> google.protobuf.Timestamp + 44, // 11: controlplane.v1.ProjectVersion.released_at:type_name -> google.protobuf.Timestamp 2, // 12: controlplane.v1.PolicyStatusSummary.status:type_name -> controlplane.v1.PolicyStatus - 34, // 13: controlplane.v1.AttestationItem.env_vars:type_name -> controlplane.v1.AttestationItem.EnvVariable - 35, // 14: controlplane.v1.AttestationItem.materials:type_name -> controlplane.v1.AttestationItem.Material - 31, // 15: controlplane.v1.AttestationItem.annotations:type_name -> controlplane.v1.AttestationItem.AnnotationsEntry - 32, // 16: controlplane.v1.AttestationItem.policy_evaluations:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationsEntry - 33, // 17: controlplane.v1.AttestationItem.policy_evaluation_status:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationStatus - 19, // 18: controlplane.v1.PolicyEvaluations.evaluations:type_name -> controlplane.v1.PolicyEvaluation - 37, // 19: controlplane.v1.PolicyEvaluation.annotations:type_name -> controlplane.v1.PolicyEvaluation.AnnotationsEntry - 38, // 20: controlplane.v1.PolicyEvaluation.with:type_name -> controlplane.v1.PolicyEvaluation.WithEntry - 20, // 21: controlplane.v1.PolicyEvaluation.violations:type_name -> controlplane.v1.PolicyViolation - 21, // 22: controlplane.v1.PolicyEvaluation.policy_reference:type_name -> controlplane.v1.PolicyReference - 21, // 23: controlplane.v1.PolicyEvaluation.group_reference:type_name -> controlplane.v1.PolicyReference - 44, // 24: controlplane.v1.PolicyViolation.vulnerability:type_name -> attestation.v1.PolicyVulnerabilityFinding - 45, // 25: controlplane.v1.PolicyViolation.sast:type_name -> attestation.v1.PolicySASTFinding - 46, // 26: controlplane.v1.PolicyViolation.license_violation:type_name -> attestation.v1.PolicyLicenseViolationFinding - 39, // 27: controlplane.v1.PolicyReference.digest:type_name -> controlplane.v1.PolicyReference.DigestEntry - 42, // 28: controlplane.v1.WorkflowContractItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 29: controlplane.v1.WorkflowContractItem.updated_at:type_name -> google.protobuf.Timestamp - 42, // 30: controlplane.v1.WorkflowContractItem.latest_revision_created_at:type_name -> google.protobuf.Timestamp - 24, // 31: controlplane.v1.WorkflowContractItem.workflow_refs:type_name -> controlplane.v1.WorkflowRef - 23, // 32: controlplane.v1.WorkflowContractItem.scoped_entity:type_name -> controlplane.v1.ScopedEntity - 42, // 33: controlplane.v1.WorkflowContractVersionItem.created_at:type_name -> google.protobuf.Timestamp - 47, // 34: controlplane.v1.WorkflowContractVersionItem.v1:type_name -> workflowcontract.v1.CraftingSchema - 40, // 35: controlplane.v1.WorkflowContractVersionItem.raw_contract:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody - 42, // 36: controlplane.v1.User.created_at:type_name -> google.protobuf.Timestamp - 42, // 37: controlplane.v1.User.updated_at:type_name -> google.protobuf.Timestamp - 28, // 38: controlplane.v1.OrgMembershipItem.org:type_name -> controlplane.v1.OrgItem - 26, // 39: controlplane.v1.OrgMembershipItem.user:type_name -> controlplane.v1.User - 42, // 40: controlplane.v1.OrgMembershipItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 41: controlplane.v1.OrgMembershipItem.updated_at:type_name -> google.protobuf.Timestamp - 5, // 42: controlplane.v1.OrgMembershipItem.role:type_name -> controlplane.v1.MembershipRole - 42, // 43: controlplane.v1.OrgItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 44: controlplane.v1.OrgItem.updated_at:type_name -> google.protobuf.Timestamp - 11, // 45: controlplane.v1.OrgItem.default_policy_violation_strategy:type_name -> controlplane.v1.OrgItem.PolicyViolationBlockingStrategy - 42, // 46: controlplane.v1.CASBackendItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 47: controlplane.v1.CASBackendItem.validated_at:type_name -> google.protobuf.Timestamp - 12, // 48: controlplane.v1.CASBackendItem.validation_status:type_name -> controlplane.v1.CASBackendItem.ValidationStatus - 41, // 49: controlplane.v1.CASBackendItem.limits:type_name -> controlplane.v1.CASBackendItem.Limits - 42, // 50: controlplane.v1.CASBackendItem.updated_at:type_name -> google.protobuf.Timestamp - 23, // 51: controlplane.v1.APITokenItem.scoped_entity:type_name -> controlplane.v1.ScopedEntity - 42, // 52: controlplane.v1.APITokenItem.created_at:type_name -> google.protobuf.Timestamp - 42, // 53: controlplane.v1.APITokenItem.revoked_at:type_name -> google.protobuf.Timestamp - 42, // 54: controlplane.v1.APITokenItem.expires_at:type_name -> google.protobuf.Timestamp - 42, // 55: controlplane.v1.APITokenItem.last_used_at:type_name -> google.protobuf.Timestamp - 18, // 56: controlplane.v1.AttestationItem.PolicyEvaluationsEntry.value:type_name -> controlplane.v1.PolicyEvaluations - 16, // 57: controlplane.v1.AttestationItem.PolicyEvaluationStatus.summary:type_name -> controlplane.v1.PolicyStatusSummary - 36, // 58: controlplane.v1.AttestationItem.Material.annotations:type_name -> controlplane.v1.AttestationItem.Material.AnnotationsEntry - 10, // 59: controlplane.v1.WorkflowContractVersionItem.RawBody.format:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody.Format - 60, // [60:60] is the sub-list for method output_type - 60, // [60:60] is the sub-list for method input_type - 60, // [60:60] is the sub-list for extension type_name - 60, // [60:60] is the sub-list for extension extendee - 0, // [0:60] is the sub-list for field type_name + 36, // 13: controlplane.v1.AttestationItem.env_vars:type_name -> controlplane.v1.AttestationItem.EnvVariable + 37, // 14: controlplane.v1.AttestationItem.materials:type_name -> controlplane.v1.AttestationItem.Material + 33, // 15: controlplane.v1.AttestationItem.annotations:type_name -> controlplane.v1.AttestationItem.AnnotationsEntry + 34, // 16: controlplane.v1.AttestationItem.policy_evaluations:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationsEntry + 35, // 17: controlplane.v1.AttestationItem.policy_evaluation_status:type_name -> controlplane.v1.AttestationItem.PolicyEvaluationStatus + 19, // 18: controlplane.v1.AttestationItem.policy_evaluations_ref:type_name -> controlplane.v1.PolicyEvaluationsRef + 10, // 19: controlplane.v1.PolicyEvaluationsRef.reason:type_name -> controlplane.v1.PolicyEvaluationsRef.Reason + 21, // 20: controlplane.v1.PolicyEvaluations.evaluations:type_name -> controlplane.v1.PolicyEvaluation + 39, // 21: controlplane.v1.PolicyEvaluation.annotations:type_name -> controlplane.v1.PolicyEvaluation.AnnotationsEntry + 40, // 22: controlplane.v1.PolicyEvaluation.with:type_name -> controlplane.v1.PolicyEvaluation.WithEntry + 22, // 23: controlplane.v1.PolicyEvaluation.violations:type_name -> controlplane.v1.PolicyViolation + 23, // 24: controlplane.v1.PolicyEvaluation.policy_reference:type_name -> controlplane.v1.PolicyReference + 23, // 25: controlplane.v1.PolicyEvaluation.group_reference:type_name -> controlplane.v1.PolicyReference + 46, // 26: controlplane.v1.PolicyViolation.vulnerability:type_name -> attestation.v1.PolicyVulnerabilityFinding + 47, // 27: controlplane.v1.PolicyViolation.sast:type_name -> attestation.v1.PolicySASTFinding + 48, // 28: controlplane.v1.PolicyViolation.license_violation:type_name -> attestation.v1.PolicyLicenseViolationFinding + 41, // 29: controlplane.v1.PolicyReference.digest:type_name -> controlplane.v1.PolicyReference.DigestEntry + 44, // 30: controlplane.v1.WorkflowContractItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 31: controlplane.v1.WorkflowContractItem.updated_at:type_name -> google.protobuf.Timestamp + 44, // 32: controlplane.v1.WorkflowContractItem.latest_revision_created_at:type_name -> google.protobuf.Timestamp + 26, // 33: controlplane.v1.WorkflowContractItem.workflow_refs:type_name -> controlplane.v1.WorkflowRef + 25, // 34: controlplane.v1.WorkflowContractItem.scoped_entity:type_name -> controlplane.v1.ScopedEntity + 44, // 35: controlplane.v1.WorkflowContractVersionItem.created_at:type_name -> google.protobuf.Timestamp + 49, // 36: controlplane.v1.WorkflowContractVersionItem.v1:type_name -> workflowcontract.v1.CraftingSchema + 42, // 37: controlplane.v1.WorkflowContractVersionItem.raw_contract:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody + 44, // 38: controlplane.v1.User.created_at:type_name -> google.protobuf.Timestamp + 44, // 39: controlplane.v1.User.updated_at:type_name -> google.protobuf.Timestamp + 30, // 40: controlplane.v1.OrgMembershipItem.org:type_name -> controlplane.v1.OrgItem + 28, // 41: controlplane.v1.OrgMembershipItem.user:type_name -> controlplane.v1.User + 44, // 42: controlplane.v1.OrgMembershipItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 43: controlplane.v1.OrgMembershipItem.updated_at:type_name -> google.protobuf.Timestamp + 5, // 44: controlplane.v1.OrgMembershipItem.role:type_name -> controlplane.v1.MembershipRole + 44, // 45: controlplane.v1.OrgItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 46: controlplane.v1.OrgItem.updated_at:type_name -> google.protobuf.Timestamp + 12, // 47: controlplane.v1.OrgItem.default_policy_violation_strategy:type_name -> controlplane.v1.OrgItem.PolicyViolationBlockingStrategy + 44, // 48: controlplane.v1.CASBackendItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 49: controlplane.v1.CASBackendItem.validated_at:type_name -> google.protobuf.Timestamp + 13, // 50: controlplane.v1.CASBackendItem.validation_status:type_name -> controlplane.v1.CASBackendItem.ValidationStatus + 43, // 51: controlplane.v1.CASBackendItem.limits:type_name -> controlplane.v1.CASBackendItem.Limits + 44, // 52: controlplane.v1.CASBackendItem.updated_at:type_name -> google.protobuf.Timestamp + 25, // 53: controlplane.v1.APITokenItem.scoped_entity:type_name -> controlplane.v1.ScopedEntity + 44, // 54: controlplane.v1.APITokenItem.created_at:type_name -> google.protobuf.Timestamp + 44, // 55: controlplane.v1.APITokenItem.revoked_at:type_name -> google.protobuf.Timestamp + 44, // 56: controlplane.v1.APITokenItem.expires_at:type_name -> google.protobuf.Timestamp + 44, // 57: controlplane.v1.APITokenItem.last_used_at:type_name -> google.protobuf.Timestamp + 20, // 58: controlplane.v1.AttestationItem.PolicyEvaluationsEntry.value:type_name -> controlplane.v1.PolicyEvaluations + 17, // 59: controlplane.v1.AttestationItem.PolicyEvaluationStatus.summary:type_name -> controlplane.v1.PolicyStatusSummary + 38, // 60: controlplane.v1.AttestationItem.Material.annotations:type_name -> controlplane.v1.AttestationItem.Material.AnnotationsEntry + 11, // 61: controlplane.v1.WorkflowContractVersionItem.RawBody.format:type_name -> controlplane.v1.WorkflowContractVersionItem.RawBody.Format + 62, // [62:62] is the sub-list for method output_type + 62, // [62:62] is the sub-list for method input_type + 62, // [62:62] is the sub-list for extension type_name + 62, // [62:62] is the sub-list for extension extendee + 0, // [0:62] is the sub-list for field type_name } func init() { file_controlplane_v1_response_messages_proto_init() } @@ -3577,23 +3731,23 @@ func file_controlplane_v1_response_messages_proto_init() { return } file_controlplane_v1_response_messages_proto_msgTypes[1].OneofWrappers = []any{} - file_controlplane_v1_response_messages_proto_msgTypes[7].OneofWrappers = []any{ + file_controlplane_v1_response_messages_proto_msgTypes[8].OneofWrappers = []any{ (*PolicyViolation_Vulnerability)(nil), (*PolicyViolation_Sast)(nil), (*PolicyViolation_LicenseViolation)(nil), } - file_controlplane_v1_response_messages_proto_msgTypes[12].OneofWrappers = []any{ + file_controlplane_v1_response_messages_proto_msgTypes[13].OneofWrappers = []any{ (*WorkflowContractVersionItem_V1)(nil), } - file_controlplane_v1_response_messages_proto_msgTypes[15].OneofWrappers = []any{} file_controlplane_v1_response_messages_proto_msgTypes[16].OneofWrappers = []any{} + file_controlplane_v1_response_messages_proto_msgTypes[17].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_controlplane_v1_response_messages_proto_rawDesc), len(file_controlplane_v1_response_messages_proto_rawDesc)), - NumEnums: 13, - NumMessages: 29, + NumEnums: 14, + NumMessages: 30, NumExtensions: 0, NumServices: 0, }, diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index 990390745..68b64c789 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -189,6 +189,12 @@ message AttestationItem { map annotations = 6; map policy_evaluations = 8; PolicyEvaluationStatus policy_evaluation_status = 9; + // Reference to the policy-evaluation bundle in the CAS backend. Populated + // only when the evaluations are NOT inlined in policy_evaluations, either + // because the bundle is larger than the server inlines or because it could + // not be resolved. Fetch the bundle by digest to inspect the violations. + // Counters and status in policy_evaluation_status remain complete either way. + PolicyEvaluationsRef policy_evaluations_ref = 11; message PolicyEvaluationStatus { string strategy = 1; @@ -236,6 +242,27 @@ message AttestationItem { } } +// Pointer to a policy-evaluation bundle held in a CAS backend, returned in +// place of the evaluations themselves. +message PolicyEvaluationsRef { + // Digest of the bundle as stored in CAS, in "sha256:" form + string digest = 1; + // Size of the bundle in bytes. Zero when the size could not be determined. + int64 size_bytes = 2; + // Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json + string media_type = 3; + // Why the evaluations were not inlined + Reason reason = 4; + + enum Reason { + REASON_UNSPECIFIED = 0; + // The bundle exceeds the maximum size the server inlines in a response + REASON_TOO_LARGE = 1; + // The bundle could not be resolved from CAS + REASON_UNAVAILABLE = 2; + } +} + message PolicyEvaluations { repeated PolicyEvaluation evaluations = 1; } 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 63f7d9084..03b9b80c3 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -617,6 +617,14 @@ export interface AttestationItem { annotations: { [key: string]: string }; policyEvaluations: { [key: string]: PolicyEvaluations }; policyEvaluationStatus?: AttestationItem_PolicyEvaluationStatus; + /** + * Reference to the policy-evaluation bundle in the CAS backend. Populated + * only when the evaluations are NOT inlined in policy_evaluations, either + * because the bundle is larger than the server inlines or because it could + * not be resolved. Fetch the bundle by digest to inspect the violations. + * Counters and status in policy_evaluation_status remain complete either way. + */ + policyEvaluationsRef?: PolicyEvaluationsRef; } export interface AttestationItem_AnnotationsEntry { @@ -695,6 +703,62 @@ export interface AttestationItem_Material_AnnotationsEntry { value: string; } +/** + * Pointer to a policy-evaluation bundle held in a CAS backend, returned in + * place of the evaluations themselves. + */ +export interface PolicyEvaluationsRef { + /** Digest of the bundle as stored in CAS, in "sha256:" form */ + digest: string; + /** Size of the bundle in bytes. Zero when the size could not be determined. */ + sizeBytes: number; + /** Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json */ + mediaType: string; + /** Why the evaluations were not inlined */ + reason: PolicyEvaluationsRef_Reason; +} + +export enum PolicyEvaluationsRef_Reason { + REASON_UNSPECIFIED = 0, + /** REASON_TOO_LARGE - The bundle exceeds the maximum size the server inlines in a response */ + REASON_TOO_LARGE = 1, + /** REASON_UNAVAILABLE - The bundle could not be resolved from CAS */ + REASON_UNAVAILABLE = 2, + UNRECOGNIZED = -1, +} + +export function policyEvaluationsRef_ReasonFromJSON(object: any): PolicyEvaluationsRef_Reason { + switch (object) { + case 0: + case "REASON_UNSPECIFIED": + return PolicyEvaluationsRef_Reason.REASON_UNSPECIFIED; + case 1: + case "REASON_TOO_LARGE": + return PolicyEvaluationsRef_Reason.REASON_TOO_LARGE; + case 2: + case "REASON_UNAVAILABLE": + return PolicyEvaluationsRef_Reason.REASON_UNAVAILABLE; + case -1: + case "UNRECOGNIZED": + default: + return PolicyEvaluationsRef_Reason.UNRECOGNIZED; + } +} + +export function policyEvaluationsRef_ReasonToJSON(object: PolicyEvaluationsRef_Reason): string { + switch (object) { + case PolicyEvaluationsRef_Reason.REASON_UNSPECIFIED: + return "REASON_UNSPECIFIED"; + case PolicyEvaluationsRef_Reason.REASON_TOO_LARGE: + return "REASON_TOO_LARGE"; + case PolicyEvaluationsRef_Reason.REASON_UNAVAILABLE: + return "REASON_UNAVAILABLE"; + case PolicyEvaluationsRef_Reason.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + export interface PolicyEvaluations { evaluations: PolicyEvaluation[]; } @@ -1805,6 +1869,7 @@ function createBaseAttestationItem(): AttestationItem { annotations: {}, policyEvaluations: {}, policyEvaluationStatus: undefined, + policyEvaluationsRef: undefined, }; } @@ -1834,6 +1899,9 @@ export const AttestationItem = { if (message.policyEvaluationStatus !== undefined) { AttestationItem_PolicyEvaluationStatus.encode(message.policyEvaluationStatus, writer.uint32(74).fork()).ldelim(); } + if (message.policyEvaluationsRef !== undefined) { + PolicyEvaluationsRef.encode(message.policyEvaluationsRef, writer.uint32(90).fork()).ldelim(); + } return writer; }, @@ -1906,6 +1974,13 @@ export const AttestationItem = { message.policyEvaluationStatus = AttestationItem_PolicyEvaluationStatus.decode(reader, reader.uint32()); continue; + case 11: + if (tag !== 90) { + break; + } + + message.policyEvaluationsRef = PolicyEvaluationsRef.decode(reader, reader.uint32()); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -1941,6 +2016,9 @@ export const AttestationItem = { policyEvaluationStatus: isSet(object.policyEvaluationStatus) ? AttestationItem_PolicyEvaluationStatus.fromJSON(object.policyEvaluationStatus) : undefined, + policyEvaluationsRef: isSet(object.policyEvaluationsRef) + ? PolicyEvaluationsRef.fromJSON(object.policyEvaluationsRef) + : undefined, }; }, @@ -1976,6 +2054,9 @@ export const AttestationItem = { message.policyEvaluationStatus !== undefined && (obj.policyEvaluationStatus = message.policyEvaluationStatus ? AttestationItem_PolicyEvaluationStatus.toJSON(message.policyEvaluationStatus) : undefined); + message.policyEvaluationsRef !== undefined && (obj.policyEvaluationsRef = message.policyEvaluationsRef + ? PolicyEvaluationsRef.toJSON(message.policyEvaluationsRef) + : undefined); return obj; }, @@ -2011,6 +2092,9 @@ export const AttestationItem = { (object.policyEvaluationStatus !== undefined && object.policyEvaluationStatus !== null) ? AttestationItem_PolicyEvaluationStatus.fromPartial(object.policyEvaluationStatus) : undefined; + message.policyEvaluationsRef = (object.policyEvaluationsRef !== undefined && object.policyEvaluationsRef !== null) + ? PolicyEvaluationsRef.fromPartial(object.policyEvaluationsRef) + : undefined; return message; }, }; @@ -2680,6 +2764,103 @@ export const AttestationItem_Material_AnnotationsEntry = { }, }; +function createBasePolicyEvaluationsRef(): PolicyEvaluationsRef { + return { digest: "", sizeBytes: 0, mediaType: "", reason: 0 }; +} + +export const PolicyEvaluationsRef = { + encode(message: PolicyEvaluationsRef, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.digest !== "") { + writer.uint32(10).string(message.digest); + } + if (message.sizeBytes !== 0) { + writer.uint32(16).int64(message.sizeBytes); + } + if (message.mediaType !== "") { + writer.uint32(26).string(message.mediaType); + } + if (message.reason !== 0) { + writer.uint32(32).int32(message.reason); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): PolicyEvaluationsRef { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePolicyEvaluationsRef(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.digest = reader.string(); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.sizeBytes = longToNumber(reader.int64() as Long); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.mediaType = reader.string(); + continue; + case 4: + if (tag !== 32) { + break; + } + + message.reason = reader.int32() as any; + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): PolicyEvaluationsRef { + return { + digest: isSet(object.digest) ? String(object.digest) : "", + sizeBytes: isSet(object.sizeBytes) ? Number(object.sizeBytes) : 0, + mediaType: isSet(object.mediaType) ? String(object.mediaType) : "", + reason: isSet(object.reason) ? policyEvaluationsRef_ReasonFromJSON(object.reason) : 0, + }; + }, + + toJSON(message: PolicyEvaluationsRef): unknown { + const obj: any = {}; + message.digest !== undefined && (obj.digest = message.digest); + message.sizeBytes !== undefined && (obj.sizeBytes = Math.round(message.sizeBytes)); + message.mediaType !== undefined && (obj.mediaType = message.mediaType); + message.reason !== undefined && (obj.reason = policyEvaluationsRef_ReasonToJSON(message.reason)); + return obj; + }, + + create, I>>(base?: I): PolicyEvaluationsRef { + return PolicyEvaluationsRef.fromPartial(base ?? {}); + }, + + fromPartial, I>>(object: I): PolicyEvaluationsRef { + const message = createBasePolicyEvaluationsRef(); + message.digest = object.digest ?? ""; + message.sizeBytes = object.sizeBytes ?? 0; + message.mediaType = object.mediaType ?? ""; + message.reason = object.reason ?? 0; + return message; + }, +}; + function createBasePolicyEvaluations(): PolicyEvaluations { return { evaluations: [] }; } diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.jsonschema.json index 1bda15d71..81ea09c69 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.jsonschema.json @@ -25,6 +25,10 @@ "type": "string" }, "type": "object" + }, + "^(policy_evaluations_ref)$": { + "$ref": "controlplane.v1.PolicyEvaluationsRef.jsonschema.json", + "description": "Reference to the policy-evaluation bundle in the CAS backend. Populated\n only when the evaluations are NOT inlined in policy_evaluations, either\n because the bundle is larger than the server inlines or because it could\n not be resolved. Fetch the bundle by digest to inspect the violations.\n Counters and status in policy_evaluation_status remain complete either way." } }, "properties": { @@ -75,6 +79,10 @@ "type": "string" }, "type": "object" + }, + "policyEvaluationsRef": { + "$ref": "controlplane.v1.PolicyEvaluationsRef.jsonschema.json", + "description": "Reference to the policy-evaluation bundle in the CAS backend. Populated\n only when the evaluations are NOT inlined in policy_evaluations, either\n because the bundle is larger than the server inlines or because it could\n not be resolved. Fetch the bundle by digest to inspect the violations.\n Counters and status in policy_evaluation_status remain complete either way." } }, "title": "Attestation Item", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.schema.json index ec1f044f2..5e5da3a00 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.schema.json @@ -25,6 +25,10 @@ "type": "string" }, "type": "object" + }, + "^(policyEvaluationsRef)$": { + "$ref": "controlplane.v1.PolicyEvaluationsRef.schema.json", + "description": "Reference to the policy-evaluation bundle in the CAS backend. Populated\n only when the evaluations are NOT inlined in policy_evaluations, either\n because the bundle is larger than the server inlines or because it could\n not be resolved. Fetch the bundle by digest to inspect the violations.\n Counters and status in policy_evaluation_status remain complete either way." } }, "properties": { @@ -75,6 +79,10 @@ "type": "string" }, "type": "object" + }, + "policy_evaluations_ref": { + "$ref": "controlplane.v1.PolicyEvaluationsRef.schema.json", + "description": "Reference to the policy-evaluation bundle in the CAS backend. Populated\n only when the evaluations are NOT inlined in policy_evaluations, either\n because the bundle is larger than the server inlines or because it could\n not be resolved. Fetch the bundle by digest to inspect the violations.\n Counters and status in policy_evaluation_status remain complete either way." } }, "title": "Attestation Item", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.jsonschema.json new file mode 100644 index 000000000..6206057e7 --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.jsonschema.json @@ -0,0 +1,71 @@ +{ + "$id": "controlplane.v1.PolicyEvaluationsRef.jsonschema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Pointer to a policy-evaluation bundle held in a CAS backend, returned in\n place of the evaluations themselves.", + "patternProperties": { + "^(media_type)$": { + "description": "Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json", + "type": "string" + }, + "^(size_bytes)$": { + "anyOf": [ + { + "exclusiveMaximum": 9223372036854776000, + "minimum": -9223372036854775808, + "type": "integer" + }, + { + "pattern": "^-?[0-9]+$", + "type": "string" + } + ], + "description": "Size of the bundle in bytes. Zero when the size could not be determined." + } + }, + "properties": { + "digest": { + "description": "Digest of the bundle as stored in CAS, in \"sha256:\u003chex\u003e\" form", + "type": "string" + }, + "mediaType": { + "description": "Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "enum": [ + "REASON_UNSPECIFIED", + "REASON_TOO_LARGE", + "REASON_UNAVAILABLE" + ], + "title": "Reason", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Why the evaluations were not inlined" + }, + "sizeBytes": { + "anyOf": [ + { + "exclusiveMaximum": 9223372036854776000, + "minimum": -9223372036854775808, + "type": "integer" + }, + { + "pattern": "^-?[0-9]+$", + "type": "string" + } + ], + "description": "Size of the bundle in bytes. Zero when the size could not be determined." + } + }, + "title": "Policy Evaluations Ref", + "type": "object" +} diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.schema.json new file mode 100644 index 000000000..6ab18bfbe --- /dev/null +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.PolicyEvaluationsRef.schema.json @@ -0,0 +1,71 @@ +{ + "$id": "controlplane.v1.PolicyEvaluationsRef.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Pointer to a policy-evaluation bundle held in a CAS backend, returned in\n place of the evaluations themselves.", + "patternProperties": { + "^(mediaType)$": { + "description": "Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json", + "type": "string" + }, + "^(sizeBytes)$": { + "anyOf": [ + { + "exclusiveMaximum": 9223372036854776000, + "minimum": -9223372036854775808, + "type": "integer" + }, + { + "pattern": "^-?[0-9]+$", + "type": "string" + } + ], + "description": "Size of the bundle in bytes. Zero when the size could not be determined." + } + }, + "properties": { + "digest": { + "description": "Digest of the bundle as stored in CAS, in \"sha256:\u003chex\u003e\" form", + "type": "string" + }, + "media_type": { + "description": "Media type of the bundle, i.e application/vnd.chainloop.policy-evaluations.v1+json", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "enum": [ + "REASON_UNSPECIFIED", + "REASON_TOO_LARGE", + "REASON_UNAVAILABLE" + ], + "title": "Reason", + "type": "string" + }, + { + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + ], + "description": "Why the evaluations were not inlined" + }, + "size_bytes": { + "anyOf": [ + { + "exclusiveMaximum": 9223372036854776000, + "minimum": -9223372036854775808, + "type": "integer" + }, + { + "pattern": "^-?[0-9]+$", + "type": "string" + } + ], + "description": "Size of the bundle in bytes. Zero when the size could not be determined." + } + }, + "title": "Policy Evaluations Ref", + "type": "object" +} diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 7446ac5fc..a06ddc2e2 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -240,6 +240,7 @@ func wireApp(contextContext context.Context, bootstrap *conf.Bootstrap, readerWr CASClient: casClientUseCase, CASMappingUC: casMappingUseCase, PolicyEvalCache: policyevalbundleCache, + BootstrapConfig: bootstrap, Opts: v5, } workflowRunService := service.NewWorkflowRunService(newWorkflowRunServiceOpts) diff --git a/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go b/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go index aeccaa51e..e265b8016 100644 --- a/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go +++ b/app/controlplane/internal/conf/controlplane/config/v1/conf.pb.go @@ -266,8 +266,15 @@ type Attestations struct { // when the workflow run's CAS backend is inline, since inline backends // do not store attestation bundles externally. SkipDbStorage bool `protobuf:"varint,1,opt,name=skip_db_storage,json=skipDbStorage,proto3" json:"skip_db_storage,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Maximum size in bytes of a policy-evaluation bundle that the workflow-run + // View API downloads from CAS and inlines in its response. Bundles above + // this size are returned as a reference for the caller to fetch directly, + // which keeps a single attestation with a very large number of violations + // from exhausting the control plane's memory. Values <= 0 select the + // built-in default. + PolicyEvaluationsMaxInlineBytes int64 `protobuf:"varint,2,opt,name=policy_evaluations_max_inline_bytes,json=policyEvaluationsMaxInlineBytes,proto3" json:"policy_evaluations_max_inline_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Attestations) Reset() { @@ -307,6 +314,13 @@ func (x *Attestations) GetSkipDbStorage() bool { return false } +func (x *Attestations) GetPolicyEvaluationsMaxInlineBytes() int64 { + if x != nil { + return x.PolicyEvaluationsMaxInlineBytes + } + return 0 +} + type OperationAuthorizationProvider struct { state protoimpl.MessageState `protogen:"open.v1"` // URL of the authorization endpoint @@ -1427,7 +1441,7 @@ type Data_Database struct { state protoimpl.MessageState `protogen:"open.v1"` Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"` Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` - // default 0 + // default 0 MinOpenConns int32 `protobuf:"varint,3,opt,name=min_open_conns,json=minOpenConns,proto3" json:"min_open_conns,omitempty"` // default max(4, runtime.NumCPU()) MaxOpenConns int32 `protobuf:"varint,4,opt,name=max_open_conns,json=maxOpenConns,proto3" json:"max_open_conns,omitempty"` @@ -1780,9 +1794,10 @@ const file_controlplane_config_v1_conf_proto_rawDesc = "" + "\x03uri\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x03uri\x12\x1f\n" + "\x05token\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01H\x00R\x05token\x12\x1a\n" + "\breplicas\x18\x03 \x01(\x05R\breplicasB\x10\n" + - "\x0eauthenticationJ\x04\b\b\x10\tR\x15referrer_shared_index\"6\n" + + "\x0eauthenticationJ\x04\b\b\x10\tR\x15referrer_shared_index\"\x84\x01\n" + "\fAttestations\x12&\n" + - "\x0fskip_db_storage\x18\x01 \x01(\bR\rskipDbStorage\"v\n" + + "\x0fskip_db_storage\x18\x01 \x01(\bR\rskipDbStorage\x12L\n" + + "#policy_evaluations_max_inline_bytes\x18\x02 \x01(\x03R\x1fpolicyEvaluationsMaxInlineBytes\"v\n" + "\x1eOperationAuthorizationProvider\x12\x1a\n" + "\x03url\x18\x01 \x01(\tB\b\xbaH\x05r\x03\x88\x01\x01R\x03url\x12\x18\n" + "\aenabled\x18\x02 \x01(\bR\aenabled\x12\x1e\n" + diff --git a/app/controlplane/internal/conf/controlplane/config/v1/conf.proto b/app/controlplane/internal/conf/controlplane/config/v1/conf.proto index 1a04c8881..4665594f7 100644 --- a/app/controlplane/internal/conf/controlplane/config/v1/conf.proto +++ b/app/controlplane/internal/conf/controlplane/config/v1/conf.proto @@ -136,6 +136,14 @@ message Attestations { // when the workflow run's CAS backend is inline, since inline backends // do not store attestation bundles externally. bool skip_db_storage = 1; + + // Maximum size in bytes of a policy-evaluation bundle that the workflow-run + // View API downloads from CAS and inlines in its response. Bundles above + // this size are returned as a reference for the caller to fetch directly, + // which keeps a single attestation with a very large number of violations + // from exhausting the control plane's memory. Values <= 0 select the + // built-in default. + int64 policy_evaluations_max_inline_bytes = 2; } message OperationAuthorizationProvider { diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 973722218..6b0ba474a 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -23,6 +23,7 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" craftingpb "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" @@ -48,6 +49,7 @@ type WorkflowRunService struct { casClient biz.CASClient casMappingUC *biz.CASMappingUseCase policyEvalCache *policyevalbundle.Cache + bootstrapConfig *conf.Bootstrap } type NewWorkflowRunServiceOpts struct { @@ -60,6 +62,7 @@ type NewWorkflowRunServiceOpts struct { CASClient biz.CASClient CASMappingUC *biz.CASMappingUseCase PolicyEvalCache *policyevalbundle.Cache + BootstrapConfig *conf.Bootstrap Opts []NewOpt } @@ -75,6 +78,7 @@ func NewWorkflowRunService(opts *NewWorkflowRunServiceOpts) *WorkflowRunService casClient: opts.CASClient, casMappingUC: opts.CASMappingUC, policyEvalCache: opts.PolicyEvalCache, + bootstrapConfig: opts.BootstrapConfig, } } @@ -87,39 +91,126 @@ func (p *casResolvedPredicate) GetPolicyEvaluations() map[string][]*chainloop.Po return p.evals } +// defaultPolicyEvaluationsMaxInlineBytes bounds the policy-evaluation bundle +// the View API is willing to download and inline in a response. A single +// attestation can carry a six-figure number of violations, and inlining one +// holds the payload in memory several times over (download buffer, decoded +// bundle, regrouped evaluations, response protos), which is enough to exhaust +// the control plane. Bundles above the cap are returned as a reference so the +// caller can fetch them directly from the CAS. +const defaultPolicyEvaluationsMaxInlineBytes = 10 << 20 // 10MiB + +// resolvedPolicyEvaluations carries either the inlined evaluations or, when +// they were deliberately left out, the reference the caller needs to fetch +// them. Exactly one of the two fields is set. +type resolvedPolicyEvaluations struct { + evaluations map[string][]*chainloop.PolicyEvaluation + ref *pb.PolicyEvaluationsRef +} + +func (s *WorkflowRunService) policyEvaluationsMaxInlineBytes() int64 { + if configured := s.bootstrapConfig.GetAttestations().GetPolicyEvaluationsMaxInlineBytes(); configured > 0 { + return configured + } + + return defaultPolicyEvaluationsMaxInlineBytes +} + +// resolvePolicyEvaluations resolves the policy-evaluation bundle referenced by +// an attestation predicate. It returns nil when the predicate carries no +// reference, meaning the caller should keep whatever the predicate itself +// holds. +// +// Anything that prevents inlining the bundle with confidence -- an oversized +// bundle, an unknown size, an unreachable or undecodable object -- yields a +// reference rather than a download attempt. func (s *WorkflowRunService) resolvePolicyEvaluations( ctx context.Context, - ref *intoto.ResourceDescriptor, + descriptor *intoto.ResourceDescriptor, orgID uuid.UUID, -) (map[string][]*chainloop.PolicyEvaluation, error) { - if ref == nil { - return nil, nil +) *resolvedPolicyEvaluations { + if descriptor == nil { + return nil } - hexDigest, ok := ref.Digest["sha256"] + mediaType := descriptor.GetMediaType() + + hexDigest, ok := descriptor.GetDigest()["sha256"] if !ok { - return nil, fmt.Errorf("no sha256 digest in policy evaluations ref") + s.log.Warnw("msg", "policy evaluations reference has no sha256 digest") + return unavailablePolicyEvaluations("", 0, mediaType) } digest := fmt.Sprintf("sha256:%s", hexDigest) + maxInlineBytes := s.policyEvaluationsMaxInlineBytes() + + // A cached bundle costs no CAS round trip. Only under-cap bundles are + // cached below, so the size check here just keeps the cap honest for + // entries written before it existed. if cached, found, err := s.policyEvalCache.Get(ctx, digest); err == nil && found { - return chainloop.PolicyEvaluationsFromBundle(cached) + if int64(len(cached)) > maxInlineBytes { + return tooLargePolicyEvaluations(digest, int64(len(cached)), mediaType) + } + + return s.decodePolicyEvaluations(cached, digest, int64(len(cached)), mediaType) } mapping, err := s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, digest, []uuid.UUID{orgID}, nil) if err != nil { - return nil, fmt.Errorf("finding CAS mapping: %w", err) + s.log.Warnw("msg", "finding CAS mapping for policy evaluations", "digest", digest, "err", err) + return unavailablePolicyEvaluations(digest, 0, mediaType) + } + + // Ask for the size before paying for the transfer. + info, err := s.casClient.Describe(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, mapping.CASBackend.OrganizationID, digest) + if err != nil { + s.log.Warnw("msg", "describing policy evaluations bundle", "digest", digest, "err", err) + return unavailablePolicyEvaluations(digest, 0, mediaType) + } + + if info.Size > maxInlineBytes { + s.log.Infow("msg", "policy evaluations bundle too large to inline", "digest", digest, "size", info.Size, "max", maxInlineBytes) + return tooLargePolicyEvaluations(digest, info.Size, mediaType) } var buf bytes.Buffer if err := s.casClient.Download(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, mapping.CASBackend.OrganizationID, &buf, digest); err != nil { - return nil, fmt.Errorf("downloading policy eval bundle: %w", err) + s.log.Warnw("msg", "downloading policy evaluations bundle", "digest", digest, "err", err) + return unavailablePolicyEvaluations(digest, info.Size, mediaType) } data := buf.Bytes() _ = s.policyEvalCache.Set(ctx, digest, data) - return chainloop.PolicyEvaluationsFromBundle(data) + return s.decodePolicyEvaluations(data, digest, info.Size, mediaType) +} + +func (s *WorkflowRunService) decodePolicyEvaluations(data []byte, digest string, size int64, mediaType string) *resolvedPolicyEvaluations { + evaluations, err := chainloop.PolicyEvaluationsFromBundle(data) + if err != nil { + s.log.Warnw("msg", "decoding policy evaluations bundle", "digest", digest, "err", err) + return unavailablePolicyEvaluations(digest, size, mediaType) + } + + return &resolvedPolicyEvaluations{evaluations: evaluations} +} + +func tooLargePolicyEvaluations(digest string, size int64, mediaType string) *resolvedPolicyEvaluations { + return &resolvedPolicyEvaluations{ref: &pb.PolicyEvaluationsRef{ + Digest: digest, + SizeBytes: size, + MediaType: mediaType, + Reason: pb.PolicyEvaluationsRef_REASON_TOO_LARGE, + }} +} + +func unavailablePolicyEvaluations(digest string, size int64, mediaType string) *resolvedPolicyEvaluations { + return &resolvedPolicyEvaluations{ref: &pb.PolicyEvaluationsRef{ + Digest: digest, + SizeBytes: size, + MediaType: mediaType, + Reason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + }} } func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServiceListRequest) (*pb.WorkflowRunServiceListResponse, error) { @@ -283,18 +374,20 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic } var predicate chainloop.NormalizablePredicate + var policyEvaluationsRef *pb.PolicyEvaluationsRef if run.Attestation != nil && run.Attestation.Envelope != nil { predicate, err = chainloop.ExtractPredicate(run.Attestation.Envelope) if err != nil { return nil, handleUseCaseErr(err, s.log) } - if ref := predicate.GetPolicyEvaluationsRef(); ref != nil { - resolved, resolveErr := s.resolvePolicyEvaluations(ctx, ref, run.Workflow.OrgID) - if resolveErr != nil { - s.log.Warnw("msg", "failed to resolve policy evaluations from CAS, using inline", "err", resolveErr) - } else if resolved != nil { - predicate = &casResolvedPredicate{NormalizablePredicate: predicate, evals: resolved} + if resolved := s.resolvePolicyEvaluations(ctx, predicate.GetPolicyEvaluationsRef(), run.Workflow.OrgID); resolved != nil { + // Either the evaluations are inlined, or the caller is handed the + // reference to fetch them from the CAS itself. + if resolved.ref != nil { + policyEvaluationsRef = resolved.ref + } else { + predicate = &casResolvedPredicate{NormalizablePredicate: predicate, evals: resolved.evaluations} } } } @@ -304,6 +397,10 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic return nil, handleUseCaseErr(err, s.log) } + if attestation != nil { + attestation.PolicyEvaluationsRef = policyEvaluationsRef + } + contractAndVersion, err := s.workflowContractUseCase.FindVersionByID(ctx, run.ContractVersionID.String()) if err != nil { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/internal/service/workflowrun_test.go b/app/controlplane/internal/service/workflowrun_test.go new file mode 100644 index 000000000..17217cfe2 --- /dev/null +++ b/app/controlplane/internal/service/workflowrun_test.go @@ -0,0 +1,260 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "context" + "errors" + "io" + "testing" + + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + bizMocks "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/mocks" + attestationpb "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" + "github.com/chainloop-dev/chainloop/pkg/cache/policyevalbundle" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/google/uuid" + intoto "github.com/in-toto/attestation/go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" +) + +const ( + sha256Alg = "sha256" + testBundleHexDigest = "cf4c9c8b7b1b4f4d0b4e3f4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d" + testBundleDigest = sha256Alg + ":" + testBundleHexDigest +) + +// policyEvaluationsBundle builds a valid protojson-encoded bundle carrying a +// single evaluation with one violation. +func policyEvaluationsBundle(t *testing.T) []byte { + t.Helper() + + bundle := &attestationpb.PolicyEvaluationBundle{ + Evaluations: []*attestationpb.PolicyEvaluation{ + { + Name: "strong-acl", + MaterialName: "registry-report", + Violations: []*attestationpb.PolicyEvaluation_Violation{ + {Subject: "HKLM\\Software", Message: "weak ACL"}, + }, + }, + }, + } + + data, err := protojson.Marshal(bundle) + require.NoError(t, err) + + return data +} + +func testResourceDescriptor() *intoto.ResourceDescriptor { + return &intoto.ResourceDescriptor{ + Name: "policy-evaluations", + Digest: map[string]string{sha256Alg: testBundleHexDigest}, + MediaType: chainloop.PolicyEvaluationsBundleMediaType, + } +} + +func TestResolvePolicyEvaluations(t *testing.T) { + orgID := uuid.New() + bundle := policyEvaluationsBundle(t) + + testCases := []struct { + name string + // descriptor defaults to a valid one when nil and useNilDescriptor is false + descriptor *intoto.ResourceDescriptor + useNilDescriptor bool + // maxInlineBytes 0 selects the built-in default + maxInlineBytes int64 + // seedCache pre-populates the bundle cache with these bytes + seedCache []byte + // mappingErr makes the CAS mapping lookup fail + mappingErr error + // describeSize is reported by the CAS when describeErr is nil + describeSize int64 + describeErr error + // downloadBody is what the CAS download writes out + downloadBody []byte + + wantNilResolution bool + wantEvaluations bool + wantRefReason pb.PolicyEvaluationsRef_Reason + wantRefSize int64 + wantDescribeCall bool + wantDownloadCall bool + }{ + { + name: "no descriptor resolves to nothing", + useNilDescriptor: true, + wantNilResolution: true, + }, + { + name: "bundle under the cap is inlined", + describeSize: int64(len(bundle)), + downloadBody: bundle, + wantEvaluations: true, + wantDescribeCall: true, + wantDownloadCall: true, + }, + { + name: "bundle over the cap is never downloaded", + maxInlineBytes: 16, + describeSize: 64 * 1024 * 1024, + wantRefReason: pb.PolicyEvaluationsRef_REASON_TOO_LARGE, + wantRefSize: 64 * 1024 * 1024, + wantDescribeCall: true, + }, + { + name: "size exactly at the cap is inlined", + maxInlineBytes: int64(len(bundle)), + describeSize: int64(len(bundle)), + downloadBody: bundle, + wantEvaluations: true, + wantDescribeCall: true, + wantDownloadCall: true, + }, + { + name: "unknown size is not downloaded", + describeErr: errors.New("cas unreachable"), + wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + wantDescribeCall: true, + }, + { + name: "missing CAS mapping is not downloaded", + mappingErr: biz.NewErrNotFound("digest"), + wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + }, + { + name: "cached bundle under the cap skips the CAS entirely", + seedCache: bundle, + wantEvaluations: true, + }, + { + name: "cached bundle over the cap is discarded", + seedCache: bundle, + maxInlineBytes: 4, + wantRefReason: pb.PolicyEvaluationsRef_REASON_TOO_LARGE, + wantRefSize: int64(len(bundle)), + }, + { + name: "undecodable bundle reports unavailable", + describeSize: 16, + downloadBody: []byte("this is not protojson"), + wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + wantRefSize: 16, + wantDescribeCall: true, + wantDownloadCall: true, + }, + { + name: "descriptor without a sha256 digest reports unavailable", + descriptor: &intoto.ResourceDescriptor{ + Name: "policy-evaluations", + Digest: map[string]string{"sha512": "abc"}, + }, + wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + + casClient := bizMocks.NewCASClient(t) + if tc.wantDescribeCall { + casClient.On("Describe", mock.Anything, mock.Anything, mock.Anything, orgID, testBundleDigest). + Return(&casclient.ResourceInfo{Digest: testBundleDigest, Size: tc.describeSize}, tc.describeErr) + } + if tc.wantDownloadCall { + casClient.On("Download", mock.Anything, mock.Anything, mock.Anything, orgID, mock.Anything, testBundleDigest). + Run(func(args mock.Arguments) { + w, ok := args.Get(4).(io.Writer) + require.True(t, ok) + _, err := w.Write(tc.downloadBody) + require.NoError(t, err) + }).Return(nil) + } + + mappingRepo := bizMocks.NewCASMappingRepo(t) + if !tc.useNilDescriptor && tc.descriptor == nil { + mapping := &biz.CASMapping{CASBackend: &biz.CASBackend{ + Provider: "OCI_REPOSITORY", + SecretName: "secret-name", + OrganizationID: orgID, + }} + if tc.mappingErr != nil { + mappingRepo.On("FindByDigestInOrgs", mock.Anything, testBundleDigest, mock.Anything, mock.Anything). + Return(nil, tc.mappingErr) + } else if tc.seedCache == nil { + mappingRepo.On("FindByDigestInOrgs", mock.Anything, testBundleDigest, mock.Anything, mock.Anything). + Return(mapping, nil) + } + } + + cache, err := policyevalbundle.New(ctx, nil, nil) + require.NoError(t, err) + if tc.seedCache != nil { + require.NoError(t, cache.Set(ctx, testBundleDigest, tc.seedCache)) + } + + svc := NewWorkflowRunService(&NewWorkflowRunServiceOpts{ + CASClient: casClient, + CASMappingUC: biz.NewCASMappingUseCase(mappingRepo, nil, nil), + PolicyEvalCache: cache, + BootstrapConfig: &conf.Bootstrap{ + Attestations: &conf.Attestations{ + PolicyEvaluationsMaxInlineBytes: tc.maxInlineBytes, + }, + }, + }) + + descriptor := tc.descriptor + if !tc.useNilDescriptor && descriptor == nil { + descriptor = testResourceDescriptor() + } + + got := svc.resolvePolicyEvaluations(ctx, descriptor, orgID) + + if tc.wantNilResolution { + assert.Nil(t, got) + return + } + + require.NotNil(t, got) + + if tc.wantEvaluations { + assert.Nil(t, got.ref, "evaluations were inlined so no ref is expected") + require.NotEmpty(t, got.evaluations) + assert.Len(t, got.evaluations["registry-report"], 1) + return + } + + assert.Empty(t, got.evaluations) + require.NotNil(t, got.ref) + assert.Equal(t, tc.wantRefReason, got.ref.GetReason()) + assert.Equal(t, tc.wantRefSize, got.ref.GetSizeBytes()) + + if !tc.wantDownloadCall { + casClient.AssertNotCalled(t, "Download", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) + } + }) + } +} diff --git a/app/controlplane/pkg/biz/.mockery.yml b/app/controlplane/pkg/biz/.mockery.yml index 93dda23d7..b717cccb1 100644 --- a/app/controlplane/pkg/biz/.mockery.yml +++ b/app/controlplane/pkg/biz/.mockery.yml @@ -16,6 +16,7 @@ packages: interfaces: APITokenRepo: CASBackendRepo: + CASClient: CASMappingRepo: OrganizationRepo: WorkflowRunRepo: diff --git a/app/controlplane/pkg/biz/casclient.go b/app/controlplane/pkg/biz/casclient.go index f09a3f0f7..ba9800de3 100644 --- a/app/controlplane/pkg/biz/casclient.go +++ b/app/controlplane/pkg/biz/casclient.go @@ -51,6 +51,10 @@ type CASUploader interface { type CASDownloader interface { Download(ctx context.Context, backendType, secretID string, orgID uuid.UUID, w io.Writer, digest string) error + // Describe reports the metadata of a stored resource, including its size in + // bytes, without transferring its content. Callers use it to decide whether + // downloading is worthwhile before paying for the transfer. + Describe(ctx context.Context, backendType, secretID string, orgID uuid.UUID, digest string) (*casclient.ResourceInfo, error) } type CASClient interface { @@ -149,6 +153,25 @@ func (uc *CASClientUseCase) Download(ctx context.Context, backendType, secretID return nil } +func (uc *CASClientUseCase) Describe(ctx context.Context, backendType, secretID string, orgID uuid.UUID, digest string) (*casclient.ResourceInfo, error) { + ctx, span := otelx.Start(ctx, casClientTracer, "CASClientUseCase.Describe") + defer span.End() + + // SourceInternal flags this as the control plane's own traffic so the CAS doesn't emit audit events for it + client, closeFn, err := uc.casAPIClient(&CASCredsOpts{BackendType: backendType, SecretPath: secretID, Role: casJWT.Downloader, OrgID: orgID, SourceInternal: true}) + if err != nil { + return nil, fmt.Errorf("failed to create cas client: %w", err) + } + defer closeFn() + + info, err := client.Describe(ctx, digest) + if err != nil { + return nil, fmt.Errorf("failed to describe content: %w", err) + } + + return info, nil +} + // create a client with a temporary set of credentials for a specific operation func (uc *CASClientUseCase) casAPIClient(backendRef *CASCredsOpts) (casclient.DownloaderUploader, func(), error) { token, err := uc.credsProvider.GenerateTemporaryCredentials(backendRef) diff --git a/app/controlplane/pkg/biz/casclient_test.go b/app/controlplane/pkg/biz/casclient_test.go index 01b5c2655..867f915ab 100644 --- a/app/controlplane/pkg/biz/casclient_test.go +++ b/app/controlplane/pkg/biz/casclient_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,14 +17,17 @@ package biz_test import ( "context" + "errors" "testing" conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/pkg/casclient" "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) func TestIsReady(t *testing.T) { @@ -82,3 +85,71 @@ func TestIsReady(t *testing.T) { }) } } + +func TestDescribe(t *testing.T) { + const digest = "sha256:cf4c9c8b7b1b4f4d0b4e3f4a5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d" + + validConf := &conf.Bootstrap_CASServer{ + Grpc: &conf.Server_GRPC{Addr: "localhost:1111"}, + } + + testCases := []struct { + name string + orgID uuid.UUID + casInfo *casclient.ResourceInfo + casErr error + want *casclient.ResourceInfo + wantErr bool + wantCall bool + }{ + { + name: "returns the resource metadata reported by the CAS", + orgID: uuid.New(), + casInfo: &casclient.ResourceInfo{Digest: digest, Filename: "policy-evaluations.json", Size: 2048}, + want: &casclient.ResourceInfo{Digest: digest, Filename: "policy-evaluations.json", Size: 2048}, + wantCall: true, + }, + { + name: "propagates the CAS error", + orgID: uuid.New(), + casErr: errors.New("not found"), + wantErr: true, + wantCall: true, + }, + { + name: "fails without reaching the CAS when the org is missing", + orgID: uuid.Nil, + wantErr: true, + }, + } + + credsProvider, err := biz.NewCASCredentialsUseCase(&conf.Auth{ + CasRobotAccountPrivateKeyPath: "./testdata/test-key.ec.pem", + }) + require.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := mocks.NewDownloaderUploader(t) + if tc.wantCall { + c.On("Describe", mock.Anything, digest).Return(tc.casInfo, tc.casErr) + } + + clientProvider := func(_ *conf.Bootstrap_CASServer, _ string) (casclient.DownloaderUploader, func(), error) { + return c, func() {}, nil + } + + uc := biz.NewCASClientUseCase(credsProvider, validConf, nil, biz.WithClientFactory(clientProvider)) + + got, err := uc.Describe(context.Background(), "OCI_REPOSITORY", "secret-name", tc.orgID, digest) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/app/controlplane/pkg/biz/mocks/CASClient.go b/app/controlplane/pkg/biz/mocks/CASClient.go index 66ebacc70..0d29cb5d2 100644 --- a/app/controlplane/pkg/biz/mocks/CASClient.go +++ b/app/controlplane/pkg/biz/mocks/CASClient.go @@ -1,66 +1,295 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify package mocks import ( - context "context" - io "io" + "context" + "io" + "github.com/chainloop-dev/chainloop/pkg/casclient" "github.com/google/uuid" mock "github.com/stretchr/testify/mock" ) +// NewCASClient creates a new instance of CASClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCASClient(t interface { + mock.TestingT + Cleanup(func()) +}) *CASClient { + mock := &CASClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + // CASClient is an autogenerated mock type for the CASClient type type CASClient struct { mock.Mock } -// Download provides a mock function with given fields: ctx, backendType, secretID, orgID, w, digest -func (_m *CASClient) Download(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, w io.Writer, digest string) error { - ret := _m.Called(ctx, backendType, secretID, orgID, w, digest) +type CASClient_Expecter struct { + mock *mock.Mock +} + +func (_m *CASClient) EXPECT() *CASClient_Expecter { + return &CASClient_Expecter{mock: &_m.Mock} +} + +// Describe provides a mock function for the type CASClient +func (_mock *CASClient) Describe(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, digest string) (*casclient.ResourceInfo, error) { + ret := _mock.Called(ctx, backendType, secretID, orgID, digest) + + if len(ret) == 0 { + panic("no return value specified for Describe") + } + + var r0 *casclient.ResourceInfo + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, string) (*casclient.ResourceInfo, error)); ok { + return returnFunc(ctx, backendType, secretID, orgID, digest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, string) *casclient.ResourceInfo); ok { + r0 = returnFunc(ctx, backendType, secretID, orgID, digest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*casclient.ResourceInfo) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, uuid.UUID, string) error); ok { + r1 = returnFunc(ctx, backendType, secretID, orgID, digest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// CASClient_Describe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Describe' +type CASClient_Describe_Call struct { + *mock.Call +} + +// Describe is a helper method to define mock.On call +// - ctx context.Context +// - backendType string +// - secretID string +// - orgID uuid.UUID +// - digest string +func (_e *CASClient_Expecter) Describe(ctx any, backendType any, secretID any, orgID any, digest any) *CASClient_Describe_Call { + return &CASClient_Describe_Call{Call: _e.mock.On("Describe", ctx, backendType, secretID, orgID, digest)} +} + +func (_c *CASClient_Describe_Call) Run(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, digest string)) *CASClient_Describe_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 uuid.UUID + if args[3] != nil { + arg3 = args[3].(uuid.UUID) + } + var arg4 string + if args[4] != nil { + arg4 = args[4].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *CASClient_Describe_Call) Return(resourceInfo *casclient.ResourceInfo, err error) *CASClient_Describe_Call { + _c.Call.Return(resourceInfo, err) + return _c +} + +func (_c *CASClient_Describe_Call) RunAndReturn(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, digest string) (*casclient.ResourceInfo, error)) *CASClient_Describe_Call { + _c.Call.Return(run) + return _c +} + +// Download provides a mock function for the type CASClient +func (_mock *CASClient) Download(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, w io.Writer, digest string) error { + ret := _mock.Called(ctx, backendType, secretID, orgID, w, digest) if len(ret) == 0 { panic("no return value specified for Download") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, io.Writer, string) error); ok { - r0 = rf(ctx, backendType, secretID, orgID, w, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, io.Writer, string) error); ok { + r0 = returnFunc(ctx, backendType, secretID, orgID, w, digest) } else { r0 = ret.Error(0) } - return r0 } -// Upload provides a mock function with given fields: ctx, backendType, secretID, orgID, content, filename, digest -func (_m *CASClient) Upload(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, content io.Reader, filename string, digest string) error { - ret := _m.Called(ctx, backendType, secretID, orgID, content, filename, digest) +// CASClient_Download_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Download' +type CASClient_Download_Call struct { + *mock.Call +} + +// Download is a helper method to define mock.On call +// - ctx context.Context +// - backendType string +// - secretID string +// - orgID uuid.UUID +// - w io.Writer +// - digest string +func (_e *CASClient_Expecter) Download(ctx any, backendType any, secretID any, orgID any, w any, digest any) *CASClient_Download_Call { + return &CASClient_Download_Call{Call: _e.mock.On("Download", ctx, backendType, secretID, orgID, w, digest)} +} + +func (_c *CASClient_Download_Call) Run(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, w io.Writer, digest string)) *CASClient_Download_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 uuid.UUID + if args[3] != nil { + arg3 = args[3].(uuid.UUID) + } + var arg4 io.Writer + if args[4] != nil { + arg4 = args[4].(io.Writer) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + ) + }) + return _c +} + +func (_c *CASClient_Download_Call) Return(err error) *CASClient_Download_Call { + _c.Call.Return(err) + return _c +} + +func (_c *CASClient_Download_Call) RunAndReturn(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, w io.Writer, digest string) error) *CASClient_Download_Call { + _c.Call.Return(run) + return _c +} + +// Upload provides a mock function for the type CASClient +func (_mock *CASClient) Upload(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, content io.Reader, filename string, digest string) error { + ret := _mock.Called(ctx, backendType, secretID, orgID, content, filename, digest) if len(ret) == 0 { panic("no return value specified for Upload") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, io.Reader, string, string) error); ok { - r0 = rf(ctx, backendType, secretID, orgID, content, filename, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, uuid.UUID, io.Reader, string, string) error); ok { + r0 = returnFunc(ctx, backendType, secretID, orgID, content, filename, digest) } else { r0 = ret.Error(0) } - return r0 } -// NewCASClient creates a new instance of CASClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewCASClient(t interface { - mock.TestingT - Cleanup(func()) -}) *CASClient { - mock := &CASClient{} - mock.Mock.Test(t) +// CASClient_Upload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upload' +type CASClient_Upload_Call struct { + *mock.Call +} - t.Cleanup(func() { mock.AssertExpectations(t) }) +// Upload is a helper method to define mock.On call +// - ctx context.Context +// - backendType string +// - secretID string +// - orgID uuid.UUID +// - content io.Reader +// - filename string +// - digest string +func (_e *CASClient_Expecter) Upload(ctx any, backendType any, secretID any, orgID any, content any, filename any, digest any) *CASClient_Upload_Call { + return &CASClient_Upload_Call{Call: _e.mock.On("Upload", ctx, backendType, secretID, orgID, content, filename, digest)} +} - return mock +func (_c *CASClient_Upload_Call) Run(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, content io.Reader, filename string, digest string)) *CASClient_Upload_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 uuid.UUID + if args[3] != nil { + arg3 = args[3].(uuid.UUID) + } + var arg4 io.Reader + if args[4] != nil { + arg4 = args[4].(io.Reader) + } + var arg5 string + if args[5] != nil { + arg5 = args[5].(string) + } + var arg6 string + if args[6] != nil { + arg6 = args[6].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5, + arg6, + ) + }) + return _c +} + +func (_c *CASClient_Upload_Call) Return(err error) *CASClient_Upload_Call { + _c.Call.Return(err) + return _c +} + +func (_c *CASClient_Upload_Call) RunAndReturn(run func(ctx context.Context, backendType string, secretID string, orgID uuid.UUID, content io.Reader, filename string, digest string) error) *CASClient_Upload_Call { + _c.Call.Return(run) + return _c } diff --git a/pkg/casclient/.mockery.yml b/pkg/casclient/.mockery.yml new file mode 100644 index 000000000..85bcd20ba --- /dev/null +++ b/pkg/casclient/.mockery.yml @@ -0,0 +1,18 @@ +all: false +formatter: goimports +include-auto-generated: false +log-level: info +recursive: false +require-template-schema-exists: true +template: testify +template-schema: "{{.Template}}.schema.json" +packages: + github.com/chainloop-dev/chainloop/pkg/casclient: + config: + dir: "{{.InterfaceDir}}/mocks" + filename: "{{.InterfaceName}}.go" + pkgname: mocks + structname: "{{.InterfaceName}}" + interfaces: + Downloader: + DownloaderUploader: diff --git a/pkg/casclient/casclient.go b/pkg/casclient/casclient.go index 0ae391760..a28708e2e 100644 --- a/pkg/casclient/casclient.go +++ b/pkg/casclient/casclient.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,6 +43,9 @@ type Uploader interface { type Downloader interface { Download(ctx context.Context, w io.Writer, digest string) error + // Describe reports the metadata of a stored resource, including its size in + // bytes, without transferring its content + Describe(ctx context.Context, digest string) (*ResourceInfo, error) // Whether the CAS is ready to accept downloads IsReady(ctx context.Context) (bool, error) } diff --git a/pkg/casclient/mocks/Downloader.go b/pkg/casclient/mocks/Downloader.go index 04a1ff680..3b553589a 100644 --- a/pkg/casclient/mocks/Downloader.go +++ b/pkg/casclient/mocks/Downloader.go @@ -1,68 +1,231 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify package mocks import ( - context "context" - io "io" + "context" + "io" + "github.com/chainloop-dev/chainloop/pkg/casclient" mock "github.com/stretchr/testify/mock" ) +// NewDownloader creates a new instance of Downloader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDownloader(t interface { + mock.TestingT + Cleanup(func()) +}) *Downloader { + mock := &Downloader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + // Downloader is an autogenerated mock type for the Downloader type type Downloader struct { mock.Mock } -// Download provides a mock function with given fields: ctx, w, digest -func (_m *Downloader) Download(ctx context.Context, w io.Writer, digest string) error { - ret := _m.Called(ctx, w, digest) +type Downloader_Expecter struct { + mock *mock.Mock +} + +func (_m *Downloader) EXPECT() *Downloader_Expecter { + return &Downloader_Expecter{mock: &_m.Mock} +} + +// Describe provides a mock function for the type Downloader +func (_mock *Downloader) Describe(ctx context.Context, digest string) (*casclient.ResourceInfo, error) { + ret := _mock.Called(ctx, digest) + + if len(ret) == 0 { + panic("no return value specified for Describe") + } + + var r0 *casclient.ResourceInfo + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*casclient.ResourceInfo, error)); ok { + return returnFunc(ctx, digest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *casclient.ResourceInfo); ok { + r0 = returnFunc(ctx, digest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*casclient.ResourceInfo) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, digest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// Downloader_Describe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Describe' +type Downloader_Describe_Call struct { + *mock.Call +} + +// Describe is a helper method to define mock.On call +// - ctx context.Context +// - digest string +func (_e *Downloader_Expecter) Describe(ctx any, digest any) *Downloader_Describe_Call { + return &Downloader_Describe_Call{Call: _e.mock.On("Describe", ctx, digest)} +} + +func (_c *Downloader_Describe_Call) Run(run func(ctx context.Context, digest string)) *Downloader_Describe_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *Downloader_Describe_Call) Return(resourceInfo *casclient.ResourceInfo, err error) *Downloader_Describe_Call { + _c.Call.Return(resourceInfo, err) + return _c +} + +func (_c *Downloader_Describe_Call) RunAndReturn(run func(ctx context.Context, digest string) (*casclient.ResourceInfo, error)) *Downloader_Describe_Call { + _c.Call.Return(run) + return _c +} + +// Download provides a mock function for the type Downloader +func (_mock *Downloader) Download(ctx context.Context, w io.Writer, digest string) error { + ret := _mock.Called(ctx, w, digest) + + if len(ret) == 0 { + panic("no return value specified for Download") + } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, io.Writer, string) error); ok { - r0 = rf(ctx, w, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, io.Writer, string) error); ok { + r0 = returnFunc(ctx, w, digest) } else { r0 = ret.Error(0) } - return r0 } -// IsReady provides a mock function with given fields: ctx -func (_m *Downloader) IsReady(ctx context.Context) (bool, error) { - ret := _m.Called(ctx) +// Downloader_Download_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Download' +type Downloader_Download_Call struct { + *mock.Call +} + +// Download is a helper method to define mock.On call +// - ctx context.Context +// - w io.Writer +// - digest string +func (_e *Downloader_Expecter) Download(ctx any, w any, digest any) *Downloader_Download_Call { + return &Downloader_Download_Call{Call: _e.mock.On("Download", ctx, w, digest)} +} + +func (_c *Downloader_Download_Call) Run(run func(ctx context.Context, w io.Writer, digest string)) *Downloader_Download_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 io.Writer + if args[1] != nil { + arg1 = args[1].(io.Writer) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *Downloader_Download_Call) Return(err error) *Downloader_Download_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Downloader_Download_Call) RunAndReturn(run func(ctx context.Context, w io.Writer, digest string) error) *Downloader_Download_Call { + _c.Call.Return(run) + return _c +} + +// IsReady provides a mock function for the type Downloader +func (_mock *Downloader) IsReady(ctx context.Context) (bool, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for IsReady") + } var r0 bool var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { - return rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { + return returnFunc(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) bool); ok { - r0 = rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) bool); ok { + r0 = returnFunc(ctx) } else { r0 = ret.Get(0).(bool) } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } - return r0, r1 } -type mockConstructorTestingTNewDownloader interface { - mock.TestingT - Cleanup(func()) +// Downloader_IsReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsReady' +type Downloader_IsReady_Call struct { + *mock.Call } -// NewDownloader creates a new instance of Downloader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDownloader(t mockConstructorTestingTNewDownloader) *Downloader { - mock := &Downloader{} - mock.Mock.Test(t) +// IsReady is a helper method to define mock.On call +// - ctx context.Context +func (_e *Downloader_Expecter) IsReady(ctx any) *Downloader_IsReady_Call { + return &Downloader_IsReady_Call{Call: _e.mock.On("IsReady", ctx)} +} - t.Cleanup(func() { mock.AssertExpectations(t) }) +func (_c *Downloader_IsReady_Call) Run(run func(ctx context.Context)) *Downloader_IsReady_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} - return mock +func (_c *Downloader_IsReady_Call) Return(b bool, err error) *Downloader_IsReady_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *Downloader_IsReady_Call) RunAndReturn(run func(ctx context.Context) (bool, error)) *Downloader_IsReady_Call { + _c.Call.Return(run) + return _c } diff --git a/pkg/casclient/mocks/DownloaderUploader.go b/pkg/casclient/mocks/DownloaderUploader.go index 10237e195..ff112989f 100644 --- a/pkg/casclient/mocks/DownloaderUploader.go +++ b/pkg/casclient/mocks/DownloaderUploader.go @@ -1,123 +1,379 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify package mocks import ( - context "context" - - casclient "github.com/chainloop-dev/chainloop/pkg/casclient" - - io "io" + "context" + "io" + "github.com/chainloop-dev/chainloop/pkg/casclient" mock "github.com/stretchr/testify/mock" ) +// NewDownloaderUploader creates a new instance of DownloaderUploader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDownloaderUploader(t interface { + mock.TestingT + Cleanup(func()) +}) *DownloaderUploader { + mock := &DownloaderUploader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + // DownloaderUploader is an autogenerated mock type for the DownloaderUploader type type DownloaderUploader struct { mock.Mock } -// Download provides a mock function with given fields: ctx, w, digest -func (_m *DownloaderUploader) Download(ctx context.Context, w io.Writer, digest string) error { - ret := _m.Called(ctx, w, digest) +type DownloaderUploader_Expecter struct { + mock *mock.Mock +} + +func (_m *DownloaderUploader) EXPECT() *DownloaderUploader_Expecter { + return &DownloaderUploader_Expecter{mock: &_m.Mock} +} + +// Describe provides a mock function for the type DownloaderUploader +func (_mock *DownloaderUploader) Describe(ctx context.Context, digest string) (*casclient.ResourceInfo, error) { + ret := _mock.Called(ctx, digest) + + if len(ret) == 0 { + panic("no return value specified for Describe") + } + + var r0 *casclient.ResourceInfo + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*casclient.ResourceInfo, error)); ok { + return returnFunc(ctx, digest) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *casclient.ResourceInfo); ok { + r0 = returnFunc(ctx, digest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*casclient.ResourceInfo) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, digest) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// DownloaderUploader_Describe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Describe' +type DownloaderUploader_Describe_Call struct { + *mock.Call +} + +// Describe is a helper method to define mock.On call +// - ctx context.Context +// - digest string +func (_e *DownloaderUploader_Expecter) Describe(ctx any, digest any) *DownloaderUploader_Describe_Call { + return &DownloaderUploader_Describe_Call{Call: _e.mock.On("Describe", ctx, digest)} +} + +func (_c *DownloaderUploader_Describe_Call) Run(run func(ctx context.Context, digest string)) *DownloaderUploader_Describe_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *DownloaderUploader_Describe_Call) Return(resourceInfo *casclient.ResourceInfo, err error) *DownloaderUploader_Describe_Call { + _c.Call.Return(resourceInfo, err) + return _c +} + +func (_c *DownloaderUploader_Describe_Call) RunAndReturn(run func(ctx context.Context, digest string) (*casclient.ResourceInfo, error)) *DownloaderUploader_Describe_Call { + _c.Call.Return(run) + return _c +} + +// Download provides a mock function for the type DownloaderUploader +func (_mock *DownloaderUploader) Download(ctx context.Context, w io.Writer, digest string) error { + ret := _mock.Called(ctx, w, digest) + + if len(ret) == 0 { + panic("no return value specified for Download") + } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, io.Writer, string) error); ok { - r0 = rf(ctx, w, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, io.Writer, string) error); ok { + r0 = returnFunc(ctx, w, digest) } else { r0 = ret.Error(0) } - return r0 } -// IsReady provides a mock function with given fields: ctx -func (_m *DownloaderUploader) IsReady(ctx context.Context) (bool, error) { - ret := _m.Called(ctx) +// DownloaderUploader_Download_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Download' +type DownloaderUploader_Download_Call struct { + *mock.Call +} + +// Download is a helper method to define mock.On call +// - ctx context.Context +// - w io.Writer +// - digest string +func (_e *DownloaderUploader_Expecter) Download(ctx any, w any, digest any) *DownloaderUploader_Download_Call { + return &DownloaderUploader_Download_Call{Call: _e.mock.On("Download", ctx, w, digest)} +} + +func (_c *DownloaderUploader_Download_Call) Run(run func(ctx context.Context, w io.Writer, digest string)) *DownloaderUploader_Download_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 io.Writer + if args[1] != nil { + arg1 = args[1].(io.Writer) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DownloaderUploader_Download_Call) Return(err error) *DownloaderUploader_Download_Call { + _c.Call.Return(err) + return _c +} + +func (_c *DownloaderUploader_Download_Call) RunAndReturn(run func(ctx context.Context, w io.Writer, digest string) error) *DownloaderUploader_Download_Call { + _c.Call.Return(run) + return _c +} + +// IsReady provides a mock function for the type DownloaderUploader +func (_mock *DownloaderUploader) IsReady(ctx context.Context) (bool, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for IsReady") + } var r0 bool var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { - return rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { + return returnFunc(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) bool); ok { - r0 = rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) bool); ok { + r0 = returnFunc(ctx) } else { r0 = ret.Get(0).(bool) } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) } else { r1 = ret.Error(1) } - return r0, r1 } -// Upload provides a mock function with given fields: ctx, r, digest, fileName -func (_m *DownloaderUploader) Upload(ctx context.Context, r io.Reader, digest string, fileName string) (*casclient.UpDownStatus, error) { - ret := _m.Called(ctx, r, digest, fileName) +// DownloaderUploader_IsReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsReady' +type DownloaderUploader_IsReady_Call struct { + *mock.Call +} + +// IsReady is a helper method to define mock.On call +// - ctx context.Context +func (_e *DownloaderUploader_Expecter) IsReady(ctx any) *DownloaderUploader_IsReady_Call { + return &DownloaderUploader_IsReady_Call{Call: _e.mock.On("IsReady", ctx)} +} + +func (_c *DownloaderUploader_IsReady_Call) Run(run func(ctx context.Context)) *DownloaderUploader_IsReady_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *DownloaderUploader_IsReady_Call) Return(b bool, err error) *DownloaderUploader_IsReady_Call { + _c.Call.Return(b, err) + return _c +} + +func (_c *DownloaderUploader_IsReady_Call) RunAndReturn(run func(ctx context.Context) (bool, error)) *DownloaderUploader_IsReady_Call { + _c.Call.Return(run) + return _c +} + +// Upload provides a mock function for the type DownloaderUploader +func (_mock *DownloaderUploader) Upload(ctx context.Context, r io.Reader, filename string, digest string) (*casclient.UpDownStatus, error) { + ret := _mock.Called(ctx, r, filename, digest) + + if len(ret) == 0 { + panic("no return value specified for Upload") + } var r0 *casclient.UpDownStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context, io.Reader, string, string) (*casclient.UpDownStatus, error)); ok { - return rf(ctx, r, digest, fileName) + if returnFunc, ok := ret.Get(0).(func(context.Context, io.Reader, string, string) (*casclient.UpDownStatus, error)); ok { + return returnFunc(ctx, r, filename, digest) } - if rf, ok := ret.Get(0).(func(context.Context, io.Reader, string, string) *casclient.UpDownStatus); ok { - r0 = rf(ctx, r, digest, fileName) + if returnFunc, ok := ret.Get(0).(func(context.Context, io.Reader, string, string) *casclient.UpDownStatus); ok { + r0 = returnFunc(ctx, r, filename, digest) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*casclient.UpDownStatus) } } - - if rf, ok := ret.Get(1).(func(context.Context, io.Reader, string, string) error); ok { - r1 = rf(ctx, r, digest, fileName) + if returnFunc, ok := ret.Get(1).(func(context.Context, io.Reader, string, string) error); ok { + r1 = returnFunc(ctx, r, filename, digest) } else { r1 = ret.Error(1) } - return r0, r1 } -// UploadFile provides a mock function with given fields: ctx, filepath -func (_m *DownloaderUploader) UploadFile(ctx context.Context, filepath string) (*casclient.UpDownStatus, error) { - ret := _m.Called(ctx, filepath) +// DownloaderUploader_Upload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upload' +type DownloaderUploader_Upload_Call struct { + *mock.Call +} + +// Upload is a helper method to define mock.On call +// - ctx context.Context +// - r io.Reader +// - filename string +// - digest string +func (_e *DownloaderUploader_Expecter) Upload(ctx any, r any, filename any, digest any) *DownloaderUploader_Upload_Call { + return &DownloaderUploader_Upload_Call{Call: _e.mock.On("Upload", ctx, r, filename, digest)} +} + +func (_c *DownloaderUploader_Upload_Call) Run(run func(ctx context.Context, r io.Reader, filename string, digest string)) *DownloaderUploader_Upload_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 io.Reader + if args[1] != nil { + arg1 = args[1].(io.Reader) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *DownloaderUploader_Upload_Call) Return(upDownStatus *casclient.UpDownStatus, err error) *DownloaderUploader_Upload_Call { + _c.Call.Return(upDownStatus, err) + return _c +} + +func (_c *DownloaderUploader_Upload_Call) RunAndReturn(run func(ctx context.Context, r io.Reader, filename string, digest string) (*casclient.UpDownStatus, error)) *DownloaderUploader_Upload_Call { + _c.Call.Return(run) + return _c +} + +// UploadFile provides a mock function for the type DownloaderUploader +func (_mock *DownloaderUploader) UploadFile(ctx context.Context, filepath string) (*casclient.UpDownStatus, error) { + ret := _mock.Called(ctx, filepath) + + if len(ret) == 0 { + panic("no return value specified for UploadFile") + } var r0 *casclient.UpDownStatus var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*casclient.UpDownStatus, error)); ok { - return rf(ctx, filepath) + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*casclient.UpDownStatus, error)); ok { + return returnFunc(ctx, filepath) } - if rf, ok := ret.Get(0).(func(context.Context, string) *casclient.UpDownStatus); ok { - r0 = rf(ctx, filepath) + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *casclient.UpDownStatus); ok { + r0 = returnFunc(ctx, filepath) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*casclient.UpDownStatus) } } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, filepath) + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, filepath) } else { r1 = ret.Error(1) } - return r0, r1 } -type mockConstructorTestingTNewDownloaderUploader interface { - mock.TestingT - Cleanup(func()) +// DownloaderUploader_UploadFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UploadFile' +type DownloaderUploader_UploadFile_Call struct { + *mock.Call } -// NewDownloaderUploader creates a new instance of DownloaderUploader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDownloaderUploader(t mockConstructorTestingTNewDownloaderUploader) *DownloaderUploader { - mock := &DownloaderUploader{} - mock.Mock.Test(t) +// UploadFile is a helper method to define mock.On call +// - ctx context.Context +// - filepath string +func (_e *DownloaderUploader_Expecter) UploadFile(ctx any, filepath any) *DownloaderUploader_UploadFile_Call { + return &DownloaderUploader_UploadFile_Call{Call: _e.mock.On("UploadFile", ctx, filepath)} +} - t.Cleanup(func() { mock.AssertExpectations(t) }) +func (_c *DownloaderUploader_UploadFile_Call) Run(run func(ctx context.Context, filepath string)) *DownloaderUploader_UploadFile_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} - return mock +func (_c *DownloaderUploader_UploadFile_Call) Return(upDownStatus *casclient.UpDownStatus, err error) *DownloaderUploader_UploadFile_Call { + _c.Call.Return(upDownStatus, err) + return _c +} + +func (_c *DownloaderUploader_UploadFile_Call) RunAndReturn(run func(ctx context.Context, filepath string) (*casclient.UpDownStatus, error)) *DownloaderUploader_UploadFile_Call { + _c.Call.Return(run) + return _c } From ba3d6e37cc2aafe36ec698c7991833e9071bd04e Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 7 Sep 2026 12:22:18 +0200 Subject: [PATCH 2/2] fix(controlplane): bound the policy evaluations download by bytes received The size check trusted the CAS backend's metadata in two ways it should not have. A reported size of zero fell through the cap comparison and started an unbounded download: some backends omit the content length and the proto getter then yields zero, so zero means unknown rather than empty. And a correct-looking size was never reconciled with the bytes actually received, so an under-reporting backend could still exhaust the cap. Zero-size responses are now reported as unavailable without a download, and the download target is a writer that refuses to grow past the cap and records that it did, so the bound holds against the bytes received rather than the size claimed. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: d400245f-b8d2-4805-998e-b23f9be4a7fb --- .../internal/service/workflowrun.go | 52 ++++++++++++++++++- .../internal/service/workflowrun_test.go | 23 +++++++- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 6b0ba474a..4e8cab18d 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -173,8 +173,29 @@ func (s *WorkflowRunService) resolvePolicyEvaluations( return tooLargePolicyEvaluations(digest, info.Size, mediaType) } - var buf bytes.Buffer - if err := s.casClient.Download(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, mapping.CASBackend.OrganizationID, &buf, digest); err != nil { + // A size of zero means the backend did not report one, not that the object + // is empty: some backends omit the content length and the proto getter then + // yields zero. Downloading on that basis would be downloading blind. + if info.Size <= 0 { + s.log.Warnw("msg", "policy evaluations bundle has no reported size", "digest", digest) + return unavailablePolicyEvaluations(digest, 0, mediaType) + } + + // The reported size is metadata, so bound the transfer itself as well. + // A backend that under-reports cannot then push us past the cap. + buf := &boundedBuffer{limit: maxInlineBytes} + err = s.casClient.Download(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, mapping.CASBackend.OrganizationID, buf, digest) + + // Checked before the error because a writer refusing to grow surfaces as a + // download failure, and because the bound must hold even if an + // implementation swallows the write error. + if buf.exceeded { + s.log.Warnw("msg", "policy evaluations bundle exceeded the cap while downloading", "digest", digest, "reportedSize", info.Size, "max", maxInlineBytes) + // The reported size is known to be wrong, so no size is reported at all. + return tooLargePolicyEvaluations(digest, 0, mediaType) + } + + if err != nil { s.log.Warnw("msg", "downloading policy evaluations bundle", "digest", digest, "err", err) return unavailablePolicyEvaluations(digest, info.Size, mediaType) } @@ -185,6 +206,33 @@ func (s *WorkflowRunService) resolvePolicyEvaluations( return s.decodePolicyEvaluations(data, digest, info.Size, mediaType) } +// boundedBuffer accumulates bytes in memory up to a limit and refuses the write +// that would exceed it, recording that it did. It exists so the policy +// evaluations cap is enforced against the bytes actually received rather than +// against the size the CAS backend claims. +type boundedBuffer struct { + buf bytes.Buffer + limit int64 + written int64 + exceeded bool +} + +func (b *boundedBuffer) Write(p []byte) (int, error) { + if b.written+int64(len(p)) > b.limit { + b.exceeded = true + return 0, fmt.Errorf("content exceeds the maximum of %d bytes", b.limit) + } + + n, err := b.buf.Write(p) + b.written += int64(n) + + return n, err +} + +func (b *boundedBuffer) Bytes() []byte { + return b.buf.Bytes() +} + func (s *WorkflowRunService) decodePolicyEvaluations(data []byte, digest string, size int64, mediaType string) *resolvedPolicyEvaluations { evaluations, err := chainloop.PolicyEvaluationsFromBundle(data) if err != nil { diff --git a/app/controlplane/internal/service/workflowrun_test.go b/app/controlplane/internal/service/workflowrun_test.go index 17217cfe2..ac2377b52 100644 --- a/app/controlplane/internal/service/workflowrun_test.go +++ b/app/controlplane/internal/service/workflowrun_test.go @@ -16,6 +16,7 @@ package service import ( + "bytes" "context" "errors" "io" @@ -138,6 +139,22 @@ func TestResolvePolicyEvaluations(t *testing.T) { wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, wantDescribeCall: true, }, + { + name: "a size of zero is unknown, not empty, so it is not downloaded", + describeSize: 0, + wantRefReason: pb.PolicyEvaluationsRef_REASON_UNAVAILABLE, + wantDescribeCall: true, + }, + { + name: "a body larger than its reported size stops at the cap", + maxInlineBytes: 16, + describeSize: 8, + downloadBody: bytes.Repeat([]byte("x"), 64), + wantRefReason: pb.PolicyEvaluationsRef_REASON_TOO_LARGE, + wantRefSize: 0, + wantDescribeCall: true, + wantDownloadCall: true, + }, { name: "missing CAS mapping is not downloaded", mappingErr: biz.NewErrNotFound("digest"), @@ -188,8 +205,10 @@ func TestResolvePolicyEvaluations(t *testing.T) { Run(func(args mock.Arguments) { w, ok := args.Get(4).(io.Writer) require.True(t, ok) - _, err := w.Write(tc.downloadBody) - require.NoError(t, err) + // The error is deliberately ignored: a bounded writer + // rejects a body past the cap and the production code, + // not the test, decides what that means. + _, _ = w.Write(tc.downloadBody) }).Return(nil) }