Skip to content

Commit b1c037d

Browse files
authored
feat: show attestation-level policy evaluations on status (#1684)
Signed-off-by: Miguel Martinez <[email protected]>
1 parent 2fb8e78 commit b1c037d

File tree

7 files changed

+166
-51
lines changed

7 files changed

+166
-51
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)
118+
res, err := statusAction.Run(cmd.Context(), attestationID, action.WithSkipPolicyEvaluation())
119119
if err != nil {
120120
return newGracefulError(err)
121121
}

app/cli/cmd/attestation_status.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/spf13/cobra"
2525

2626
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
27+
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
2728
)
2829

2930
var full bool
@@ -91,7 +92,7 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b
9192
}
9293

9394
gt.AppendRow(table.Row{"Version", projectVersion})
94-
gt.AppendRow(table.Row{"Contract Revision", meta.ContractRevision})
95+
gt.AppendRow(table.Row{"Contract", fmt.Sprintf("%s (revision %s)", meta.ContractName, meta.ContractRevision)})
9596
if status.RunnerContext.JobURL != "" {
9697
gt.AppendRow(table.Row{"Runner Type", status.RunnerContext.RunnerType})
9798
gt.AppendRow(table.Row{"Runner URL", status.RunnerContext.JobURL})
@@ -108,6 +109,11 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b
108109
}
109110
}
110111

112+
evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation]
113+
if len(evs) > 0 {
114+
gt.AppendRow(table.Row{"Policies", "------"})
115+
policiesTable(evs, gt)
116+
}
111117
gt.Render()
112118

113119
if err := materialsTable(status, full); err != nil {

app/cli/internal/action/attestation_status.go

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ import (
2020
"fmt"
2121
"time"
2222

23+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2324
pbc "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2425
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2526
v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
27+
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer"
28+
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
29+
intoto "github.com/in-toto/attestation/go/v1"
2630
)
2731

2832
type AttestationStatusOpts struct {
@@ -36,20 +40,22 @@ type AttestationStatus struct {
3640
*ActionsOpts
3741
c *crafter.Crafter
3842
// Do not show information about the project version release status
39-
isPushed bool
43+
isPushed bool
44+
skipPolicyEvaluation bool
4045
}
4146

4247
type AttestationStatusResult struct {
43-
AttestationID string `json:"attestationID"`
44-
InitializedAt *time.Time `json:"initializedAt"`
45-
WorkflowMeta *AttestationStatusWorkflowMeta `json:"workflowMeta"`
46-
Materials []AttestationStatusResultMaterial `json:"materials"`
47-
EnvVars map[string]string `json:"envVars"`
48-
RunnerContext *AttestationResultRunnerContext `json:"runnerContext"`
49-
DryRun bool `json:"dryRun"`
50-
Annotations []*Annotation `json:"annotations"`
51-
IsPushed bool `json:"isPushed"`
52-
PolicyEvaluations map[string][]*PolicyEvaluation `json:"policy_evaluations,omitempty"`
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"`
5359
}
5460

5561
type AttestationResultRunnerContext struct {
@@ -58,8 +64,8 @@ type AttestationResultRunnerContext struct {
5864
}
5965

6066
type AttestationStatusWorkflowMeta struct {
61-
WorkflowID, Name, Team, Project, ContractRevision, Organization string
62-
ProjectVersion *ProjectVersion
67+
WorkflowID, Name, Team, Project, ContractRevision, ContractName, Organization string
68+
ProjectVersion *ProjectVersion
6369
}
6470

6571
type AttestationStatusResultMaterial struct {
@@ -80,7 +86,19 @@ func NewAttestationStatus(cfg *AttestationStatusOpts) (*AttestationStatus, error
8086
}, nil
8187
}
8288

83-
func (action *AttestationStatus) Run(ctx context.Context, attestationID string) (*AttestationStatusResult, error) {
89+
func WithSkipPolicyEvaluation() func(*AttestationStatus) {
90+
return func(opts *AttestationStatus) {
91+
opts.skipPolicyEvaluation = true
92+
}
93+
}
94+
95+
type AttestationStatusOpt func(*AttestationStatus)
96+
97+
func (action *AttestationStatus) Run(ctx context.Context, attestationID string, opts ...AttestationStatusOpt) (*AttestationStatusResult, error) {
98+
for _, opt := range opts {
99+
opt(action)
100+
}
101+
84102
c := action.c
85103

86104
if initialized, err := c.AlreadyInitialized(ctx, attestationID); err != nil {
@@ -106,24 +124,35 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string)
106124
Project: workflowMeta.GetProject(),
107125
Team: workflowMeta.GetTeam(),
108126
ContractRevision: workflowMeta.GetSchemaRevision(),
127+
ContractName: workflowMeta.GetContractName(),
109128
},
110129
InitializedAt: toTimePtr(att.InitializedAt.AsTime()),
111130
DryRun: c.CraftingState.DryRun,
112131
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
113132
IsPushed: action.isPushed,
114133
}
115134

116-
// grouped by material name
117-
evaluations := make(map[string][]*PolicyEvaluation)
118-
for _, v := range att.GetPolicyEvaluations() {
119-
if existing, ok := evaluations[v.MaterialName]; ok {
120-
evaluations[v.MaterialName] = append(existing, policyEvaluationStateToActionForStatus(v))
121-
} else {
122-
evaluations[v.MaterialName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)}
135+
if !action.skipPolicyEvaluation {
136+
// We need to render the statement to get the policy evaluations
137+
attClient := pb.NewAttestationServiceClient(action.CPConnection)
138+
renderer, err := renderer.NewAttestationRenderer(c.CraftingState, attClient, "", "", nil, renderer.WithLogger(action.Logger))
139+
if err != nil {
140+
return nil, fmt.Errorf("rendering statement: %w", err)
123141
}
124-
}
125142

126-
res.PolicyEvaluations = evaluations
143+
// We do not want to evaluate policies here during render since we want to do it in a separate step
144+
statement, err := renderer.RenderStatement(ctx, chainloop.WithSkipPolicyEvaluation(true))
145+
if err != nil {
146+
return nil, fmt.Errorf("rendering statement: %w", err)
147+
}
148+
149+
res.PolicyEvaluations, err = action.getPolicyEvaluations(ctx, c, statement)
150+
if err != nil {
151+
return nil, fmt.Errorf("getting policy evaluations: %w", err)
152+
}
153+
154+
res.HasPolicyViolations = len(res.PolicyEvaluations) > 0
155+
}
127156

128157
if v := workflowMeta.GetVersion(); v != nil {
129158
res.WorkflowMeta.ProjectVersion = &ProjectVersion{
@@ -157,6 +186,7 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string)
157186
for _, err := range errors {
158187
combinedErrs += (*err).Error() + "\n"
159188
}
189+
160190
if len(errors) > 0 && !c.CraftingState.DryRun {
161191
return nil, fmt.Errorf("error resolving env vars: %s", combinedErrs)
162192
}
@@ -170,6 +200,37 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string)
170200
return res, nil
171201
}
172202

203+
// getPolicyEvaluations retrieves both material-level and attestation-level policy evaluations
204+
func (action *AttestationStatus) getPolicyEvaluations(ctx context.Context, c *crafter.Crafter, statement *intoto.Statement) (map[string][]*PolicyEvaluation, error) {
205+
// grouped by material name
206+
evaluations := make(map[string][]*PolicyEvaluation)
207+
208+
// Add material-level policy evaluations
209+
for _, v := range c.CraftingState.Attestation.GetPolicyEvaluations() {
210+
if existing, ok := evaluations[v.MaterialName]; ok {
211+
evaluations[v.MaterialName] = append(existing, policyEvaluationStateToActionForStatus(v))
212+
} else {
213+
evaluations[v.MaterialName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)}
214+
}
215+
}
216+
217+
// Add attestation-level policy evaluations
218+
attestationEvaluations, err := c.EvaluateAttestationPolicies(ctx, statement)
219+
if err != nil {
220+
return nil, fmt.Errorf("evaluating attestation policies: %w", err)
221+
}
222+
223+
for _, v := range attestationEvaluations {
224+
if existing, ok := evaluations[chainloop.AttPolicyEvaluation]; ok {
225+
evaluations[chainloop.AttPolicyEvaluation] = append(existing, policyEvaluationStateToActionForStatus(v))
226+
} else {
227+
evaluations[chainloop.AttPolicyEvaluation] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)}
228+
}
229+
}
230+
231+
return evaluations, nil
232+
}
233+
173234
// populateMaterials populates the materials in the attestation result regardless of where they are defined
174235
// (contract schema or inline in the attestation)
175236
func populateMaterials(craftingState *v1.CraftingState, res *AttestationStatusResult) error {

pkg/attestation/crafter/crafter.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/go-git/go-git/v5"
3636
"github.com/go-git/go-git/v5/plumbing"
3737
"github.com/google/go-containerregistry/pkg/authn"
38+
intoto "github.com/in-toto/attestation/go/v1"
3839
"github.com/rs/zerolog"
3940
"google.golang.org/protobuf/types/known/timestamppb"
4041
)
@@ -592,6 +593,25 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M
592593
return nil
593594
}
594595

596+
func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, statement *intoto.Statement) ([]*api.PolicyEvaluation, error) {
597+
// evaluate attestation-level policies
598+
pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger)
599+
policyResults, err := pv.VerifyStatement(ctx, statement)
600+
if err != nil {
601+
return nil, fmt.Errorf("evaluating policies in statement: %w", err)
602+
}
603+
604+
pgv := policies.NewPolicyGroupVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger)
605+
policyGroupResults, err := pgv.VerifyStatement(ctx, statement)
606+
if err != nil {
607+
return nil, fmt.Errorf("evaluating policy groups in statement: %w", err)
608+
}
609+
610+
policyResults = append(policyResults, policyGroupResults...)
611+
612+
return policyResults, nil
613+
}
614+
595615
func (c *Crafter) ValidateAttestation() error {
596616
if err := c.requireStateLoaded(); err != nil {
597617
return err

pkg/attestation/renderer/chainloop/v02.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,24 @@ func NewChainloopRendererV02(att *v1.Attestation, schema *schemaapi.CraftingSche
8686
}
8787
}
8888

89-
func (r *RendererV02) Statement(ctx context.Context) (*intoto.Statement, error) {
89+
type RenderOptions struct {
90+
evaluatePolicies bool
91+
}
92+
93+
type RenderOpt func(*RenderOptions)
94+
95+
func WithSkipPolicyEvaluation(skip bool) RenderOpt {
96+
return func(o *RenderOptions) {
97+
o.evaluatePolicies = !skip
98+
}
99+
}
100+
101+
func (r *RendererV02) Statement(ctx context.Context, opts ...RenderOpt) (*intoto.Statement, error) {
90102
var evaluations []*v1.PolicyEvaluation
103+
options := &RenderOptions{evaluatePolicies: true}
104+
for _, opt := range opts {
105+
opt(options)
106+
}
91107

92108
subject, err := r.subject()
93109
if err != nil {
@@ -106,27 +122,29 @@ func (r *RendererV02) Statement(ctx context.Context) (*intoto.Statement, error)
106122
Predicate: predicate,
107123
}
108124

109-
// Validate policy groups
110-
pgv := policies.NewPolicyGroupVerifier(r.schema, r.attClient, r.logger)
111-
policyGroupResults, err := pgv.VerifyStatement(ctx, statement)
112-
if err != nil {
113-
return nil, fmt.Errorf("error applying policy groups to statement: %w", err)
114-
}
115-
evaluations = append(evaluations, policyGroupResults...)
125+
if options.evaluatePolicies {
126+
// Validate policy groups
127+
pgv := policies.NewPolicyGroupVerifier(r.schema, r.attClient, r.logger)
128+
policyGroupResults, err := pgv.VerifyStatement(ctx, statement)
129+
if err != nil {
130+
return nil, fmt.Errorf("error applying policy groups to statement: %w", err)
131+
}
132+
evaluations = append(evaluations, policyGroupResults...)
116133

117-
// validate attestation-level policies
118-
pv := policies.NewPolicyVerifier(r.schema, r.attClient, r.logger)
119-
policyResults, err := pv.VerifyStatement(ctx, statement)
120-
if err != nil {
121-
return nil, fmt.Errorf("applying policies to statement: %w", err)
122-
}
123-
evaluations = append(evaluations, policyResults...)
124-
// log policy violations
125-
policies.LogPolicyEvaluations(evaluations, r.logger)
134+
// validate attestation-level policies
135+
pv := policies.NewPolicyVerifier(r.schema, r.attClient, r.logger)
136+
policyResults, err := pv.VerifyStatement(ctx, statement)
137+
if err != nil {
138+
return nil, fmt.Errorf("applying policies to statement: %w", err)
139+
}
140+
evaluations = append(evaluations, policyResults...)
141+
// log policy violations
142+
policies.LogPolicyEvaluations(evaluations, r.logger)
126143

127-
// insert attestation level policy results into statement
128-
if err = addPolicyResults(statement, evaluations); err != nil {
129-
return nil, fmt.Errorf("adding policy results to statement: %w", err)
144+
// insert attestation level policy results into statement
145+
if err = addPolicyResults(statement, evaluations); err != nil {
146+
return nil, fmt.Errorf("adding policy results to statement: %w", err)
147+
}
130148
}
131149

132150
return statement, nil

pkg/attestation/renderer/renderer.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type AttestationRenderer struct {
5454
}
5555

5656
type r interface {
57-
Statement(ctx context.Context) (*intoto.Statement, error)
57+
Statement(ctx context.Context, opts ...chainloop.RenderOpt) (*intoto.Statement, error)
5858
}
5959

6060
type Opt func(*AttestationRenderer)
@@ -94,6 +94,16 @@ func NewAttestationRenderer(state *crafter.VersionedCraftingState, attClient pb.
9494
return r, nil
9595
}
9696

97+
// Render the in-toto statement skipping validations, dsse envelope wrapping nor signing
98+
func (ab *AttestationRenderer) RenderStatement(ctx context.Context, opts ...chainloop.RenderOpt) (*intoto.Statement, error) {
99+
statement, err := ab.renderer.Statement(ctx, opts...)
100+
if err != nil {
101+
return nil, fmt.Errorf("generating in-toto statement: %w", err)
102+
}
103+
104+
return statement, nil
105+
}
106+
97107
// Attestation (dsee envelope) -> { message: { Statement(in-toto): [subject, predicate] }, signature: "sig" }.
98108
// NOTE: It currently only supports cosign key based signing.
99109
func (ab *AttestationRenderer) Render(ctx context.Context) (*dsse.Envelope, error) {

pkg/policies/policies.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme
129129
}
130130

131131
if opts.name != "" {
132-
pv.logger.Info().Msgf("evaluating policy %s against %s", policy.Metadata.Name, opts.name)
132+
pv.logger.Debug().Msgf("evaluating policy %s against %s", policy.Metadata.Name, opts.name)
133133
} else {
134-
pv.logger.Info().Msgf("evaluating policy %s against attestation", policy.Metadata.Name)
134+
pv.logger.Debug().Msgf("evaluating policy %s against attestation", policy.Metadata.Name)
135135
}
136136

137137
args, err := ComputeArguments(policy.GetSpec().GetInputs(), attachment.GetWith(), opts.bindings, pv.logger)
@@ -567,12 +567,12 @@ func LogPolicyEvaluations(evaluations []*v12.PolicyEvaluation, logger *zerolog.L
567567
}
568568

569569
if policyEval.Skipped {
570-
logger.Warn().Msgf("policy evaluation skipped (%s) for %s. Reasons: %s", policyEval.Name, subject, policyEval.SkipReasons)
570+
logger.Debug().Msgf("policy evaluation skipped (%s) for %s. Reasons: %s", policyEval.Name, subject, policyEval.SkipReasons)
571571
}
572572
if len(policyEval.Violations) > 0 {
573-
logger.Warn().Msgf("found policy violations (%s) for %s", policyEval.Name, subject)
573+
logger.Debug().Msgf("found policy violations (%s) for %s", policyEval.Name, subject)
574574
for _, v := range policyEval.Violations {
575-
logger.Warn().Msgf(" - %s", v.Message)
575+
logger.Debug().Msgf(" - %s", v.Message)
576576
}
577577
}
578578
}

0 commit comments

Comments
 (0)