Skip to content

Commit

Permalink
feat: fail attestation process if policy violation (chainloop-dev#1691)
Browse files Browse the repository at this point in the history
Signed-off-by: Miguel Martinez <[email protected]>
  • Loading branch information
migmartri authored Jan 3, 2025
1 parent 88239fc commit 20cd71a
Show file tree
Hide file tree
Showing 36 changed files with 934 additions and 596 deletions.
2 changes: 1 addition & 1 deletion app/cli/cmd/attestation_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func newAttestationInitCmd() *cobra.Command {
return newGracefulError(err)
}

res, err := statusAction.Run(cmd.Context(), attestationID, action.WithSkipPolicyEvaluation())
res, err := statusAction.Run(cmd.Context(), attestationID)
if err != nil {
return newGracefulError(err)
}
Expand Down
15 changes: 8 additions & 7 deletions app/cli/cmd/attestation_push.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 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.
Expand Down Expand Up @@ -94,18 +94,17 @@ func newAttestationPushCmd() *cobra.Command {
return newGracefulError(err)
}

// In JSON format, we encode the full attestation
if flagOutputFormat == formatJSON {
return encodeJSON(res)
}
res.Status.Digest = res.Digest

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

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

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

return cmd
}

var ErrBlockedByPolicyViolation = errors.New("the operator requires that the attestation process must have all policies passing before continue, please fix them and try again")
14 changes: 13 additions & 1 deletion app/cli/cmd/attestation_status.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 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.
Expand All @@ -20,6 +20,7 @@ import (
"time"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/muesli/reflow/wrap"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -83,6 +84,9 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b
gt.AppendSeparator()
meta := status.WorkflowMeta
gt.AppendRow(table.Row{"Attestation ID", status.AttestationID})
if status.Digest != "" {
gt.AppendRow(table.Row{"Digest", status.Digest})
}
gt.AppendRow(table.Row{"Organization", meta.Organization})
gt.AppendRow(table.Row{"Name", meta.Name})
gt.AppendRow(table.Row{"Project", meta.Project})
Expand Down Expand Up @@ -111,6 +115,14 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b

evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation]
if len(evs) > 0 {
var blockingColor text.Color
var blockingText = "ADVISORY"
if status.MustBlockOnPolicyViolations {
blockingColor = text.FgHiYellow
blockingText = "ENFORCED"
}

gt.AppendRow(table.Row{"Policy violation strategy", blockingColor.Sprint(blockingText)})
gt.AppendRow(table.Row{"Policies", "------"})
policiesTable(evs, gt)
}
Expand Down
16 changes: 9 additions & 7 deletions app/cli/internal/action/attestation_init.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 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.
Expand Down Expand Up @@ -152,6 +152,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun

// Identifier of this attestation instance
var attestationID string
var blockOnPolicyViolation bool

// Init in the control plane if needed including the runner context
if !action.dryRun {
Expand All @@ -174,7 +175,7 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
workflowRun := runResp.GetResult().GetWorkflowRun()
workflowMeta.WorkflowRunId = workflowRun.GetId()
workflowMeta.Organization = runResp.GetResult().GetOrganization()

blockOnPolicyViolation = runResp.GetResult().GetBlockOnPolicyViolation()
if v := workflowMeta.Version; v != nil {
workflowMeta.Version.Prerelease = runResp.GetResult().GetWorkflowRun().Version.GetPrerelease()
}
Expand All @@ -187,11 +188,12 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
// NOTE: important to run this initialization here since workflowMeta is populated
// with the workflowRunId that comes from the control plane
initOpts := &crafter.InitOpts{
WfInfo: workflowMeta,
SchemaV1: contractVersion.GetV1(),
DryRun: action.dryRun,
AttestationID: attestationID,
Runner: discoveredRunner,
WfInfo: workflowMeta,
SchemaV1: contractVersion.GetV1(),
DryRun: action.dryRun,
AttestationID: attestationID,
Runner: discoveredRunner,
BlockOnPolicyViolation: blockOnPolicyViolation,
}

if err := action.c.Init(ctx, initOpts); err != nil {
Expand Down
53 changes: 30 additions & 23 deletions app/cli/internal/action/attestation_status.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 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.
Expand Down Expand Up @@ -45,17 +45,20 @@ type AttestationStatus struct {
}

type AttestationStatusResult struct {
AttestationID string `json:"attestationID"`
InitializedAt *time.Time `json:"initializedAt"`
WorkflowMeta *AttestationStatusWorkflowMeta `json:"workflowMeta"`
Materials []AttestationStatusResultMaterial `json:"materials"`
EnvVars map[string]string `json:"envVars"`
RunnerContext *AttestationResultRunnerContext `json:"runnerContext"`
DryRun bool `json:"dryRun"`
Annotations []*Annotation `json:"annotations"`
IsPushed bool `json:"isPushed"`
PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"`
HasPolicyViolations bool `json:"hasPolicyViolations"`
AttestationID string `json:"attestationID"`
InitializedAt *time.Time `json:"initializedAt"`
WorkflowMeta *AttestationStatusWorkflowMeta `json:"workflowMeta"`
Materials []AttestationStatusResultMaterial `json:"materials"`
EnvVars map[string]string `json:"envVars"`
RunnerContext *AttestationResultRunnerContext `json:"runnerContext"`
DryRun bool `json:"dryRun"`
Annotations []*Annotation `json:"annotations"`
IsPushed bool `json:"isPushed"`
PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"`
HasPolicyViolations bool `json:"has_policy_violations"`
MustBlockOnPolicyViolations bool `json:"must_block_on_policy_violations"`
// This might only be set if the attestation is pushed
Digest string `json:"digest"`
}

type AttestationResultRunnerContext struct {
Expand Down Expand Up @@ -126,10 +129,11 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string,
ContractRevision: workflowMeta.GetSchemaRevision(),
ContractName: workflowMeta.GetContractName(),
},
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
DryRun: c.CraftingState.DryRun,
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
IsPushed: action.isPushed,
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
DryRun: c.CraftingState.DryRun,
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
IsPushed: action.isPushed,
MustBlockOnPolicyViolations: att.GetBlockOnPolicyViolation(),
}

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

res.PolicyEvaluations, err = action.getPolicyEvaluations(ctx, c, attestationID, statement)
res.PolicyEvaluations, res.HasPolicyViolations, err = action.getPolicyEvaluations(ctx, c, attestationID, statement)
if err != nil {
return nil, fmt.Errorf("getting policy evaluations: %w", err)
}

res.HasPolicyViolations = len(res.PolicyEvaluations) > 0
}

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

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

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

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

if len(v.GetViolations()) > 0 {
hasViolations = true
}

if existing, ok := evaluations[keyName]; ok {
evaluations[keyName] = append(existing, policyEvaluationStateToActionForStatus(v))
} else {
evaluations[keyName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)}
}
}

return evaluations, nil
return evaluations, hasViolations, nil
}

// populateMaterials populates the materials in the attestation result regardless of where they are defined
Expand Down
3 changes: 3 additions & 0 deletions app/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ 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
}

return msg, exitCode
Expand Down
Loading

0 comments on commit 20cd71a

Please sign in to comment.