Skip to content

Commit

Permalink
⭐️ support loading direct dependencies for npm package lock
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-rock committed Feb 16, 2024
1 parent 7c454e4 commit a0331f6
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 61 deletions.
8 changes: 7 additions & 1 deletion providers/os/resources/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import (
)

type Parser interface {
Parse(r io.Reader) (*Package, []*Package, error)
Parse(r io.Reader) (NpmPackageInfo, error)
}

type NpmPackageInfo interface {
Root() *Package
Direct() []*Package
Transitive() []*Package
}

type Package struct {
Expand Down
39 changes: 26 additions & 13 deletions providers/os/resources/npm/package_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,36 +160,49 @@ func (a *packageJsonLicense) UnmarshalJSON(b []byte) error {

type PackageJsonParser struct{}

func (p *PackageJsonParser) Parse(r io.Reader) (*Package, []*Package, error) {
func (p *PackageJsonParser) Parse(r io.Reader) (NpmPackageInfo, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, nil, err
return nil, err
}

var packageJson packageJson
err = json.Unmarshal(data, &packageJson)
if err != nil {
return nil, nil, err
return nil, err
}

// add own package
return &packageJson, nil
}

func (p *packageJson) Root() *Package {

// root package
root := &Package{
Name: packageJson.Name,
Version: packageJson.Version,
Purl: NewPackageUrl(packageJson.Name, packageJson.Version),
Cpes: NewCpes(packageJson.Name, packageJson.Version),
Name: p.Name,
Version: p.Version,
Purl: NewPackageUrl(p.Name, p.Version),
Cpes: NewCpes(p.Name, p.Version),
}

// add all dependencies
entries := []*Package{}
for k, v := range packageJson.Dependencies {
entries = append(entries, &Package{
return root
}

func (p *packageJson) Direct() []*Package {
return nil
}

func (p *packageJson) Transitive() []*Package {
// transitive dependencies, includes the root package
transitive := []*Package{}
for k, v := range p.Dependencies {
transitive = append(transitive, &Package{
Name: k,
Version: v,
Purl: NewPackageUrl(k, v),
Cpes: NewCpes(k, v),
})
}

return root, entries, nil
return transitive
}
10 changes: 6 additions & 4 deletions providers/os/resources/npm/package_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,26 +274,28 @@ func TestPackageJsonParser(t *testing.T) {
require.NoError(t, err)
defer f.Close()

root, pkgs, err := (&PackageJsonParser{}).Parse(f)
info, err := (&PackageJsonParser{}).Parse(f)
assert.Nil(t, err)
assert.Equal(t, 30, len(pkgs))

root := info.Root()
assert.Equal(t, &Package{
Name: "express",
Version: "4.16.4",
Purl: "pkg:npm/[email protected]",
Cpes: []string{"cpe:2.3:a:express:express:4.16.4:*:*:*:*:*:*:*"},
}, root)

p := findPkg(pkgs, "path-to-regexp")
transitive := info.Transitive()
assert.Equal(t, 30, len(transitive))
p := findPkg(transitive, "path-to-regexp")
assert.Equal(t, &Package{
Name: "path-to-regexp",
Version: "0.1.7",
Purl: "pkg:npm/[email protected]",
Cpes: []string{"cpe:2.3:a:path-to-regexp:path-to-regexp:0.1.7:*:*:*:*:*:*:*"},
}, p)

p = findPkg(pkgs, "range-parser")
p = findPkg(transitive, "range-parser")
assert.Equal(t, &Package{
Name: "range-parser",
Version: "~1.2.0",
Expand Down
91 changes: 67 additions & 24 deletions providers/os/resources/npm/package_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package npm

import (
"io"
"strings"

"encoding/json"
)
Expand Down Expand Up @@ -36,11 +37,12 @@ type packageLockDependency struct {
}

type packageLockPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
License packageLockLicense `json:"license"`
Name string `json:"name"`
Version string `json:"version"`
Resolved string `json:"resolved"`
Integrity string `json:"integrity"`
License packageLockLicense `json:"license"`
Dependencies map[string]string `json:"dependencies"`
}

type packageLockLicense []string
Expand Down Expand Up @@ -69,46 +71,87 @@ func (l *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
// see https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json
type PackageLockParser struct{}

func (p *PackageLockParser) Parse(r io.Reader) (*Package, []*Package, error) {
func (p *PackageLockParser) Parse(r io.Reader) (NpmPackageInfo, error) {
var packageJsonLock packageLock
err := json.NewDecoder(r).Decode(&packageJsonLock)
if err != nil {
return nil, nil, err
return nil, err
}

// add own package
return &packageJsonLock, nil
}

func (p *packageLock) Root() *Package {
root := &Package{
Name: packageJsonLock.Name,
Version: packageJsonLock.Version,
Purl: NewPackageUrl(packageJsonLock.Name, packageJsonLock.Version),
Cpes: NewCpes(packageJsonLock.Name, packageJsonLock.Version),
Name: p.Name,
Version: p.Version,
Purl: NewPackageUrl(p.Name, p.Version),
Cpes: NewCpes(p.Name, p.Version),
}
return root
}

func (p *packageLock) Direct() []*Package {
// search for root package, read the packages field

// at this point we only support lockfileVersion: 2 with direct dependencies
if p.Packages == nil {
return nil
}

rootPkg, ok := p.Packages[""]
if !ok {
return nil
}

// add all dependencies
entries := []*Package{}
if packageJsonLock.Packages != nil {
for k, v := range packageJsonLock.Packages {
filteredList := []*Package{}
for k := range rootPkg.Dependencies {
pkg, ok := p.Packages[k]
if !ok {
continue
}

filteredList = append(filteredList, &Package{
Name: packageLockPackageName(k),
Version: pkg.Version,
Purl: NewPackageUrl(k, pkg.Version),
Cpes: NewCpes(k, pkg.Version),
})
}

return filteredList
}

func (p *packageLock) Transitive() []*Package {
transitive := []*Package{}
if p.Packages != nil {
for k, v := range p.Packages {
name := k
// skip root package since we have that already
if name == "" {
name = v.Name
}
entries = append(entries, &Package{
Name: name,

transitive = append(transitive, &Package{
Name: packageLockPackageName(name),
Version: v.Version,
Purl: NewPackageUrl(name, v.Version),
Cpes: NewCpes(name, v.Version),
Purl: NewPackageUrl(k, v.Version),
Cpes: NewCpes(k, v.Version),
})
}
} else if packageJsonLock.Dependencies != nil {
for k, v := range packageJsonLock.Dependencies {
entries = append(entries, &Package{
} else if p.Dependencies != nil {
for k, v := range p.Dependencies {
transitive = append(transitive, &Package{
Name: k,
Version: v.Version,
Purl: NewPackageUrl(k, v.Version),
Cpes: NewCpes(k, v.Version),
})
}
}
return transitive
}

return root, entries, nil
func packageLockPackageName(path string) string {
return strings.TrimPrefix(path, "node_modules/")
}
53 changes: 47 additions & 6 deletions providers/os/resources/npm/package_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,18 @@ func TestPackageLock(t *testing.T) {
License: packageLockLicense(
[]string{"Artistic-2.0"},
),
Dependencies: map[string]string{
"@npmcli/arborist": "^1.0.0",
"@npmcli/ci-detect": "^1.2.0",
},
},
"node_modules/@babel/code-frame": {
Version: "7.10.4",
Resolved: "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
Integrity: "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
Dependencies: map[string]string{
"@babel/highlight": "^7.10.4",
},
},
},
Dependencies: map[string]packageLockDependency{
Expand Down Expand Up @@ -116,6 +123,9 @@ func TestPackageLock(t *testing.T) {
License: packageLockLicense(
[]string{"Artistic-2.0"},
),
Dependencies: map[string]string{
"@isaacs/string-locale-compare": "^1.1.0",
},
},
"node_modules/@isaacs/string-locale-compare": {
Version: "1.1.0",
Expand Down Expand Up @@ -150,32 +160,63 @@ func TestPackageLock(t *testing.T) {
}
}

func TestPackageJsonLockParser(t *testing.T) {
f, err := os.Open("./testdata/package-lock/workbox-package-lock.json")
func TestPackageJsonLockWithPackages(t *testing.T) {
f, err := os.Open("./testdata/package-lock/lockfile-v2.json")
require.NoError(t, err)
defer f.Close()

info, err := (&PackageLockParser{}).Parse(f)
assert.Nil(t, err)

root := info.Root()
assert.Equal(t, &Package{
Name: "npm",
Version: "7.0.0",
Purl: "pkg:npm/[email protected]",
Cpes: []string{"cpe:2.3:a:npm:npm:7.0.0:*:*:*:*:*:*:*"},
}, root)

transitive := info.Transitive()
assert.Equal(t, 1, len(transitive))

p := findPkg(transitive, "@babel/code-frame")
assert.Equal(t, &Package{
Name: "@babel/code-frame",
Version: "7.10.4",
Purl: "pkg:npm/node-modules/%[email protected]",
Cpes: []string{"cpe:2.3:a:node_modules\\/\\@babel\\/code-frame:node_modules\\/\\@babel\\/code-frame:7.10.4:*:*:*:*:*:*:*"},
}, p)

}

func TestPackageJsonLockWithDependencies(t *testing.T) {
f, err := os.Open("./testdata/package-lock/workbox-package-lock.json")
require.NoError(t, err)
defer f.Close()

root, pkgs, err := (&PackageLockParser{}).Parse(f)
info, err := (&PackageLockParser{}).Parse(f)
assert.Nil(t, err)
assert.Equal(t, 1299, len(pkgs))

root := info.Root()
assert.Equal(t, &Package{
Name: "workbox",
Version: "0.0.0",
Purl: "pkg:npm/[email protected]",
Cpes: []string{"cpe:2.3:a:workbox:workbox:0.0.0:*:*:*:*:*:*:*"},
}, root)

p := findPkg(pkgs, "@babel/generator")
transitive := info.Transitive()
assert.Equal(t, 1299, len(transitive))

p := findPkg(transitive, "@babel/generator")
assert.Equal(t, &Package{
Name: "@babel/generator",
Version: "7.0.0",
Purl: "pkg:npm/%40babel/[email protected]",
Cpes: []string{"cpe:2.3:a:\\@babel\\/generator:\\@babel\\/generator:7.0.0:*:*:*:*:*:*:*"},
}, p)

p = findPkg(pkgs, "@lerna/changed")
p = findPkg(transitive, "@lerna/changed")
assert.Equal(t, &Package{
Name: "@lerna/changed",
Version: "3.3.2",
Expand Down
Loading

0 comments on commit a0331f6

Please sign in to comment.