github.com/rrashidov/libpak@v0.0.0-20230911084305-75119185bb4d/sbom/sbom_test.go (about)

     1  package sbom_test
     2  
     3  import (
     4  	"io"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/buildpacks/libcnb"
    11  	. "github.com/onsi/gomega"
    12  	"github.com/paketo-buildpacks/libpak/bard"
    13  	"github.com/paketo-buildpacks/libpak/effect"
    14  	"github.com/paketo-buildpacks/libpak/effect/mocks"
    15  	"github.com/paketo-buildpacks/libpak/sbom"
    16  	"github.com/sclevine/spec"
    17  	"github.com/stretchr/testify/mock"
    18  )
    19  
    20  func testSBOM(t *testing.T, context spec.G, it spec.S) {
    21  	var (
    22  		Expect = NewWithT(t).Expect
    23  
    24  		layers   libcnb.Layers
    25  		layer    libcnb.Layer
    26  		executor mocks.Executor
    27  		scanner  sbom.SBOMScanner
    28  	)
    29  
    30  	it.Before(func() {
    31  		executor = mocks.Executor{}
    32  
    33  		layers.Path = t.TempDir()
    34  
    35  		layer = libcnb.Layer{
    36  			Path: filepath.Join(layers.Path, "layer"),
    37  			Name: "test-layer",
    38  		}
    39  
    40  		Expect(os.MkdirAll(layer.Path, 0755)).To(Succeed())
    41  	})
    42  
    43  	context("syft", func() {
    44  		it("generates artifact id", func() {
    45  			artifact := sbom.SyftArtifact{Name: "foo", Version: "1.2.3"}
    46  			ID, err := artifact.Hash()
    47  			Expect(err).ToNot(HaveOccurred())
    48  			Expect(ID).To(Equal("7f6c18a85645bd7c"))
    49  		})
    50  
    51  		it("runs syft once to generate JSON", func() {
    52  			format := libcnb.SyftJSON
    53  			outputPath := layers.BuildSBOMPath(format)
    54  
    55  			executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
    56  				return e.Command == "syft" &&
    57  					len(e.Args) == 5 &&
    58  					strings.HasPrefix(e.Args[3], "json=") &&
    59  					e.Args[4] == "dir:something"
    60  			})).Run(func(args mock.Arguments) {
    61  				Expect(os.WriteFile(outputPath, []byte("succeed1"), 0644)).To(Succeed())
    62  			}).Return(nil)
    63  
    64  			// uses interface here intentionally, to force that inteface and implementation match
    65  			scanner = sbom.NewSyftCLISBOMScanner(layers, &executor, bard.NewLogger(io.Discard))
    66  
    67  			Expect(scanner.ScanBuild("something", format)).To(Succeed())
    68  
    69  			result, err := os.ReadFile(outputPath)
    70  			Expect(err).ToNot(HaveOccurred())
    71  			Expect(string(result)).To(Equal("succeed1"))
    72  		})
    73  
    74  		it("runs syft to generate reproducible cycloneDX JSON", func() {
    75  			format := libcnb.CycloneDXJSON
    76  			outputPath := layers.BuildSBOMPath(format)
    77  
    78  			executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
    79  				return e.Command == "syft" &&
    80  					len(e.Args) == 5 &&
    81  					strings.HasPrefix(e.Args[3], "cyclonedx-json=") &&
    82  					e.Args[4] == "dir:something"
    83  			})).Run(func(args mock.Arguments) {
    84  				Expect(os.WriteFile(outputPath, []byte(`{
    85    "bomFormat": "CycloneDX",
    86    "specVersion": "1.4",
    87    "serialNumber": "urn:uuid:fcfa5e19-bf49-47b4-8c85-ab61e2728f8e",
    88    "version": 1,
    89    "metadata": {
    90      "timestamp": "2022-05-05T11:33:13-04:00",
    91      "tools": [
    92        {
    93          "vendor": "anchore",
    94          "name": "syft",
    95          "version": "0.45.1"
    96        }
    97      ],
    98      "component": {
    99        "bom-ref": "555d623e4777b7ae",
   100        "type": "file",
   101        "name": "target/demo-0.0.1-SNAPSHOT.jar"
   102      }
   103    }
   104  }`), 0644)).To(Succeed())
   105  			}).Return(nil)
   106  
   107  			// uses interface here intentionally, to force that inteface and implementation match
   108  			scanner = sbom.NewSyftCLISBOMScanner(layers, &executor, bard.NewLogger(io.Discard))
   109  
   110  			Expect(scanner.ScanBuild("something", format)).To(Succeed())
   111  
   112  			result, err := os.ReadFile(outputPath)
   113  			Expect(err).ToNot(HaveOccurred())
   114  			Expect(string(result)).ToNot(ContainSubstring("serialNumber"))
   115  			Expect(string(result)).ToNot(ContainSubstring("urn:uuid:fcfa5e19-bf49-47b4-8c85-ab61e2728f8e"))
   116  			Expect(string(result)).ToNot(ContainSubstring("timestamp"))
   117  			Expect(string(result)).ToNot(ContainSubstring("2022-05-05T11:33:13-04:00"))
   118  		})
   119  
   120  		it("runs syft once to generate layer-specific JSON", func() {
   121  			format := libcnb.SyftJSON
   122  			outputPath := layer.SBOMPath(format)
   123  
   124  			executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
   125  				return e.Command == "syft" &&
   126  					len(e.Args) == 5 &&
   127  					strings.HasPrefix(e.Args[3], "json=") &&
   128  					e.Args[4] == "dir:something"
   129  			})).Run(func(args mock.Arguments) {
   130  				Expect(os.WriteFile(outputPath, []byte("succeed2"), 0644)).To(Succeed())
   131  			}).Return(nil)
   132  
   133  			scanner := sbom.SyftCLISBOMScanner{
   134  				Executor: &executor,
   135  				Layers:   layers,
   136  				Logger:   bard.NewLogger(io.Discard),
   137  			}
   138  
   139  			Expect(scanner.ScanLayer(layer, "something", format)).To(Succeed())
   140  
   141  			result, err := os.ReadFile(outputPath)
   142  			Expect(err).ToNot(HaveOccurred())
   143  			Expect(string(result)).To(Equal("succeed2"))
   144  		})
   145  
   146  		it("runs syft once for all three formats", func() {
   147  			executor.On("Execute", mock.MatchedBy(func(e effect.Execution) bool {
   148  				return e.Command == "syft" &&
   149  					len(e.Args) == 9 &&
   150  					strings.HasPrefix(e.Args[3], sbom.SBOMFormatToSyftOutputFormat(libcnb.CycloneDXJSON)) &&
   151  					strings.HasPrefix(e.Args[5], sbom.SBOMFormatToSyftOutputFormat(libcnb.SyftJSON)) &&
   152  					strings.HasPrefix(e.Args[7], sbom.SBOMFormatToSyftOutputFormat(libcnb.SPDXJSON)) &&
   153  					e.Args[8] == "dir:something"
   154  			})).Run(func(args mock.Arguments) {
   155  				Expect(os.WriteFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON), []byte(`{"succeed":1}`), 0644)).To(Succeed())
   156  				Expect(os.WriteFile(layers.LaunchSBOMPath(libcnb.SyftJSON), []byte(`{"succeed":2}`), 0644)).To(Succeed())
   157  				Expect(os.WriteFile(layers.LaunchSBOMPath(libcnb.SPDXJSON), []byte(`{"succeed":3}`), 0644)).To(Succeed())
   158  			}).Return(nil)
   159  
   160  			scanner := sbom.SyftCLISBOMScanner{
   161  				Executor: &executor,
   162  				Layers:   layers,
   163  				Logger:   bard.NewLogger(io.Discard),
   164  			}
   165  
   166  			Expect(scanner.ScanLaunch("something", libcnb.CycloneDXJSON, libcnb.SyftJSON, libcnb.SPDXJSON)).To(Succeed())
   167  
   168  			result, err := os.ReadFile(layers.LaunchSBOMPath(libcnb.CycloneDXJSON))
   169  			Expect(err).ToNot(HaveOccurred())
   170  			Expect(string(result)).To(HavePrefix(`{"succeed":1}`))
   171  
   172  			result, err = os.ReadFile(layers.LaunchSBOMPath(libcnb.SyftJSON))
   173  			Expect(err).ToNot(HaveOccurred())
   174  			Expect(string(result)).To(HavePrefix(`{"succeed":2}`))
   175  
   176  			result, err = os.ReadFile(layers.LaunchSBOMPath(libcnb.SPDXJSON))
   177  			Expect(err).ToNot(HaveOccurred())
   178  			Expect(string(result)).To(HavePrefix(`{"succeed":3}`))
   179  		})
   180  
   181  		it("writes out a manual BOM entry", func() {
   182  			dep := sbom.SyftDependency{
   183  				Artifacts: []sbom.SyftArtifact{
   184  					{
   185  						ID:      "1234",
   186  						Name:    "test-dep",
   187  						Version: "1.2.3",
   188  						Type:    "UnknownPackage",
   189  						FoundBy: "java-buildpack",
   190  						Locations: []sbom.SyftLocation{
   191  							{Path: "/some/path"},
   192  						},
   193  						Licenses: []string{"GPL-2.0 WITH Classpath-exception-2.0"},
   194  						Language: "java",
   195  						CPEs: []string{
   196  							"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*",
   197  						},
   198  						PURL: "pkg:generic/some-java11@11.0.2?arch=amd64",
   199  					},
   200  				},
   201  				Source: sbom.SyftSource{
   202  					Type:   "directory",
   203  					Target: "path/to/layer",
   204  				},
   205  				Descriptor: sbom.SyftDescriptor{
   206  					Name:    "syft",
   207  					Version: "0.32.0",
   208  				},
   209  				Schema: sbom.SyftSchema{
   210  					Version: "1.1.0",
   211  					URL:     "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json",
   212  				},
   213  			}
   214  			outputFile := filepath.Join(layers.Path, "test-bom.json")
   215  			Expect(dep.WriteTo(outputFile)).To(Succeed())
   216  			Expect(outputFile).To(BeARegularFile())
   217  
   218  			data, err := os.ReadFile(outputFile)
   219  			Expect(err).ToNot(HaveOccurred())
   220  			Expect(string(data)).To(ContainSubstring(`"Artifacts":[`))
   221  			Expect(string(data)).To(ContainSubstring(`"FoundBy":"java-buildpack",`))
   222  			Expect(string(data)).To(ContainSubstring(`"PURL":"pkg:generic/some-java11@11.0.2?arch=amd64"`))
   223  			Expect(string(data)).To(ContainSubstring(`"Schema":{`))
   224  			Expect(string(data)).To(ContainSubstring(`"Descriptor":{`))
   225  			Expect(string(data)).To(ContainSubstring(`"Source":{`))
   226  		})
   227  
   228  		it("writes out a manual BOM entry with help", func() {
   229  			dep := sbom.NewSyftDependency("path/to/layer", []sbom.SyftArtifact{
   230  				{
   231  					ID:      "1234",
   232  					Name:    "test-dep",
   233  					Version: "1.2.3",
   234  					Type:    "UnknownPackage",
   235  					FoundBy: "java-buildpack",
   236  					Locations: []sbom.SyftLocation{
   237  						{Path: "/some/path"},
   238  					},
   239  					Licenses: []string{"GPL-2.0 WITH Classpath-exception-2.0"},
   240  					Language: "java",
   241  					CPEs: []string{
   242  						"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*",
   243  					},
   244  					PURL: "pkg:generic/some-java11@11.0.2?arch=amd64",
   245  				},
   246  			})
   247  
   248  			outputFile := filepath.Join(layers.Path, "test-bom.json")
   249  			Expect(dep.WriteTo(outputFile)).To(Succeed())
   250  			Expect(outputFile).To(BeARegularFile())
   251  
   252  			data, err := os.ReadFile(outputFile)
   253  			Expect(err).ToNot(HaveOccurred())
   254  			Expect(string(data)).To(ContainSubstring(`"Artifacts":[`))
   255  			Expect(string(data)).To(ContainSubstring(`"FoundBy":"java-buildpack",`))
   256  			Expect(string(data)).To(ContainSubstring(`"PURL":"pkg:generic/some-java11@11.0.2?arch=amd64"`))
   257  			Expect(string(data)).To(ContainSubstring(`"Schema":{`))
   258  			Expect(string(data)).To(ContainSubstring(`"Descriptor":{`))
   259  			Expect(string(data)).To(ContainSubstring(`"Source":{`))
   260  		})
   261  	})
   262  
   263  }