Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions cmd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ func AuditCommand() *cobra.Command {
}
}

var npmProjects []resolver.Project
if cfg.Npm.Lockfile != "" {
npmProjects, err = resolver.ProjectsFromNpmLockfileV3(cfg.Npm.Lockfile)

if err != nil {
return errors.Wrap(err, "failed to discover dependencies from npm lockfile "+cfg.Npm.Lockfile)
}
}

var depProjects []resolver.Project
if cfg.Dep.Lockfile != "" {
depProjects, err = resolver.ProjectsFromDepLockfile(cfg.Dep.Lockfile)
Expand All @@ -59,10 +68,10 @@ func AuditCommand() *cobra.Command {
var yarnResolved = make(map[string]resolver.Dependency)
if len(yarnProjects) > 0 {
dirList := cfg.Yarn.NodeModulesDirs
fmt.Printf("Processing JS deps directories: %v \n", dirList)
fmt.Printf("Processing JS (yarn) deps directories: %v \n", dirList)
currentDeps, err := resolver.LocateProjects(dirList, yarnProjects)
if err != nil {
return errors.Wrapf(err, "failed to locate js dependencies in dirs %v", dirList)
return errors.Wrapf(err, "failed to locate js dependencies (yarn) in dirs %v", dirList)
}
for _, v := range currentDeps {
fmt.Printf("Target dependency: %s \n", v)
Expand All @@ -71,7 +80,18 @@ func AuditCommand() *cobra.Command {

yarnResolved[keyWithVersion] = v
}
}

var npmResolved = make(map[string]resolver.Dependency)
if len(npmProjects) > 0 {
rootDir := cfg.Npm.NodeModulesDir
currentDeps, err := resolver.LocateNpmPackageLockV3Projects(rootDir, npmProjects)
if err != nil {
return errors.Wrapf(err, "failed to locate js dependencies (npm) in dir %v", rootDir)
}
for versionedKey, dep := range currentDeps {
npmResolved[versionedKey] = dep
}
}

var depResolved map[string]resolver.Dependency
Expand All @@ -92,7 +112,7 @@ func AuditCommand() *cobra.Command {
}
}

dependencies, err := joinDeps(cfg.PatternConfig, yarnResolved, depResolved, goModResolved)
dependencies, err := joinDeps(cfg.PatternConfig, yarnResolved, npmResolved, depResolved, goModResolved)
if err != nil {
return errors.Wrap(err, "resolving dependencies")
}
Expand Down
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Config struct {
Dep DepConfig `json:"dep"`
GoMod GoModConfig `json:"gomod"`
Yarn YarnConfig `json:"yarn"`
Npm NpmConfig `json:"npm"`
PatternConfig
}

Expand All @@ -34,6 +35,11 @@ type YarnConfig struct {
Lockfile string `json:"lockfile"`
}

type NpmConfig struct {
NodeModulesDir string `json:"node-modules-dir"`
Lockfile string `json:"lockfile"`
}

func Load(filename string) (*Config, error) {
body, err := ioutil.ReadFile(filename)
if err != nil {
Expand Down
78 changes: 78 additions & 0 deletions resolver/npm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package resolver

import (
"encoding/json"
"os"
"strings"
)

func ProjectsFromNpmLockfileV3(filename string) ([]Project, error) {
byteValue, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

var packageLock PackageLockV3

json.Unmarshal(byteValue, &packageLock)

return asNpmProjects(packageLock), nil
}

type NpmProject struct {
name string
version string
optional bool
}

var _ Project = (*NpmProject)(nil)

func (p NpmProject) Name() string {
return p.name
}

func (p NpmProject) Optional() bool {
return p.optional
}

func (p NpmProject) Version() string {
return p.version
}

type PackageLockV3 struct {
Name string `json:"name"`
Version string `json:"version"`
Packages map[string]NpmPackage `json:"packages"`
}

type NpmPackage struct {
Name string `json:"name"`
Version string `json:"version"`
}

func asNpmProjects(packageLock PackageLockV3) []Project {
projectSet := make(map[string]NpmProject, len(packageLock.Packages)-1)

for pkg, entry := range packageLock.Packages {
if pkg == "" {
// skip the top-level package as it refers to the project itself
continue
}

projectSet[pkg] = NpmProject{
// Remove the `node_modules/` root directory prefix from each `pkg`
name: strings.TrimPrefix(pkg, "node_modules/"),
version: entry.Version,
// optional packages that are included as dependencies in the build will be declared at the top
// level of packageLock.Packages, so we can explicitly mark optional as false here
optional: false,
}
}

projectList := make([]Project, 0, len(projectSet))
for _, project := range projectSet {
projectList = append(projectList, project)
}

return projectList
}
41 changes: 41 additions & 0 deletions resolver/npm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package resolver

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestProjectsFromNpmLockfileV3(t *testing.T) {
actualProjects, err := ProjectsFromNpmLockfileV3("testdata/package-lock.json")
require.Nil(t, err)

expectedProjects := make(map[string]NpmProject, 4)
expectedProjects["@aashutoshrathi/word-wrap"] = NpmProject{
name: "@aashutoshrathi/word-wrap",
optional: false,
version: "1.2.6",
}
expectedProjects["@adobe/css-tools"] = NpmProject{
name: "@adobe/css-tools",
optional: false,
version: "4.3.2",
}
expectedProjects["yup/node_modules/type-fest"] = NpmProject{
name: "yup/node_modules/type-fest",
optional: false,
version: "2.19.0",
}
expectedProjects["@apollo/client"] = NpmProject{
name: "@apollo/client",
optional: false,
version: "3.8.7",
}

for _, actualProject := range actualProjects {
expectedProject, ok := expectedProjects[actualProject.Name()]
require.True(t, ok)
assert.Equal(t, expectedProject, actualProject)
}
}
29 changes: 29 additions & 0 deletions resolver/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package resolver
import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"

Expand All @@ -23,6 +24,8 @@ type Dependency struct {
Version string
}

var nodeModulePrefixRegexp = regexp.MustCompile(`.*node_modules\/`)

func LocateGoModProjects(projects []GoModProject) (map[string]Dependency, error) {
deps := make(map[string]Dependency, len(projects))

Expand All @@ -38,6 +41,32 @@ func LocateGoModProjects(projects []GoModProject) (map[string]Dependency, error)
return deps, nil
}

func LocateNpmPackageLockV3Projects(root string, projects []Project) (map[string]Dependency, error) {
deps := make(map[string]Dependency, len(projects))

for _, project := range projects {
baseDependencyName := nodeModulePrefixRegexp.ReplaceAllString(project.Name(), "")
sourceDir := filepath.Join(root, project.Name())
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
// If a direct path to a nested dependency is not found, it was hoisted to the root to
// be shared among multiple parent dependencies.
//
// e.g. If multiple dependencies share a common transitive dependency on `type-fest`, the
// path `node_modules/yup/node_modules/type-fest` could become `node_modules/type-fest`
// when installed on disk.
sourceDir = filepath.Join(root, baseDependencyName)
}
dep := Dependency{
Name: baseDependencyName,
Version: project.Version(),
SourceDir: sourceDir,
}
deps[baseDependencyName+dep.Version] = dep
}

return deps, nil
}

func LocateProjects(roots []string, projects []Project) (map[string]Dependency, error) {
locations := make(map[string]Dependency)

Expand Down
82 changes: 82 additions & 0 deletions resolver/testdata/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.