Skip to content

Commit c3b1081

Browse files
authored
Use cobra to handle multiple commands. (#27)
* Rearranges to split commands from logic. * Adds go.work * Adds .gitignore to get rid of tuf stuff Signed-off-by: Tom Hennen <[email protected]>
1 parent 8ed3d4b commit c3b1081

File tree

9 files changed

+322
-211
lines changed

9 files changed

+322
-211
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tuf-repo-cdn.sigstage.dev.json
2+
tuf-repo-cdn.sigstage.dev/

sourcetool/cmd/checklevel.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log"
10+
"os"
11+
12+
"github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/checklevel"
13+
"github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/vsa"
14+
15+
"github.com/google/go-github/v68/github"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
// checklevelCmd represents the checklevel command
20+
var (
21+
commit, owner, repo, branch, outputVsa string
22+
minDays int
23+
24+
checklevelCmd = &cobra.Command{
25+
Use: "checklevel",
26+
Short: "A brief description of your command",
27+
Long: `A longer description that spans multiple lines and likely contains examples
28+
and usage of using your command. For example:
29+
30+
Cobra is a CLI library for Go that empowers applications.
31+
This application is a tool to generate the needed files
32+
to quickly create a Cobra application.`,
33+
Run: func(cmd *cobra.Command, args []string) {
34+
doCheckLevel()
35+
},
36+
}
37+
)
38+
39+
func doCheckLevel() {
40+
if commit == "" || owner == "" || repo == "" || branch == "" {
41+
log.Fatal("Must set commit, owner, repo, and branch flags.")
42+
}
43+
44+
gh_client := github.NewClient(nil)
45+
ctx := context.Background()
46+
47+
sourceLevel, err := checklevel.DetermineSourceLevel(ctx, gh_client, commit, owner, repo, branch, minDays)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
fmt.Print(sourceLevel)
52+
53+
if outputVsa != "" {
54+
// This will output in the sigstore bundle format.
55+
signedVsa, err := vsa.CreateSignedSourceVsa(owner, repo, commit, sourceLevel)
56+
if err != nil {
57+
log.Fatal(err)
58+
}
59+
err = os.WriteFile(outputVsa, []byte(signedVsa), 0644)
60+
if err != nil {
61+
log.Fatal(err)
62+
}
63+
}
64+
}
65+
66+
func init() {
67+
rootCmd.AddCommand(checklevelCmd)
68+
69+
// Here you will define your flags and configuration settings.
70+
71+
// Cobra supports Persistent Flags which will work for this command
72+
// and all subcommands, e.g.:
73+
// checklevelCmd.PersistentFlags().String("foo", "", "A help for foo")
74+
75+
// Cobra supports local flags which will only run when this command
76+
// is called directly, e.g.:
77+
// checklevelCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
78+
79+
checklevelCmd.Flags().StringVar(&commit, "commit", "", "The commit to check.")
80+
checklevelCmd.Flags().StringVar(&owner, "owner", "", "The GitHub repository owner - required.")
81+
checklevelCmd.Flags().StringVar(&repo, "repo", "", "The GitHub repository name - required.")
82+
checklevelCmd.Flags().StringVar(&branch, "branch", "", "The branch within the repository - required.")
83+
checklevelCmd.Flags().IntVar(&minDays, "min_days", 1, "The minimum duration that the rules need to have been enabled for.")
84+
checklevelCmd.Flags().StringVar(&outputVsa, "output_vsa", "", "The path to write a signed VSA with the determined level.")
85+
}

sourcetool/cmd/root.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3+
4+
*/
5+
package cmd
6+
7+
import (
8+
"os"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
14+
15+
// rootCmd represents the base command when called without any subcommands
16+
var rootCmd = &cobra.Command{
17+
Use: "sourcetool",
18+
Short: "A brief description of your application",
19+
Long: `A longer description that spans multiple lines and likely contains
20+
examples and usage of using your application. For example:
21+
22+
Cobra is a CLI library for Go that empowers applications.
23+
This application is a tool to generate the needed files
24+
to quickly create a Cobra application.`,
25+
// Uncomment the following line if your bare application
26+
// has an action associated with it:
27+
// Run: func(cmd *cobra.Command, args []string) { },
28+
}
29+
30+
// Execute adds all child commands to the root command and sets flags appropriately.
31+
// This is called by main.main(). It only needs to happen once to the rootCmd.
32+
func Execute() {
33+
err := rootCmd.Execute()
34+
if err != nil {
35+
os.Exit(1)
36+
}
37+
}
38+
39+
func init() {
40+
// Here you will define your flags and configuration settings.
41+
// Cobra supports persistent flags, which, if defined here,
42+
// will be global for your application.
43+
44+
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sourcetool.yaml)")
45+
46+
// Cobra also supports local flags, which will only run
47+
// when this action is called directly.
48+
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
49+
}
50+
51+

sourcetool/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ require (
66
github.com/google/go-github/v68 v68.0.0
77
github.com/in-toto/attestation v1.1.1-0.20250123155712-7017ad824404
88
github.com/sigstore/sigstore-go v0.6.3-0.20250123153628-85b2bcba8898
9+
github.com/spf13/cobra v1.8.1
10+
github.com/theupdateframework/go-tuf/v2 v2.0.2
911
google.golang.org/protobuf v1.36.3
1012
)
1113

@@ -66,12 +68,10 @@ require (
6668
github.com/sourcegraph/conc v0.3.0 // indirect
6769
github.com/spf13/afero v1.11.0 // indirect
6870
github.com/spf13/cast v1.7.0 // indirect
69-
github.com/spf13/cobra v1.8.1 // indirect
7071
github.com/spf13/pflag v1.0.5 // indirect
7172
github.com/spf13/viper v1.19.0 // indirect
7273
github.com/subosito/gotenv v1.6.0 // indirect
7374
github.com/theupdateframework/go-tuf v0.7.0 // indirect
74-
github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect
7575
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
7676
github.com/transparency-dev/merkle v0.0.2 // indirect
7777
go.mongodb.org/mongo-driver v1.14.0 // indirect

sourcetool/go.work

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go 1.24
2+
3+
use .

sourcetool/go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=

sourcetool/main.go

Lines changed: 5 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,212 +1,10 @@
1+
/*
2+
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3+
*/
14
package main
25

3-
import (
4-
"context"
5-
"encoding/json"
6-
"errors"
7-
"flag"
8-
"fmt"
9-
"log"
10-
"os"
11-
"time"
6+
import "github.com/slsa-framework/slsa-source-poc/sourcetool/cmd"
127

13-
"github.com/google/go-github/v68/github"
14-
)
15-
16-
const (
17-
SlsaSourceLevel1 = "SLSA_SOURCE_LEVEL_1"
18-
SlsaSourceLevel2 = "SLSA_SOURCE_LEVEL_2"
19-
SourcePolicyRepoOwner = "slsa-framework"
20-
SourcePolicyRepo = "slsa-source-poc"
21-
)
22-
23-
type activity struct {
24-
Id int
25-
Before string
26-
After string
27-
Ref string
28-
Timestamp time.Time
29-
ActivityType string `json:"activity_type"`
30-
}
31-
32-
type protectedBranch struct {
33-
Name string
34-
Since time.Time
35-
TargetSlsaSourceLevel string `json:"target_slsa_source_level"`
36-
}
37-
type repoPolicy struct {
38-
// I'm actually not sure we need this. Consider removing?
39-
CanonicalRepo string `json:"canonical_repo"`
40-
ProtectedBranches []protectedBranch `json:"protected_branches"`
41-
}
42-
43-
func getBranchPolicy(ctx context.Context, gh_client *github.Client, owner string, repo string, branch string) (*protectedBranch, error) {
44-
path := fmt.Sprintf("policy/github.com/%s/%s/source-policy.json", owner, repo)
45-
46-
policyContents, _, _, err := gh_client.Repositories.GetContents(ctx, SourcePolicyRepoOwner, SourcePolicyRepo, path, nil)
47-
if err != nil {
48-
return nil, err
49-
}
50-
51-
content, err := policyContents.GetContent()
52-
if err != nil {
53-
return nil, err
54-
}
55-
var p repoPolicy
56-
err = json.Unmarshal([]byte(content), &p)
57-
if err != nil {
58-
return nil, err
59-
}
60-
61-
for _, pb := range p.ProtectedBranches {
62-
if pb.Name == branch {
63-
return &pb, nil
64-
}
65-
}
66-
67-
return nil, errors.New(fmt.Sprintf("Could not find rule for branch %s", branch))
68-
}
69-
70-
// Checks to see if the rule meets our requirements.
71-
func checkRule(ctx context.Context, gh_client *github.Client, owner string, repo string, rule *github.RepositoryRule, minTime time.Time) (bool, error) {
72-
ruleset, _, err := gh_client.Repositories.GetRuleset(ctx, owner, repo, rule.RulesetID, false)
73-
if err != nil {
74-
return false, err
75-
}
76-
77-
// We need rules to be 'active' and to have been updated no later than minTime.
78-
if ruleset.Enforcement != "active" {
79-
return false, nil
80-
}
81-
82-
if minTime.Before(ruleset.UpdatedAt.Time) {
83-
return false, nil
84-
}
85-
86-
return true, nil
87-
}
88-
89-
func commitPushTime(ctx context.Context, gh_client *github.Client, commit string, owner string, repo string, branch string) (time.Time, error) {
90-
// Unfortunately the gh_client doesn't have native support for this...'
91-
reqUrl := fmt.Sprintf("repos/%s/%s/activity", owner, repo)
92-
req, err := gh_client.NewRequest("GET", reqUrl, nil)
93-
if err != nil {
94-
return time.Time{}, err
95-
}
96-
97-
var result []*activity
98-
_, err = gh_client.Do(ctx, req, &result)
99-
if err != nil {
100-
return time.Time{}, err
101-
}
102-
103-
targetRef := fmt.Sprintf("refs/heads/%s", branch)
104-
for _, activity := range result {
105-
if activity.ActivityType != "push" && activity.ActivityType != "force_push" {
106-
continue
107-
}
108-
if activity.After == commit && activity.Ref == targetRef {
109-
// Found it
110-
return activity.Timestamp, nil
111-
}
112-
}
113-
114-
return time.Time{}, errors.New(fmt.Sprintf("Could not find repo activity for commit %s", commit))
115-
}
116-
117-
func determineSourceLevel(ctx context.Context, gh_client *github.Client, commit string, owner string, repo string, branch string, minDays int) (string, error) {
118-
rules, _, err := gh_client.Repositories.GetRulesForBranch(ctx, owner, repo, branch)
119-
120-
if err != nil {
121-
return "", err
122-
}
123-
124-
var deletionRule *github.RepositoryRule
125-
var nonFastFowardRule *github.RepositoryRule
126-
for _, rule := range rules {
127-
switch rule.Type {
128-
case "deletion":
129-
deletionRule = rule
130-
case "non_fast_forward":
131-
nonFastFowardRule = rule
132-
default:
133-
// ignore
134-
}
135-
}
136-
137-
if deletionRule == nil && nonFastFowardRule == nil {
138-
// For L2 we need deletion and non-fast-forward rules.
139-
return SlsaSourceLevel1, nil
140-
}
141-
142-
// We want to know when this commit was pushed to ensure the rules were active _then_.
143-
pushTime, err := commitPushTime(ctx, gh_client, commit, owner, repo, branch)
144-
if err != nil {
145-
return "", err
146-
}
147-
148-
// We want to check to ensure the repo hasn't enabled/disabled the rules since
149-
// setting the 'since' field in their policy.
150-
branchPolicy, err := getBranchPolicy(ctx, gh_client, owner, repo, branch)
151-
if err != nil {
152-
return "", err
153-
}
154-
155-
if pushTime.Before(branchPolicy.Since) {
156-
// This commit was pushed before they had an explicit policy.
157-
return SlsaSourceLevel1, nil
158-
}
159-
160-
deletionGood, err := checkRule(ctx, gh_client, owner, repo, deletionRule, branchPolicy.Since)
161-
if err != nil {
162-
return "", err
163-
}
164-
nonFFGood, err := checkRule(ctx, gh_client, owner, repo, nonFastFowardRule, branchPolicy.Since)
165-
if err != nil {
166-
return "", err
167-
}
168-
169-
if deletionGood && nonFFGood {
170-
return SlsaSourceLevel2, nil
171-
}
172-
173-
return SlsaSourceLevel1, nil
174-
}
175-
176-
// Determines the source level of a repo.
1778
func main() {
178-
var commit, owner, repo, branch, outputVsa string
179-
var minDays int
180-
flag.StringVar(&commit, "commit", "", "The commit to check.")
181-
flag.StringVar(&owner, "owner", "", "The GitHub repository owner - required.")
182-
flag.StringVar(&repo, "repo", "", "The GitHub repository name - required.")
183-
flag.StringVar(&branch, "branch", "", "The branch within the repository - required.")
184-
flag.IntVar(&minDays, "min_days", 1, "The minimum duration that the rules need to have been enabled for.")
185-
flag.StringVar(&outputVsa, "output_vsa", "", "The path to write a signed VSA with the determined level.")
186-
flag.Parse()
187-
188-
if commit == "" || owner == "" || repo == "" || branch == "" {
189-
log.Fatal("Must set commit, owner, repo, and branch flags.")
190-
}
191-
192-
gh_client := github.NewClient(nil)
193-
ctx := context.Background()
194-
195-
sourceLevel, err := determineSourceLevel(ctx, gh_client, commit, owner, repo, branch, minDays)
196-
if err != nil {
197-
log.Fatal(err)
198-
}
199-
fmt.Print(sourceLevel)
200-
201-
if outputVsa != "" {
202-
// This will output in the sigstore bundle format.
203-
signedVsa, err := createSignedSourceVsa(owner, repo, commit, sourceLevel)
204-
if err != nil {
205-
log.Fatal(err)
206-
}
207-
err = os.WriteFile(outputVsa, []byte(signedVsa), 0644)
208-
if err != nil {
209-
log.Fatal(err)
210-
}
211-
}
9+
cmd.Execute()
21210
}

0 commit comments

Comments
 (0)