2024-02-19 15:16:35 +04:00
package npm
import (
"fmt"
2024-06-20 06:48:08 +04:00
"maps"
2024-02-19 15:16:35 +04:00
"path"
2024-03-27 12:08:58 +06:00
"slices"
2024-02-19 15:16:35 +04:00
"sort"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
2024-03-12 10:56:10 +04:00
"github.com/aquasecurity/trivy/pkg/dependency"
2024-02-19 15:16:35 +04:00
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
2024-03-12 10:56:10 +04:00
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
2024-02-26 09:55:15 +04:00
"github.com/aquasecurity/trivy/pkg/log"
2024-12-24 13:47:21 +09:00
"github.com/aquasecurity/trivy/pkg/set"
2024-02-26 09:55:15 +04:00
xio "github.com/aquasecurity/trivy/pkg/x/io"
2025-04-09 18:22:57 +06:00
xjson "github.com/aquasecurity/trivy/pkg/x/json"
2024-02-19 15:16:35 +04:00
)
const nodeModulesDir = "node_modules"
type LockFile struct {
Dependencies map [ string ] Dependency ` json:"dependencies" `
Packages map [ string ] Package ` json:"packages" `
LockfileVersion int ` json:"lockfileVersion" `
}
type Dependency struct {
Version string ` json:"version" `
Dev bool ` json:"dev" `
Dependencies map [ string ] Dependency ` json:"dependencies" `
Requires map [ string ] string ` json:"requires" `
Resolved string ` json:"resolved" `
2025-04-09 18:22:57 +06:00
xjson . Location
2024-02-19 15:16:35 +04:00
}
type Package struct {
Name string ` json:"name" `
Version string ` json:"version" `
Dependencies map [ string ] string ` json:"dependencies" `
OptionalDependencies map [ string ] string ` json:"optionalDependencies" `
DevDependencies map [ string ] string ` json:"devDependencies" `
2024-12-05 16:57:12 +09:00
PeerDependencies map [ string ] string ` json:"peerDependencies" `
2024-02-19 15:16:35 +04:00
Resolved string ` json:"resolved" `
Dev bool ` json:"dev" `
Link bool ` json:"link" `
Workspaces [ ] string ` json:"workspaces" `
2025-04-09 18:22:57 +06:00
xjson . Location
2024-02-19 15:16:35 +04:00
}
2024-04-11 22:59:09 +04:00
type Parser struct {
logger * log . Logger
}
2024-02-19 15:16:35 +04:00
2024-05-07 16:25:52 +04:00
func NewParser ( ) * Parser {
2024-04-11 22:59:09 +04:00
return & Parser {
logger : log . WithPrefix ( "npm" ) ,
}
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
func ( p * Parser ) Parse ( r xio . ReadSeekerAt ) ( [ ] ftypes . Package , [ ] ftypes . Dependency , error ) {
2024-02-19 15:16:35 +04:00
var lockFile LockFile
2025-04-09 18:22:57 +06:00
if err := xjson . UnmarshalRead ( r , & lockFile ) ; err != nil {
2024-02-19 15:16:35 +04:00
return nil , nil , xerrors . Errorf ( "decode error: %w" , err )
}
2024-05-07 16:25:52 +04:00
var pkgs [ ] ftypes . Package
var deps [ ] ftypes . Dependency
2024-02-19 15:16:35 +04:00
if lockFile . LockfileVersion == 1 {
2024-05-07 16:25:52 +04:00
pkgs , deps = p . parseV1 ( lockFile . Dependencies , make ( map [ string ] string ) )
2024-02-19 15:16:35 +04:00
} else {
2024-05-07 16:25:52 +04:00
pkgs , deps = p . parseV2 ( lockFile . Packages )
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
return utils . UniquePackages ( pkgs ) , uniqueDeps ( deps ) , nil
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
func ( p * Parser ) parseV2 ( packages map [ string ] Package ) ( [ ] ftypes . Package , [ ] ftypes . Dependency ) {
pkgs := make ( map [ string ] ftypes . Package , len ( packages ) - 1 )
var deps [ ] ftypes . Dependency
2024-02-19 15:16:35 +04:00
// Resolve links first
// https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#packages
2024-04-11 22:59:09 +04:00
p . resolveLinks ( packages )
2024-02-19 15:16:35 +04:00
2024-12-24 13:47:21 +09:00
directDeps := set . New [ string ] ( )
2024-12-05 16:57:12 +09:00
for name , version := range lo . Assign ( packages [ "" ] . Dependencies , packages [ "" ] . OptionalDependencies , packages [ "" ] . DevDependencies , packages [ "" ] . PeerDependencies ) {
2024-02-19 15:16:35 +04:00
pkgPath := joinPaths ( nodeModulesDir , name )
if _ , ok := packages [ pkgPath ] ; ! ok {
2024-04-11 22:59:09 +04:00
p . logger . Debug ( "Unable to find the direct dependency" ,
log . String ( "name" , name ) , log . String ( "version" , version ) )
2024-02-19 15:16:35 +04:00
continue
}
// Store the package paths of direct dependencies
// e.g. node_modules/body-parser
2024-12-24 13:47:21 +09:00
directDeps . Append ( pkgPath )
2024-02-19 15:16:35 +04:00
}
for pkgPath , pkg := range packages {
if ! strings . HasPrefix ( pkgPath , "node_modules" ) {
continue
}
// pkg.Name exists when package name != folder name
pkgName := pkg . Name
if pkgName == "" {
2024-04-11 22:59:09 +04:00
pkgName = p . pkgNameFromPath ( pkgPath )
2024-02-19 15:16:35 +04:00
}
2024-03-12 10:56:10 +04:00
pkgID := packageID ( pkgName , pkg . Version )
2024-02-19 15:16:35 +04:00
2024-05-07 16:25:52 +04:00
var ref ftypes . ExternalRef
2024-03-27 12:08:58 +06:00
if pkg . Resolved != "" {
2024-05-07 16:25:52 +04:00
ref = ftypes . ExternalRef {
Type : ftypes . RefOther ,
2024-03-27 12:08:58 +06:00
URL : pkg . Resolved ,
}
}
2024-05-07 16:25:52 +04:00
pkgIndirect := isIndirectPkg ( pkgPath , directDeps )
2024-03-27 12:08:58 +06:00
2024-05-07 16:25:52 +04:00
// There are cases when similar packages use same dependencies
2024-02-19 15:16:35 +04:00
// we need to add location for each these dependencies
2024-05-07 16:25:52 +04:00
if savedPkg , ok := pkgs [ pkgID ] ; ok {
savedPkg . Dev = savedPkg . Dev && pkg . Dev
if savedPkg . Relationship == ftypes . RelationshipIndirect && ! pkgIndirect {
savedPkg . Relationship = ftypes . RelationshipDirect
2024-04-27 13:15:12 +04:00
}
2024-03-27 12:08:58 +06:00
2024-05-07 16:25:52 +04:00
if ref . URL != "" && ! slices . Contains ( savedPkg . ExternalReferences , ref ) {
savedPkg . ExternalReferences = append ( savedPkg . ExternalReferences , ref )
sortExternalReferences ( savedPkg . ExternalReferences )
2024-03-27 12:08:58 +06:00
}
2025-04-09 18:22:57 +06:00
savedPkg . Locations = append ( savedPkg . Locations , ftypes . Location ( pkg . Location ) )
2024-05-07 16:25:52 +04:00
sort . Sort ( savedPkg . Locations )
2024-03-27 12:08:58 +06:00
2024-05-07 16:25:52 +04:00
pkgs [ pkgID ] = savedPkg
2024-02-19 15:16:35 +04:00
continue
}
2024-05-07 16:25:52 +04:00
newPkg := ftypes . Package {
2024-03-27 12:08:58 +06:00
ID : pkgID ,
Name : pkgName ,
Version : pkg . Version ,
2024-05-07 16:25:52 +04:00
Relationship : lo . Ternary ( pkgIndirect , ftypes . RelationshipIndirect , ftypes . RelationshipDirect ) ,
2024-03-27 12:08:58 +06:00
Dev : pkg . Dev ,
2024-05-07 16:25:52 +04:00
ExternalReferences : lo . Ternary ( ref . URL != "" , [ ] ftypes . ExternalRef { ref } , nil ) ,
2025-04-09 18:22:57 +06:00
Locations : [ ] ftypes . Location { ftypes . Location ( pkg . Location ) } ,
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
pkgs [ pkgID ] = newPkg
2024-02-19 15:16:35 +04:00
// npm builds graph using optional deps. e.g.:
// └─┬ watchpack@1.7.5
// ├─┬ chokidar@3.5.3 - optional dependency
// │ └── glob-parent@5.1.
2024-12-05 16:57:12 +09:00
dependencies := lo . Assign ( pkg . Dependencies , pkg . OptionalDependencies , pkg . PeerDependencies )
2024-02-19 15:16:35 +04:00
dependsOn := make ( [ ] string , 0 , len ( dependencies ) )
for depName , depVersion := range dependencies {
depID , err := findDependsOn ( pkgPath , depName , packages )
if err != nil {
2024-04-11 22:59:09 +04:00
p . logger . Debug ( "Unable to resolve the version" ,
log . String ( "name" , depName ) , log . String ( "version" , depVersion ) )
2024-02-19 15:16:35 +04:00
continue
}
dependsOn = append ( dependsOn , depID )
}
if len ( dependsOn ) > 0 {
2024-05-07 16:25:52 +04:00
deps = append ( deps , ftypes . Dependency {
ID : newPkg . ID ,
2024-02-19 15:16:35 +04:00
DependsOn : dependsOn ,
} )
}
}
2024-06-20 06:48:08 +04:00
return lo . Values ( pkgs ) , deps
2024-02-19 15:16:35 +04:00
}
// for local package npm uses links. e.g.:
// function/func1 -> target of package
// node_modules/func1 -> link to target
// see `package-lock_v3_with_workspace.json` to better understanding
2024-04-11 22:59:09 +04:00
func ( p * Parser ) resolveLinks ( packages map [ string ] Package ) {
2024-06-10 12:30:27 +06:00
links := lo . PickBy ( packages , func ( pkgPath string , pkg Package ) bool {
if ! pkg . Link {
return false
}
if pkg . Resolved == "" {
p . logger . Warn ( "`package-lock.json` contains broken link with empty `resolved` field. This package will be skipped to avoid receiving an empty package" , log . String ( "pkg" , pkgPath ) )
delete ( packages , pkgPath )
return false
}
return true
2024-02-19 15:16:35 +04:00
} )
// Early return
if len ( links ) == 0 {
return
}
rootPkg := packages [ "" ]
if rootPkg . Dependencies == nil {
rootPkg . Dependencies = make ( map [ string ] string )
}
workspaces := rootPkg . Workspaces
2024-06-10 12:30:27 +06:00
// Changing the map during the map iteration causes unexpected behavior,
// so we need to iterate over the cloned `packages` map, but change the original `packages` map.
for pkgPath , pkg := range maps . Clone ( packages ) {
2024-02-19 15:16:35 +04:00
for linkPath , link := range links {
if ! strings . HasPrefix ( pkgPath , link . Resolved ) {
continue
}
// The target doesn't have the "resolved" field, so we need to copy it from the link.
if pkg . Resolved == "" {
pkg . Resolved = link . Resolved
}
// Resolve the link package so all packages are located under "node_modules".
resolvedPath := strings . ReplaceAll ( pkgPath , link . Resolved , linkPath )
packages [ resolvedPath ] = pkg
// Delete the target package
delete ( packages , pkgPath )
2024-04-11 22:59:09 +04:00
if p . isWorkspace ( pkgPath , workspaces ) {
rootPkg . Dependencies [ p . pkgNameFromPath ( linkPath ) ] = pkg . Version
2024-02-19 15:16:35 +04:00
}
break
}
}
packages [ "" ] = rootPkg
}
2024-04-11 22:59:09 +04:00
func ( p * Parser ) isWorkspace ( pkgPath string , workspaces [ ] string ) bool {
2024-02-19 15:16:35 +04:00
for _ , workspace := range workspaces {
if match , err := path . Match ( workspace , pkgPath ) ; err != nil {
2024-04-11 22:59:09 +04:00
p . logger . Debug ( "Unable to parse workspace" ,
log . String ( "workspace" , workspace ) , log . String ( "pkg_path" , pkgPath ) )
2024-02-19 15:16:35 +04:00
} else if match {
return true
}
}
return false
}
func findDependsOn ( pkgPath , depName string , packages map [ string ] Package ) ( string , error ) {
depPath := joinPaths ( pkgPath , nodeModulesDir )
paths := strings . Split ( depPath , "/" )
// Try to resolve the version with the nearest directory
// e.g. for pkgPath == `node_modules/body-parser/node_modules/debug`, depName == `ms`:
// - "node_modules/body-parser/node_modules/debug/node_modules/ms"
// - "node_modules/body-parser/node_modules/ms"
// - "node_modules/ms"
for i := len ( paths ) - 1 ; i >= 0 ; i -- {
if paths [ i ] != nodeModulesDir {
continue
}
modulePath := joinPaths ( paths [ : i + 1 ] ... )
modulePath = joinPaths ( modulePath , depName )
if dep , ok := packages [ modulePath ] ; ok {
2024-03-12 10:56:10 +04:00
return packageID ( depName , dep . Version ) , nil
2024-02-19 15:16:35 +04:00
}
}
// It should not reach here.
return "" , xerrors . Errorf ( "can't find dependsOn for %s" , depName )
}
2024-05-07 16:25:52 +04:00
func ( p * Parser ) parseV1 ( dependencies map [ string ] Dependency , versions map [ string ] string ) ( [ ] ftypes . Package , [ ] ftypes . Dependency ) {
2024-02-19 15:16:35 +04:00
// Update package name and version mapping.
for pkgName , dep := range dependencies {
// Overwrite the existing package version so that the nested version can take precedence.
versions [ pkgName ] = dep . Version
}
2024-05-07 16:25:52 +04:00
var pkgs [ ] ftypes . Package
var deps [ ] ftypes . Dependency
2024-03-12 10:56:10 +04:00
for pkgName , dep := range dependencies {
2024-05-07 16:25:52 +04:00
pkg := ftypes . Package {
2024-04-27 13:15:12 +04:00
ID : packageID ( pkgName , dep . Version ) ,
Name : pkgName ,
Version : dep . Version ,
Dev : dep . Dev ,
2024-05-07 16:25:52 +04:00
Relationship : ftypes . RelationshipUnknown , // lockfile v1 schema doesn't have information about direct dependencies
ExternalReferences : [ ] ftypes . ExternalRef {
2024-02-19 15:16:35 +04:00
{
2024-05-07 16:25:52 +04:00
Type : ftypes . RefOther ,
2024-03-12 10:56:10 +04:00
URL : dep . Resolved ,
2024-02-19 15:16:35 +04:00
} ,
} ,
2025-04-09 18:22:57 +06:00
Locations : [ ] ftypes . Location { ftypes . Location ( dep . Location ) } ,
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
pkgs = append ( pkgs , pkg )
2024-02-19 15:16:35 +04:00
2024-03-12 10:56:10 +04:00
dependsOn := make ( [ ] string , 0 , len ( dep . Requires ) )
2024-05-07 16:25:52 +04:00
for pName , requiredVer := range dep . Requires {
2024-02-19 15:16:35 +04:00
// Try to resolve the version with nested dependencies first
2024-05-07 16:25:52 +04:00
if resolvedDep , ok := dep . Dependencies [ pName ] ; ok {
pkgID := packageID ( pName , resolvedDep . Version )
dependsOn = append ( dependsOn , pkgID )
2024-02-19 15:16:35 +04:00
continue
}
// Try to resolve the version with the higher level dependencies
2024-05-07 16:25:52 +04:00
if ver , ok := versions [ pName ] ; ok {
dependsOn = append ( dependsOn , packageID ( pName , ver ) )
2024-02-19 15:16:35 +04:00
continue
}
// It should not reach here.
2024-04-11 22:59:09 +04:00
p . logger . Warn ( "Unable to resolve the version" ,
2024-05-07 16:25:52 +04:00
log . String ( "name" , pName ) , log . String ( "version" , requiredVer ) )
2024-02-19 15:16:35 +04:00
}
if len ( dependsOn ) > 0 {
2024-05-07 16:25:52 +04:00
deps = append ( deps , ftypes . Dependency {
ID : packageID ( pkg . Name , pkg . Version ) ,
2024-02-19 15:16:35 +04:00
DependsOn : dependsOn ,
} )
}
2024-03-12 10:56:10 +04:00
if dep . Dependencies != nil {
2024-02-19 15:16:35 +04:00
// Recursion
2024-05-07 16:25:52 +04:00
childpkgs , childDeps := p . parseV1 ( dep . Dependencies , maps . Clone ( versions ) )
pkgs = append ( pkgs , childpkgs ... )
2024-02-19 15:16:35 +04:00
deps = append ( deps , childDeps ... )
}
}
2024-05-07 16:25:52 +04:00
return pkgs , deps
2024-02-19 15:16:35 +04:00
}
2024-04-11 22:59:09 +04:00
func ( p * Parser ) pkgNameFromPath ( pkgPath string ) string {
// lock file contains path to dependency in `node_modules`. e.g.:
// node_modules/string-width
// node_modules/string-width/node_modules/strip-ansi
// we renamed to `node_modules` directory prefixes `workspace` when resolving Links
// node_modules/function1
// node_modules/nested_func/node_modules/debug
if index := strings . LastIndex ( pkgPath , nodeModulesDir ) ; index != - 1 {
return pkgPath [ index + len ( nodeModulesDir ) + 1 : ]
}
p . logger . Warn ( "Package path doesn't have `node_modules` prefix" , log . String ( "pkg_path" , pkgPath ) )
return pkgPath
}
2024-05-07 16:25:52 +04:00
func uniqueDeps ( deps [ ] ftypes . Dependency ) [ ] ftypes . Dependency {
var uniqDeps ftypes . Dependencies
2024-12-24 13:47:21 +09:00
unique := set . New [ string ] ( )
2024-02-19 15:16:35 +04:00
for _ , dep := range deps {
sort . Strings ( dep . DependsOn )
depKey := fmt . Sprintf ( "%s:%s" , dep . ID , strings . Join ( dep . DependsOn , "," ) )
2024-12-24 13:47:21 +09:00
if ! unique . Contains ( depKey ) {
unique . Append ( depKey )
2024-02-19 15:16:35 +04:00
uniqDeps = append ( uniqDeps , dep )
}
}
2024-05-07 16:25:52 +04:00
sort . Sort ( uniqDeps )
2024-02-19 15:16:35 +04:00
return uniqDeps
}
2024-12-24 13:47:21 +09:00
func isIndirectPkg ( pkgPath string , directDeps set . Set [ string ] ) bool {
2024-02-19 15:16:35 +04:00
// A project can contain 2 different versions of the same dependency.
// e.g. `node_modules/string-width/node_modules/strip-ansi` and `node_modules/string-ansi`
2024-05-07 16:25:52 +04:00
// direct dependencies always have root path (`node_modules/<pkg_name>`)
2024-12-24 13:47:21 +09:00
if directDeps . Contains ( pkgPath ) {
2024-02-19 15:16:35 +04:00
return false
}
return true
}
func joinPaths ( paths ... string ) string {
return strings . Join ( paths , "/" )
}
2024-03-12 10:56:10 +04:00
func packageID ( name , version string ) string {
return dependency . ID ( ftypes . Npm , name , version )
}
2024-03-27 12:08:58 +06:00
2024-05-07 16:25:52 +04:00
func sortExternalReferences ( refs [ ] ftypes . ExternalRef ) {
2024-03-27 12:08:58 +06:00
sort . Slice ( refs , func ( i , j int ) bool {
if refs [ i ] . Type != refs [ j ] . Type {
return refs [ i ] . Type < refs [ j ] . Type
}
return refs [ i ] . URL < refs [ j ] . URL
} )
}