github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go (about) 1 package javascript 2 3 import ( 4 "context" 5 "io" 6 "net/http" 7 "net/http/httptest" 8 "os" 9 "testing" 10 11 "github.com/anchore/syft/syft/artifact" 12 "github.com/anchore/syft/syft/file" 13 "github.com/anchore/syft/syft/pkg" 14 "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" 15 ) 16 17 func TestParsePnpmLock(t *testing.T) { 18 var expectedRelationships []artifact.Relationship 19 fixture := "test-fixtures/pnpm/pnpm-lock.yaml" 20 21 locationSet := file.NewLocationSet(file.NewLocation(fixture)) 22 23 expectedPkgs := []pkg.Package{ 24 { 25 Name: "nanoid", 26 Version: "3.3.4", 27 PURL: "pkg:npm/nanoid@3.3.4", 28 Locations: locationSet, 29 Language: pkg.JavaScript, 30 Type: pkg.NpmPkg, 31 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}}, 32 }, 33 { 34 Name: "picocolors", 35 Version: "1.0.0", 36 PURL: "pkg:npm/picocolors@1.0.0", 37 Locations: locationSet, 38 Language: pkg.JavaScript, 39 Type: pkg.NpmPkg, 40 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}}, 41 }, 42 { 43 Name: "source-map-js", 44 Version: "1.0.2", 45 PURL: "pkg:npm/source-map-js@1.0.2", 46 Locations: locationSet, 47 Language: pkg.JavaScript, 48 Type: pkg.NpmPkg, 49 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{}}, 50 }, 51 { 52 Name: "@bcoe/v8-coverage", 53 Version: "0.2.3", 54 PURL: "pkg:npm/%40bcoe/v8-coverage@0.2.3", 55 Locations: locationSet, 56 Language: pkg.JavaScript, 57 Type: pkg.NpmPkg, 58 Metadata: pkg.PnpmLockEntry{ 59 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="}, 60 Dependencies: map[string]string{}, 61 }, 62 }, 63 } 64 65 adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) 66 pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) 67 } 68 69 func TestParsePnpmV6Lock(t *testing.T) { 70 var expectedRelationships []artifact.Relationship 71 fixture := "test-fixtures/pnpm-v6/pnpm-lock.yaml" 72 73 locationSet := file.NewLocationSet(file.NewLocation(fixture)) 74 75 expectedPkgs := []pkg.Package{ 76 { 77 Name: "@testing-library/jest-dom", 78 Version: "5.16.5", 79 PURL: "pkg:npm/%40testing-library/jest-dom@5.16.5", 80 Locations: locationSet, 81 Language: pkg.JavaScript, 82 Type: pkg.NpmPkg, 83 Metadata: pkg.PnpmLockEntry{ 84 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA=="}, 85 Dependencies: map[string]string{ 86 "@adobe/css-tools": "4.2.0", 87 "@babel/runtime": "7.21.0", 88 "@types/testing-library__jest-dom": "5.14.5", 89 "aria-query": "5.1.3", 90 "chalk": "3.0.0", 91 "css.escape": "1.5.1", 92 "dom-accessibility-api": "0.5.16", 93 "lodash": "4.17.21", 94 "redent": "3.0.0", 95 }}, 96 }, 97 { 98 Name: "@testing-library/react", 99 Version: "13.4.0", 100 PURL: "pkg:npm/%40testing-library/react@13.4.0", 101 Locations: locationSet, 102 Language: pkg.JavaScript, 103 Type: pkg.NpmPkg, 104 Metadata: pkg.PnpmLockEntry{ 105 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw=="}, 106 Dependencies: map[string]string{ 107 "@babel/runtime": "7.21.0", 108 "@testing-library/dom": "8.20.0", 109 "@types/react-dom": "18.2.1", 110 "react": "18.2.0", 111 "react-dom": "18.2.0", 112 }, 113 }, 114 }, 115 { 116 Name: "@testing-library/user-event", 117 Version: "13.5.0", 118 PURL: "pkg:npm/%40testing-library/user-event@13.5.0", 119 Locations: locationSet, 120 Language: pkg.JavaScript, 121 Type: pkg.NpmPkg, 122 Metadata: pkg.PnpmLockEntry{ 123 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg=="}, 124 Dependencies: map[string]string{ 125 "@babel/runtime": "7.21.0", 126 "@testing-library/dom": "9.2.0", 127 }, 128 }, 129 }, 130 { 131 Name: "react", 132 Version: "18.2.0", 133 PURL: "pkg:npm/react@18.2.0", 134 Locations: locationSet, 135 Language: pkg.JavaScript, 136 Type: pkg.NpmPkg, 137 Metadata: pkg.PnpmLockEntry{ 138 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="}, 139 Dependencies: map[string]string{ 140 "loose-envify": "1.4.0", 141 }, 142 }, 143 }, 144 { 145 Name: "react-dom", 146 Version: "18.2.0", 147 PURL: "pkg:npm/react-dom@18.2.0", 148 Locations: locationSet, 149 Language: pkg.JavaScript, 150 Type: pkg.NpmPkg, 151 Metadata: pkg.PnpmLockEntry{ 152 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="}, 153 Dependencies: map[string]string{ 154 "loose-envify": "1.4.0", 155 "react": "18.2.0", 156 "scheduler": "0.23.0", 157 }, 158 }, 159 }, 160 { 161 Name: "web-vitals", 162 Version: "2.1.4", 163 PURL: "pkg:npm/web-vitals@2.1.4", 164 Locations: locationSet, 165 Language: pkg.JavaScript, 166 Type: pkg.NpmPkg, 167 Metadata: pkg.PnpmLockEntry{ 168 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="}, 169 Dependencies: map[string]string{}, 170 }, 171 }, 172 { 173 Name: "@babel/core", 174 Version: "7.21.4", 175 PURL: "pkg:npm/%40babel/core@7.21.4", 176 Locations: locationSet, 177 Language: pkg.JavaScript, 178 Type: pkg.NpmPkg, 179 Metadata: pkg.PnpmLockEntry{ 180 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA=="}, 181 Dependencies: map[string]string{ 182 "@ampproject/remapping": "2.2.1", 183 "@babel/code-frame": "7.21.4", 184 "@babel/generator": "7.21.4", 185 "@babel/helper-compilation-targets": "7.21.4", 186 "@babel/helper-module-transforms": "7.21.2", 187 "@babel/helpers": "7.21.0", 188 "@babel/parser": "7.21.4", 189 "@babel/template": "7.20.7", 190 "@babel/traverse": "7.21.4", 191 "@babel/types": "7.21.4", 192 "convert-source-map": "1.9.0", 193 "debug": "4.3.4", 194 "gensync": "1.0.0-beta.2", 195 "json5": "2.2.3", 196 "semver": "6.3.0", 197 }, 198 }, 199 }, 200 { 201 Name: "@types/eslint", 202 Version: "8.37.0", 203 PURL: "pkg:npm/%40types/eslint@8.37.0", 204 Locations: locationSet, 205 Language: pkg.JavaScript, 206 Type: pkg.NpmPkg, 207 Metadata: pkg.PnpmLockEntry{ 208 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ=="}, 209 Dependencies: map[string]string{ 210 "@types/estree": "1.0.1", 211 "@types/json-schema": "7.0.11", 212 }, 213 }, 214 }, 215 { 216 Name: "read-cache", 217 Version: "1.0.0", 218 PURL: "pkg:npm/read-cache@1.0.0", 219 Locations: locationSet, 220 Language: pkg.JavaScript, 221 Type: pkg.NpmPkg, 222 Metadata: pkg.PnpmLockEntry{ 223 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="}, 224 Dependencies: map[string]string{ 225 "pify": "2.3.0", 226 }, 227 }, 228 }, 229 { 230 Name: "schema-utils", 231 Version: "3.1.2", 232 PURL: "pkg:npm/schema-utils@3.1.2", 233 Locations: locationSet, 234 Language: pkg.JavaScript, 235 Type: pkg.NpmPkg, 236 Metadata: pkg.PnpmLockEntry{ 237 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg=="}, 238 Dependencies: map[string]string{ 239 "@types/json-schema": "7.0.11", 240 "ajv": "6.12.6", 241 "ajv-keywords": "3.5.2", 242 }, 243 }, 244 }, 245 } 246 expectedRelationships = []artifact.Relationship{ 247 { 248 From: expectedPkgs[3], 249 To: expectedPkgs[1], 250 Type: artifact.DependencyOfRelationship, 251 }, 252 { 253 From: expectedPkgs[3], 254 To: expectedPkgs[4], 255 Type: artifact.DependencyOfRelationship, 256 }, 257 { 258 From: expectedPkgs[4], 259 To: expectedPkgs[1], 260 Type: artifact.DependencyOfRelationship, 261 }, 262 } 263 adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) 264 pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) 265 } 266 267 func TestParsePnpmLockV9(t *testing.T) { 268 var expectedRelationships []artifact.Relationship 269 fixture := "test-fixtures/pnpm-v9/pnpm-lock.yaml" 270 locationSet := file.NewLocationSet(file.NewLocation(fixture)) 271 272 expected := []pkg.Package{ 273 { 274 Name: "@babel/core", 275 Version: "7.24.7", 276 PURL: "pkg:npm/%40babel/core@7.24.7", 277 Locations: locationSet, 278 Language: pkg.JavaScript, 279 Type: pkg.NpmPkg, 280 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-4RjkiFFI42+268iBv2nC+iMLTJGQW3u9P7YvA3x/6MDrJ9IYZ8I/xx5a2GIhY5gBTOcI4iC5S5in2fGjE+P4Yw=="}}, 281 }, 282 { 283 Name: "@babel/helper-plugin-utils", 284 Version: "7.24.7", 285 PURL: "pkg:npm/%40babel/helper-plugin-utils@7.24.7", 286 Locations: locationSet, 287 Language: pkg.JavaScript, 288 Type: pkg.NpmPkg, 289 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-8A2+zKm53/3w4rwbX11FMW/yFS6c5Vam02P/dw01aK6KbwkKqBaIt3eEATiKtn9I2uS1itk8/aZ2yZ/kURee4Q=="}}, 290 }, 291 { 292 Name: "is-positive", 293 Version: "3.1.0", 294 PURL: "pkg:npm/is-positive@3.1.0", 295 Locations: locationSet, 296 Language: pkg.JavaScript, 297 Type: pkg.NpmPkg, 298 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-9ffLCf_f5sopimAhg2g91a7b9Rw5A1aA9eI6S391S3VEzYw99I3iKjcZGxLp25s0cRxNBV5aL2mhn7421SSlA=="}}, 299 }, 300 { 301 Name: "rollup", 302 Version: "4.18.0", 303 PURL: "pkg:npm/rollup@4.18.0", 304 Locations: locationSet, 305 Language: pkg.JavaScript, 306 Type: pkg.NpmPkg, 307 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-QpQY2Q5i0y0Q3RoAvoChE/R5iN2k05N//bNvQbC2XvRjHFT1qWJ2r3n1bNqE+gGRJaeuQf0BxE42D7CyuLh3ZQ=="}}, 308 }, 309 } 310 adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) 311 // TODO: no relationships are under test 312 pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expected, expectedRelationships) 313 } 314 315 func TestParsePnpmLockV9WithDependencies(t *testing.T) { 316 adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) 317 fixture := "test-fixtures/pnpm-v9-snapshots/pnpm-lock.yaml" 318 locationSet := file.NewLocationSet(file.NewLocation(fixture)) 319 expectedPkgs := []pkg.Package{ 320 { 321 Name: "cross-spawn", 322 Version: "7.0.6", 323 PURL: "pkg:npm/cross-spawn@7.0.6", 324 Locations: locationSet, 325 Language: pkg.JavaScript, 326 Type: pkg.NpmPkg, 327 Metadata: pkg.PnpmLockEntry{ 328 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="}, 329 Dependencies: map[string]string{ 330 "path-key": "3.1.1", 331 "shebang-command": "2.0.0", 332 "which": "2.0.2", 333 }, 334 }, 335 }, 336 { 337 Name: "isexe", 338 Version: "2.0.0", 339 PURL: "pkg:npm/isexe@2.0.0", 340 Locations: locationSet, 341 Language: pkg.JavaScript, 342 Type: pkg.NpmPkg, 343 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="}, 344 Dependencies: map[string]string{}, 345 }, 346 }, 347 { 348 Name: "path-key", 349 Version: "3.1.1", 350 PURL: "pkg:npm/path-key@3.1.1", 351 Locations: locationSet, 352 Language: pkg.JavaScript, 353 Type: pkg.NpmPkg, 354 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="}, 355 Dependencies: map[string]string{}, 356 }, 357 }, 358 { 359 Name: "shebang-command", 360 Version: "2.0.0", 361 PURL: "pkg:npm/shebang-command@2.0.0", 362 Locations: locationSet, 363 Language: pkg.JavaScript, 364 Type: pkg.NpmPkg, 365 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="}, 366 Dependencies: map[string]string{ 367 "shebang-regex": "3.0.0", 368 }, 369 }, 370 }, 371 { 372 Name: "shebang-regex", 373 Version: "3.0.0", 374 PURL: "pkg:npm/shebang-regex@3.0.0", 375 Locations: locationSet, 376 Language: pkg.JavaScript, 377 Type: pkg.NpmPkg, 378 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="}, 379 Dependencies: map[string]string{}, 380 }, 381 }, 382 { 383 Name: "which", 384 Version: "2.0.2", 385 PURL: "pkg:npm/which@2.0.2", 386 Locations: locationSet, 387 Language: pkg.JavaScript, 388 Type: pkg.NpmPkg, 389 Metadata: pkg.PnpmLockEntry{Resolution: pkg.PnpmLockResolution{Integrity: "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="}, 390 Dependencies: map[string]string{ 391 "isexe": "2.0.0", 392 }, 393 }, 394 }, 395 } 396 expectedRelationships := []artifact.Relationship{ 397 { 398 From: expectedPkgs[1], 399 To: expectedPkgs[5], 400 Type: artifact.DependencyOfRelationship, 401 }, 402 { 403 From: expectedPkgs[2], 404 To: expectedPkgs[0], 405 Type: artifact.DependencyOfRelationship, 406 }, 407 { 408 From: expectedPkgs[3], 409 To: expectedPkgs[0], 410 Type: artifact.DependencyOfRelationship, 411 }, 412 { 413 From: expectedPkgs[4], 414 To: expectedPkgs[3], 415 Type: artifact.DependencyOfRelationship, 416 }, 417 { 418 From: expectedPkgs[5], 419 To: expectedPkgs[0], 420 Type: artifact.DependencyOfRelationship, 421 }, 422 } 423 pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) 424 } 425 426 func TestSearchPnpmForLicenses(t *testing.T) { 427 ctx := context.TODO() 428 fixture := "test-fixtures/pnpm-remote/pnpm-lock.yaml" 429 locations := file.NewLocationSet(file.NewLocation(fixture)) 430 mux, url, teardown := setupNpmRegistry() 431 defer teardown() 432 tests := []struct { 433 name string 434 fixture string 435 config CatalogerConfig 436 requestHandlers []handlerPath 437 expectedPackages []pkg.Package 438 }{ 439 { 440 name: "search remote licenses returns the expected licenses when search is set to true", 441 config: CatalogerConfig{SearchRemoteLicenses: true}, 442 requestHandlers: []handlerPath{ 443 { 444 // https://registry.npmjs.org/nanoid/3.3.4 445 path: "/nanoid/3.3.4", 446 handler: generateMockNpmRegistryHandler("test-fixtures/pnpm-remote/registry_response.json"), 447 }, 448 }, 449 expectedPackages: []pkg.Package{ 450 { 451 Name: "nanoid", 452 Version: "3.3.4", 453 Locations: locations, 454 PURL: "pkg:npm/nanoid@3.3.4", 455 Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), 456 Language: pkg.JavaScript, 457 Type: pkg.NpmPkg, 458 Metadata: pkg.PnpmLockEntry{ 459 Resolution: pkg.PnpmLockResolution{Integrity: "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="}, 460 Dependencies: map[string]string{}, 461 }, 462 }, 463 }, 464 }, 465 } 466 467 for _, tc := range tests { 468 t.Run(tc.name, func(t *testing.T) { 469 // set up the mock server 470 for _, handler := range tc.requestHandlers { 471 mux.HandleFunc(handler.path, handler.handler) 472 } 473 tc.config.NPMBaseURL = url 474 adapter := newGenericPnpmLockAdapter(tc.config) 475 pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, tc.expectedPackages, nil) 476 }) 477 } 478 } 479 func Test_corruptPnpmLock(t *testing.T) { 480 adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) 481 pkgtest.NewCatalogTester(). 482 FromFile(t, "test-fixtures/corrupt/pnpm-lock.yaml"). 483 WithError(). 484 TestParser(t, adapter.parsePnpmLock) 485 } 486 487 func generateMockNpmRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { 488 return func(w http.ResponseWriter, r *http.Request) { 489 w.WriteHeader(http.StatusOK) 490 // Copy the file's content to the response writer 491 file, err := os.Open(responseFixture) 492 if err != nil { 493 http.Error(w, err.Error(), http.StatusInternalServerError) 494 return 495 } 496 defer file.Close() 497 498 _, err = io.Copy(w, file) 499 if err != nil { 500 http.Error(w, err.Error(), http.StatusInternalServerError) 501 return 502 } 503 } 504 } 505 506 // setup sets up a test HTTP server for mocking requests to a particular registry. 507 // The returned url is injected into the Config so the client uses the test server. 508 // Tests should register handlers on mux to simulate the expected request/response structure 509 func setupNpmRegistry() (mux *http.ServeMux, serverURL string, teardown func()) { 510 // mux is the HTTP request multiplexer used with the test server. 511 mux = http.NewServeMux() 512 513 // We want to ensure that tests catch mistakes where the endpoint URL is 514 // specified as absolute rather than relative. It only makes a difference 515 // when there's a non-empty base URL path. So, use that. See issue #752. 516 apiHandler := http.NewServeMux() 517 apiHandler.Handle("/", mux) 518 // server is a test HTTP server used to provide mock API responses. 519 server := httptest.NewServer(apiHandler) 520 521 return mux, server.URL, server.Close 522 }