|
| 1 | +/* |
| 2 | +Copyright © 2025 NAME HERE <EMAIL ADDRESS> |
| 3 | +*/ |
1 | 4 | package main |
2 | 5 |
|
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" |
12 | 7 |
|
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. |
177 | 8 | 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() |
212 | 10 | } |
0 commit comments