diff --git a/sbom/sbom.go b/sbom/sbom.go index 7154215..475fe1b 100644 --- a/sbom/sbom.go +++ b/sbom/sbom.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "os" "github.com/buildpacks/libcnb" "github.com/mitchellh/hashstructure/v2" @@ -139,38 +138,20 @@ func (b SyftCLISBOMScanner) ScanLaunch(scanDir string, formats ...libcnb.SBOMFor } func (b SyftCLISBOMScanner) scan(sbomPathCreator func(libcnb.SBOMFormat) string, scanDir string, formats ...libcnb.SBOMFormat) error { - // syft doesn't presently support outputting multiple formats at once - // to workaround this we are running syft multiple times - // when syft supports multiple output formats or conversion between formats, this method should change - for _, format := range formats { - sbomLocation := sbomPathCreator(format) + args := []string{"packages", "-q"} - if err := b.runSyft(sbomLocation, scanDir, format); err != nil { - return fmt.Errorf("unable to run syft\n%w", err) - } + for _, format := range formats { + args = append(args, "-o", fmt.Sprintf("%s=%s", SBOMFormatToSyftOutputFormat(format), sbomPathCreator(format))) } - return nil -} + args = append(args, fmt.Sprintf("dir:%s", scanDir)) -func (b SyftCLISBOMScanner) runSyft(sbomOutputPath string, scanDir string, format libcnb.SBOMFormat) error { - writer, err := os.Create(sbomOutputPath) - if err != nil { - return fmt.Errorf("unable to open output BOM file %s\n%w", sbomOutputPath, err) - } - defer writer.Close() - - err = b.Executor.Execute(effect.Execution{ + return b.Executor.Execute(effect.Execution{ Command: "syft", - Args: []string{"packages", "-q", "-o", SBOMFormatToSyftOutputFormat(format), fmt.Sprintf("dir:%s", scanDir)}, - Stdout: writer, + Args: args, + Stdout: b.Logger.TerminalErrorWriter(), Stderr: b.Logger.TerminalErrorWriter(), }) - if err != nil { - return fmt.Errorf("unable to run syft on directory %s\n%w", scanDir, err) - } - - return nil } // SBOMFormatToSyftOutputFormat converts a libcnb.SBOMFormat to the syft matching syft output format string diff --git a/sbom/sbom_test.go b/sbom/sbom_test.go index c06b4ee..0b29e1e 100644 --- a/sbom/sbom_test.go +++ b/sbom/sbom_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/buildpacks/libcnb" @@ -62,7 +63,7 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { return e.Command == "syft" && len(e.Args) == 5 && - e.Args[3] == "json" && + strings.HasPrefix(e.Args[3], "json=") && e.Args[4] == "dir:something" })).Run(func(args mock.Arguments) { Expect(ioutil.WriteFile(outputPath, []byte("succeed1"), 0644)).To(Succeed()) @@ -85,7 +86,7 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { return e.Command == "syft" && len(e.Args) == 5 && - e.Args[3] == "json" && + strings.HasPrefix(e.Args[3], "json=") && e.Args[4] == "dir:something" })).Run(func(args mock.Arguments) { Expect(ioutil.WriteFile(outputPath, []byte("succeed2"), 0644)).To(Succeed()) @@ -104,35 +105,39 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { Expect(string(result)).To(Equal("succeed2")) }) - it("runs syft thrice, once per format", func() { - outputPaths := map[libcnb.SBOMFormat]string{ - libcnb.SPDXJSON: layers.LaunchSBOMPath(libcnb.SPDXJSON), - libcnb.SyftJSON: layers.LaunchSBOMPath(libcnb.SyftJSON), - libcnb.CycloneDXJSON: layers.LaunchSBOMPath(libcnb.CycloneDXJSON), - } + it("runs syft once for all three formats", func() { + executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { + return e.Command == "syft" && + len(e.Args) == 9 && + strings.HasPrefix(e.Args[3], sbom.SBOMFormatToSyftOutputFormat(libcnb.CycloneDXJSON)) && + strings.HasPrefix(e.Args[5], sbom.SBOMFormatToSyftOutputFormat(libcnb.SyftJSON)) && + strings.HasPrefix(e.Args[7], sbom.SBOMFormatToSyftOutputFormat(libcnb.SPDXJSON)) && + e.Args[8] == "dir:something" + })).Run(func(args mock.Arguments) { + Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON), []byte("succeed1"), 0644)).To(Succeed()) + Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SyftJSON), []byte("succeed2"), 0644)).To(Succeed()) + Expect(ioutil.WriteFile(layers.LaunchSBOMPath(libcnb.SPDXJSON), []byte("succeed3"), 0644)).To(Succeed()) + }).Return(nil) - for format, outputPath := range outputPaths { - executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool { - return e.Command == "syft" && - len(e.Args) == 5 && - e.Args[3] == sbom.SBOMFormatToSyftOutputFormat(format) && - e.Args[4] == "dir:something" - })).Run(func(args mock.Arguments) { - Expect(ioutil.WriteFile(outputPath, []byte("succeed3"), 0644)).To(Succeed()) - }).Return(nil) - - scanner := sbom.SyftCLISBOMScanner{ - Executor: &executor, - Layers: layers, - Logger: bard.NewLogger(io.Discard), - } - - Expect(scanner.ScanLaunch("something", format)).To(Succeed()) - - result, err := ioutil.ReadFile(outputPath) - Expect(err).ToNot(HaveOccurred()) - Expect(string(result)).To(Equal("succeed3")) + scanner := sbom.SyftCLISBOMScanner{ + Executor: &executor, + Layers: layers, + Logger: bard.NewLogger(io.Discard), } + + Expect(scanner.ScanLaunch("something", libcnb.CycloneDXJSON, libcnb.SyftJSON, libcnb.SPDXJSON)).To(Succeed()) + + result, err := ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON)) + Expect(err).ToNot(HaveOccurred()) + Expect(string(result)).To(Equal("succeed1")) + + result, err = ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.SyftJSON)) + Expect(err).ToNot(HaveOccurred()) + Expect(string(result)).To(Equal("succeed2")) + + result, err = ioutil.ReadFile(layers.LaunchSBOMPath(libcnb.SPDXJSON)) + Expect(err).ToNot(HaveOccurred()) + Expect(string(result)).To(Equal("succeed3")) }) it("writes out a manual BOM entry", func() {