Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flag is to simplify checks out json output

cat /tmp/foo.json | jq .status.hasPolicyViolations
true

}

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If statement comes from renderer.Statement(), then this call is not needed, because policies are already evaluated in that method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you might be talking about Render which is the final output as DSSE envelope. renderer.statement is a new method that doesn't have the policies yes, afaik

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you were right, see the new change, thanks

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will prevent users from seeing these violations when doing an att add

}
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
Loading