-
Couldn't load subscription status.
- Fork 37
feat: show attestation-level policy evaluations on status #1684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
migmartri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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,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{ | ||
|
|
@@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you might be talking about There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will prevent users from seeing these violations when doing an |
||
| } | ||
| 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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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