|
| 1 | +// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package cmd |
| 5 | + |
| 6 | +import ( |
| 7 | + "errors" |
| 8 | + "fmt" |
| 9 | + "os" |
| 10 | + "slices" |
| 11 | + |
| 12 | + "github.com/spf13/cobra" |
| 13 | + |
| 14 | + "github.com/slsa-framework/source-tool/pkg/sourcetool" |
| 15 | +) |
| 16 | + |
| 17 | +type attestOptions struct { |
| 18 | + revisionOpts |
| 19 | + pushOptions |
| 20 | + fromOptions |
| 21 | + allowMergeCommitsOptions |
| 22 | + provenance bool |
| 23 | + vsa bool |
| 24 | + sign bool |
| 25 | + output string |
| 26 | + useLocalPolicy string |
| 27 | + silentDowngrade bool |
| 28 | +} |
| 29 | + |
| 30 | +func (ao *attestOptions) Validate() error { |
| 31 | + errs := []error{ |
| 32 | + ao.revisionOpts.Validate(), |
| 33 | + ao.pushOptions.Validate(), |
| 34 | + ao.fromOptions.Validate(), |
| 35 | + } |
| 36 | + if !ao.provenance && !ao.vsa { |
| 37 | + errs = append(errs, errors.New("nothing to generate: enable --provenance and/or --vsa")) |
| 38 | + } |
| 39 | + return errors.Join(errs...) |
| 40 | +} |
| 41 | + |
| 42 | +func (ao *attestOptions) AddFlags(cmd *cobra.Command) { |
| 43 | + ao.revisionOpts.AddFlags(cmd) |
| 44 | + ao.pushOptions.AddFlags(cmd) |
| 45 | + ao.fromOptions.AddFlags(cmd) |
| 46 | + ao.allowMergeCommitsOptions.AddFlags(cmd) |
| 47 | + cmd.PersistentFlags().BoolVar(&ao.provenance, "provenance", true, "write the provenance attestation") |
| 48 | + cmd.PersistentFlags().BoolVar(&ao.vsa, "vsa", true, "write the verification summary attestation (VSA)") |
| 49 | + cmd.PersistentFlags().BoolVar(&ao.sign, "sign", true, "sign the attestations") |
| 50 | + cmd.PersistentFlags().StringVar(&ao.output, "output", "", "path to write the attestation bundle (default: stdout)") |
| 51 | + cmd.PersistentFlags().StringVar(&ao.useLocalPolicy, "use-local-policy", "", "path to a local policy file to evaluate instead of the community policy") |
| 52 | + cmd.PersistentFlags().BoolVar(&ao.silentDowngrade, "silent-downgrade", false, "warn instead of failing when the achieved level is below the policy target") |
| 53 | +} |
| 54 | + |
| 55 | +func addAttest(parentCmd *cobra.Command) { |
| 56 | + opts := attestOptions{} |
| 57 | + attestCmd := &cobra.Command{ |
| 58 | + Use: "attest [flags] owner/repo[@ref]", |
| 59 | + GroupID: cmdGroupAttestation, |
| 60 | + Short: "Generate the source attestations for a revision", |
| 61 | + Long: `Generate the SLSA source attestations for a revision. |
| 62 | +
|
| 63 | +attest creates the source provenance and the verification summary |
| 64 | +attestation (VSA) for a commit or a tag. Use --provenance and --vsa to |
| 65 | +select which of the two are written. When the VSA is disabled the |
| 66 | +repository policy is not evaluated and only the provenance is produced. |
| 67 | +
|
| 68 | +The attestations are written to stdout as a JSONL bundle unless --output |
| 69 | +is given, and can be pushed to storage with --push.`, |
| 70 | + SilenceUsage: true, |
| 71 | + SilenceErrors: true, |
| 72 | + PreRunE: func(cmd *cobra.Command, args []string) error { |
| 73 | + if len(args) > 0 { |
| 74 | + if err := opts.ParseLocator(args[0]); err != nil { |
| 75 | + return err |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + if err := opts.repoOptions.Validate(); err != nil { |
| 80 | + return err |
| 81 | + } |
| 82 | + |
| 83 | + return opts.EnsureDefaults() |
| 84 | + }, |
| 85 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 86 | + if err := opts.Validate(); err != nil { |
| 87 | + return fmt.Errorf("validating options: %w", err) |
| 88 | + } |
| 89 | + |
| 90 | + var githubStorer, notesStorer, pushAttestations bool |
| 91 | + if slices.Contains(opts.pushLocation, pushRepoGithub) { |
| 92 | + pushAttestations = true |
| 93 | + githubStorer = true |
| 94 | + } |
| 95 | + if slices.Contains(opts.pushLocation, pushRepoNote) { |
| 96 | + pushAttestations = true |
| 97 | + notesStorer = true |
| 98 | + } |
| 99 | + |
| 100 | + authenticator, err := CheckAuth() |
| 101 | + if err != nil { |
| 102 | + return err |
| 103 | + } |
| 104 | + |
| 105 | + srctool, err := sourcetool.New( |
| 106 | + sourcetool.WithAuthenticator(authenticator), |
| 107 | + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), |
| 108 | + sourcetool.WithNotesStorer(notesStorer), |
| 109 | + sourcetool.WithGithubStorer(githubStorer), |
| 110 | + sourcetool.WithGithubCollector(opts.readGithub()), |
| 111 | + sourcetool.WithNotesCollector(opts.readNotes()), |
| 112 | + ) |
| 113 | + if err != nil { |
| 114 | + return fmt.Errorf("creating sourcetool: %w", err) |
| 115 | + } |
| 116 | + |
| 117 | + result, err := srctool.AttestRevision( |
| 118 | + cmd.Context(), opts.GetBranch(), opts.GetRevision(), |
| 119 | + sourcetool.WithProvenance(opts.provenance), |
| 120 | + sourcetool.WithVSA(opts.vsa), |
| 121 | + sourcetool.WithSign(opts.sign), |
| 122 | + sourcetool.WithLocalPolicy(opts.useLocalPolicy), |
| 123 | + sourcetool.WithOutputPath(opts.output), |
| 124 | + sourcetool.WithUseStdout(opts.output == ""), |
| 125 | + sourcetool.WithPush(pushAttestations), |
| 126 | + ) |
| 127 | + if err != nil { |
| 128 | + return fmt.Errorf("attesting revision: %w", err) |
| 129 | + } |
| 130 | + |
| 131 | + // The attestations are generated (and optionally pushed) regardless |
| 132 | + // of the policy outcome. When the achieved level is below the policy |
| 133 | + // target return exit code 2, or just a warning with --silent-downgrade. |
| 134 | + if result.Shortfall != nil { |
| 135 | + msg := fmt.Sprintf( |
| 136 | + "policy target level %s not met; achieved %s: %s", |
| 137 | + result.Shortfall.TargetLevel, result.Shortfall.AchievedLevel, result.Shortfall.Reason, |
| 138 | + ) |
| 139 | + if opts.silentDowngrade { |
| 140 | + fmt.Fprintf(os.Stderr, "warning: %s\n", msg) |
| 141 | + return nil |
| 142 | + } |
| 143 | + return &exitError{code: 2, err: errors.New(msg)} |
| 144 | + } |
| 145 | + |
| 146 | + return nil |
| 147 | + }, |
| 148 | + } |
| 149 | + opts.AddFlags(attestCmd) |
| 150 | + parentCmd.AddCommand(attestCmd) |
| 151 | +} |
0 commit comments