Skip to content

Commit 2311a98

Browse files
authored
Control prechecks, implement fork (#266)
* Add control precheck to VCS backends Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]> * Regenerate fakes Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]> * Expose ctl prechecks in sourcetool API Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]> * Run control prechecks on setup Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]> * Implement github fork precheck Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]> --------- Signed-off-by: Adolfo García Veytia (Puerco) <[email protected]>
1 parent 6aca8e1 commit 2311a98

File tree

7 files changed

+329
-13
lines changed

7 files changed

+329
-13
lines changed

internal/cmd/setup.go

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,45 @@ Alternatively, to enable each control individually use: sourcetool setup control
151151
return err
152152
}
153153

154+
// Check the control prerequisites
155+
preReqOut := false
156+
for _, cc := range []models.ControlConfiguration{
157+
models.CONFIG_TAG_RULES, models.CONFIG_GEN_PROVENANCE, models.CONFIG_BRANCH_RULES,
158+
} {
159+
ok, actionDescr, remediateFn, err := srctool.ControlPrecheck(
160+
opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc,
161+
)
162+
if err != nil {
163+
return fmt.Errorf("checking prerequisites for %s: %w", cc, err)
164+
}
165+
166+
if !ok {
167+
if !preReqOut {
168+
fmt.Println()
169+
fmt.Println("🟠 " + w("Prerequisites Check:"))
170+
preReqOut = true
171+
}
172+
fmt.Println(">> " + actionDescr)
173+
fmt.Println()
174+
175+
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
176+
if err != nil {
177+
return err
178+
}
179+
180+
if !s {
181+
return fmt.Errorf("prerequisites for %s not met", cc)
182+
}
183+
184+
msg, err := remediateFn()
185+
if err != nil {
186+
return err
187+
}
188+
189+
fmt.Printf("☑️ %s\n", msg)
190+
}
191+
}
192+
154193
if opts.interactive {
155194
fmt.Printf(`
156195
sourcetool is about to perform the following actions on your behalf:
@@ -165,7 +204,7 @@ sourcetool is about to perform the following actions on your behalf:
165204
srctool.ControlConfigurationDescr(opts.GetBranch(), models.CONFIG_BRANCH_RULES),
166205
)
167206

168-
_, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3)
207+
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
169208
if err != nil {
170209
return err
171210
}
@@ -323,18 +362,57 @@ a fork of the repository you want to protect.
323362
return err
324363
}
325364
}
365+
questions := ""
366+
preReqOut := false
367+
for _, c := range opts.configs {
368+
// Run the control preflight check
369+
cc := models.ControlConfiguration(c)
370+
371+
// Check the control prerequisites
372+
ok, actionDescr, remediateFn, err := srctool.ControlPrecheck(
373+
opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc,
374+
)
375+
if err != nil {
376+
return fmt.Errorf("checking prerequisites for %s: %w", cc, err)
377+
}
378+
379+
if !ok {
380+
if !preReqOut {
381+
fmt.Println()
382+
fmt.Println("🟠 " + w("Prerequisites Check:"))
383+
preReqOut = true
384+
}
385+
fmt.Println(">> " + actionDescr)
386+
fmt.Println()
387+
388+
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
389+
if err != nil {
390+
return err
391+
}
392+
393+
if !s {
394+
return fmt.Errorf("prerequisites for %s not met", cc)
395+
}
396+
397+
msg, err := remediateFn()
398+
if err != nil {
399+
return err
400+
}
401+
402+
fmt.Printf("☑️ %s\n", msg)
403+
}
404+
405+
cs = append(cs, cc)
406+
questions += fmt.Sprintf(" - %s.\n", srctool.ControlConfigurationDescr(opts.GetBranch(), models.ControlConfiguration(c)))
407+
}
326408

327409
fmt.Println()
328410
fmt.Println("sourcetool is about to perform the following actions on your behalf:")
329411
fmt.Println()
412+
fmt.Print(questions)
413+
fmt.Println()
330414

331-
for _, c := range opts.configs {
332-
cs = append(cs, models.ControlConfiguration(c))
333-
fmt.Printf(" - %s.\n", srctool.ControlConfigurationDescr(opts.GetBranch(), models.ControlConfiguration(c)))
334-
}
335-
fmt.Println("")
336-
337-
_, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3)
415+
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
338416
if err != nil {
339417
return err
340418
}
@@ -345,7 +423,20 @@ a fork of the repository you want to protect.
345423
}
346424
} else {
347425
for _, c := range opts.configs {
348-
cs = append(cs, models.ControlConfiguration(c))
426+
cc := models.ControlConfiguration(c)
427+
// Run the prerequisites and run any remediations
428+
ok, _, remediateFn, err := srctool.ControlPrecheck(opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc)
429+
if err != nil {
430+
return fmt.Errorf("checking prerequisites for %q: %w", cc, err)
431+
}
432+
if !ok {
433+
msg, err := remediateFn()
434+
if err != nil {
435+
return fmt.Errorf("running remedaition for %q prereqs: %w", cc, err)
436+
}
437+
fmt.Println(msg)
438+
}
439+
cs = append(cs, cc)
349440
}
350441
}
351442
err = srctool.ConfigureControls(

pkg/sourcetool/backends/vcs/github/github.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ import (
2020
func New() *Backend {
2121
return &Backend{
2222
authenticator: auth.New(),
23+
Options: Options{UseFork: true},
2324
}
2425
}
2526

27+
type Options struct {
28+
UseFork bool
29+
}
30+
2631
// Backend implemets the GitHub sourcetool backend
2732
type Backend struct {
2833
authenticator *auth.Authenticator
34+
Options Options
2935
}
3036

3137
// getGitHubConnection builds a github connector to a repository

pkg/sourcetool/backends/vcs/github/manage.go

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"net/http"
1011
"strings"
1112

1213
"github.com/google/go-github/v69/github"
@@ -49,6 +50,30 @@ jobs:
4950
`
5051
)
5152

53+
// checkPushAccess
54+
func (b *Backend) checkPushAccess(r *models.Repository) (bool, error) {
55+
client, err := b.authenticator.GetGitHubClient()
56+
if err != nil {
57+
return false, err
58+
}
59+
owner, repoName, err := r.PathAsGitHubOwnerName()
60+
if err != nil {
61+
return false, err
62+
}
63+
64+
//nolint:noctx
65+
resp, err := client.Client().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/collaborators", owner, repoName))
66+
if resp.StatusCode == http.StatusForbidden {
67+
return false, nil
68+
}
69+
if err != nil {
70+
resp.Body.Close() //nolint:errcheck,gosec
71+
return false, fmt.Errorf("checking repository access: %w", err)
72+
}
73+
resp.Body.Close() //nolint:errcheck,gosec
74+
return true, nil
75+
}
76+
5277
// CreateWorkflowPR creates the pull request to add the provenance workflow
5378
// to the specified repository.
5479
func (b *Backend) CreateWorkflowPR(r *models.Repository, branches []*models.Branch) (*models.PullRequest, error) {
@@ -63,10 +88,20 @@ func (b *Backend) CreateWorkflowPR(r *models.Repository, branches []*models.Bran
6388
}
6489
workflowYAML := fmt.Sprintf(workflowData, strings.Join(quotedBranchesList, ", "))
6590

91+
// We need to determine if the user needs a fork
92+
hasPush, err := b.checkPushAccess(r)
93+
if err != nil {
94+
return nil, fmt.Errorf("checking for repository push access: %w", err)
95+
}
96+
97+
// If user does not have push access, use a fork
98+
if err := b.CheckWorkflowFork(r); err != nil {
99+
return nil, fmt.Errorf("checking for required repository fork: %w", err)
100+
}
101+
66102
// Create a PR manager
67103
prManager := repo.NewPullRequestManager(repo.WithAuthenticator(b.authenticator))
68-
69-
// TODO(puerco): Honor forks settings, etc
104+
prManager.Options.UseFork = !hasPush
70105

71106
// Open the pull request
72107
pr, err := prManager.PullRequestFileList(
@@ -93,7 +128,7 @@ func (b *Backend) CreateWorkflowPR(r *models.Repository, branches []*models.Bran
93128
// CheckWorkflowFork verifies that the user has a fork of the repository
94129
// we are configuring.
95130
func (b *Backend) CheckWorkflowFork(r *models.Repository) error {
96-
// Create a PAR manager
131+
// Create a PR manager
97132
prManager := repo.NewPullRequestManager(repo.WithAuthenticator(b.authenticator))
98133

99134
// TODO(puerco): Support forkname from options
@@ -195,6 +230,75 @@ func (b *Backend) CreateTagRuleset(r *models.Repository) error {
195230
return nil
196231
}
197232

233+
// CreateRepositoryFork creates a fork of a repo into the logged-in user's org.
234+
// Optionally the fork can have a different name than the original.
235+
func (b *Backend) createRepositoryFork(
236+
src *models.Repository, forkName string,
237+
) error {
238+
client, err := b.authenticator.GetGitHubClient()
239+
if err != nil {
240+
return fmt.Errorf("creating GitHub client: %w", err)
241+
}
242+
243+
srcOrg, srcName, err := src.PathAsGitHubOwnerName()
244+
if err != nil {
245+
return err
246+
}
247+
248+
if forkName == "" {
249+
forkName = srcName
250+
}
251+
252+
// Create the fork
253+
_, resp, err := client.Repositories.CreateFork(
254+
context.Background(), srcOrg, srcName, &github.RepositoryCreateForkOptions{
255+
Name: forkName,
256+
},
257+
)
258+
259+
// GitHub will return 202 for larger repos that are cloned async
260+
if err != nil && resp.StatusCode != http.StatusAccepted {
261+
return fmt.Errorf("creating repository fork: %w", err)
262+
}
263+
264+
return nil
265+
}
266+
267+
// ControlPrecheck checks if the prerequisites to enable the controls are OK
268+
func (b *Backend) ControlPrecheck(
269+
r *models.Repository, branches []*models.Branch, config models.ControlConfiguration,
270+
) (ok bool, remediationMessage string, remediateFn models.ControlPreRemediationFn, err error) {
271+
//nolint:exhaustive // Not all configs have prechecks
272+
switch config {
273+
case models.CONFIG_GEN_PROVENANCE:
274+
sino, err := b.checkPushAccess(r)
275+
if err != nil {
276+
return false, "", nil, fmt.Errorf("checking for push access: %w", err)
277+
}
278+
// If user has push access, everything is OK
279+
if sino {
280+
return true, "", nil, nil
281+
}
282+
283+
// No push access, check if user has a fork
284+
if err := b.CheckWorkflowFork(r); err == nil {
285+
// Fork found, all ok
286+
return true, "", nil, nil
287+
}
288+
msg := "No fork found of repository %s\n"
289+
msg += "and user has no push access.\n\n"
290+
msg += "Would you like to create a fork in your account?\n"
291+
return false, fmt.Sprintf(msg, r.Path), func() (string, error) {
292+
if err := b.createRepositoryFork(r, ""); err != nil {
293+
return "", fmt.Errorf("creating repository fork: %w", err)
294+
}
295+
return "successfully created the repository fork", nil
296+
}, nil
297+
default:
298+
return true, "", nil, nil
299+
}
300+
}
301+
198302
// ConfigureControls configure the SLSA controls in the repository
199303
func (b *Backend) ConfigureControls(r *models.Repository, branches []*models.Branch, configs []models.ControlConfiguration) error {
200304
errs := []error{}

pkg/sourcetool/implementation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (impl *defaultToolImplementation) CreatePolicyPR(a *auth.Authenticator, opt
100100
return nil, fmt.Errorf("checking policy repository fork: %w", err)
101101
}
102102

103-
// MArshal the policy json
103+
// Marshal the policy json
104104
policyJson, err := json.MarshalIndent(p, "", " ")
105105
if err != nil {
106106
return nil, fmt.Errorf("marshaling policy data: %w", err)

pkg/sourcetool/models/models.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ type VcsBackend interface {
4242
ControlConfigurationDescr(*Branch, ControlConfiguration) string
4343
ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error
4444
GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error)
45+
ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error)
4546
}
4647

48+
// ControlPreRemediation is a function returned by the VCS backends
49+
// when checking for prerequisites that the user may optionally run
50+
type ControlPreRemediationFn func() (string, error)
51+
4752
type ControlConfiguration string
4853

4954
const (

0 commit comments

Comments
 (0)