Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 6 additions & 12 deletions app/cli/cmd/attestation_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -194,17 +194,11 @@ func validatePolicyEnforcement(status *action.AttestationStatusResult, bypassPol
}
}

// Do a final check in case the operator has configured the attestation
// to be blocked on any policy violation.
if status.MustBlockOnPolicyViolations {
if bypassPolicyCheck {
logger.Warn().Msg(exceptionBypassPolicyCheck)
return nil
}

if status.HasPolicyViolations {
return ErrBlockedByPolicyViolation
}
// Block on any policy violation only when configured (bypass handled above).
// When we have policy evaluations, gate semantics are already enforced in the loop above.
if status.MustBlockOnPolicyViolations && bypassPolicyCheck {
logger.Warn().Msg(exceptionBypassPolicyCheck)
return nil
}

return nil
Expand Down
21 changes: 21 additions & 0 deletions app/cli/cmd/attestation_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 12 additions & 8 deletions app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
// 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
optional bool gate = 7;

message MaterialSelector {
// material name
Expand Down
1 change: 1 addition & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ modules:
except:
- EXTENSION_NO_DELETE
- FIELD_SAME_DEFAULT
- FIELD_SAME_CARDINALITY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure we want to disable this globally but instead you coudl add an annotation in your specific case?

Copy link
Contributor Author

@matiasinsaurralde matiasinsaurralde Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that breaking change detection doesn't support inline comment ignores.

Also exceptions are only per file/directory level.

Given that we control the gRPC clients/servers that use these messages and that the bool to optional bool doesn't imply a wire format change in PB, it should be safe to regenerate the bindings and update any other parts of the code that use this message.

Also buf breaking always compares to latest main. If this PR lands (with or without the exceptions, which we can also leave out), a subsequent PR won't raise a breaking change error - cc @jiparis

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, unfortunately buf breaking doesn't support inline ignores as buf lint does. To be honest, I'm wondering how this is the first time we are facing this issue. Did we update buf recently?
The only solution is adding this top level except option. The change to optional is safe, as it only affects to generated code. Wired bytes are exactly the same (although with different semantics from now on).

- path: app/controlplane/internal/conf
lint:
use:
Expand Down
19 changes: 17 additions & 2 deletions pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion pkg/policies/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type PolicyVerifier struct {
client v13.AttestationServiceClient
grpcConn *grpc.ClientConn
allowedHostnames []string
defaultGate bool
includeRawData bool
enablePrint bool
evalPhase EvalPhase
Expand All @@ -87,6 +88,7 @@ var _ Verifier = (*PolicyVerifier)(nil)

type PolicyVerifierOptions struct {
AllowedHostnames []string
DefaultGate bool
IncludeRawData bool
EnablePrint bool
GRPCConn *grpc.ClientConn
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so before the default gate was outside the context of policy verifier and now you are bringing both in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, PolicyEvaluation.Gate was populated with the raw attachment value (attachment.GetGate()), while the org-level default (block_on_policy_violation) was applied later in CLI enforcement.

The efective gate is now resolved inside PolicyVerifier (policyAttachmentGate(attachment, pv.defaultGate)), following the 3 state behavior (to achieve default on / opt-out per policy):

  • Explicit gate: true: blocking.
  • Explicit gate: false: non-blocking (opt-out)
  • Unset gate: inherits org default.

}, 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)
Expand Down
54 changes: 54 additions & 0 deletions pkg/policies/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading