diff --git a/sourcetool/internal/cmd/policy.go b/sourcetool/internal/cmd/policy.go new file mode 100644 index 00000000..1223f423 --- /dev/null +++ b/sourcetool/internal/cmd/policy.go @@ -0,0 +1,222 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/policy" + "github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/sourcetool" + "github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/sourcetool/models" +) + +type policyViewOpts struct { + repoOptions +} + +type policyCreateOpts struct { + branchOptions + openPullRequest bool +} + +func (pco *policyCreateOpts) AddFlags(cmd *cobra.Command) { + pco.branchOptions.AddFlags(cmd) + cmd.PersistentFlags().BoolVar(&pco.openPullRequest, "pr", true, "Open a pull request to check-in the policy") +} + +func addPolicy(parentCmd *cobra.Command) { + policyCmd := &cobra.Command{ + Short: "tools to work with source policies", + Long: fmt.Sprintf(` +%s %s + +The policy subcommands can be used to view, create and update the source +policy for a repository. The policy family has two subcommands: + +%s +Shows current repository policy for a repository checjed into the SLSA community +policy repo. + +%s +Creates a new policy for a repository and, optionally, check it into the +SLSA community repository. + +`, w("sourcetool policy:"), w2("configure SLSA source policies for a repo"), + w("sourcetool policy view"), w("sourcetool policy create")), + Use: "policy", + SilenceUsage: true, + SilenceErrors: true, + } + + addPolicyView(policyCmd) + addPolicyCreate(policyCmd) + parentCmd.AddCommand(policyCmd) +} + +func addPolicyView(parent *cobra.Command) { + opts := &policyViewOpts{} + policyViewCmd := &cobra.Command{ + Short: "view the policy of a repository", + Long: `The view subcommand retrieves the policy stored in the SLSA community +repository for a repository and displays it. +`, + Use: "view owner/repo", + SilenceUsage: false, + SilenceErrors: true, + PreRunE: func(_ *cobra.Command, args []string) error { + if len(args) > 0 { + if err := opts.ParseSlug(args[0]); err != nil { + return err + } + } + + // Validate early the repository options to provide a more + // useful message to the user + if err := opts.Validate(); err != nil { + return err + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if err := opts.Validate(); err != nil { + return err + } + + // At this point options are valid, no help needed. + cmd.SilenceUsage = true + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + // sourcetool.WithPolicyRepo(opts.policyRepo), + ) + if err != nil { + return err + } + + pcy, err := srctool.GetRepositoryPolicy(context.Background(), opts.GetRepository()) + if err != nil { + return err + } + + if err := displayPolicy(opts.repoOptions, pcy); err != nil { + return err + } + + return nil + }, + } + opts.AddFlags(policyViewCmd) + parent.AddCommand(policyViewCmd) +} + +// addPolicyCreate adds the create subcreate +func addPolicyCreate(parent *cobra.Command) { + opts := &policyCreateOpts{} + policyViewCmd := &cobra.Command{ + Short: "creates a source policy for a repository", + Long: `The create subcommand inspects the controls in place for a repo +and creates a new policy for it. +`, + Use: "create owner/repo@branch", + SilenceUsage: false, + SilenceErrors: true, + PreRunE: func(_ *cobra.Command, args []string) error { + if len(args) > 0 { + if err := opts.ParseLocator(args[0]); err != nil { + return err + } + } + + // Validate early the repository options to provide a more + // useful message to the user + if err := opts.repoOptions.Validate(); err != nil { + return err + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if err := opts.Validate(); err != nil { + return err + } + + // At this point options are valid, no help needed. + cmd.SilenceUsage = true + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + // sourcetool.WithPolicyRepo(opts.policyRepo), + ) + if err != nil { + return err + } + + epcy, err := srctool.GetRepositoryPolicy(context.Background(), opts.GetRepository()) + if err != nil { + return fmt.Errorf("checking for existing policy: %w", err) + } + if epcy != nil { + return fmt.Errorf("repository already has a policy checked into the community repo") + } + + // Create the policy, this will open the pull request in the community + // repo if the options say so. + pcy, pr, err := srctool.CreateRepositoryPolicy( + context.Background(), opts.GetRepository(), []*models.Branch{opts.GetBranch()}, + ) + if err != nil { + return err + } + + if err := displayPolicy(opts.repoOptions, pcy); err != nil { + return err + } + + if opts.openPullRequest && pr != nil { + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "Opened pull request: https://github.com/%s/pulls/%d\n\n", pr.Repo.Path, pr.Number) + } + + return nil + }, + } + opts.AddFlags(policyViewCmd) + parent.AddCommand(policyViewCmd) +} + +func displayPolicy(opts repoOptions, pcy *policy.RepoPolicy) error { + if pcy == nil { + fmt.Println("\n" + w(fmt.Sprintf("✖️ No source policy found for %s/%s", opts.owner, opts.repository))) + fmt.Println("To create and check-in a policy for the repository run:") + fmt.Println() + fmt.Printf(" sourcetool policy create %s/%s\n", opts.owner, opts.repository) + fmt.Println() + return nil + } + + data, err := json.MarshalIndent(pcy, "", " ") + if err != nil { + return fmt.Errorf("marshaling policy data: %w", err) + } + + fmt.Fprint(os.Stderr, w(fmt.Sprintf("\n🛡️ Source policy for %s/%s:\n\n", opts.owner, opts.repository))) + fmt.Println(string(data)) + fmt.Println() + return nil +} diff --git a/sourcetool/internal/cmd/root.go b/sourcetool/internal/cmd/root.go index b9ed6991..1feb5495 100644 --- a/sourcetool/internal/cmd/root.go +++ b/sourcetool/internal/cmd/root.go @@ -54,6 +54,7 @@ controls and much more. addCheckTag(rootCmd) addCreatePolicy(rootCmd) addAuth(rootCmd) + addPolicy(rootCmd) return rootCmd } diff --git a/sourcetool/pkg/sourcetool/options.go b/sourcetool/pkg/sourcetool/options.go index 4c68d73d..06f9545d 100644 --- a/sourcetool/pkg/sourcetool/options.go +++ b/sourcetool/pkg/sourcetool/options.go @@ -25,6 +25,13 @@ func WithEnforce(enforce bool) ConfigFn { } } +func WithCreatePolicyPR(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.CreatePolicyPR = yesno + return nil + } +} + func WithUserForkOrg(org string) ConfigFn { return func(t *Tool) error { t.Options.UserForkOrg = org diff --git a/sourcetool/pkg/sourcetool/options/options.go b/sourcetool/pkg/sourcetool/options/options.go index 6cc067aa..a11301f4 100644 --- a/sourcetool/pkg/sourcetool/options/options.go +++ b/sourcetool/pkg/sourcetool/options/options.go @@ -13,12 +13,15 @@ type Options struct { UseSSH bool UpdateRepo bool + CreatePolicyPR bool + // PolicyRepo is the repository where the policies are stored PolicyRepo string } // DefaultOptions holds the default options the tool initializes with var Default = Options{ - PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), - UseSSH: true, + PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), + UseSSH: true, + CreatePolicyPR: true, } diff --git a/sourcetool/pkg/sourcetool/tool.go b/sourcetool/pkg/sourcetool/tool.go index 38648f46..8887c55c 100644 --- a/sourcetool/pkg/sourcetool/tool.go +++ b/sourcetool/pkg/sourcetool/tool.go @@ -174,7 +174,7 @@ func (t *Tool) CheckPolicyRepoFork(repo *models.Repository) (bool, error) { // CreateBranchPolicy creates a repository policy func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, branches []*models.Branch) (*policy.RepoPolicy, error) { if len(branches) > 1 { - // Chanfe this once we support merging policies + // Change this once we support merging policies return nil, fmt.Errorf("only one branch is supported at a time") } if branches == nil { @@ -241,3 +241,33 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control } return p, nil } + +// GetRepositoryPolicy retrieves the policy of repo from the community +func (t *Tool) GetRepositoryPolicy(ctx context.Context, r *models.Repository) (*policy.RepoPolicy, error) { + pe := policy.NewPolicyEvaluator() + p, _, err := pe.GetPolicy(ctx, r) + if err != nil { + return nil, fmt.Errorf("getting repository policy: %w", err) + } + + return p, nil +} + +// CreateRepositoryPolicy creates a policy for a repository +func (t *Tool) CreateRepositoryPolicy(ctx context.Context, r *models.Repository, branches []*models.Branch) (*policy.RepoPolicy, *models.PullRequest, error) { + pcy, err := t.CreateBranchPolicy(ctx, r, branches) + if err != nil { + return nil, nil, fmt.Errorf("creating policy for: %w", err) + } + + var pr *models.PullRequest + + // If the option is set, open the pull request + if t.Options.CreatePolicyPR { + pr, err = t.impl.CreatePolicyPR(t.Authenticator, &t.Options, r, pcy) + if err != nil { + return nil, nil, fmt.Errorf("opening the policy pull request: %w", err) + } + } + return pcy, pr, nil +}