|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "slices" |
| 7 | + |
| 8 | + "github.com/spf13/cobra" |
| 9 | + "sigs.k8s.io/release-utils/util" |
| 10 | + |
| 11 | + "github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/policy" |
| 12 | + "github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/sourcetool" |
| 13 | +) |
| 14 | + |
| 15 | +type setupOpts struct { |
| 16 | + branchOptions |
| 17 | + policyRepo string |
| 18 | + userForkOrg string |
| 19 | + enforce bool |
| 20 | + interactive bool |
| 21 | +} |
| 22 | + |
| 23 | +func (so *setupOpts) AddFlags(cmd *cobra.Command) { |
| 24 | + so.branchOptions.AddFlags(cmd) |
| 25 | + |
| 26 | + cmd.PersistentFlags().BoolVar( |
| 27 | + &so.enforce, "enforce", false, "create enforcement rules", |
| 28 | + ) |
| 29 | + cmd.PersistentFlags().StringVar( |
| 30 | + &so.userForkOrg, "user-fork", "", "GitHub organization to look for forks of repos (for pull requests)", |
| 31 | + ) |
| 32 | + cmd.PersistentFlags().StringVar( |
| 33 | + &so.policyRepo, "policy-repo", fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), "repository to store the SLSA source policy", |
| 34 | + ) |
| 35 | + |
| 36 | + cmd.PersistentFlags().BoolVar( |
| 37 | + &so.interactive, "interactive", true, "confirm before performing changes", |
| 38 | + ) |
| 39 | +} |
| 40 | + |
| 41 | +// Validate checks the options in context with arguments |
| 42 | +func (so *setupOpts) Validate() error { |
| 43 | + errs := []error{ |
| 44 | + so.branchOptions.Validate(), |
| 45 | + } |
| 46 | + return errors.Join(errs...) |
| 47 | +} |
| 48 | + |
| 49 | +func addSetup(parentCmd *cobra.Command) { |
| 50 | + setupCmd := &cobra.Command{ |
| 51 | + Short: "configure SLSA source features in a repository", |
| 52 | + Long: fmt.Sprintf(` |
| 53 | +%s %s |
| 54 | +
|
| 55 | +The setup subcommands can be used to protect a repository with the |
| 56 | +SLSA Source tooling by automatically configuring the required security |
| 57 | +controls in a repository. |
| 58 | +
|
| 59 | +The setup family has two subcommands: |
| 60 | +
|
| 61 | +%s |
| 62 | +A "one shot" setup process enabling all the security controls required |
| 63 | +at once. This is ideal for new repositories or when you are sure the |
| 64 | +changes will not disrupt existing repositories. |
| 65 | +
|
| 66 | +%s |
| 67 | +Enables fine-grained control when configuring security features in a |
| 68 | +repository. The setup control subcommand can configure each security |
| 69 | +control individually. |
| 70 | +
|
| 71 | +`, w("sourcetool setup:"), w2("configure SLSA source controls on a repository"), |
| 72 | + w("sourcetool setup repo"), w("sourcetool setup controls")), |
| 73 | + Use: "setup", |
| 74 | + SilenceUsage: true, |
| 75 | + SilenceErrors: true, |
| 76 | + } |
| 77 | + |
| 78 | + AddSetupRepo(setupCmd) |
| 79 | + AddSetupControls(setupCmd) |
| 80 | + parentCmd.AddCommand(setupCmd) |
| 81 | +} |
| 82 | + |
| 83 | +func AddSetupRepo(parent *cobra.Command) { |
| 84 | + opts := &setupOpts{} |
| 85 | + setupRepoCmd := &cobra.Command{ |
| 86 | + Short: "configure all the SLSA source features in a repository", |
| 87 | + Long: `The setup repo subcommand is a "one shot" setup process enabling all |
| 88 | +the security controls required to get a repository to a specific SLSA level. |
| 89 | +
|
| 90 | +This command is ideal for new repositories or when you are sure the implemented |
| 91 | +changes will not disrupt existing workflows. |
| 92 | +
|
| 93 | +To use this subcommand you need to export a GitHub token as an environment |
| 94 | +variable called GITHUB_TOKEN. The token needs admin permissions on the repo |
| 95 | +to configure the branch rules. |
| 96 | +
|
| 97 | +`, |
| 98 | + Use: "repo owner/repo", |
| 99 | + SilenceUsage: false, |
| 100 | + SilenceErrors: true, |
| 101 | + PreRunE: func(_ *cobra.Command, args []string) error { |
| 102 | + if len(args) > 0 { |
| 103 | + if err := opts.ParseLocator(args[0]); err != nil { |
| 104 | + return err |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + // Validate early the repository options to provide a more |
| 109 | + // useful message to the user |
| 110 | + if err := opts.repoOptions.Validate(); err != nil { |
| 111 | + return err |
| 112 | + } |
| 113 | + |
| 114 | + if err := opts.EnsureDefaults(); err != nil { |
| 115 | + return err |
| 116 | + } |
| 117 | + |
| 118 | + return nil |
| 119 | + }, |
| 120 | + RunE: func(cmd *cobra.Command, args []string) (err error) { |
| 121 | + if err := opts.Validate(); err != nil { |
| 122 | + return err |
| 123 | + } |
| 124 | + |
| 125 | + // At this point options are valid, no help needed. |
| 126 | + cmd.SilenceUsage = true |
| 127 | + |
| 128 | + // Create a new sourcetool object |
| 129 | + srctool, err := sourcetool.New( |
| 130 | + sourcetool.WithOwner(opts.owner), |
| 131 | + sourcetool.WithRepo(opts.repository), |
| 132 | + sourcetool.WithBranch(opts.branch), |
| 133 | + ) |
| 134 | + if err != nil { |
| 135 | + return err |
| 136 | + } |
| 137 | + |
| 138 | + if opts.interactive { |
| 139 | + fmt.Printf(` |
| 140 | +sourcetool is about to perform the following actions on your behalf: |
| 141 | +
|
| 142 | + - %s. |
| 143 | + - %s. |
| 144 | + - %s. |
| 145 | +
|
| 146 | +`, |
| 147 | + srctool.ControlConfigurationDescr(sourcetool.CONFIG_POLICY), |
| 148 | + srctool.ControlConfigurationDescr(sourcetool.CONFIG_PROVENANCE_WORKFLOW), |
| 149 | + srctool.ControlConfigurationDescr(sourcetool.CONFIG_BRANCH_RULES), |
| 150 | + ) |
| 151 | + |
| 152 | + _, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3) |
| 153 | + if err != nil { |
| 154 | + return err |
| 155 | + } |
| 156 | + |
| 157 | + if !s { |
| 158 | + fmt.Println("Cancelled.") |
| 159 | + return nil |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + err = srctool.OnboardRepository( |
| 164 | + sourcetool.WithEnforce(opts.enforce), |
| 165 | + sourcetool.WithUserForkOrg(opts.userForkOrg), |
| 166 | + sourcetool.WithPolicyRepo(opts.policyRepo), |
| 167 | + ) |
| 168 | + if err != nil { |
| 169 | + return fmt.Errorf("onboarding repo: %w", err) |
| 170 | + } |
| 171 | + |
| 172 | + return nil |
| 173 | + }, |
| 174 | + } |
| 175 | + opts.AddFlags(setupRepoCmd) |
| 176 | + parent.AddCommand(setupRepoCmd) |
| 177 | +} |
| 178 | + |
| 179 | +type setupCtlOpts struct { |
| 180 | + setupOpts |
| 181 | + configs []string |
| 182 | +} |
| 183 | + |
| 184 | +func (so *setupCtlOpts) AddFlags(cmd *cobra.Command) { |
| 185 | + so.setupOpts.AddFlags(cmd) |
| 186 | + |
| 187 | + cmd.PersistentFlags().StringSliceVar( |
| 188 | + &so.configs, "config", []string{}, fmt.Sprintf("control to configure %+v", sourcetool.ControlConfigurations), |
| 189 | + ) |
| 190 | +} |
| 191 | + |
| 192 | +// Validate checks the options in context with arguments |
| 193 | +func (so *setupCtlOpts) Validate() error { |
| 194 | + errs := []error{ |
| 195 | + so.setupOpts.Validate(), |
| 196 | + } |
| 197 | + for _, c := range so.configs { |
| 198 | + if !slices.Contains(sourcetool.ControlConfigurations, sourcetool.ControlConfiguration(c)) { |
| 199 | + errs = append(errs, fmt.Errorf("unknown configuration: %q", c)) |
| 200 | + } |
| 201 | + } |
| 202 | + return errors.Join(errs...) |
| 203 | +} |
| 204 | + |
| 205 | +func AddSetupControls(parent *cobra.Command) { |
| 206 | + opts := &setupCtlOpts{} |
| 207 | + setupControlsCmd := &cobra.Command{ |
| 208 | + Short: "configure specific SLSA controls in a repository", |
| 209 | + Long: fmt.Sprintf(` |
| 210 | +%s %s |
| 211 | +
|
| 212 | +The setup controls subcommand configures the specified SLSA security |
| 213 | +controls in a repository. As opposed to "setup repo", this subcommand lets you |
| 214 | +configure each security control individually. |
| 215 | +
|
| 216 | +To use this subcommand you need to export a GitHub token as an environment |
| 217 | +variable called GITHUB_TOKEN. To configure the branch rules, the token needs |
| 218 | +admin permissions on the repo. For the other configuration it is only required |
| 219 | +as an identity source. |
| 220 | +
|
| 221 | +The values for --config are as follows: |
| 222 | +
|
| 223 | +%s |
| 224 | +Configures push and delete protection in the repository, required to reach slsa |
| 225 | +source level 2+. |
| 226 | +
|
| 227 | +%s |
| 228 | +Opens a pull request in the repository to add the provenance generation workflow |
| 229 | +after every push. |
| 230 | +
|
| 231 | +%s |
| 232 | +Opens a pull request on the SLSA policy repository to check in a SLSA Source |
| 233 | +policy for the repository. |
| 234 | +
|
| 235 | +Setting up repository forks |
| 236 | +
|
| 237 | +The controls that open pull requests require that you have a fork of the |
| 238 | +repositories. Make sure you have a fork of the SLSA source policy repo and |
| 239 | +a fork of the repository you want to protect. |
| 240 | +
|
| 241 | +`, w("sourcetool setup controls"), w2("configure a repository for SLSA source"), |
| 242 | + w2("CONFIG_BRANCH_RULES"), w2("CONFIG_PROVENANCE_WORKFLOW"), w2("CONFIG_POLICY")), |
| 243 | + Use: "controls owner/repo --config=CONTROL1 --config=CONTROL2", |
| 244 | + SilenceUsage: false, |
| 245 | + SilenceErrors: true, |
| 246 | + PreRunE: func(_ *cobra.Command, args []string) error { |
| 247 | + if len(args) > 0 { |
| 248 | + if err := opts.ParseLocator(args[0]); err != nil { |
| 249 | + return err |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + // Validate early the repository options to provide a more |
| 254 | + // useful message to the user |
| 255 | + if err := opts.repoOptions.Validate(); err != nil { |
| 256 | + return err |
| 257 | + } |
| 258 | + |
| 259 | + if err := opts.EnsureDefaults(); err != nil { |
| 260 | + return err |
| 261 | + } |
| 262 | + |
| 263 | + return nil |
| 264 | + }, |
| 265 | + RunE: func(cmd *cobra.Command, args []string) (err error) { |
| 266 | + if err := opts.Validate(); err != nil { |
| 267 | + return err |
| 268 | + } |
| 269 | + |
| 270 | + // At this point options are valid, no help needed. |
| 271 | + cmd.SilenceUsage = true |
| 272 | + |
| 273 | + // Create a new sourcetool object |
| 274 | + srctool, err := sourcetool.New( |
| 275 | + sourcetool.WithOwner(opts.owner), |
| 276 | + sourcetool.WithRepo(opts.repository), |
| 277 | + sourcetool.WithBranch(opts.branch), |
| 278 | + sourcetool.WithPolicyRepo(opts.policyRepo), |
| 279 | + sourcetool.WithUserForkOrg(opts.userForkOrg), |
| 280 | + ) |
| 281 | + if err != nil { |
| 282 | + return err |
| 283 | + } |
| 284 | + cs := []sourcetool.ControlConfiguration{} |
| 285 | + if opts.interactive { |
| 286 | + fmt.Println("\nsourcetool is about to perform the following actions on your behalf:") |
| 287 | + fmt.Println("") |
| 288 | + |
| 289 | + for _, c := range opts.configs { |
| 290 | + cs = append(cs, sourcetool.ControlConfiguration(c)) |
| 291 | + fmt.Printf(" - %s.\n", srctool.ControlConfigurationDescr(sourcetool.ControlConfiguration(c))) |
| 292 | + } |
| 293 | + fmt.Println("") |
| 294 | + |
| 295 | + _, s, err := util.Ask("Type 'yes' if you want to continue?", "yes|no|no", 3) |
| 296 | + if err != nil { |
| 297 | + return err |
| 298 | + } |
| 299 | + |
| 300 | + if !s { |
| 301 | + fmt.Println("Cancelled.") |
| 302 | + return nil |
| 303 | + } |
| 304 | + } else { |
| 305 | + for _, c := range opts.configs { |
| 306 | + cs = append(cs, sourcetool.ControlConfiguration(c)) |
| 307 | + } |
| 308 | + } |
| 309 | + err = srctool.ConfigureControls( |
| 310 | + cs, sourcetool.WithEnforce(opts.enforce), |
| 311 | + ) |
| 312 | + if err != nil { |
| 313 | + return fmt.Errorf("configuring controls: %w", err) |
| 314 | + } |
| 315 | + |
| 316 | + return nil |
| 317 | + }, |
| 318 | + } |
| 319 | + opts.AddFlags(setupControlsCmd) |
| 320 | + parent.AddCommand(setupControlsCmd) |
| 321 | +} |
0 commit comments