diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index 2f46683498..9bde351b52 100644 --- a/cmd/podman/common/build.go +++ b/cmd/podman/common/build.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "slices" "strings" "syscall" "time" @@ -514,6 +515,24 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *Buil } } + var sbomScanOptions []buildahDefine.SBOMScanOptions + if c.Flag("sbom").Changed || c.Flag("sbom-scanner-command").Changed || c.Flag("sbom-scanner-image").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-merge-strategy").Changed || c.Flag("sbom-output").Changed || c.Flag("sbom-image-output").Changed || c.Flag("sbom-purl-output").Changed || c.Flag("sbom-image-purl-output").Changed { + sbomScanOption, err := parse.SBOMScanOptions(c) + if err != nil { + return nil, err + } + if !slices.Contains(sbomScanOption.ContextDir, contextDir) { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, contextDir) + } + for _, abc := range additionalBuildContext { + if !abc.IsURL && !abc.IsImage { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, abc.Value) + } + } + sbomScanOption.PullPolicy = pullPolicy + sbomScanOptions = append(sbomScanOptions, *sbomScanOption) + } + opts := buildahDefine.BuildOptions{ AddCapabilities: flags.CapAdd, AdditionalTags: tags, @@ -570,6 +589,7 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *Buil Runtime: podmanConfig.RuntimePath, RuntimeArgs: runtimeFlags, RusageLogFile: flags.RusageLogFile, + SBOMScanOptions: sbomScanOptions, SignBy: flags.SignBy, SignaturePolicyPath: flags.SignaturePolicy, Squash: flags.Squash, diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 7c0b7d09ed..3be691fe58 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "syscall" @@ -171,6 +172,13 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { UnsetEnvs []string `schema:"unsetenv"` UnsetLabels []string `schema:"unsetlabel"` Volumes []string `schema:"volume"` + SBOMOutput string `schema:"sbom-output"` + SBOMPURLOutput string `schema:"sbom-purl-output"` + ImageSBOMOutput string `schema:"sbom-image-output"` + ImageSBOMPURLOutput string `schema:"sbom-image-purl-output"` + ImageSBOM string `schema:"sbom-scanner-image"` + SBOMCommands string `schema:"sbom-scanner-command"` + SBOMMergeStrategy string `schema:"sbom-merge-strategy"` }{ Dockerfile: "Dockerfile", IdentityLabel: true, @@ -693,6 +701,46 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } } + var sbomScanOptions []buildahDefine.SBOMScanOptions + if query.ImageSBOM != "" || + query.SBOMOutput != "" || + query.ImageSBOMOutput != "" || + query.SBOMPURLOutput != "" || + query.ImageSBOMPURLOutput != "" || + query.SBOMCommands != "" || + query.SBOMMergeStrategy != "" { + sbomScanOption := &buildahDefine.SBOMScanOptions{ + SBOMOutput: query.SBOMOutput, + PURLOutput: query.SBOMPURLOutput, + ImageSBOMOutput: query.ImageSBOMOutput, + ImagePURLOutput: query.ImageSBOMPURLOutput, + Image: query.ImageSBOM, + MergeStrategy: buildahDefine.SBOMMergeStrategy(query.SBOMMergeStrategy), + PullPolicy: pullPolicy, + } + + if _, found := r.URL.Query()["sbom-scanner-command"]; found { + var m = []string{} + if err := json.Unmarshal([]byte(query.SBOMCommands), &m); err != nil { + utils.BadRequest(w, "sbom-scanner-command", query.SBOMCommands, err) + return + } + sbomScanOption.Commands = m + } + + if !slices.Contains(sbomScanOption.ContextDir, contextDirectory) { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, contextDirectory) + } + + for _, abc := range additionalBuildContexts { + if !abc.IsURL && !abc.IsImage { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, abc.Value) + } + } + + sbomScanOptions = append(sbomScanOptions, *sbomScanOption) + } + buildOptions := buildahDefine.BuildOptions{ AddCapabilities: addCaps, AdditionalBuildContexts: additionalBuildContexts, @@ -772,6 +820,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Target: query.Target, UnsetEnvs: query.UnsetEnvs, UnsetLabels: query.UnsetLabels, + SBOMScanOptions: sbomScanOptions, } platforms := query.Platform diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 417eeb1603..3ff3d8195c 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -483,6 +483,42 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti stdout = options.Out } + if len(options.SBOMScanOptions) > 0 { + for _, sbomScanOpts := range options.SBOMScanOptions { + if sbomScanOpts.SBOMOutput != "" { + params.Set("sbom-output", sbomScanOpts.SBOMOutput) + } + + if sbomScanOpts.PURLOutput != "" { + params.Set("sbom-purl-output", sbomScanOpts.PURLOutput) + } + + if sbomScanOpts.ImageSBOMOutput != "" { + params.Set("sbom-image-output", sbomScanOpts.ImageSBOMOutput) + } + + if sbomScanOpts.ImagePURLOutput != "" { + params.Set("sbom-image-purl-output", sbomScanOpts.ImagePURLOutput) + } + + if sbomScanOpts.Image != "" { + params.Set("sbom-scanner-image", sbomScanOpts.Image) + } + + if commands := sbomScanOpts.Commands; len(commands) > 0 { + c, err := jsoniter.MarshalToString(commands) + if err != nil { + return nil, err + } + params.Add("sbom-scanner-command", c) + } + + if sbomScanOpts.MergeStrategy != "" { + params.Set("sbom-merge-strategy", string(sbomScanOpts.MergeStrategy)) + } + } + } + contextDir, err = filepath.Abs(options.ContextDirectory) if err != nil { logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err) diff --git a/test/e2e/build_test.go b/test/e2e/build_test.go index fe7ef8f10c..4f35f458bb 100644 --- a/test/e2e/build_test.go +++ b/test/e2e/build_test.go @@ -4,7 +4,9 @@ package integration import ( "bytes" + "errors" "fmt" + "io/fs" "os" "os/exec" "path/filepath" @@ -972,4 +974,25 @@ RUN ls /dev/test1`, CITEST_IMAGE) session.WaitWithDefaultTimeout() Expect(session).Should(ExitWithError(1, `building at STEP "RUN --mount=type=cache,target=/test,z cat /test/world": while running runtime: exit status 1`)) }) + It("podman build with sbom flags", func() { + podmanTest.AddImageToRWStore(ALPINE) + + podmanTest.PodmanExitCleanly("build", "-t", "sbom-img", "--sbom-output=localsbom.txt", "--sbom-purl-output=localpurl.txt", "--sbom-image-output=/tmp/sbom.txt", "--sbom-image-purl-output=/tmp/purl.txt", + "--sbom-scanner-image=alpine", "--sbom-scanner-command=/bin/sh -c 'echo SCANNED ROOT {ROOTFS} > {OUTPUT}'", "--sbom-scanner-command=/bin/sh -c 'echo SCANNED BUILD CONTEXT {CONTEXT} > {OUTPUT}'", + "--sbom-merge-strategy=cat", "build/basicalpine") + + defer os.Remove("./localsbom.txt") + if _, err := os.Stat("./localsbom.txt"); err != nil { + Expect(errors.Is(err, fs.ErrNotExist)).To(BeFalse()) + } + + defer os.Remove("./localpurl.txt") + if _, err := os.Stat("./localpurl.txt"); err != nil { + Expect(errors.Is(err, fs.ErrNotExist)).To(BeFalse()) + } + + session := podmanTest.PodmanExitCleanly("run", "--rm", "sbom-img", "ls", "/tmp") + Expect(session.OutputToString()).To(ContainSubstring("purl.txt")) + Expect(session.OutputToString()).To(ContainSubstring("sbom.txt")) + }) })