Fix missing nested packages in package-lock.json v1 (#5108)

* fix(javascript): catalog nested package-lock v1 dependencies

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>

* test(javascript): cover nested package-lock v1 dependencies

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>

---------

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>
This commit is contained in:
Eljees 2026-07-27 22:18:48 +03:00 committed by GitHub
parent 295454945c
commit 1286689419
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 146 additions and 12 deletions

View File

@ -27,10 +27,11 @@ type packageLock struct {
// lockDependency represents a single package dependency listed in the package.lock json file
type lockDependency struct {
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
Dev bool `json:"dev"`
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
Dev bool `json:"dev"`
Dependencies map[string]lockDependency `json:"dependencies"`
}
type lockPackage struct {
@ -77,14 +78,7 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve
}
if lock.LockfileVersion == 1 {
for name, pkgMeta := range lock.Dependencies {
// skip packages that are only present as a dev dependency
if !a.cfg.IncludeDevDependencies && pkgMeta.Dev {
continue
}
pkgs = append(pkgs, newPackageLockV1Package(ctx, a.cfg, resolver, reader.Location, name, pkgMeta))
}
pkgs = append(pkgs, a.packageLockV1Packages(ctx, resolver, reader.Location, lock.Dependencies)...)
}
if lock.LockfileVersion == 2 || lock.LockfileVersion == 3 {
@ -116,6 +110,32 @@ func (a genericPackageLockAdapter) parsePackageLock(ctx context.Context, resolve
return pkgs, dependency.Resolve(packageLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages")
}
func (a genericPackageLockAdapter) packageLockV1Packages(ctx context.Context, resolver file.Resolver, location file.Location, dependencies map[string]lockDependency) []pkg.Package {
var pkgs []pkg.Package
seen := make(map[string]struct{})
var walk func(map[string]lockDependency)
walk = func(dependencies map[string]lockDependency) {
for name, pkgMeta := range dependencies {
// Skipping a dev-only dependency also skips its dev-only subtree.
if !a.cfg.IncludeDevDependencies && pkgMeta.Dev {
continue
}
p := newPackageLockV1Package(ctx, a.cfg, resolver, location, name, pkgMeta)
if _, exists := seen[p.PURL]; !exists {
pkgs = append(pkgs, p)
seen[p.PURL] = struct{}{}
}
walk(pkgMeta.Dependencies)
}
}
walk(dependencies)
return pkgs
}
func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
// The license field could be either a string or an array.

View File

@ -111,6 +111,66 @@ func TestParsePackageLock(t *testing.T) {
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, expectedRelationships)
}
func TestParsePackageLockV1NestedDependencies(t *testing.T) {
fixture := "testdata/pkg-lock/nested-package-lock-1.json"
expectedPkgs := []pkg.Package{
{
Name: "duplicate",
Version: "1.0.0",
PURL: "pkg:npm/duplicate@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz", Integrity: "sha512-duplicate"},
},
{
Name: "middle",
Version: "1.0.0",
PURL: "pkg:npm/middle@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz", Integrity: "sha512-middle"},
},
{
Name: "parent",
Version: "1.0.0",
PURL: "pkg:npm/parent@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz", Integrity: "sha512-parent"},
},
{
Name: "shared",
Version: "0.5.0",
PURL: "pkg:npm/shared@0.5.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz", Integrity: "sha512-shared-0.5"},
},
{
Name: "shared",
Version: "1.0.0",
PURL: "pkg:npm/shared@1.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz", Integrity: "sha512-shared-1"},
},
{
Name: "shared",
Version: "2.0.0",
PURL: "pkg:npm/shared@2.0.0",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.NpmPackageLockEntry{Resolved: "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz", Integrity: "sha512-shared-2"},
},
}
for i := range expectedPkgs {
expectedPkgs[i].Locations.Add(file.NewLocation(fixture))
}
adapter := newGenericPackageLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parsePackageLock, expectedPkgs, nil)
}
func TestParsePackageLockV2(t *testing.T) {
ctx := context.TODO()
fixture := "testdata/pkg-lock/package-lock-2.json"

View File

@ -0,0 +1,54 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"parent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/parent/-/parent-1.0.0.tgz",
"integrity": "sha512-parent",
"dependencies": {
"shared": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shared/-/shared-1.0.0.tgz",
"integrity": "sha512-shared-1"
},
"middle": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/middle/-/middle-1.0.0.tgz",
"integrity": "sha512-middle",
"dependencies": {
"shared": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/shared/-/shared-0.5.0.tgz",
"integrity": "sha512-shared-0.5"
}
}
},
"duplicate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz",
"integrity": "sha512-duplicate"
}
}
},
"shared": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shared/-/shared-2.0.0.tgz",
"integrity": "sha512-shared-2"
},
"duplicate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/duplicate/-/duplicate-1.0.0.tgz",
"integrity": "sha512-duplicate"
},
"dev-parent": {
"version": "1.0.0",
"dev": true,
"dependencies": {
"dev-nested": {
"version": "1.0.0"
}
}
}
}
}