diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index dec5133b82..4adb0173d6 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" @@ -515,6 +516,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, @@ -571,6 +590,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 452f201037..3a691f6639 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" @@ -172,6 +173,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, @@ -694,6 +702,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, @@ -774,6 +822,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 d73507709d..b6936f713c 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -489,6 +489,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/pkg/machine/e2e/basic_test.go b/pkg/machine/e2e/basic_test.go index 695327a56d..b10b432903 100644 --- a/pkg/machine/e2e/basic_test.go +++ b/pkg/machine/e2e/basic_test.go @@ -285,6 +285,47 @@ var _ = Describe("run basic podman commands", func() { Expect(run).To(Exit(0)) Expect(build.outputToString()).To(ContainSubstring(name)) }) + + It("podman build with sbom flags", func() { + + name := randomString() + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withNow()).run() + Expect(err).ToNot(HaveOccurred()) + Expect(session).To(Exit(0)) + + ALPINE := "quay.io/libpod/alpine:latest" + bm := basicMachine{} + + contextDir := GinkgoT().TempDir() + cfile := filepath.Join(contextDir, "Containerfile") + err = os.WriteFile(cfile, []byte("FROM "+ALPINE), 0o644) + Expect(err).ToNot(HaveOccurred()) + + build, err := mb.setCmd(bm.withPodmanCommand([]string{"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", contextDir})).run() + + Expect(err).ToNot(HaveOccurred()) + Expect(build).To(Exit(0)) + + // 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()) + // } + + run, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "--rm", "sbom-img", "ls", "/tmp"})).run() + fmt.Println("ALEX TEST: " + run.outputToString()) + Expect(err).ToNot(HaveOccurred()) + Expect(run).To(Exit(0)) + // Expect(run.outputToString()).To(ContainSubstring("purl.txt")) + // Expect(run.outputToString()).To(ContainSubstring("sbom.txt")) + }) }) func testHTTPServer(port string, shouldErr bool, expectedResponse string) { 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")) + }) })