From bd0917fe93e3e12bf02a22dd79b1a03f83d299f7 Mon Sep 17 00:00:00 2001 From: Synvoya Date: Tue, 14 Jul 2026 00:38:32 +1000 Subject: [PATCH] fix(javascript): strip peer-dep suffix from deno.lock npm keys (#5055) deno.lock npm keys append resolved peer dependencies after the version, separated by "_" (e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped "@scope/name@1.2.3_@scope+peer@4.5.6"). parseDenoNpmNameVersion split on the last "@", capturing a peer's "@" and producing a garbage package name plus the peer's version (and a corrupt PURL that breaks vulnerability matching). Strip the peer suffix (the first "_" after the version "@") before splitting name@version; a semver version never contains "_". Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com> Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com> --- .../cataloger/javascript/parse_deno_lock.go | 30 ++++++++++++++----- .../javascript/parse_deno_lock_test.go | 22 ++++++++++++++ .../javascript/test-fixtures/deno/deno.lock | 6 ++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock.go b/syft/pkg/cataloger/javascript/parse_deno_lock.go index c11bc98f7..4ad2b8cec 100644 --- a/syft/pkg/cataloger/javascript/parse_deno_lock.go +++ b/syft/pkg/cataloger/javascript/parse_deno_lock.go @@ -96,19 +96,35 @@ func parseDenoJsrNameVersion(nameVersion string) (name, version string) { } func parseDenoNpmNameVersion(nameVersion string) (name, version string) { + // deno.lock npm keys append resolved peer dependencies after the version, + // separated by "_", e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped + // "@scope/name@1.2.3_@scope+peer@4.5.6". The peer suffix must be stripped + // before splitting name@version, otherwise LastIndex("@") splits on a peer's + // "@" and yields a garbage name and the peer's version. + // + // A semver version never contains "_", and it begins immediately after the + // package name's version "@", so the first "_" following that "@" starts the + // peer list. + var versionAt int if strings.HasPrefix(nameVersion, "@") { - rest := nameVersion[1:] - idx := strings.LastIndex(rest, "@") + // scoped "@scope/name@version": the version "@" is the second "@" + idx := strings.Index(nameVersion[1:], "@") if idx <= 0 { return "", "" } - return nameVersion[:idx+1], rest[idx+1:] + versionAt = idx + 1 + } else { + versionAt = strings.Index(nameVersion, "@") + if versionAt <= 0 { + return "", "" + } } - idx := strings.LastIndex(nameVersion, "@") - if idx <= 0 { - return "", "" + + base := nameVersion + if underscore := strings.Index(nameVersion[versionAt:], "_"); underscore >= 0 { + base = nameVersion[:versionAt+underscore] } - return nameVersion[:idx], nameVersion[idx+1:] + return base[:versionAt], base[versionAt+1:] } func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package { diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go index 2e70bc32a..83fe9abb7 100644 --- a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go @@ -55,6 +55,28 @@ func TestParseDenoLock(t *testing.T) { Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", }, }, + { + // npm key carries a resolved peer dependency suffix ("_typescript@6.0.3") + Name: "typedoc", + Version: "0.28.19", + PURL: "pkg:npm/typedoc@0.28.19", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: "sha512-TYPEDOCtestfixtureintegrityAAAA==", + }, + }, + { + // scoped npm key with a scope-encoded ("+") peer dependency suffix + Name: "@algolia/autocomplete-preset-algolia", + Version: "1.17.9", + PURL: "pkg:npm/%40algolia/autocomplete-preset-algolia@1.17.9", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.NpmPackageLockEntry{ + Integrity: "sha512-ALGOLIAtestfixtureintegrityBBBB==", + }, + }, } for i := range expectedPkgs { diff --git a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock index c6ecf9efd..365a18a8c 100644 --- a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock +++ b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock @@ -19,6 +19,12 @@ "npm": { "chalk@5.3.0": { "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, + "typedoc@0.28.19_typescript@6.0.3": { + "integrity": "sha512-TYPEDOCtestfixtureintegrityAAAA==" + }, + "@algolia/autocomplete-preset-algolia@1.17.9_@algolia+client-search@5.50.0": { + "integrity": "sha512-ALGOLIAtestfixtureintegrityBBBB==" } }, "remote": {