Skip to content

Commit 20cd71a

Browse files
authored
feat: fail attestation process if policy violation (chainloop-dev#1691)
Signed-off-by: Miguel Martinez <[email protected]>
1 parent 88239fc commit 20cd71a

36 files changed

+934
-596
lines changed

app/cli/cmd/attestation_init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func newAttestationInitCmd() *cobra.Command {
115115
return newGracefulError(err)
116116
}
117117

118-
res, err := statusAction.Run(cmd.Context(), attestationID, action.WithSkipPolicyEvaluation())
118+
res, err := statusAction.Run(cmd.Context(), attestationID)
119119
if err != nil {
120120
return newGracefulError(err)
121121
}

app/cli/cmd/attestation_push.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -94,18 +94,17 @@ func newAttestationPushCmd() *cobra.Command {
9494
return newGracefulError(err)
9595
}
9696

97-
// In JSON format, we encode the full attestation
98-
if flagOutputFormat == formatJSON {
99-
return encodeJSON(res)
100-
}
97+
res.Status.Digest = res.Digest
10198

10299
// In TABLE format, we render the attestation status
103100
if err := encodeOutput(res.Status, fullStatusTable); err != nil {
104101
return fmt.Errorf("failed to render output: %w", err)
105102
}
106103

107-
if res.Digest != "" {
108-
fmt.Printf("Attestation Digest: %s", res.Digest)
104+
// We do a final check to see if the attestation has policy violations
105+
// and fail the command if needed
106+
if res.Status.HasPolicyViolations && res.Status.MustBlockOnPolicyViolations {
107+
return ErrBlockedByPolicyViolation
109108
}
110109

111110
return nil
@@ -123,3 +122,5 @@ func newAttestationPushCmd() *cobra.Command {
123122

124123
return cmd
125124
}
125+
126+
var ErrBlockedByPolicyViolation = errors.New("the operator requires that the attestation process must have all policies passing before continue, please fix them and try again")

app/cli/cmd/attestation_status.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
"github.com/jedib0t/go-pretty/v6/table"
23+
"github.com/jedib0t/go-pretty/v6/text"
2324
"github.com/muesli/reflow/wrap"
2425
"github.com/spf13/cobra"
2526

@@ -83,6 +84,9 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b
8384
gt.AppendSeparator()
8485
meta := status.WorkflowMeta
8586
gt.AppendRow(table.Row{"Attestation ID", status.AttestationID})
87+
if status.Digest != "" {
88+
gt.AppendRow(table.Row{"Digest", status.Digest})
89+
}
8690
gt.AppendRow(table.Row{"Organization", meta.Organization})
8791
gt.AppendRow(table.Row{"Name", meta.Name})
8892
gt.AppendRow(table.Row{"Project", meta.Project})
@@ -111,6 +115,14 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b
111115

112116
evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation]
113117
if len(evs) > 0 {
118+
var blockingColor text.Color
119+
var blockingText = "ADVISORY"
120+
if status.MustBlockOnPolicyViolations {
121+
blockingColor = text.FgHiYellow
122+
blockingText = "ENFORCED"
123+
}
124+
125+
gt.AppendRow(table.Row{"Policy violation strategy", blockingColor.Sprint(blockingText)})
114126
gt.AppendRow(table.Row{"Policies", "------"})
115127
policiesTable(evs, gt)
116128
}

app/cli/internal/action/attestation_init.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -152,6 +152,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
152152

153153
// Identifier of this attestation instance
154154
var attestationID string
155+
var blockOnPolicyViolation bool
155156

156157
// Init in the control plane if needed including the runner context
157158
if !action.dryRun {
@@ -174,7 +175,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
174175
workflowRun := runResp.GetResult().GetWorkflowRun()
175176
workflowMeta.WorkflowRunId = workflowRun.GetId()
176177
workflowMeta.Organization = runResp.GetResult().GetOrganization()
177-
178+
blockOnPolicyViolation = runResp.GetResult().GetBlockOnPolicyViolation()
178179
if v := workflowMeta.Version; v != nil {
179180
workflowMeta.Version.Prerelease = runResp.GetResult().GetWorkflowRun().Version.GetPrerelease()
180181
}
@@ -187,11 +188,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
187188
// NOTE: important to run this initialization here since workflowMeta is populated
188189
// with the workflowRunId that comes from the control plane
189190
initOpts := &crafter.InitOpts{
190-
WfInfo: workflowMeta,
191-
SchemaV1: contractVersion.GetV1(),
192-
DryRun: action.dryRun,
193-
AttestationID: attestationID,
194-
Runner: discoveredRunner,
191+
WfInfo: workflowMeta,
192+
SchemaV1: contractVersion.GetV1(),
193+
DryRun: action.dryRun,
194+
AttestationID: attestationID,
195+
Runner: discoveredRunner,
196+
BlockOnPolicyViolation: blockOnPolicyViolation,
195197
}
196198

197199
if err := action.c.Init(ctx, initOpts); err != nil {

app/cli/internal/action/attestation_status.go

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -45,17 +45,20 @@ type AttestationStatus struct {
4545
}
4646

4747
type AttestationStatusResult struct {
48-
AttestationID string `json:"attestationID"`
49-
InitializedAt *time.Time `json:"initializedAt"`
50-
WorkflowMeta *AttestationStatusWorkflowMeta `json:"workflowMeta"`
51-
Materials []AttestationStatusResultMaterial `json:"materials"`
52-
EnvVars map[string]string `json:"envVars"`
53-
RunnerContext *AttestationResultRunnerContext `json:"runnerContext"`
54-
DryRun bool `json:"dryRun"`
55-
Annotations []*Annotation `json:"annotations"`
56-
IsPushed bool `json:"isPushed"`
57-
PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"`
58-
HasPolicyViolations bool `json:"hasPolicyViolations"`
48+
AttestationID string `json:"attestationID"`
49+
InitializedAt *time.Time `json:"initializedAt"`
50+
WorkflowMeta *AttestationStatusWorkflowMeta `json:"workflowMeta"`
51+
Materials []AttestationStatusResultMaterial `json:"materials"`
52+
EnvVars map[string]string `json:"envVars"`
53+
RunnerContext *AttestationResultRunnerContext `json:"runnerContext"`
54+
DryRun bool `json:"dryRun"`
55+
Annotations []*Annotation `json:"annotations"`
56+
IsPushed bool `json:"isPushed"`
57+
PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"`
58+
HasPolicyViolations bool `json:"has_policy_violations"`
59+
MustBlockOnPolicyViolations bool `json:"must_block_on_policy_violations"`
60+
// This might only be set if the attestation is pushed
61+
Digest string `json:"digest"`
5962
}
6063

6164
type AttestationResultRunnerContext struct {
@@ -126,10 +129,11 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string,
126129
ContractRevision: workflowMeta.GetSchemaRevision(),
127130
ContractName: workflowMeta.GetContractName(),
128131
},
129-
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
130-
DryRun: c.CraftingState.DryRun,
131-
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
132-
IsPushed: action.isPushed,
132+
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
133+
DryRun: c.CraftingState.DryRun,
134+
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
135+
IsPushed: action.isPushed,
136+
MustBlockOnPolicyViolations: att.GetBlockOnPolicyViolation(),
133137
}
134138

135139
if !action.skipPolicyEvaluation {
@@ -146,12 +150,10 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string,
146150
return nil, fmt.Errorf("rendering statement: %w", err)
147151
}
148152

149-
res.PolicyEvaluations, err = action.getPolicyEvaluations(ctx, c, attestationID, statement)
153+
res.PolicyEvaluations, res.HasPolicyViolations, err = action.getPolicyEvaluations(ctx, c, attestationID, statement)
150154
if err != nil {
151155
return nil, fmt.Errorf("getting policy evaluations: %w", err)
152156
}
153-
154-
res.HasPolicyViolations = len(res.PolicyEvaluations) > 0
155157
}
156158

157159
if v := workflowMeta.GetVersion(); v != nil {
@@ -200,14 +202,15 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string,
200202
return res, nil
201203
}
202204

203-
// getPolicyEvaluations retrieves both material-level and attestation-level policy evaluations
204-
func (action *AttestationStatus) getPolicyEvaluations(ctx context.Context, c *crafter.Crafter, attestationID string, statement *intoto.Statement) (map[string][]*PolicyEvaluation, error) {
205+
// getPolicyEvaluations retrieves both material-level and attestation-level policy evaluations and returns if it has violations
206+
func (action *AttestationStatus) getPolicyEvaluations(ctx context.Context, c *crafter.Crafter, attestationID string, statement *intoto.Statement) (map[string][]*PolicyEvaluation, bool, error) {
205207
// grouped by material name
206208
evaluations := make(map[string][]*PolicyEvaluation)
209+
var hasViolations bool
207210

208211
// Add attestation-level policy evaluations
209212
if err := c.EvaluateAttestationPolicies(ctx, attestationID, statement); err != nil {
210-
return nil, fmt.Errorf("evaluating attestation policies: %w", err)
213+
return nil, false, fmt.Errorf("evaluating attestation policies: %w", err)
211214
}
212215

213216
// map evaluations
@@ -217,14 +220,18 @@ func (action *AttestationStatus) getPolicyEvaluations(ctx context.Context, c *cr
217220
keyName = chainloop.AttPolicyEvaluation
218221
}
219222

223+
if len(v.GetViolations()) > 0 {
224+
hasViolations = true
225+
}
226+
220227
if existing, ok := evaluations[keyName]; ok {
221228
evaluations[keyName] = append(existing, policyEvaluationStateToActionForStatus(v))
222229
} else {
223230
evaluations[keyName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)}
224231
}
225232
}
226233

227-
return evaluations, nil
234+
return evaluations, hasViolations, nil
228235
}
229236

230237
// populateMaterials populates the materials in the attestation result regardless of where they are defined

app/cli/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ func errorInfo(err error, logger zerolog.Logger) (string, int) {
9797
logger.Debug().Msg("GracefulErrorExit enabled (exitCode 0). If you want to disable it set --graceful-exit=false")
9898
exitCode = 0
9999
}
100+
case errors.Is(err, cmd.ErrBlockedByPolicyViolation):
101+
// default exit code for policy violations
102+
exitCode = 3
100103
}
101104

102105
return msg, exitCode

0 commit comments

Comments
 (0)