mirror of
https://github.com/anchore/syft.git
synced 2026-08-22 01:53:35 +02:00
fix(javascript): strip underscore peer-dep suffixes from pnpm v5 lockfile versions (#5175)
pnpm v5 lockfiles encode resolved peer dependencies as an underscore-delimited suffix of the dependency path, e.g. /acorn-jsx/5.3.2_acorn@8.8.0 or a hashed form like /webpack-cli/4.10.0_fzn43tb6bdtdxy2s3aqevve2su. Only the v6+ parenthesized form was stripped, so for v5 lockfiles the suffix leaked into reported versions and PURLs, and peer variants of the same name+version were not deduplicated. Strip the suffix on the v5 code path (version < 6.0) wherever a version is extracted: top-level dependencies values, packages keys, and per-package dependencies map values. Only values with a leading digit are stripped so link:/file:/git specifiers are unaffected. Fixes #5174 Signed-off-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com> Co-authored-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5c6cf08a8c
commit
78f39ee3a5
@ -89,6 +89,8 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage,
|
||||
return nil, fmt.Errorf("failed to unmarshal pnpm v6 lockfile: %w", err)
|
||||
}
|
||||
|
||||
isV5 := version < 6.0
|
||||
|
||||
packages := make(map[string]pnpmPackage)
|
||||
|
||||
// Direct dependencies — use sorted keys for deterministic output
|
||||
@ -98,6 +100,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage,
|
||||
log.WithFields("package", name, "error", err).Trace("unable to parse pnpm dependency")
|
||||
continue
|
||||
}
|
||||
if isV5 {
|
||||
ver = stripPnpmV5PeerSuffix(ver)
|
||||
}
|
||||
key := name + "@" + ver
|
||||
packages[key] = pnpmPackage{Name: name, Version: ver}
|
||||
}
|
||||
@ -114,6 +119,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage,
|
||||
log.WithFields("key", key).Trace("unable to parse pnpm package key")
|
||||
continue
|
||||
}
|
||||
if isV5 {
|
||||
ver = stripPnpmV5PeerSuffix(ver)
|
||||
}
|
||||
pkgKey := name + "@" + ver
|
||||
|
||||
integrity := ""
|
||||
@ -124,6 +132,9 @@ func (p *pnpmV6LockYaml) Parse(version float64, doc *yaml.Node) ([]pnpmPackage,
|
||||
dependencies := make(map[string]string)
|
||||
for depName, depVersion := range sortedIter(pkgInfo.Dependencies) {
|
||||
var normalizedVersion = strings.SplitN(depVersion, "(", 2)[0]
|
||||
if isV5 {
|
||||
normalizedVersion = stripPnpmV5PeerSuffix(normalizedVersion)
|
||||
}
|
||||
dependencies[depName] = normalizedVersion
|
||||
}
|
||||
|
||||
@ -315,6 +326,22 @@ func parseVersionField(name string, info any) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// stripPnpmV5PeerSuffix removes the underscore-delimited peer dependency suffix used by
|
||||
// pnpm v5 lockfiles, e.g. "5.3.2_acorn@8.8.0" or "4.10.0_fzn43tb6bdtdxy2s3aqevve2su" -> "5.3.2" / "4.10.0".
|
||||
// Lockfile v6+ encodes the same information in parentheses, which is stripped separately.
|
||||
// Only values that look like a registry version (leading digit) are stripped, so that
|
||||
// link:/file:/git specifiers are left untouched.
|
||||
func stripPnpmV5PeerSuffix(version string) string {
|
||||
idx := strings.Index(version, "_")
|
||||
if idx <= 0 {
|
||||
return version
|
||||
}
|
||||
if version[0] < '0' || version[0] > '9' {
|
||||
return version
|
||||
}
|
||||
return version[:idx]
|
||||
}
|
||||
|
||||
// parsePnpmPackageKey extracts the package name and version from a lockfile package key.
|
||||
// Handles formats like:
|
||||
// - /@babel/runtime/7.16.7
|
||||
|
||||
@ -310,6 +310,66 @@ func TestParsePnpmV6Lock(t *testing.T) {
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParsePnpmLockV5PeerSuffix(t *testing.T) {
|
||||
// pnpm v5 lockfiles encode resolved peer dependencies as an underscore-delimited
|
||||
// suffix of the dependency path, either readable (e.g. "5.3.2_acorn@8.8.0") or
|
||||
// hashed (e.g. "4.10.0_fzn43tb6bdtdxy2s3aqevve2su"). The suffix must be stripped
|
||||
// from reported versions, just like the parenthesized form in v6+ lockfiles.
|
||||
fixture := "testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml"
|
||||
|
||||
locationSet := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
expectedPkgs := []pkg.Package{
|
||||
{
|
||||
Name: "acorn",
|
||||
Version: "8.8.0",
|
||||
PURL: "pkg:npm/acorn@8.8.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.PnpmLockEntry{
|
||||
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="},
|
||||
Dependencies: map[string]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "acorn-jsx",
|
||||
Version: "5.3.2",
|
||||
PURL: "pkg:npm/acorn-jsx@5.3.2",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.PnpmLockEntry{
|
||||
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="},
|
||||
Dependencies: map[string]string{
|
||||
"acorn": "8.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "webpack-cli",
|
||||
Version: "4.10.0",
|
||||
PURL: "pkg:npm/webpack-cli@4.10.0",
|
||||
Locations: locationSet,
|
||||
Language: pkg.JavaScript,
|
||||
Type: pkg.NpmPkg,
|
||||
Metadata: pkg.PnpmLockEntry{
|
||||
Resolution: pkg.PnpmLockResolution{Integrity: "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w=="},
|
||||
Dependencies: map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedRelationships := []artifact.Relationship{
|
||||
{
|
||||
From: expectedPkgs[0],
|
||||
To: expectedPkgs[1],
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
},
|
||||
}
|
||||
adapter := newGenericPnpmLockAdapter(CatalogerConfig{})
|
||||
pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships)
|
||||
}
|
||||
|
||||
func TestParsePnpmLockV9(t *testing.T) {
|
||||
var expectedRelationships []artifact.Relationship
|
||||
fixture := "testdata/pnpm-v9/pnpm-lock.yaml"
|
||||
@ -592,6 +652,27 @@ packages:
|
||||
assert.Equal(t, "1.0.0", pkgs[0].Version)
|
||||
assert.Equal(t, "sha512-BBB", pkgs[0].Integrity, "expected last lexicographic key to win")
|
||||
}
|
||||
|
||||
// v5 lockfile with two entries that collapse to the same key (underscore peer-dep suffixes)
|
||||
lockfileV5 := []byte(`
|
||||
lockfileVersion: 5.4
|
||||
packages:
|
||||
/some-pkg/1.0.0_peer-b@2.0.0:
|
||||
resolution: {integrity: sha512-BBB}
|
||||
/some-pkg/1.0.0_peer-a@1.0.0:
|
||||
resolution: {integrity: sha512-AAA}
|
||||
`)
|
||||
|
||||
for range 10 {
|
||||
parser := &pnpmV6LockYaml{}
|
||||
pkgs, err := parser.Parse(5.4, lockfileV5)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pkgs, 1, "expected exactly one package after key collision")
|
||||
|
||||
assert.Equal(t, "some-pkg", pkgs[0].Name)
|
||||
assert.Equal(t, "1.0.0", pkgs[0].Version)
|
||||
assert.Equal(t, "sha512-BBB", pkgs[0].Integrity, "expected last lexicographic key to win")
|
||||
}
|
||||
}
|
||||
|
||||
func generateMockNpmRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
27
syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml
generated
vendored
Normal file
27
syft/pkg/cataloger/javascript/testdata/pnpm-v5-peer-suffix/pnpm-lock.yaml
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
acorn: ^8.8.0
|
||||
acorn-jsx: ^5.3.2
|
||||
webpack-cli: ^4.10.0
|
||||
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
acorn-jsx: 5.3.2_acorn@8.8.0
|
||||
webpack-cli: 4.10.0_fzn43tb6bdtdxy2s3aqevve2su
|
||||
|
||||
packages:
|
||||
/acorn/8.8.0:
|
||||
resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@8.8.0:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
acorn: 8.8.0
|
||||
|
||||
/webpack-cli/4.10.0_fzn43tb6bdtdxy2s3aqevve2su:
|
||||
resolution: {integrity: sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==}
|
||||
Loading…
x
Reference in New Issue
Block a user