github.com/yogeshkumararora/slsa-github-generator@v1.10.1-0.20240520161934-11278bd5afb4/internal/builders/docker/commands.go (about) 1 // Copyright 2022 SLSA Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 // This file contains definitions of the subcommands of the 18 // `slsa-container-based-generator` command. 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "io" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 "github.com/spf13/cobra" 31 32 "github.com/yogeshkumararora/slsa-github-generator/internal/builders/docker/pkg" 33 "github.com/yogeshkumararora/slsa-github-generator/internal/utils" 34 ) 35 36 // DryRunCmd returns a new *cobra.Command that validates the input flags, and 37 // generates a BuildDefinition from them, or terminates with an error. 38 func DryRunCmd(check func(error)) *cobra.Command { 39 inputOptions := &pkg.InputOptions{} 40 var buildDefinitionPath string 41 42 cmd := &cobra.Command{ 43 Use: "dry-run [FLAGS]", 44 Short: "Generates and stores a JSON-formatted BuildDefinition based on the input arguments.", 45 Run: func(_ *cobra.Command, _ []string) { 46 w, err := utils.CreateNewFileUnderCurrentDirectory(buildDefinitionPath, os.O_WRONLY) 47 check(err) 48 49 config, err := pkg.NewDockerBuildConfig(inputOptions) 50 check(err) 51 52 builder, err := pkg.NewBuilderWithGitFetcher(config) 53 check(err) 54 55 db, err := builder.SetUpBuildState() 56 check(err) 57 // Remove any temporary files that were fetched during the setup. 58 defer db.RepoInfo.Cleanup() 59 60 check(writeJSONToFile(*db.CreateBuildDefinition(), w)) 61 }, 62 } 63 64 inputOptions.AddFlags(cmd) 65 cmd.Flags().StringVarP(&buildDefinitionPath, "build-definition-path", "o", "", 66 "Required - Path to store the generated BuildDefinition to.") 67 68 return cmd 69 } 70 71 // BuildCmd returns a new *cobra.Command that builds the artifacts using the 72 // input flags, and prints out their digests, or terminates with an error. 73 func BuildCmd(check func(error)) *cobra.Command { 74 inputOptions := &pkg.InputOptions{} 75 var subjectsPath string 76 var outputFolder string 77 78 cmd := &cobra.Command{ 79 Use: "build [FLAGS]", 80 Short: "Builds the artifacts using the build config, source repo, and the builder image.", 81 Run: func(_ *cobra.Command, _ []string) { 82 // Validate that the output folder is a /tmp subfolder. 83 absoluteOutputFolder, err := filepath.Abs(outputFolder) 84 check(err) 85 if !strings.HasPrefix(filepath.Dir(absoluteOutputFolder), "/tmp") { 86 check(fmt.Errorf("output folder must be in /tmp: %s", absoluteOutputFolder)) 87 } 88 check(pkg.CheckExistingFiles(absoluteOutputFolder)) 89 90 w, err := utils.CreateNewFileUnderCurrentDirectory(subjectsPath, os.O_WRONLY) 91 check(err) 92 config, err := pkg.NewDockerBuildConfig(inputOptions) 93 check(err) 94 95 builder, err := pkg.NewBuilderWithGitFetcher(config) 96 check(err) 97 98 db, err := builder.SetUpBuildState() 99 check(err) 100 // Remove any temporary files that were generated during the setup. 101 defer db.RepoInfo.Cleanup() 102 103 // Build artifacts and write them to the output folder. 104 artifacts, err := db.BuildArtifacts(absoluteOutputFolder) 105 check(err) 106 check(writeJSONToFile(artifacts, w)) 107 }, 108 } 109 110 inputOptions.AddFlags(cmd) 111 cmd.Flags().StringVarP(&subjectsPath, "subjects-path", "o", "", 112 "Required - Path to store a JSON-encoded array of subjects of the generated artifacts.") 113 cmd.Flags().StringVar(&outputFolder, "output-folder", "", 114 "Required - Path to a folder to store the generated artifacts. MUST be under /tmp.") 115 check(cmd.MarkFlagRequired("output-folder")) 116 117 return cmd 118 } 119 120 // VerifyCmd returns a new *cobra.Command that takes a provenance file, and 121 // verifies it by running the build steps and comparing the generated artifacts 122 // to the subject of the provenance file. 123 func VerifyCmd(check func(error)) *cobra.Command { 124 var provenancePath string 125 126 cmd := &cobra.Command{ 127 Use: "verify [FLAGS]", 128 Short: "Verifies as SLSLv1.0 provenance.", 129 Run: func(_ *cobra.Command, _ []string) { 130 err := verifyProvenance(provenancePath) 131 check(err) 132 }, 133 } 134 135 cmd.Flags().StringVarP(&provenancePath, "provenance-path", "o", "", 136 "Required - Path to the input provenance file.") 137 138 return cmd 139 } 140 141 func verifyProvenance(provenancePath string) error { 142 // Note: We can use os.ReadFile here directly without checking for directory 143 // traversal. This is a verification tool, and not used by the build 144 // workflows. 145 bytes, err := os.ReadFile(provenancePath) 146 if err != nil { 147 return fmt.Errorf("reading provenance file: %w", err) 148 } 149 150 provenance, err := pkg.ParseProvenance(bytes) 151 if err != nil { 152 return fmt.Errorf("parsing provenance file: %w", err) 153 } 154 155 config, err := provenance.ToDockerBuildConfig(true) 156 if err != nil { 157 return fmt.Errorf("creating DockerBuildConfig from provenance: %w", err) 158 } 159 160 builder, err := pkg.NewBuilderWithGitFetcher(config) 161 if err != nil { 162 return fmt.Errorf("creating BuilderWithGitFetcher: %w", err) 163 } 164 165 db, err := builder.SetUpBuildState() 166 if err != nil { 167 return fmt.Errorf("setting up the build state: %w", err) 168 } 169 // Remove any temporary files that were fetched during the setup. 170 defer db.RepoInfo.Cleanup() 171 172 // Build artifacts and get their digests. 173 artifacts, err := db.BuildArtifacts("") 174 if err != nil { 175 return fmt.Errorf("building the artifacts: %w", err) 176 } 177 178 less := func(a, b string) bool { return a < b } 179 diff := cmp.Diff(artifacts, provenance.Subject, cmpopts.SortSlices(less)) 180 if diff != "" { 181 return fmt.Errorf("comparing the subjects artifacts: %w", err) 182 } 183 184 return nil 185 } 186 187 func writeJSONToFile[T any](obj T, w io.Writer) error { 188 bytes, err := json.Marshal(obj) 189 if err != nil { 190 return fmt.Errorf("marshaling the object failed: %w", err) 191 } 192 193 if _, err := w.Write(bytes); err != nil { 194 return fmt.Errorf("writing to file failed: %w", err) 195 } 196 return nil 197 }