Skip to content

Commit cfe06d6

Browse files
authored
CLI ui simplification (#420)
* Deprecate createpolicy in favor of 'policy create' Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * correct binary name in policy recommendation Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Rename verifycommit to verify and keep a deprecated alias Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Recognize tags when verifying a revision Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Assign parsed branch to the branch field in ParseLocator Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Let AttestRevision select provenance and VSA output Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Add attest subcommand Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Deprecate checklevelprov, checktag and prov in favor of attest Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Add attest subcommand Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Deprecate checklevel and add --level to status Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Move status to verification group, drop assessment Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Add get subcommand and new FetchRevisionAttestations to atteester Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Add --from to control where we get attestations Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Update the GETTING_STARTED doc Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Settle on kebab-case (remaining 3 vestigial falgs) Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> * Update design doc Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev> --------- Signed-off-by: Adolfo Garcia Veytia (puerco) <puerco@carabiner.dev>
1 parent 11bf93e commit cfe06d6

18 files changed

Lines changed: 695 additions & 103 deletions

GETTING_STARTED.md

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ commits using the embedded attestations stored in git notes in your git reposito
6666
To verify the latest commit, run:
6767

6868
```bash
69-
sourcetool verifycommit yourorg/yourrepo
69+
sourcetool verify yourorg/yourrepo
7070
```
7171

7272
## Onboarding Guide
@@ -151,6 +151,15 @@ was found and the current SLSA level of the repository:
151151

152152
![](docs/media/image04-status.png)
153153

154+
When the controls are eligible for a higher level than the policy verifies,
155+
status explains the gap (for example, when the policy targets a lower level or
156+
is not fully met). To print only the policy-verified SLSA source level, for
157+
example when scripting, pass `--level`:
158+
159+
```bash
160+
sourcetool status yourorg/yourrepo --level
161+
```
162+
154163
## One-shot Repository Set Up
155164

156165
The quickest way to set up a repository is to use sourcetool’s one-shot set up
@@ -238,22 +247,61 @@ sourcetool policy view yourorg/yourrepo
238247

239248
![](docs/media/image09-policy.png )
240249

241-
## Verifying Commits
250+
## Verifying Commits and Tags
242251

243252
Once the SLSA controls are in place, each commit pushed into the repository will
244253
generate source provenance metadata and store it in git notes by default. These
245254
attestations and VSAs can be used to verify the SLSA level of the repository.
246255

247-
To verify a commit, use the `verifycommit` subcommand. Pass it a commit locator
256+
To verify a revision, use the `verify` subcommand. Pass it a commit locator
248257
like this:
249258

250259
```bash
251-
sourcetool verifycommit slsa-framework/slsa-source-poc@fc0f59a9332e7873bb146b95cc4b39232eada7d2
260+
sourcetool verify slsa-framework/source-tool@fc0f59a9332e7873bb146b95cc4b39232eada7d2
252261
```
253262

254263
![](docs/media/image10-pcy-json.png)
255264

256265
If you omit the commit SHA, sourcetool will verify the last commit in the branch.
266+
You can also verify a tag with `--tag`:
267+
268+
```bash
269+
sourcetool verify yourorg/yourrepo --tag v1.0.0
270+
```
271+
272+
`verify` reports the policy-verified SLSA source level and exits with a non-zero
273+
status when the revision cannot be verified, which makes it convenient to gate
274+
scripts and CI.
275+
276+
### Reading From Different Attestation Sources
277+
278+
By default the reading subcommands (`verify`, `get`, `attest` and `audit`) read
279+
attestations from the git notes stored in your repository. If your attestations
280+
are also published to the GitHub attestations API (with `--push=github`), point
281+
the reader at that source with `--from`:
282+
283+
```bash
284+
sourcetool verify yourorg/yourrepo --from=github
285+
sourcetool verify yourorg/yourrepo --from=github,note
286+
```
287+
288+
Git notes is the default because reading from the GitHub attestations API
289+
requires the token to have attestations read access, which is not always granted
290+
in CI.
291+
292+
## Retrieving Attestations
293+
294+
To fetch and print the raw attestations for a revision, use the `get` subcommand:
295+
296+
```bash
297+
sourcetool get yourorg/yourrepo
298+
```
299+
300+
By default `get` prints both the source provenance and the VSA; use
301+
`--provenance` or `--vsa` to select just one. It verifies every attestation it
302+
prints and writes a warning to stderr when verification fails. Pass
303+
`--require-verified` to make it exit with a non-zero status on a verification
304+
failure.
257305

258306
## Troubleshooting
259307

docs/DESIGN.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ using GitHub's existing functionality.
77

88
* Users create a [policy](#policy) for the repo & branches they want to protect,
99
indicating their desired SLSA level.
10-
* Users call the .github/workflows/slsa_with_provenance.yml reusable workflow on any
10+
* Users call the `compute_slsa_source.yml` reusable workflow from
11+
[source-actions](https://github.com/slsa-framework/source-actions) on any
1112
`push` changes to protected branches.
12-
* The slsa_with_provenance workflow gets the attestations, if any, for the prior
13-
commit.
14-
* The slsa_with_provenance workflow evaluates their controls, the current commit, and
15-
prior attestations, to determine the SLSA Source level of the current commit.
16-
* A VSA, 'source provenance', are created within the workflow, and
13+
* The workflow gets the attestations, if any, for the prior commit.
14+
* The workflow evaluates their controls, the current commit, and prior
15+
attestations, to determine the SLSA Source level of the current commit.
16+
* A VSA and 'source provenance' are created within the workflow, and
1717
are stored in [git notes](https://git-scm.com/docs/git-notes) for the current commit.
1818
* Downstream users can get the VSA for the revision they're consuming by getting the
1919
git notes for that revision.
@@ -39,7 +39,7 @@ purposes and may be deprecated.
3939

4040
TODO: Should we cut this section and feature?
4141

42-
In the control-only approach the `sourcetool` with the `checklevel` command fetches the
42+
In the control-only approach the `sourcetool status` command fetches the
4343
rulesets that are _currently_ enabled on the source repository.
4444

4545
If all of the following are true:
@@ -243,7 +243,7 @@ Source provenance covers changes to a branch. It indicates:
243243
}
244244
}
245245
],
246-
"predicateType": "https://github.com/slsa-framework/source-tool/source-provenance/v1-draft",
246+
"predicateType": "https://github.com/slsa-framework/slsa-source-poc/source-provenance/v1-draft",
247247
"predicate": {
248248
"activity_type": "pr_merge",
249249
"actor": "TomHennen",

internal/cmd/attest.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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+
}

internal/cmd/audit.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type auditOpts struct {
6363
branchOptions
6464
verifierOptions
6565
outputOptions
66+
fromOptions
6667
auditDepth int
6768
endingCommit string
6869
auditMode AuditMode
@@ -104,6 +105,7 @@ func (ao *auditOpts) Validate() error {
104105
ao.branchOptions.Validate(),
105106
ao.verifierOptions.Validate(),
106107
ao.outputOptions.Validate(),
108+
ao.fromOptions.Validate(),
107109
}
108110
return errors.Join(errs...)
109111
}
@@ -112,6 +114,7 @@ func (ao *auditOpts) AddFlags(cmd *cobra.Command) {
112114
ao.branchOptions.AddFlags(cmd)
113115
ao.verifierOptions.AddFlags(cmd)
114116
ao.outputOptions.AddFlags(cmd)
117+
ao.fromOptions.AddFlags(cmd)
115118
cmd.PersistentFlags().IntVar(&ao.auditDepth, "depth", 0, "The max number of revisions to audit (depth <= audit all revisions).")
116119
cmd.PersistentFlags().StringVar(&ao.endingCommit, "ending-commit", "", "The commit to stop auditing at.")
117120
ao.auditMode = AuditModeBasic
@@ -165,6 +168,8 @@ Future:
165168
srctool, err := sourcetool.New(
166169
sourcetool.WithAuthenticator(authenticator),
167170
sourcetool.WithExpectedIdentity(opts.expectedIssuer, opts.expectedSan),
171+
sourcetool.WithGithubCollector(opts.readGithub()),
172+
sourcetool.WithNotesCollector(opts.readNotes()),
168173
)
169174
if err != nil {
170175
return err

internal/cmd/checklevel.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ func addCheckLevel(parentCmd *cobra.Command) {
4343
opts := checkLevelOpts{}
4444

4545
checklevelCmd := &cobra.Command{
46-
Use: "checklevel",
47-
GroupID: cmdGroupAssessment,
48-
Short: "Determines the SLSA Source Level of the repo",
46+
Use: "checklevel",
47+
Hidden: true,
48+
Deprecated: `use "sourcetool status --level" instead`,
49+
Short: "Determines the SLSA Source Level of the repo",
4950
Long: `Determines the SLSA Source Level of the repo.
5051
5152
This is meant to be run within the corresponding GitHub Actions workflow.`,

internal/cmd/checklevelprov.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,11 @@ func addCheckLevelProv(parentCmd *cobra.Command) {
9595
opts := &checkLevelProvOpts{}
9696

9797
checklevelprovCmd := &cobra.Command{
98-
Use: "checklevelprov",
99-
GroupID: cmdGroupAssessment,
100-
Example: `sourcetool checklevelprov owner/repo --push=note`,
101-
Short: "Checks the given commit against policy using & creating provenance",
98+
Use: "checklevelprov",
99+
Hidden: true,
100+
Deprecated: `use "sourcetool attest" instead`,
101+
Example: `sourcetool checklevelprov owner/repo --push=note`,
102+
Short: "Checks the given commit against policy using & creating provenance",
102103
Long: `Checks the given commit against policy using & creating provenance.
103104
104105
The checklevelprov subcommand computes the SLSA level of a commit by retrieving

internal/cmd/checktag.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ func addCheckTag(parentCmd *cobra.Command) {
5555
opts := &checkTagOptions{}
5656

5757
checktagCmd := &cobra.Command{
58-
Use: "checktag",
59-
GroupID: cmdGroupAssessment,
60-
Short: "Checks to see if the tag operation should be allowed and issues a VSA",
58+
Use: "checktag",
59+
Hidden: true,
60+
Deprecated: `use "sourcetool attest --tag <name>" instead`,
61+
Short: "Checks to see if the tag operation should be allowed and issues a VSA",
6162
PreRunE: func(cmd *cobra.Command, args []string) error {
6263
if len(args) > 0 {
6364
if err := opts.ParseLocator(args[0]); err != nil {

internal/cmd/createpolicy.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ func addCreatePolicy(parentCmd *cobra.Command) {
3030
opts := createPolicyOptions{}
3131

3232
createpolicyCmd := &cobra.Command{
33-
Use: "createpolicy",
34-
GroupID: cmdGroupPolicy,
35-
Short: "Creates a policy in a local copy of source-policies",
33+
Use: "createpolicy",
34+
Short: "Creates a policy in a local copy of source-policies",
3635
Long: `Creates a SLSA source policy in a local copy of source-policies.
3736
3837
The created policy should then be sent as a PR to slsa-framework/source-policies.`,
38+
// Deprecated in favor of "sourcetool policy create". Kept hidden and
39+
// functional during the phase-out period.
40+
Hidden: true,
41+
Deprecated: `use "sourcetool policy create" instead`,
3942
RunE: func(cmd *cobra.Command, args []string) error {
4043
if err := opts.Validate(); err != nil {
4144
return err

0 commit comments

Comments
 (0)