diff --git a/app/cli/cmd/attestation_add.go b/app/cli/cmd/attestation_add.go index ca7334d30..981716e7b 100644 --- a/app/cli/cmd/attestation_add.go +++ b/app/cli/cmd/attestation_add.go @@ -38,6 +38,7 @@ func newAttestationAddCmd() *cobra.Command { var name, value, kind string var artifactCASConn *grpc.ClientConn var annotationsFlag []string + var bypassPolicyCheck bool // OCI registry credentials can be passed as flags or environment variables var registryServer, registryUsername, registryPassword string @@ -129,7 +130,15 @@ func newAttestationAddCmd() *cobra.Command { for _, evaluations := range policies { for _, eval := range evaluations { if len(eval.Violations) > 0 && eval.Gate { - return NewGateError(eval.Name) + if !bypassPolicyCheck { + // Auto-push incomplete attestation + logger.Info().Msgf("gated policy %q failed during material add, pushing attestation", eval.Name) + + if err := a.PushIncompleteAttestation(cmd.Context(), attestationID); err != nil { + logger.Warn().Msgf("failed to push attestation: %v", err) + } + return NewGateError(eval.Name) + } } } } @@ -155,6 +164,7 @@ func newAttestationAddCmd() *cobra.Command { cmd.Flags().StringSliceVar(&annotationsFlag, "annotation", nil, "additional annotation in the format of key=value") flagAttestationID(cmd) cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("kind of the material to be recorded: %q", schemaapi.ListAvailableMaterialKind())) + cmd.Flags().BoolVar(&bypassPolicyCheck, exceptionFlagName, false, "do not push attestation when a gated policy fails") // Optional OCI registry credentials cmd.Flags().StringVar(®istryServer, "registry-server", "", fmt.Sprintf("OCI repository server, ($%s)", registryServerEnvVarName)) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 21c14c386..98099a828 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -197,15 +197,16 @@ chainloop attestation add --value https://example.com/sbom.json Options ``` ---annotation strings additional annotation in the format of key=value ---attestation-id string Unique identifier of the in-progress attestation --h, --help help for add ---kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] ---name string name of the material as shown in the contract ---registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) ---registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER) ---registry-username string registry username, ($CHAINLOOP_REGISTRY_USERNAME) ---value string value to be recorded +--annotation strings additional annotation in the format of key=value +--attestation-id string Unique identifier of the in-progress attestation +--exception-bypass-policy-check do not push attestation when a gated policy fails +-h, --help help for add +--kind string kind of the material to be recorded: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] +--name string name of the material as shown in the contract +--registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) +--registry-server string OCI repository server, ($CHAINLOOP_REGISTRY_SERVER) +--registry-username string registry username, ($CHAINLOOP_REGISTRY_USERNAME) +--value string value to be recorded ``` Options inherited from parent commands diff --git a/app/cli/pkg/action/attestation_add.go b/app/cli/pkg/action/attestation_add.go index 22b7e76d3..f6b42fd33 100644 --- a/app/cli/pkg/action/attestation_add.go +++ b/app/cli/pkg/action/attestation_add.go @@ -177,3 +177,37 @@ func (action *AttestationAdd) GetPolicyEvaluations(ctx context.Context, attestat return policyEvaluations, nil } + +// PushIncompleteAttestation pushes an attestation to the control plane +// Note: All required materials must be present for the push to succeed and only keyless signing is supported + +func (action *AttestationAdd) PushIncompleteAttestation(ctx context.Context, attestationID string) error { + // Check if keyless signing is available + crafter, err := newCrafter(&newCrafterStateOpts{enableRemoteState: (attestationID != ""), localStatePath: action.localStatePath}, action.CPConnection, action.opts...) + if err != nil { + return fmt.Errorf("loading crafter: %w", err) + } + + if err := crafter.LoadCraftingState(ctx, attestationID); err != nil { + return fmt.Errorf("loading existing attestation: %w", err) + } + + if crafter.CraftingState.GetAttestation().GetSigningOptions().GetSigningCa() == "" { + return fmt.Errorf("keyless signing not configured") + } + + pushAction, err := NewAttestationPush(&AttestationPushOpts{ + ActionsOpts: action.ActionsOpts, + LocalStatePath: action.localStatePath, + }) + if err != nil { + return fmt.Errorf("creating push action: %w", err) + } + + _, err = pushAction.Run(ctx, attestationID, nil, false) + if err != nil { + return fmt.Errorf("pushing attestation: %w", err) + } + + return nil +}