diff --git a/app/cli/cmd/attestation_add.go b/app/cli/cmd/attestation_add.go index 672a9ceba..fb8ed3859 100644 --- a/app/cli/cmd/attestation_add.go +++ b/app/cli/cmd/attestation_add.go @@ -19,6 +19,8 @@ import ( "fmt" "os" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/muesli/reflow/wrap" "github.com/spf13/cobra" "github.com/spf13/viper" "google.golang.org/grpc" @@ -27,6 +29,8 @@ import ( schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" ) +const NotSet = "[NOT SET]" + func newAttestationAddCmd() *cobra.Command { var name, value, kind string var artifactCASConn *grpc.ClientConn @@ -90,14 +94,21 @@ func newAttestationAddCmd() *cobra.Command { return runWithBackoffRetry( func() error { // TODO: take the material output and show render it - _, err := a.Run(cmd.Context(), attestationID, name, value, kind, annotations) + resp, err := a.Run(cmd.Context(), attestationID, name, value, kind, annotations) if err != nil { return err } logger.Info().Msg("material added to attestation") - return nil + policies, err := a.GetPolicyEvaluations(cmd.Context(), attestationID) + if err != nil { + return err + } + + return encodeOutput(resp, func(s *action.AttestationStatusMaterial) error { + return displayMaterialInfo(s, policies[resp.Name]) + }) }, ) }, @@ -138,3 +149,53 @@ func newAttestationAddCmd() *cobra.Command { return cmd } + +// displayMaterialInfo prints the material information in a table format. +func displayMaterialInfo(status *action.AttestationStatusMaterial, policyEvaluations []*action.PolicyEvaluation) error { + if status == nil { + return nil + } + + mt := newTableWriter() + + mt.AppendRow(table.Row{"Name", status.Material.Name}) + mt.AppendRow(table.Row{"Type", status.Material.Type}) + mt.AppendRow(table.Row{"Required", hBool(status.Required)}) + + if status.IsOutput { + mt.AppendRow(table.Row{"Is output", "Yes"}) + } + + if status.Value != "" { + v := status.Value + if status.Tag != "" { + v = fmt.Sprintf("%s:%s", v, status.Tag) + } + mt.AppendRow(table.Row{"Value", wrap.String(v, 100)}) + } + + if status.Hash != "" { + mt.AppendRow(table.Row{"Digest", status.Hash}) + } + + if len(status.Material.Annotations) > 0 { + mt.AppendRow(table.Row{"Annotations", "------"}) + for _, a := range status.Material.Annotations { + value := a.Value + if value == "" { + value = NotSet + } + mt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, value)}) + } + } + + if len(policyEvaluations) > 0 { + mt.AppendRow(table.Row{"Policy evaluations", "------"}) + } + + policiesTable(policyEvaluations, mt) + mt.SetStyle(table.StyleLight) + mt.Style().Options.SeparateRows = true + mt.Render() + return nil +} diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 520043cc2..3c033977e 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -109,7 +109,7 @@ func attestationStatusTableOutput(status *action.AttestationStatusResult, full b for _, a := range status.Annotations { value := a.Value if value == "" { - value = "[NOT SET]" + value = NotSet } gt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, value)}) } @@ -211,7 +211,7 @@ func materialsTable(status *action.AttestationStatusResult, full bool) error { for _, a := range m.Annotations { value := a.Value if value == "" { - value = "[NOT SET]" + value = NotSet } mt.AppendRow(table.Row{"", fmt.Sprintf("%s: %s", a.Name, value)}) diff --git a/app/cli/cmd/output.go b/app/cli/cmd/output.go index 9b7abef82..4db02f53c 100644 --- a/app/cli/cmd/output.go +++ b/app/cli/cmd/output.go @@ -50,7 +50,8 @@ type tabulatedData interface { []*action.CASBackendItem | []*action.OrgInvitationItem | *action.APITokenItem | - []*action.APITokenItem + []*action.APITokenItem | + *action.AttestationStatusMaterial } var ErrOutputFormatNotImplemented = errors.New("format not implemented") diff --git a/app/cli/internal/action/attestation_add.go b/app/cli/internal/action/attestation_add.go index a7e1c44f7..39751b5ac 100644 --- a/app/cli/internal/action/attestation_add.go +++ b/app/cli/internal/action/attestation_add.go @@ -180,3 +180,30 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa return materialResult, nil } + +// GetPolicyEvaluations is a Wrapper around the getPolicyEvaluations +func (action *AttestationAdd) GetPolicyEvaluations(ctx context.Context, attestationID string) (map[string][]*PolicyEvaluation, error) { + crafter, err := newCrafter(&newCrafterStateOpts{enableRemoteState: (attestationID != ""), localStatePath: action.localStatePath}, action.CPConnection, action.newCrafterOpts.opts...) + if err != nil { + return nil, fmt.Errorf("failed to load crafter: %w", err) + } + + if initialized, err := crafter.AlreadyInitialized(ctx, attestationID); err != nil { + return nil, fmt.Errorf("checking if attestation is already initialized: %w", err) + } else if !initialized { + return nil, ErrAttestationNotInitialized + } + + if err := crafter.LoadCraftingState(ctx, attestationID); err != nil { + action.Logger.Err(err).Msg("loading existing attestation") + return nil, err + } + + policyEvaluations, _, err := getPolicyEvaluations(crafter) + + if err != nil { + return nil, fmt.Errorf("getting policy evaluations: %w", err) + } + + return policyEvaluations, nil +} diff --git a/app/cli/internal/action/attestation_status.go b/app/cli/internal/action/attestation_status.go index de44ad978..8fbe5ec58 100644 --- a/app/cli/internal/action/attestation_status.go +++ b/app/cli/internal/action/attestation_status.go @@ -26,7 +26,6 @@ import ( 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 { @@ -150,7 +149,12 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string, return nil, fmt.Errorf("rendering statement: %w", err) } - res.PolicyEvaluations, res.HasPolicyViolations, err = action.getPolicyEvaluations(ctx, c, attestationID, statement) + // Add attestation-level policy evaluations + if err := c.EvaluateAttestationPolicies(ctx, attestationID, statement); err != nil { + return nil, fmt.Errorf("evaluating attestation policies: %w", err) + } + + res.PolicyEvaluations, res.HasPolicyViolations, err = getPolicyEvaluations(c) if err != nil { return nil, fmt.Errorf("getting policy evaluations: %w", err) } @@ -203,16 +207,11 @@ func (action *AttestationStatus) Run(ctx context.Context, attestationID string, } // getPolicyEvaluations retrieves both material-level and attestation-level policy evaluations and returns if it has violations -func (action *AttestationStatus) getPolicyEvaluations(ctx context.Context, c *crafter.Crafter, attestationID string, statement *intoto.Statement) (map[string][]*PolicyEvaluation, bool, error) { +func getPolicyEvaluations(c *crafter.Crafter) (map[string][]*PolicyEvaluation, bool, error) { // grouped by material name evaluations := make(map[string][]*PolicyEvaluation) var hasViolations bool - // Add attestation-level policy evaluations - if err := c.EvaluateAttestationPolicies(ctx, attestationID, statement); err != nil { - return nil, false, fmt.Errorf("evaluating attestation policies: %w", err) - } - // map evaluations for _, v := range c.CraftingState.Attestation.GetPolicyEvaluations() { keyName := v.MaterialName