From f82b21dd16cc41b63378be35e8d4973dd045b4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Mon, 23 Feb 2026 19:59:36 -0300 Subject: [PATCH 1/8] feat(policies): support per-policy gate override with org-default inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make PolicyAttachment.gate presence-aware to distinguish unset from explicit false. Resolve effective gate at evaluation time using org default when unset. Update attestation push enforcement to block on effective gated violations. Signed-off-by: Matías Insaurralde --- app/cli/cmd/attestation_push.go | 8 ++- app/cli/cmd/attestation_push_test.go | 21 ++++++++ .../workflowcontract/v1/crafting_schema.ts | 17 +++--- ...ntract.v1.PolicyAttachment.jsonschema.json | 2 +- ...owcontract.v1.PolicyAttachment.schema.json | 2 +- .../workflowcontract/v1/crafting_schema.pb.go | 20 ++++--- .../workflowcontract/v1/crafting_schema.proto | 7 ++- pkg/attestation/crafter/crafter.go | 19 ++++++- pkg/policies/policies.go | 23 +++++++- pkg/policies/policies_test.go | 54 +++++++++++++++++++ 10 files changed, 151 insertions(+), 22 deletions(-) diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index 9b9972d8d..d2f4bf30c 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -180,10 +180,13 @@ func (e *GateError) Error() string { } func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPolicyCheck bool) error { + hasGatedViolations := false + // Block if any of the policies has been configured as a gate. for _, evaluations := range status.PolicyEvaluations { for _, eval := range evaluations { if len(eval.Violations) > 0 && eval.Gate { + hasGatedViolations = true if bypassPolicyCheck { logger.Warn().Msg(exceptionBypassPolicyCheck) continue @@ -202,7 +205,10 @@ func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPol return nil } - if status.HasPolicyViolations { + // Effective gate semantics are already resolved in policy evaluations. + // For backwards compatibility, fall back to aggregate status only if + // no evaluations are available. + if hasGatedViolations || (len(status.PolicyEvaluations) == 0 && status.HasPolicyViolations) { return ErrBlockedByPolicyViolation } } diff --git a/app/cli/cmd/attestation_push_test.go b/app/cli/cmd/attestation_push_test.go index 101662a66..0100e805a 100644 --- a/app/cli/cmd/attestation_push_test.go +++ b/app/cli/cmd/attestation_push_test.go @@ -67,4 +67,25 @@ func TestValidatePolicyEnforcement(t *testing.T) { require.ErrorAs(t, err, &gateErr) require.Equal(t, "cdx-fresh", gateErr.PolicyName) }) + + t.Run("does not block when strategy is enforced and policy is explicitly not gated", func(t *testing.T) { + status := &action.AttestationStatusResult{ + PolicyEvaluations: map[string][]*action.PolicyEvaluation{ + "materials": { + { + Name: "cdx-fresh", + Gate: false, + Violations: []*action.PolicyViolation{ + {Message: "policy violation"}, + }, + }, + }, + }, + HasPolicyViolations: true, + MustBlockOnPolicyViolations: true, + } + + err := validatePolicyEnforcement(status, false) + require.NoError(t, err) + }) } diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index da3e00ec3..af825f895 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -487,8 +487,13 @@ export interface PolicyAttachment { with: { [key: string]: string }; /** List of requirements this policy contributes to satisfy */ requirements: string[]; - /** If true, the policy will act as a gate, returning an error code if the policy fails */ - gate: boolean; + /** + * If set, controls whether this policy acts as a gate. + * - true: policy violations are blocking for this policy + * - false: policy violations are non-blocking for this policy + * - unset: inherit organization-level default behavior + */ + gate?: boolean | undefined; } export interface PolicyAttachment_WithEntry { @@ -1414,7 +1419,7 @@ function createBasePolicyAttachment(): PolicyAttachment { disabled: false, with: {}, requirements: [], - gate: false, + gate: undefined, }; } @@ -1438,7 +1443,7 @@ export const PolicyAttachment = { for (const v of message.requirements) { writer.uint32(50).string(v!); } - if (message.gate === true) { + if (message.gate !== undefined) { writer.uint32(56).bool(message.gate); } return writer; @@ -1525,7 +1530,7 @@ export const PolicyAttachment = { }, {}) : {}, requirements: Array.isArray(object?.requirements) ? object.requirements.map((e: any) => String(e)) : [], - gate: isSet(object.gate) ? Boolean(object.gate) : false, + gate: isSet(object.gate) ? Boolean(object.gate) : undefined, }; }, @@ -1572,7 +1577,7 @@ export const PolicyAttachment = { return acc; }, {}); message.requirements = object.requirements?.map((e) => e) || []; - message.gate = object.gate ?? false; + message.gate = object.gate ?? undefined; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json index f3b99559c..ba42eec8b 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json @@ -13,7 +13,7 @@ "description": "meant to be used to embed the policy in the contract" }, "gate": { - "description": "If true, the policy will act as a gate, returning an error code if the policy fails", + "description": "If set, controls whether this policy acts as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", "type": "boolean" }, "ref": { diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json index bfcf6c43e..6b816b6cf 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json @@ -13,7 +13,7 @@ "description": "meant to be used to embed the policy in the contract" }, "gate": { - "description": "If true, the policy will act as a gate, returning an error code if the policy fails", + "description": "If set, controls whether this policy acts as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", "type": "boolean" }, "ref": { diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 6f7a1d2e3..af7601bcd 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -713,8 +713,11 @@ type PolicyAttachment struct { With map[string]string `protobuf:"bytes,5,rep,name=with,proto3" json:"with,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // List of requirements this policy contributes to satisfy Requirements []string `protobuf:"bytes,6,rep,name=requirements,proto3" json:"requirements,omitempty"` - // If true, the policy will act as a gate, returning an error code if the policy fails - Gate bool `protobuf:"varint,7,opt,name=gate,proto3" json:"gate,omitempty"` + // If set, controls whether this policy acts as a gate. + // - true: policy violations are blocking for this policy + // - false: policy violations are non-blocking for this policy + // - unset: inherit organization-level default behavior + Gate *bool `protobuf:"varint,7,opt,name=gate,proto3,oneof" json:"gate,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -803,8 +806,8 @@ func (x *PolicyAttachment) GetRequirements() []string { } func (x *PolicyAttachment) GetGate() bool { - if x != nil { - return x.Gate + if x != nil && x.Gate != nil { + return *x.Gate } return false } @@ -1987,21 +1990,22 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value\"\x98\x01\n" + "\bPolicies\x12C\n" + "\tmaterials\x18\x01 \x03(\v2%.workflowcontract.v1.PolicyAttachmentR\tmaterials\x12G\n" + - "\vattestation\x18\x02 \x03(\v2%.workflowcontract.v1.PolicyAttachmentR\vattestation\"\x8a\x04\n" + + "\vattestation\x18\x02 \x03(\v2%.workflowcontract.v1.PolicyAttachmentR\vattestation\"\x98\x04\n" + "\x10PolicyAttachment\x12\x1b\n" + "\x03ref\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01H\x00R\x03ref\x129\n" + "\bembedded\x18\x02 \x01(\v2\x1b.workflowcontract.v1.PolicyH\x00R\bembedded\x12R\n" + "\bselector\x18\x03 \x01(\v26.workflowcontract.v1.PolicyAttachment.MaterialSelectorR\bselector\x12\x1a\n" + "\bdisabled\x18\x04 \x01(\bR\bdisabled\x12C\n" + "\x04with\x18\x05 \x03(\v2/.workflowcontract.v1.PolicyAttachment.WithEntryR\x04with\x12c\n" + - "\frequirements\x18\x06 \x03(\tB?\xbaH<\x92\x019\"7r523^([a-z0-9-]+\\/)?([^\\s\\/]+\\/)([^\\s@\\/]+)(@[^\\s@]+)?$R\frequirements\x12\x12\n" + - "\x04gate\x18\a \x01(\bR\x04gate\x1a7\n" + + "\frequirements\x18\x06 \x03(\tB?\xbaH<\x92\x019\"7r523^([a-z0-9-]+\\/)?([^\\s\\/]+\\/)([^\\s@\\/]+)(@[^\\s@]+)?$R\frequirements\x12\x17\n" + + "\x04gate\x18\a \x01(\bH\x01R\x04gate\x88\x01\x01\x1a7\n" + "\tWithEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a&\n" + "\x10MaterialSelector\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04nameB\x0f\n" + - "\x06policy\x12\x05\xbaH\x02\b\x01\"\xf6\x01\n" + + "\x06policy\x12\x05\xbaH\x02\b\x01B\a\n" + + "\x05_gate\"\xf6\x01\n" + "\x06Policy\x12I\n" + "\vapi_version\x18\x01 \x01(\tB(\xbaH%r#\n" + "!workflowcontract.chainloop.dev/v1R\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 9d9185345..2b94aee56 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -236,8 +236,11 @@ message PolicyAttachment { } }]; - // If true, the policy will act as a gate, returning an error code if the policy fails - bool gate = 7; + // If set, controls whether this policy acts as a gate. + // - true: policy violations are blocking for this policy + // - false: policy violations are non-blocking for this policy + // - unset: inherit organization-level default behavior + optional bool gate = 7; message MaterialSelector { // material name diff --git a/pkg/attestation/crafter/crafter.go b/pkg/attestation/crafter/crafter.go index 99f3f8928..5918e0648 100644 --- a/pkg/attestation/crafter/crafter.go +++ b/pkg/attestation/crafter/crafter.go @@ -728,7 +728,14 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M return i.MaterialName == m.Name }) - pgv := policies.NewPolicyGroupVerifier(c.CraftingState.GetPolicyGroups(), c.CraftingState.GetPolicies(), c.attClient, c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...)) + pgv := policies.NewPolicyGroupVerifier( + c.CraftingState.GetPolicyGroups(), + c.CraftingState.GetPolicies(), + c.attClient, + c.Logger, + policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), + policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + ) policyGroupResults, err := pgv.VerifyMaterial(ctx, mt, value) if err != nil { return nil, fmt.Errorf("error applying policy groups to material: %w", err) @@ -739,7 +746,13 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M policies.LogPolicyEvaluations(policyGroupResults, c.Logger) // Validate policies - pv := policies.NewPolicyVerifier(c.CraftingState.GetPolicies(), c.attClient, c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...)) + pv := policies.NewPolicyVerifier( + c.CraftingState.GetPolicies(), + c.attClient, + c.Logger, + policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), + policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + ) policyResults, err := pv.VerifyMaterial(ctx, mt, value) if err != nil { return nil, fmt.Errorf("error applying policies to material: %w", err) @@ -772,6 +785,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID // evaluate attestation-level policies pv := policies.NewPolicyVerifier(c.CraftingState.GetPolicies(), c.attClient, c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), + policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), ) policyEvaluations, err := pv.VerifyStatement(ctx, statement) @@ -781,6 +795,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID pgv := policies.NewPolicyGroupVerifier(c.CraftingState.GetPolicyGroups(), c.CraftingState.GetPolicies(), c.attClient, c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), + policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), ) policyGroupResults, err := pgv.VerifyStatement(ctx, statement) diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index 625101765..486cf58ee 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -78,6 +78,7 @@ type PolicyVerifier struct { client v13.AttestationServiceClient grpcConn *grpc.ClientConn allowedHostnames []string + defaultGate bool includeRawData bool enablePrint bool evalPhase EvalPhase @@ -87,6 +88,7 @@ var _ Verifier = (*PolicyVerifier)(nil) type PolicyVerifierOptions struct { AllowedHostnames []string + DefaultGate bool IncludeRawData bool EnablePrint bool GRPCConn *grpc.ClientConn @@ -101,6 +103,12 @@ func WithAllowedHostnames(hostnames ...string) PolicyVerifierOption { } } +func WithDefaultGate(defaultGate bool) PolicyVerifierOption { + return func(o *PolicyVerifierOptions) { + o.DefaultGate = defaultGate + } +} + func WithIncludeRawData(include bool) PolicyVerifierOption { return func(o *PolicyVerifierOptions) { o.IncludeRawData = include @@ -137,6 +145,7 @@ func NewPolicyVerifier(policies *v1.Policies, client v13.AttestationServiceClien logger: logger, grpcConn: options.GRPCConn, allowedHostnames: options.AllowedHostnames, + defaultGate: options.DefaultGate, includeRawData: options.IncludeRawData, enablePrint: options.EnablePrint, evalPhase: options.EvalPhase, @@ -336,10 +345,22 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme SkipReasons: reasons, Requirements: attachment.Requirements, RawResults: engineRawResultsToAPIRawResults(rawResults), - Gate: attachment.GetGate(), + Gate: policyAttachmentGate(attachment, pv.defaultGate), }, nil } +func policyAttachmentGate(attachment *v1.PolicyAttachment, defaultGate bool) bool { + if attachment == nil { + return defaultGate + } + + if attachment.Gate != nil { + return attachment.GetGate() + } + + return defaultGate +} + // ComputeArguments takes a list of arguments, and matches it against the expected inputs. It also applies a set of interpolations if needed. func ComputeArguments(name string, inputs []*v1.PolicyInput, args map[string]string, bindings map[string]string, logger *zerolog.Logger) (map[string]string, error) { result := make(map[string]string) diff --git a/pkg/policies/policies_test.go b/pkg/policies/policies_test.go index 4e165cd42..b0b8a046a 100644 --- a/pkg/policies/policies_test.go +++ b/pkg/policies/policies_test.go @@ -1434,3 +1434,57 @@ func (s *testSuite) TestIsURLPath() { }) } } + +func (s *testSuite) TestPolicyAttachmentGate() { + trueGate := true + falseGate := false + + cases := []struct { + name string + attachment *v12.PolicyAttachment + defaultGate bool + expectedGate bool + }{ + { + name: "nil attachment falls back to default true", + attachment: nil, + defaultGate: true, + expectedGate: true, + }, + { + name: "unset gate inherits default false", + attachment: &v12.PolicyAttachment{}, + defaultGate: false, + expectedGate: false, + }, + { + name: "unset gate inherits default true", + attachment: &v12.PolicyAttachment{}, + defaultGate: true, + expectedGate: true, + }, + { + name: "explicit gate false overrides default true", + attachment: &v12.PolicyAttachment{ + Gate: &falseGate, + }, + defaultGate: true, + expectedGate: false, + }, + { + name: "explicit gate true overrides default false", + attachment: &v12.PolicyAttachment{ + Gate: &trueGate, + }, + defaultGate: false, + expectedGate: true, + }, + } + + for _, tc := range cases { + s.Run(tc.name, func() { + got := policyAttachmentGate(tc.attachment, tc.defaultGate) + s.Equal(tc.expectedGate, got) + }) + } +} From 51f558d217ed2fd06cd96dc6ca71ddd65362bbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 17:12:16 -0300 Subject: [PATCH 2/8] chore(proto): scope gate cardinality break suppression inline Move the FIELD_SAME_CARDINALITY exception from buf.yaml to an inline ignore on PolicyAttachment.gate in crafting_schema.proto limiting the suppression to the specific field change. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- app/controlplane/api/workflowcontract/v1/crafting_schema.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 2b94aee56..14f2e35fd 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -240,6 +240,7 @@ message PolicyAttachment { // - true: policy violations are blocking for this policy // - false: policy violations are non-blocking for this policy // - unset: inherit organization-level default behavior + // buf:breaking:ignore FIELD_SAME_CARDINALIT optional bool gate = 7; message MaterialSelector { From 97b7099c8459d26b465e16988760fd61cf4078f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 17:27:18 -0300 Subject: [PATCH 3/8] Fix typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- app/controlplane/api/workflowcontract/v1/crafting_schema.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 14f2e35fd..126b8f3d3 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -240,7 +240,7 @@ message PolicyAttachment { // - true: policy violations are blocking for this policy // - false: policy violations are non-blocking for this policy // - unset: inherit organization-level default behavior - // buf:breaking:ignore FIELD_SAME_CARDINALIT + // buf:breaking:ignore FIELD_SAME_CARDINALITY optional bool gate = 7; message MaterialSelector { From 46d76cd3ba88a786c3c8c73c2b1056d7e1ed6879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 18:07:51 -0300 Subject: [PATCH 4/8] Temporarily add PB exceptions for cardinality changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- .github/workflows/lint.yml | 1 + buf.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e8d7c3628..984f57925 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -56,6 +56,7 @@ jobs: token: ${{ secrets.buf_api_token }} breaking: true pr_comment: false + exclude_imports: true lint-dagger-module: runs-on: ubuntu-latest diff --git a/buf.yaml b/buf.yaml index 7a9c5041b..a12692e91 100644 --- a/buf.yaml +++ b/buf.yaml @@ -49,6 +49,7 @@ modules: except: - EXTENSION_NO_DELETE - FIELD_SAME_DEFAULT + - FIELD_SAME_CARDINALITY - path: app/controlplane/internal/conf lint: use: From b478f5cad3298d4718a0f1c5b8b134ea9d3c1165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 18:08:22 -0300 Subject: [PATCH 5/8] Update crafting_schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- app/controlplane/api/workflowcontract/v1/crafting_schema.proto | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 126b8f3d3..6e2e1f25e 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -236,11 +236,10 @@ message PolicyAttachment { } }]; - // If set, controls whether this policy acts as a gate. + // Controls whether policy violations act as a gate. // - true: policy violations are blocking for this policy // - false: policy violations are non-blocking for this policy // - unset: inherit organization-level default behavior - // buf:breaking:ignore FIELD_SAME_CARDINALITY optional bool gate = 7; message MaterialSelector { From 7e2bed6081ba4f4561f4e282029b4698f22670dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 18:20:24 -0300 Subject: [PATCH 6/8] Regenerate code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- .../api/gen/frontend/workflowcontract/v1/crafting_schema.ts | 2 +- .../workflowcontract.v1.PolicyAttachment.jsonschema.json | 2 +- .../jsonschema/workflowcontract.v1.PolicyAttachment.schema.json | 2 +- app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index af825f895..6c6eb8aab 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -488,7 +488,7 @@ export interface PolicyAttachment { /** List of requirements this policy contributes to satisfy */ requirements: string[]; /** - * If set, controls whether this policy acts as a gate. + * Controls whether policy violations act as a gate. * - true: policy violations are blocking for this policy * - false: policy violations are non-blocking for this policy * - unset: inherit organization-level default behavior diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json index ba42eec8b..1ab915cdb 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.jsonschema.json @@ -13,7 +13,7 @@ "description": "meant to be used to embed the policy in the contract" }, "gate": { - "description": "If set, controls whether this policy acts as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", + "description": "Controls whether policy violations act as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", "type": "boolean" }, "ref": { diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json index 6b816b6cf..a7764cefe 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyAttachment.schema.json @@ -13,7 +13,7 @@ "description": "meant to be used to embed the policy in the contract" }, "gate": { - "description": "If set, controls whether this policy acts as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", + "description": "Controls whether policy violations act as a gate.\n - true: policy violations are blocking for this policy\n - false: policy violations are non-blocking for this policy\n - unset: inherit organization-level default behavior", "type": "boolean" }, "ref": { diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index af7601bcd..06cfb540b 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -713,7 +713,7 @@ type PolicyAttachment struct { With map[string]string `protobuf:"bytes,5,rep,name=with,proto3" json:"with,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // List of requirements this policy contributes to satisfy Requirements []string `protobuf:"bytes,6,rep,name=requirements,proto3" json:"requirements,omitempty"` - // If set, controls whether this policy acts as a gate. + // Controls whether policy violations act as a gate. // - true: policy violations are blocking for this policy // - false: policy violations are non-blocking for this policy // - unset: inherit organization-level default behavior From 2bae0764b827ec6f02f6c0f6360b5a2f9365a9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Tue, 24 Feb 2026 19:05:35 -0300 Subject: [PATCH 7/8] refactor(cli): simplify attestation policy gate enforcement fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- app/cli/cmd/attestation_push.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index d2f4bf30c..749ea9d78 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -64,7 +64,7 @@ func newAttestationPushCmd() *cobra.Command { Annotations: map[string]string{ useAPIToken: "true", }, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { info, err := executableInfo() if err != nil { return fmt.Errorf("getting executable information: %w", err) @@ -180,13 +180,10 @@ func (e *GateError) Error() string { } func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPolicyCheck bool) error { - hasGatedViolations := false - // Block if any of the policies has been configured as a gate. for _, evaluations := range status.PolicyEvaluations { for _, eval := range evaluations { if len(eval.Violations) > 0 && eval.Gate { - hasGatedViolations = true if bypassPolicyCheck { logger.Warn().Msg(exceptionBypassPolicyCheck) continue @@ -208,7 +205,7 @@ func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPol // Effective gate semantics are already resolved in policy evaluations. // For backwards compatibility, fall back to aggregate status only if // no evaluations are available. - if hasGatedViolations || (len(status.PolicyEvaluations) == 0 && status.HasPolicyViolations) { + if len(status.PolicyEvaluations) == 0 && status.HasPolicyViolations { return ErrBlockedByPolicyViolation } } From ba4372e733ee272eff929e9e54653b2db66c94f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Insaurralde?= Date: Fri, 27 Feb 2026 13:31:49 -0300 Subject: [PATCH 8/8] refactor(cli): remove unreachable policy-violation fallback handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matías Insaurralde --- app/cli/cmd/attestation_push.go | 9 +-------- app/cli/main.go | 5 +---- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index 749ea9d78..8b5495ccd 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -163,8 +163,7 @@ func newAttestationPushCmd() *cobra.Command { const exceptionFlagName = "exception-bypass-policy-check" var ( - ErrBlockedByPolicyViolation = fmt.Errorf("the operator requires all policies to pass before continuing, please fix them and try again or temporarily bypass the policy check using --%s", exceptionFlagName) - exceptionBypassPolicyCheck = fmt.Sprintf("Attention: You have opted to bypass the policy enforcement check and an operator has been notified of this exception.\nPlease make sure you are back on track with the policy evaluations and remove the --%s as soon as possible.", exceptionFlagName) + exceptionBypassPolicyCheck = fmt.Sprintf("Attention: You have opted to bypass the policy enforcement check and an operator has been notified of this exception.\nPlease make sure you are back on track with the policy evaluations and remove the --%s as soon as possible.", exceptionFlagName) ) type GateError struct { @@ -202,12 +201,6 @@ func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPol return nil } - // Effective gate semantics are already resolved in policy evaluations. - // For backwards compatibility, fall back to aggregate status only if - // no evaluations are available. - if len(status.PolicyEvaluations) == 0 && status.HasPolicyViolations { - return ErrBlockedByPolicyViolation - } } return nil diff --git a/app/cli/main.go b/app/cli/main.go index c14cf0c1a..86da7300b 100644 --- a/app/cli/main.go +++ b/app/cli/main.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. @@ -100,9 +100,6 @@ func errorInfo(err error, logger zerolog.Logger) (string, int) { logger.Debug().Msg("GracefulErrorExit enabled (exitCode 0). If you want to disable it set --graceful-exit=false") exitCode = 0 } - case errors.Is(err, cmd.ErrBlockedByPolicyViolation): - // default exit code for policy violations - exitCode = 3 case errors.As(err, &gateErr): // exit code for gate errors exitCode = 4