Skip to content

Commit

Permalink
feat: show attestation-level policy evaluations on status (#1684)
Browse files Browse the repository at this point in the history
Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
  • Loading branch information
migmartri authored Dec 20, 2024
1 parent 2fb8e78 commit b1c037d
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 51 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)
res, err := statusAction.Run(cmd.Context(), attestationID, action.WithSkipPolicyEvaluation())
if err != nil {
return newGracefulError(err)
}
Expand Down
8 changes: 7 additions & 1 deletion app/cli/cmd/attestation_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand All @@ -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 {
Expand Down
107 changes: 84 additions & 23 deletions app/cli/internal/action/attestation_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -106,24 +124,35 @@ 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,
Annotations: pbAnnotationsToAction(c.CraftingState.InputSchema.GetAnnotations()),
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{
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
58 changes: 38 additions & 20 deletions pkg/attestation/renderer/chainloop/v02.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion pkg/attestation/renderer/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 5 additions & 5 deletions pkg/policies/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Expand Down

0 comments on commit b1c037d

Please sign in to comment.