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 }