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  }