From b64d5adc6b90fde205e4eeaf7b58332bf10372d2 Mon Sep 17 00:00:00 2001 From: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com> Date: Mon, 29 Dec 2025 11:57:06 +0600 Subject: [PATCH] feat(nodejs): parse licenses from `package-lock.json` file (#9983) --- docs/guide/coverage/language/nodejs.md | 5 +++-- pkg/dependency/parser/nodejs/npm/parse.go | 12 ++++++++++++ pkg/dependency/parser/nodejs/npm/parse_testcase.go | 12 ++++++++++++ pkg/fanal/analyzer/language/nodejs/npm/npm.go | 10 ++++++---- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/guide/coverage/language/nodejs.md b/docs/guide/coverage/language/nodejs.md index f206a3e940..292b963a59 100644 --- a/docs/guide/coverage/language/nodejs.md +++ b/docs/guide/coverage/language/nodejs.md @@ -37,8 +37,9 @@ Trivy parses your files generated by package managers in filesystem/repository s ### npm Trivy parses `package-lock.json`. -To identify licenses, you need to download dependencies to `node_modules` beforehand. -Trivy analyzes `node_modules` for licenses. + +Trivy retrieves dependency license information from the lock file (starting with v2). +If license information is missing, Trivy checks the `package.json` files from the `node_modules` folder. By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. diff --git a/pkg/dependency/parser/nodejs/npm/parse.go b/pkg/dependency/parser/nodejs/npm/parse.go index 8e3c6ad047..73aff1ea63 100644 --- a/pkg/dependency/parser/nodejs/npm/parse.go +++ b/pkg/dependency/parser/nodejs/npm/parse.go @@ -41,6 +41,7 @@ type Dependency struct { type Package struct { Name string `json:"name"` Version string `json:"version"` + License string `json:"license"` Dependencies map[string]string `json:"dependencies"` OptionalDependencies map[string]string `json:"optionalDependencies"` DevDependencies map[string]string `json:"devDependencies"` @@ -139,14 +140,25 @@ func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftype savedPkg.Locations = append(savedPkg.Locations, ftypes.Location(pkg.Location)) sort.Sort(savedPkg.Locations) + // If for some reason license is missing in savedPkg, but exists in the current pkg, add it. + if len(savedPkg.Licenses) == 0 && pkg.License != "" { + savedPkg.Licenses = []string{pkg.License} + } + pkgs[pkgID] = savedPkg continue } + var licenses []string + if pkg.License != "" { + licenses = []string{pkg.License} + } + newPkg := ftypes.Package{ ID: pkgID, Name: pkgName, Version: pkg.Version, + Licenses: licenses, Relationship: lo.Ternary(pkgIndirect, ftypes.RelationshipIndirect, ftypes.RelationshipDirect), Dev: pkg.Dev, ExternalReferences: lo.Ternary(ref.URL != "", []ftypes.ExternalRef{ref}, nil), diff --git a/pkg/dependency/parser/nodejs/npm/parse_testcase.go b/pkg/dependency/parser/nodejs/npm/parse_testcase.go index 6ebcf0a60e..0d23e10f4b 100644 --- a/pkg/dependency/parser/nodejs/npm/parse_testcase.go +++ b/pkg/dependency/parser/nodejs/npm/parse_testcase.go @@ -1336,6 +1336,7 @@ var ( ID: "function1", Name: "function1", Version: "", + Licenses: []string{"ISC"}, Relationship: ftypes.RelationshipDirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1354,6 +1355,7 @@ var ( ID: "nested_func@1.0.0", Name: "nested_func", Version: "1.0.0", + Licenses: []string{"ISC"}, Relationship: ftypes.RelationshipDirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1466,6 +1468,7 @@ var ( ID: "lodash@4.17.21", Name: "lodash", Version: "4.17.21", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipIndirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1495,6 +1498,7 @@ var ( ID: "lodash@4.17.21", Name: "lodash", Version: "4.17.21", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipDirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1513,6 +1517,7 @@ var ( ID: "winston-mail@2.0.0", Name: "winston-mail", Version: "2.0.0", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipDirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1531,6 +1536,7 @@ var ( ID: "mustache@2.3.2", Name: "mustache", Version: "2.3.2", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipIndirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1549,6 +1555,7 @@ var ( ID: "triple-beam@1.4.1", Name: "triple-beam", Version: "1.4.1", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipIndirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1567,6 +1574,7 @@ var ( ID: "winston@3.17.0", Name: "winston", Version: "3.17.0", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipIndirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1607,6 +1615,7 @@ var ( ID: "func1@1.0.0", Name: "func1", Version: "1.0.0", + Licenses: []string{"ISC"}, Relationship: ftypes.RelationshipDirect, ExternalReferences: []ftypes.ExternalRef{ { @@ -1702,6 +1711,7 @@ var ( URL: "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", }, }, + Licenses: []string{"MIT"}, Locations: []ftypes.Location{ { StartLine: 38, @@ -1717,6 +1727,7 @@ var ( ID: "mkdirp@0.5.1", Name: "mkdirp", Version: "0.5.1", + Licenses: []string{"MIT"}, Relationship: ftypes.RelationshipIndirect, Dev: true, Locations: []ftypes.Location{ @@ -1730,6 +1741,7 @@ var ( ID: "node-pre-gyp@0.12.0", Name: "node-pre-gyp", Version: "0.12.0", + Licenses: []string{"BSD-3-Clause"}, Relationship: ftypes.RelationshipIndirect, Dev: true, Locations: []ftypes.Location{ diff --git a/pkg/fanal/analyzer/language/nodejs/npm/npm.go b/pkg/fanal/analyzer/language/nodejs/npm/npm.go index a769e879db..bdb3c2340a 100644 --- a/pkg/fanal/analyzer/language/nodejs/npm/npm.go +++ b/pkg/fanal/analyzer/language/nodejs/npm/npm.go @@ -66,10 +66,12 @@ func (a npmLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.Post return nil } - // Fill licenses - for i, lib := range app.Packages { - if ll, ok := licenses[lib.ID]; ok { - app.Packages[i].Licenses = ll + // Fill licenses from package.json only if not present in lock file + for i, pkg := range app.Packages { + if len(pkg.Licenses) == 0 { + if ll, ok := licenses[pkg.ID]; ok { + app.Packages[i].Licenses = ll + } } }