diff --git a/app/cli/cmd/attestation_init.go b/app/cli/cmd/attestation_init.go index ee8c5c215..736b94499 100644 --- a/app/cli/cmd/attestation_init.go +++ b/app/cli/cmd/attestation_init.go @@ -115,7 +115,7 @@ func newAttestationInitCmd() *cobra.Command { return newGracefulError(err) } - res, err := statusAction.Run(cmd.Context(), attestationID) + res, err := statusAction.Run(cmd.Context(), attestationID, action.WithSkipPolicyEvaluation()) if err != nil { return newGracefulError(err) } diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 957fcefcb..eec1f60da 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/chainloop-dev/chainloop/app/cli/internal/action" + "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" ) var full bool @@ -91,7 +92,7 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b } gt.AppendRow(table.Row{"Version", projectVersion}) - gt.AppendRow(table.Row{"Contract Revision", meta.ContractRevision}) + gt.AppendRow(table.Row{"Contract", fmt.Sprintf("%s (revision %s)", meta.ContractName, meta.ContractRevision)}) if status.RunnerContext.JobURL != "" { gt.AppendRow(table.Row{"Runner Type", status.RunnerContext.RunnerType}) gt.AppendRow(table.Row{"Runner URL", status.RunnerContext.JobURL}) @@ -108,6 +109,11 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b } } + evs := status.PolicyEvaluations[chainloop.AttPolicyEvaluation] + if len(evs) > 0 { + gt.AppendRow(table.Row{"Policies", "------"}) + policiesTable(evs, gt) + } gt.Render() if err := materialsTable(status, full); err != nil { diff --git a/app/cli/internal/action/attestation_status.go b/app/cli/internal/action/attestation_status.go index f294251ce..b21de03c9 100644 --- a/app/cli/internal/action/attestation_status.go +++ b/app/cli/internal/action/attestation_status.go @@ -20,9 +20,13 @@ import ( "fmt" "time" + pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" pbc "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter" v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/renderer" + "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" + intoto "github.com/in-toto/attestation/go/v1" ) type AttestationStatusOpts struct { @@ -36,20 +40,22 @@ type AttestationStatus struct { *ActionsOpts c *crafter.Crafter // Do not show information about the project version release status - isPushed bool + isPushed bool + skipPolicyEvaluation bool } 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"` + 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"` } type AttestationResultRunnerContext struct { @@ -58,8 +64,8 @@ type AttestationResultRunnerContext struct { } type AttestationStatusWorkflowMeta struct { - WorkflowID, Name, Team, Project, ContractRevision, Organization string - ProjectVersion *ProjectVersion + WorkflowID, Name, Team, Project, ContractRevision, ContractName, Organization string + ProjectVersion *ProjectVersion } type AttestationStatusResultMaterial struct { @@ -80,7 +86,19 @@ func NewAttestationStatus(cfg *AttestationStatusOpts) (*AttestationStatus, error }, nil } -func (action *AttestationStatus) Run(ctx context.Context, attestationID string) (*AttestationStatusResult, error) { +func WithSkipPolicyEvaluation() func(*AttestationStatus) { + return func(opts *AttestationStatus) { + opts.skipPolicyEvaluation = true + } +} + +type AttestationStatusOpt func(*AttestationStatus) + +func (action *AttestationStatus) Run(ctx context.Context, attestationID string, opts ...AttestationStatusOpt) (*AttestationStatusResult, error) { + for _, opt := range opts { + opt(action) + } + c := action.c if initialized, err := c.AlreadyInitialized(ctx, attestationID); err != nil { @@ -106,6 +124,7 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string) Project: workflowMeta.GetProject(), Team: workflowMeta.GetTeam(), ContractRevision: workflowMeta.GetSchemaRevision(), + ContractName: workflowMeta.GetContractName(), }, InitializedAt: toTimePtr(att.InitializedAt.AsTime()), DryRun: c.CraftingState.DryRun, @@ -113,17 +132,27 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string) IsPushed: action.isPushed, } - // grouped by material name - evaluations := make(map[string][]*PolicyEvaluation) - for _, v := range att.GetPolicyEvaluations() { - if existing, ok := evaluations[v.MaterialName]; ok { - evaluations[v.MaterialName] = append(existing, policyEvaluationStateToActionForStatus(v)) - } else { - evaluations[v.MaterialName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)} + if !action.skipPolicyEvaluation { + // We need to render the statement to get the policy evaluations + attClient := pb.NewAttestationServiceClient(action.CPConnection) + renderer, err := renderer.NewAttestationRenderer(c.CraftingState, attClient, "", "", nil, renderer.WithLogger(action.Logger)) + if err != nil { + return nil, fmt.Errorf("rendering statement: %w", err) } - } - res.PolicyEvaluations = evaluations + // We do not want to evaluate policies here during render since we want to do it in a separate step + statement, err := renderer.RenderStatement(ctx, chainloop.WithSkipPolicyEvaluation(true)) + if err != nil { + return nil, fmt.Errorf("rendering statement: %w", err) + } + + res.PolicyEvaluations, err = action.getPolicyEvaluations(ctx, c, 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 { res.WorkflowMeta.ProjectVersion = &ProjectVersion{ @@ -157,6 +186,7 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string) for _, err := range errors { combinedErrs += (*err).Error() + "\n" } + if len(errors) > 0 && !c.CraftingState.DryRun { return nil, fmt.Errorf("error resolving env vars: %s", combinedErrs) } @@ -170,6 +200,37 @@ 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, statement *intoto.Statement) (map[string][]*PolicyEvaluation, error) { + // grouped by material name + evaluations := make(map[string][]*PolicyEvaluation) + + // Add material-level policy evaluations + for _, v := range c.CraftingState.Attestation.GetPolicyEvaluations() { + if existing, ok := evaluations[v.MaterialName]; ok { + evaluations[v.MaterialName] = append(existing, policyEvaluationStateToActionForStatus(v)) + } else { + evaluations[v.MaterialName] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)} + } + } + + // Add attestation-level policy evaluations + attestationEvaluations, err := c.EvaluateAttestationPolicies(ctx, statement) + if err != nil { + return nil, fmt.Errorf("evaluating attestation policies: %w", err) + } + + for _, v := range attestationEvaluations { + if existing, ok := evaluations[chainloop.AttPolicyEvaluation]; ok { + evaluations[chainloop.AttPolicyEvaluation] = append(existing, policyEvaluationStateToActionForStatus(v)) + } else { + evaluations[chainloop.AttPolicyEvaluation] = []*PolicyEvaluation{policyEvaluationStateToActionForStatus(v)} + } + } + + return evaluations, nil +} + // populateMaterials populates the materials in the attestation result regardless of where they are defined // (contract schema or inline in the attestation) func populateMaterials(craftingState *v1.CraftingState, res *AttestationStatusResult) error { diff --git a/pkg/attestation/crafter/crafter.go b/pkg/attestation/crafter/crafter.go index 78e834607..3a74bf5b0 100644 --- a/pkg/attestation/crafter/crafter.go +++ b/pkg/attestation/crafter/crafter.go @@ -35,6 +35,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/google/go-containerregistry/pkg/authn" + intoto "github.com/in-toto/attestation/go/v1" "github.com/rs/zerolog" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -592,6 +593,25 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M return nil } +func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, statement *intoto.Statement) ([]*api.PolicyEvaluation, error) { + // evaluate attestation-level policies + pv := policies.NewPolicyVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger) + policyResults, err := pv.VerifyStatement(ctx, statement) + if err != nil { + return nil, fmt.Errorf("evaluating policies in statement: %w", err) + } + + pgv := policies.NewPolicyGroupVerifier(c.CraftingState.InputSchema, c.attClient, c.Logger) + policyGroupResults, err := pgv.VerifyStatement(ctx, statement) + if err != nil { + return nil, fmt.Errorf("evaluating policy groups in statement: %w", err) + } + + policyResults = append(policyResults, policyGroupResults...) + + return policyResults, nil +} + func (c *Crafter) ValidateAttestation() error { if err := c.requireStateLoaded(); err != nil { return err diff --git a/pkg/attestation/renderer/chainloop/v02.go b/pkg/attestation/renderer/chainloop/v02.go index 4ef8f3919..741e5814b 100644 --- a/pkg/attestation/renderer/chainloop/v02.go +++ b/pkg/attestation/renderer/chainloop/v02.go @@ -86,8 +86,24 @@ func NewChainloopRendererV02(att *v1.Attestation, schema *schemaapi.CraftingSche } } -func (r *RendererV02) Statement(ctx context.Context) (*intoto.Statement, error) { +type RenderOptions struct { + evaluatePolicies bool +} + +type RenderOpt func(*RenderOptions) + +func WithSkipPolicyEvaluation(skip bool) RenderOpt { + return func(o *RenderOptions) { + o.evaluatePolicies = !skip + } +} + +func (r *RendererV02) Statement(ctx context.Context, opts ...RenderOpt) (*intoto.Statement, error) { var evaluations []*v1.PolicyEvaluation + options := &RenderOptions{evaluatePolicies: true} + for _, opt := range opts { + opt(options) + } subject, err := r.subject() if err != nil { @@ -106,27 +122,29 @@ func (r *RendererV02) Statement(ctx context.Context) (*intoto.Statement, error) Predicate: predicate, } - // Validate policy groups - pgv := policies.NewPolicyGroupVerifier(r.schema, r.attClient, r.logger) - policyGroupResults, err := pgv.VerifyStatement(ctx, statement) - if err != nil { - return nil, fmt.Errorf("error applying policy groups to statement: %w", err) - } - evaluations = append(evaluations, policyGroupResults...) + if options.evaluatePolicies { + // Validate policy groups + pgv := policies.NewPolicyGroupVerifier(r.schema, r.attClient, r.logger) + policyGroupResults, err := pgv.VerifyStatement(ctx, statement) + if err != nil { + return nil, fmt.Errorf("error applying policy groups to statement: %w", err) + } + evaluations = append(evaluations, policyGroupResults...) - // validate attestation-level policies - pv := policies.NewPolicyVerifier(r.schema, r.attClient, r.logger) - policyResults, err := pv.VerifyStatement(ctx, statement) - if err != nil { - return nil, fmt.Errorf("applying policies to statement: %w", err) - } - evaluations = append(evaluations, policyResults...) - // log policy violations - policies.LogPolicyEvaluations(evaluations, r.logger) + // validate attestation-level policies + pv := policies.NewPolicyVerifier(r.schema, r.attClient, r.logger) + policyResults, err := pv.VerifyStatement(ctx, statement) + if err != nil { + return nil, fmt.Errorf("applying policies to statement: %w", err) + } + evaluations = append(evaluations, policyResults...) + // log policy violations + policies.LogPolicyEvaluations(evaluations, r.logger) - // insert attestation level policy results into statement - if err = addPolicyResults(statement, evaluations); err != nil { - return nil, fmt.Errorf("adding policy results to statement: %w", err) + // insert attestation level policy results into statement + if err = addPolicyResults(statement, evaluations); err != nil { + return nil, fmt.Errorf("adding policy results to statement: %w", err) + } } return statement, nil diff --git a/pkg/attestation/renderer/renderer.go b/pkg/attestation/renderer/renderer.go index 9f0496dde..319ef0360 100644 --- a/pkg/attestation/renderer/renderer.go +++ b/pkg/attestation/renderer/renderer.go @@ -54,7 +54,7 @@ type AttestationRenderer struct { } type r interface { - Statement(ctx context.Context) (*intoto.Statement, error) + Statement(ctx context.Context, opts ...chainloop.RenderOpt) (*intoto.Statement, error) } type Opt func(*AttestationRenderer) @@ -94,6 +94,16 @@ func NewAttestationRenderer(state *crafter.VersionedCraftingState, attClient pb. return r, nil } +// Render the in-toto statement skipping validations, dsse envelope wrapping nor signing +func (ab *AttestationRenderer) RenderStatement(ctx context.Context, opts ...chainloop.RenderOpt) (*intoto.Statement, error) { + statement, err := ab.renderer.Statement(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("generating in-toto statement: %w", err) + } + + return statement, nil +} + // Attestation (dsee envelope) -> { message: { Statement(in-toto): [subject, predicate] }, signature: "sig" }. // NOTE: It currently only supports cosign key based signing. func (ab *AttestationRenderer) Render(ctx context.Context) (*dsse.Envelope, error) { diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index 38b19bb67..42c3cd07d 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -129,9 +129,9 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme } if opts.name != "" { - pv.logger.Info().Msgf("evaluating policy %s against %s", policy.Metadata.Name, opts.name) + pv.logger.Debug().Msgf("evaluating policy %s against %s", policy.Metadata.Name, opts.name) } else { - pv.logger.Info().Msgf("evaluating policy %s against attestation", policy.Metadata.Name) + pv.logger.Debug().Msgf("evaluating policy %s against attestation", policy.Metadata.Name) } args, err := ComputeArguments(policy.GetSpec().GetInputs(), attachment.GetWith(), opts.bindings, pv.logger) @@ -567,12 +567,12 @@ func LogPolicyEvaluations(evaluations []*v12.PolicyEvaluation, logger *zerolog.L } if policyEval.Skipped { - logger.Warn().Msgf("policy evaluation skipped (%s) for %s. Reasons: %s", policyEval.Name, subject, policyEval.SkipReasons) + logger.Debug().Msgf("policy evaluation skipped (%s) for %s. Reasons: %s", policyEval.Name, subject, policyEval.SkipReasons) } if len(policyEval.Violations) > 0 { - logger.Warn().Msgf("found policy violations (%s) for %s", policyEval.Name, subject) + logger.Debug().Msgf("found policy violations (%s) for %s", policyEval.Name, subject) for _, v := range policyEval.Violations { - logger.Warn().Msgf(" - %s", v.Message) + logger.Debug().Msgf(" - %s", v.Message) } } }