Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 100 additions & 9 deletions internal/cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,45 @@ Alternatively, to enable each control individually use: sourcetool setup control
return err
}

// Check the control prerequisites
preReqOut := false
for _, cc := range []models.ControlConfiguration{
models.CONFIG_TAG_RULES, models.CONFIG_GEN_PROVENANCE, models.CONFIG_BRANCH_RULES,
} {
ok, actionDescr, remediateFn, err := srctool.ControlPrecheck(
opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc,
)
if err != nil {
return fmt.Errorf("checking prerequisites for %s: %w", cc, err)
}

if !ok {
if !preReqOut {
fmt.Println()
fmt.Println("🟠 " + w("Prerequisites Check:"))
preReqOut = true
}
fmt.Println(">> " + actionDescr)
fmt.Println()

_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
if err != nil {
return err
}

if !s {
return fmt.Errorf("prerequisites for %s not met", cc)
}

msg, err := remediateFn()
if err != nil {
return err
}

fmt.Printf("☑️ %s\n", msg)
}
}

if opts.interactive {
fmt.Printf(`
sourcetool is about to perform the following actions on your behalf:
Expand All @@ -165,7 +204,7 @@ sourcetool is about to perform the following actions on your behalf:
srctool.ControlConfigurationDescr(opts.GetBranch(), models.CONFIG_BRANCH_RULES),
)

_, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3)
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
if err != nil {
return err
}
Expand Down Expand Up @@ -323,18 +362,57 @@ a fork of the repository you want to protect.
return err
}
}
questions := ""
preReqOut := false
for _, c := range opts.configs {
// Run the control preflight check
cc := models.ControlConfiguration(c)

// Check the control prerequisites
ok, actionDescr, remediateFn, err := srctool.ControlPrecheck(
opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc,
)
if err != nil {
return fmt.Errorf("checking prerequisites for %s: %w", cc, err)
}

if !ok {
if !preReqOut {
fmt.Println()
fmt.Println("🟠 " + w("Prerequisites Check:"))
preReqOut = true
}
fmt.Println(">> " + actionDescr)
fmt.Println()

_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
if err != nil {
return err
}

if !s {
return fmt.Errorf("prerequisites for %s not met", cc)
}

msg, err := remediateFn()
if err != nil {
return err
}

fmt.Printf("☑️ %s\n", msg)
}

cs = append(cs, cc)
questions += fmt.Sprintf(" - %s.\n", srctool.ControlConfigurationDescr(opts.GetBranch(), models.ControlConfiguration(c)))
}

fmt.Println()
fmt.Println("sourcetool is about to perform the following actions on your behalf:")
fmt.Println()
fmt.Print(questions)
fmt.Println()

for _, c := range opts.configs {
cs = append(cs, models.ControlConfiguration(c))
fmt.Printf(" - %s.\n", srctool.ControlConfigurationDescr(opts.GetBranch(), models.ControlConfiguration(c)))
}
fmt.Println("")

_, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3)
_, s, err := util.Ask("Type 'yes' if you want to continue", "yes|no|no", 3)
if err != nil {
return err
}
Expand All @@ -345,7 +423,20 @@ a fork of the repository you want to protect.
}
} else {
for _, c := range opts.configs {
cs = append(cs, models.ControlConfiguration(c))
cc := models.ControlConfiguration(c)
// Run the prerequisites and run any remediations
ok, _, remediateFn, err := srctool.ControlPrecheck(opts.GetBranch().Repository, []*models.Branch{opts.GetBranch()}, cc)
if err != nil {
return fmt.Errorf("checking prerequisites for %q: %w", cc, err)
}
if !ok {
msg, err := remediateFn()
if err != nil {
return fmt.Errorf("running remedaition for %q prereqs: %w", cc, err)
}
fmt.Println(msg)
}
cs = append(cs, cc)
}
}
err = srctool.ConfigureControls(
Expand Down
6 changes: 6 additions & 0 deletions pkg/sourcetool/backends/vcs/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ import (
func New() *Backend {
return &Backend{
authenticator: auth.New(),
Options: Options{UseFork: true},
}
}

type Options struct {
UseFork bool
}

// Backend implemets the GitHub sourcetool backend
type Backend struct {
authenticator *auth.Authenticator
Options Options
}

// getGitHubConnection builds a github connector to a repository
Expand Down
110 changes: 107 additions & 3 deletions pkg/sourcetool/backends/vcs/github/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strings"

"github.com/google/go-github/v69/github"
Expand Down Expand Up @@ -49,6 +50,30 @@ jobs:
`
)

// checkPushAccess
func (b *Backend) checkPushAccess(r *models.Repository) (bool, error) {
client, err := b.authenticator.GetGitHubClient()
if err != nil {
return false, err
}
owner, repoName, err := r.PathAsGitHubOwnerName()
if err != nil {
return false, err
}

//nolint:noctx
resp, err := client.Client().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/collaborators", owner, repoName))
if resp.StatusCode == http.StatusForbidden {
return false, nil
}
if err != nil {
resp.Body.Close() //nolint:errcheck,gosec
return false, fmt.Errorf("checking repository access: %w", err)
}
resp.Body.Close() //nolint:errcheck,gosec
return true, nil
}

// CreateWorkflowPR creates the pull request to add the provenance workflow
// to the specified repository.
func (b *Backend) CreateWorkflowPR(r *models.Repository, branches []*models.Branch) (*models.PullRequest, error) {
Expand All @@ -63,10 +88,20 @@ func (b *Backend) CreateWorkflowPR(r *models.Repository, branches []*models.Bran
}
workflowYAML := fmt.Sprintf(workflowData, strings.Join(quotedBranchesList, ", "))

// We need to determine if the user needs a fork
hasPush, err := b.checkPushAccess(r)
if err != nil {
return nil, fmt.Errorf("checking for repository push access: %w", err)
}

// If user does not have push access, use a fork
if err := b.CheckWorkflowFork(r); err != nil {
return nil, fmt.Errorf("checking for required repository fork: %w", err)
}

// Create a PR manager
prManager := repo.NewPullRequestManager(repo.WithAuthenticator(b.authenticator))

// TODO(puerco): Honor forks settings, etc
prManager.Options.UseFork = !hasPush

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

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

// CreateRepositoryFork creates a fork of a repo into the logged-in user's org.
// Optionally the fork can have a different name than the original.
func (b *Backend) createRepositoryFork(
src *models.Repository, forkName string,
) error {
client, err := b.authenticator.GetGitHubClient()
if err != nil {
return fmt.Errorf("creating GitHub client: %w", err)
}

srcOrg, srcName, err := src.PathAsGitHubOwnerName()
if err != nil {
return err
}

if forkName == "" {
forkName = srcName
}

// Create the fork
_, resp, err := client.Repositories.CreateFork(
context.Background(), srcOrg, srcName, &github.RepositoryCreateForkOptions{
Name: forkName,
},
)

// GitHub will return 202 for larger repos that are cloned async
if err != nil && resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("creating repository fork: %w", err)
}

return nil
}

// ControlPrecheck checks if the prerequisites to enable the controls are OK
func (b *Backend) ControlPrecheck(
r *models.Repository, branches []*models.Branch, config models.ControlConfiguration,
) (ok bool, remediationMessage string, remediateFn models.ControlPreRemediationFn, err error) {
//nolint:exhaustive // Not all configs have prechecks
switch config {
case models.CONFIG_GEN_PROVENANCE:
sino, err := b.checkPushAccess(r)
if err != nil {
return false, "", nil, fmt.Errorf("checking for push access: %w", err)
}
// If user has push access, everything is OK
if sino {
return true, "", nil, nil
}

// No push access, check if user has a fork
if err := b.CheckWorkflowFork(r); err == nil {
// Fork found, all ok
return true, "", nil, nil
}
msg := "No fork found of repository %s\n"
msg += "and user has no push access.\n\n"
msg += "Would you like to create a fork in your account?\n"
return false, fmt.Sprintf(msg, r.Path), func() (string, error) {
if err := b.createRepositoryFork(r, ""); err != nil {
return "", fmt.Errorf("creating repository fork: %w", err)
}
return "successfully created the repository fork", nil
}, nil
default:
return true, "", nil, nil
}
}

// ConfigureControls configure the SLSA controls in the repository
func (b *Backend) ConfigureControls(r *models.Repository, branches []*models.Branch, configs []models.ControlConfiguration) error {
errs := []error{}
Expand Down
2 changes: 1 addition & 1 deletion pkg/sourcetool/implementation.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (impl *defaultToolImplementation) CreatePolicyPR(a *auth.Authenticator, opt
return nil, fmt.Errorf("checking policy repository fork: %w", err)
}

// MArshal the policy json
// Marshal the policy json
policyJson, err := json.MarshalIndent(p, "", " ")
if err != nil {
return nil, fmt.Errorf("marshaling policy data: %w", err)
Expand Down
5 changes: 5 additions & 0 deletions pkg/sourcetool/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ type VcsBackend interface {
ControlConfigurationDescr(*Branch, ControlConfiguration) string
ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error
GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error)
ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error)
}

// ControlPreRemediation is a function returned by the VCS backends
// when checking for prerequisites that the user may optionally run
type ControlPreRemediationFn func() (string, error)

type ControlConfiguration string

const (
Expand Down
Loading
Loading