diff --git a/cmd/podman/common/build.go b/cmd/podman/common/build.go index 804977e185..488a162bcb 100644 --- a/cmd/podman/common/build.go +++ b/cmd/podman/common/build.go @@ -7,6 +7,7 @@ import ( "maps" "os" "path/filepath" + "slices" "strconv" "strings" "syscall" @@ -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 f151dd1699..02f2802caa 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "path/filepath" + "slices" "strconv" "strings" "syscall" @@ -125,6 +126,13 @@ type BuildQuery struct { UnsetLabels []string `schema:"unsetlabel"` UnsetAnnotations []string `schema:"unsetannotation"` 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"` } // BuildContext represents processed build context and metadata for container image builds. @@ -619,6 +627,44 @@ func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues u return nil, cleanup, utils.GetBadRequestError("retry-delay", query.RetryDelay, err) } } + 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 { + return nil, cleanup, utils.GetBadRequestError("sbom-scanner-command", query.SBOMCommands, err) + } + sbomScanOption.Commands = m + } + + if !slices.Contains(sbomScanOption.ContextDir, buildCtx.ContextDirectory) { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, buildCtx.ContextDirectory) + } + + for _, abc := range buildCtx.AdditionalBuildContexts { + if !abc.IsURL && !abc.IsImage { + sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, abc.Value) + } + } + + sbomScanOptions = append(sbomScanOptions, *sbomScanOption) + } // Create build options buildOptions := &buildahDefine.BuildOptions{ @@ -702,6 +748,7 @@ func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues u UnsetEnvs: query.UnsetEnvs, UnsetLabels: query.UnsetLabels, UnsetAnnotations: query.UnsetAnnotations, + SBOMScanOptions: sbomScanOptions, } // Process platforms diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 0d2c8b25fd..8a78332698 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -670,6 +670,42 @@ func prepareRequestBody(ctx context.Context, requestParts *RequestParts, buildFi return nil, err } + if len(options.SBOMScanOptions) > 0 { + for _, sbomScanOpts := range options.SBOMScanOptions { + if sbomScanOpts.SBOMOutput != "" { + requestParts.Params.Set("sbom-output", sbomScanOpts.SBOMOutput) + } + + if sbomScanOpts.PURLOutput != "" { + requestParts.Params.Set("sbom-purl-output", sbomScanOpts.PURLOutput) + } + + if sbomScanOpts.ImageSBOMOutput != "" { + requestParts.Params.Set("sbom-image-output", sbomScanOpts.ImageSBOMOutput) + } + + if sbomScanOpts.ImagePURLOutput != "" { + requestParts.Params.Set("sbom-image-purl-output", sbomScanOpts.ImagePURLOutput) + } + + if sbomScanOpts.Image != "" { + requestParts.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 + } + requestParts.Params.Add("sbom-scanner-command", c) + } + + if sbomScanOpts.MergeStrategy != "" { + requestParts.Params.Set("sbom-merge-strategy", string(sbomScanOpts.MergeStrategy)) + } + } + } + if len(options.AdditionalBuildContexts) == 0 { requestParts.Body = tarfile logrus.Debugf("Using main build context: %q", options.ContextDirectory) diff --git a/test/e2e/build_test.go b/test/e2e/build_test.go index 95458142ce..5ad8fd0282 100644 --- a/test/e2e/build_test.go +++ b/test/e2e/build_test.go @@ -973,6 +973,24 @@ RUN ls /dev/test1`, CITEST_IMAGE) 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) + + localsbomFile := filepath.Join(podmanTest.TempDir, "localsbom.txt") + localPurlFile := filepath.Join(podmanTest.TempDir, "localpurl.txt") + + podmanTest.PodmanExitCleanly("build", "-t", "sbom-img", "--sbom-output="+localsbomFile, "--sbom-purl-output="+localPurlFile, "--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") + + Expect(localsbomFile).To(BeARegularFile()) + Expect(localPurlFile).To(BeARegularFile()) + + session := podmanTest.PodmanExitCleanly("run", "--rm", "sbom-img", "ls", "/tmp") + Expect(session.OutputToString()).To(ContainSubstring("purl.txt")) + Expect(session.OutputToString()).To(ContainSubstring("sbom.txt")) + }) + It("podman build --build-context: local source", func() { podmanTest.RestartRemoteService()