diff --git a/integration/fs_test.go b/integration/fs_test.go index 14142ac104..ab86816c87 100644 --- a/integration/fs_test.go +++ b/integration/fs_test.go @@ -65,13 +65,13 @@ func TestFilesystem(t *testing.T) { golden: "testdata/gomod-skip.json.golden", }, { - name: "nodejs", + name: "npm", args: args{ scanner: types.VulnerabilityScanner, - input: "testdata/fixtures/fs/nodejs", + input: "testdata/fixtures/fs/npm", listAllPkgs: true, }, - golden: "testdata/nodejs.json.golden", + golden: "testdata/npm.json.golden", }, { name: "yarn", diff --git a/integration/testdata/fixtures/fs/npm/node_modules/jquery/package.json b/integration/testdata/fixtures/fs/npm/node_modules/jquery/package.json new file mode 100644 index 0000000000..db81fb8f69 --- /dev/null +++ b/integration/testdata/fixtures/fs/npm/node_modules/jquery/package.json @@ -0,0 +1,113 @@ +{ + "name": "jquery", + "title": "jQuery", + "description": "JavaScript library for DOM operations", + "version": "3.3.9", + "main": "dist/jquery.js", + "homepage": "https://jquery.com", + "author": { + "name": "JS Foundation and other contributors", + "url": "https://github.com/jquery/jquery/blob/3.4.0/AUTHORS.txt" + }, + "repository": { + "type": "git", + "url": "https://github.com/jquery/jquery.git" + }, + "keywords": [ + "jquery", + "javascript", + "browser", + "library" + ], + "bugs": { + "url": "https://github.com/jquery/jquery/issues" + }, + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "@babel/core": "7.3.3", + "@babel/plugin-transform-for-of": "7.2.0", + "commitplease": "3.2.0", + "core-js": "2.6.5", + "eslint-config-jquery": "1.0.1", + "grunt": "1.0.3", + "grunt-babel": "8.0.0", + "grunt-cli": "1.3.2", + "grunt-compare-size": "0.4.2", + "grunt-contrib-uglify": "3.4.0", + "grunt-contrib-watch": "1.1.0", + "grunt-eslint": "21.0.0", + "grunt-git-authors": "3.2.0", + "grunt-jsonlint": "1.1.0", + "grunt-karma": "3.0.1", + "grunt-newer": "1.3.0", + "grunt-npmcopy": "0.1.0", + "gzip-js": "0.3.2", + "husky": "1.3.1", + "insight": "0.10.1", + "jsdom": "13.2.0", + "karma": "4.0.1", + "karma-browserstack-launcher": "1.4.0", + "karma-chrome-launcher": "2.2.0", + "karma-firefox-launcher": "1.1.0", + "karma-ie-launcher": "1.0.0", + "karma-jsdom-launcher": "7.1.0", + "karma-qunit": "3.0.0", + "load-grunt-tasks": "4.0.0", + "native-promise-only": "0.8.1", + "promises-aplus-tests": "2.1.2", + "q": "1.5.1", + "qunit": "2.9.2", + "raw-body": "2.3.3", + "requirejs": "2.3.6", + "sinon": "2.3.7", + "sizzle": "2.3.4", + "strip-json-comments": "2.0.1", + "testswarm": "1.1.0", + "uglify-js": "3.4.7" + }, + "scripts": { + "build": "npm install && grunt", + "start": "grunt watch", + "test:browserless": "grunt && grunt test:slow", + "test:browser": "grunt && grunt karma:main", + "test": "grunt && grunt test:slow && grunt karma:main", + "jenkins": "npm run test:browserless" + }, + "commitplease": { + "nohook": true, + "components": [ + "Docs", + "Tests", + "Build", + "Support", + "Release", + "Core", + "Ajax", + "Attributes", + "Callbacks", + "CSS", + "Data", + "Deferred", + "Deprecated", + "Dimensions", + "Effects", + "Event", + "Manipulation", + "Offset", + "Queue", + "Selector", + "Serialize", + "Traversing", + "Wrap" + ], + "markerPattern": "^((clos|fix|resolv)(e[sd]|ing))|^(refs?)", + "ticketPattern": "^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])" + }, + "husky": { + "hooks": { + "commit-msg": "node node_modules/commitplease", + "pre-commit": "grunt lint:newer qunit_fixture" + } + } +} diff --git a/integration/testdata/fixtures/fs/npm/node_modules/promise/package.json b/integration/testdata/fixtures/fs/npm/node_modules/promise/package.json new file mode 100644 index 0000000000..6b6df7e49f --- /dev/null +++ b/integration/testdata/fixtures/fs/npm/node_modules/promise/package.json @@ -0,0 +1,35 @@ +{ + "name": "promise", + "version": "8.0.3", + "description": "Bare bones Promises/A+ implementation", + "main": "index.js", + "scripts": { + "prepublish": "node build", + "pretest": "node build", + "pretest-resolve": "node build", + "pretest-extensions": "node build", + "pretest-memory-leak": "node build", + "test": "mocha --bail --timeout 200 --slow 99999 -R dot && npm run test-memory-leak", + "test-resolve": "mocha test/resolver-tests.js --timeout 200 --slow 999999", + "test-extensions": "mocha test/extensions-tests.js --timeout 200 --slow 999999", + "test-memory-leak": "node --expose-gc test/memory-leak.js", + "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --bail --timeout 200 --slow 99999 -R dot" + }, + "repository": { + "type": "git", + "url": "https://github.com/then/promise.git" + }, + "author": "ForbesLindesay", + "license": "MIT", + "devDependencies": { + "acorn": "^1.0.1", + "better-assert": "*", + "istanbul": "^0.3.13", + "mocha": "*", + "promises-aplus-tests": "*", + "rimraf": "^2.3.2" + }, + "dependencies": { + "asap": "~2.0.6" + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/fs/npm/node_modules/react-is/package.json b/integration/testdata/fixtures/fs/npm/node_modules/react-is/package.json new file mode 100644 index 0000000000..6e502546f8 --- /dev/null +++ b/integration/testdata/fixtures/fs/npm/node_modules/react-is/package.json @@ -0,0 +1,27 @@ +{ + "name": "react-is", + "version": "16.8.6", + "description": "Brand checking of React Elements.", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react-is" + }, + "keywords": [ + "react" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://reactjs.org/", + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "cjs/", + "umd/" + ] +} diff --git a/integration/testdata/fixtures/fs/npm/node_modules/react/package.json b/integration/testdata/fixtures/fs/npm/node_modules/react/package.json new file mode 100644 index 0000000000..fb0216747f --- /dev/null +++ b/integration/testdata/fixtures/fs/npm/node_modules/react/package.json @@ -0,0 +1,39 @@ +{ + "name": "react", + "description": "React is a JavaScript library for building user interfaces.", + "keywords": [ + "react" + ], + "version": "16.8.6", + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "cjs/", + "umd/" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react" + }, + "engines": { + "node": ">=0.10.0" + }, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + } +} diff --git a/integration/testdata/fixtures/fs/npm/node_modules/redux/package.json b/integration/testdata/fixtures/fs/npm/node_modules/redux/package.json new file mode 100644 index 0000000000..449e8fa08b --- /dev/null +++ b/integration/testdata/fixtures/fs/npm/node_modules/redux/package.json @@ -0,0 +1,105 @@ +{ + "name": "redux", + "version": "4.0.1", + "description": "Predictable state container for JavaScript apps", + "license": "MIT", + "homepage": "http://redux.js.org", + "repository": "github:reduxjs/redux", + "bugs": "https://github.com/reduxjs/redux/issues", + "keywords": [ + "redux", + "reducer", + "state", + "predictable", + "functional", + "immutable", + "hot", + "live", + "replay", + "flux", + "elm" + ], + "authors": [ + "Dan Abramov (https://github.com/gaearon)", + "Andrew Clark (https://github.com/acdlite)" + ], + "main": "lib/redux.js", + "unpkg": "dist/redux.js", + "module": "es/redux.js", + "typings": "./index.d.ts", + "files": [ + "dist", + "lib", + "es", + "src", + "index.d.ts" + ], + "scripts": { + "clean": "rimraf lib dist es coverage", + "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts", + "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts", + "lint": "eslint src test", + "pretest": "npm run build", + "test": "jest", + "test:watch": "npm test -- --watch", + "test:cov": "npm test -- --coverage", + "build": "rollup -c", + "prepare": "npm run clean && npm run format:check && npm run lint && npm test", + "examples:lint": "eslint examples", + "examples:test": "cross-env CI=true babel-node examples/testAll.js" + }, + "dependencies": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/node": "^7.0.0", + "@babel/plugin-external-helpers": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-flow": "^7.0.0", + "@babel/register": "^7.0.0", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^9.0.0", + "babel-jest": "^23.6.0", + "cross-env": "^5.2.0", + "eslint": "^5.6.0", + "eslint-config-react-app": "^2.1.0", + "eslint-plugin-flowtype": "^2.50.1", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jsx-a11y": "^6.1.1", + "eslint-plugin-react": "^7.11.1", + "glob": "^7.1.3", + "jest": "^23.6.0", + "prettier": "^1.14.3", + "rimraf": "^2.6.2", + "rollup": "^0.66.2", + "rollup-plugin-babel": "^4.0.1", + "rollup-plugin-node-resolve": "^3.4.0", + "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-terser": "^3.0.0", + "rxjs": "^6.3.2", + "typescript": "^3.0.3", + "typings-tester": "^0.3.2" + }, + "npmName": "redux", + "npmFileMap": [ + { + "basePath": "/dist/", + "files": [ + "*.js" + ] + } + ], + "browserify": { + "transform": [ + "loose-envify" + ] + }, + "jest": { + "testRegex": "(/test/.*\\.spec.js)$" + }, + "sideEffects": false +} diff --git a/integration/testdata/fixtures/fs/nodejs/package-lock.json b/integration/testdata/fixtures/fs/npm/package-lock.json similarity index 95% rename from integration/testdata/fixtures/fs/nodejs/package-lock.json rename to integration/testdata/fixtures/fs/npm/package-lock.json index d957f85907..6002f59fdf 100644 --- a/integration/testdata/fixtures/fs/nodejs/package-lock.json +++ b/integration/testdata/fixtures/fs/npm/package-lock.json @@ -8,11 +8,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, "jquery": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz", diff --git a/integration/testdata/nodejs.json.golden b/integration/testdata/npm.json.golden similarity index 82% rename from integration/testdata/nodejs.json.golden rename to integration/testdata/npm.json.golden index f28c7072de..4ab43682c0 100644 --- a/integration/testdata/nodejs.json.golden +++ b/integration/testdata/npm.json.golden @@ -1,6 +1,6 @@ { "SchemaVersion": 2, - "ArtifactName": "testdata/fixtures/fs/nodejs", + "ArtifactName": "testdata/fixtures/fs/npm", "ArtifactType": "filesystem", "Metadata": { "ImageConfig": { @@ -39,10 +39,13 @@ "Version": "3.3.9", "Indirect": true, "Layer": {}, + "Licenses": [ + "MIT" + ], "Locations": [ { - "StartLine": 16, - "EndLine": 20 + "StartLine": 11, + "EndLine": 15 } ] }, @@ -54,21 +57,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 21, - "EndLine": 25 - } - ] - }, - { - "ID": "lodash@4.17.4", - "Name": "lodash", - "Version": "4.17.4", - "Indirect": true, - "Layer": {}, - "Locations": [ - { - "StartLine": 11, - "EndLine": 15 + "StartLine": 16, + "EndLine": 20 } ] }, @@ -83,8 +73,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 26, - "EndLine": 33 + "StartLine": 21, + "EndLine": 28 } ] }, @@ -96,8 +86,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 34, - "EndLine": 38 + "StartLine": 29, + "EndLine": 33 } ] }, @@ -110,10 +100,13 @@ "asap@2.0.6" ], "Layer": {}, + "Licenses": [ + "MIT" + ], "Locations": [ { - "StartLine": 39, - "EndLine": 46 + "StartLine": 34, + "EndLine": 41 } ] }, @@ -130,8 +123,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 47, - "EndLine": 56 + "StartLine": 42, + "EndLine": 51 } ] }, @@ -147,10 +140,13 @@ "scheduler@0.13.6" ], "Layer": {}, + "Licenses": [ + "MIT" + ], "Locations": [ { - "StartLine": 57, - "EndLine": 67 + "StartLine": 52, + "EndLine": 62 } ] }, @@ -160,10 +156,13 @@ "Version": "16.8.6", "Indirect": true, "Layer": {}, + "Licenses": [ + "MIT" + ], "Locations": [ { - "StartLine": 68, - "EndLine": 72 + "StartLine": 63, + "EndLine": 67 } ] }, @@ -177,10 +176,13 @@ "symbol-observable@1.2.0" ], "Layer": {}, + "Licenses": [ + "MIT" + ], "Locations": [ { - "StartLine": 73, - "EndLine": 81 + "StartLine": 68, + "EndLine": 76 } ] }, @@ -196,8 +198,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 82, - "EndLine": 90 + "StartLine": 77, + "EndLine": 85 } ] }, @@ -209,8 +211,8 @@ "Layer": {}, "Locations": [ { - "StartLine": 91, - "EndLine": 95 + "StartLine": 86, + "EndLine": 90 } ] } @@ -331,51 +333,6 @@ ], "PublishedDate": "2019-04-20T00:29:00Z", "LastModifiedDate": "2021-10-20T11:15:00Z" - }, - { - "VulnerabilityID": "CVE-2019-10744", - "PkgID": "lodash@4.17.4", - "PkgName": "lodash", - "InstalledVersion": "4.17.4", - "FixedVersion": "4.17.12", - "Layer": {}, - "SeveritySource": "ghsa", - "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-10744", - "DataSource": { - "ID": "ghsa", - "Name": "GitHub Security Advisory Npm", - "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" - }, - "Title": "nodejs-lodash: prototype pollution in defaultsDeep function leading to modifying properties", - "Description": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", - "Severity": "CRITICAL", - "CVSS": { - "nvd": { - "V2Vector": "AV:N/AC:L/Au:N/C:N/I:P/A:P", - "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", - "V2Score": 6.4, - "V3Score": 9.1 - }, - "redhat": { - "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", - "V3Score": 9.1 - } - }, - "References": [ - "https://access.redhat.com/errata/RHSA-2019:3024", - "https://access.redhat.com/security/cve/CVE-2019-10744", - "https://github.com/advisories/GHSA-jf85-cpcp-j695", - "https://github.com/lodash/lodash/pull/4336", - "https://nvd.nist.gov/vuln/detail/CVE-2019-10744", - "https://security.netapp.com/advisory/ntap-20191004-0005/", - "https://snyk.io/vuln/SNYK-JS-LODASH-450202", - "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", - "https://www.npmjs.com/advisories/1065", - "https://www.oracle.com/security-alerts/cpujan2021.html", - "https://www.oracle.com/security-alerts/cpuoct2020.html" - ], - "PublishedDate": "2019-07-26T00:15:00Z", - "LastModifiedDate": "2021-03-16T13:57:00Z" } ] } diff --git a/pkg/fanal/analyzer/language/nodejs/npm/npm.go b/pkg/fanal/analyzer/language/nodejs/npm/npm.go index 3747f965d2..a375d9e795 100644 --- a/pkg/fanal/analyzer/language/nodejs/npm/npm.go +++ b/pkg/fanal/analyzer/language/nodejs/npm/npm.go @@ -2,40 +2,99 @@ package npm import ( "context" + "errors" + "io/fs" "os" + "path" "path/filepath" + "strings" "golang.org/x/xerrors" + dio "github.com/aquasecurity/go-dep-parser/pkg/io" "github.com/aquasecurity/go-dep-parser/pkg/nodejs/npm" + "github.com/aquasecurity/go-dep-parser/pkg/nodejs/packagejson" + godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types" "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" "github.com/aquasecurity/trivy/pkg/fanal/types" - "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" ) func init() { - analyzer.RegisterAnalyzer(&npmLibraryAnalyzer{}) + analyzer.RegisterPostAnalyzer(analyzer.TypeNpmPkgLock, newNpmLibraryAnalyzer) } -const version = 1 +const ( + version = 1 +) -var requiredFiles = []string{types.NpmPkgLock} +type npmLibraryAnalyzer struct { + lockParser godeptypes.Parser + packageParser godeptypes.Parser +} -type npmLibraryAnalyzer struct{} +func newNpmLibraryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &npmLibraryAnalyzer{ + lockParser: npm.NewParser(), + packageParser: packagejson.NewParser(), + }, nil +} -func (a npmLibraryAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { - p := npm.NewParser() - res, err := language.Analyze(types.Npm, input.FilePath, input.Content, p) - if err != nil { - return nil, xerrors.Errorf("unable to parse %s: %w", input.FilePath, err) +func (a npmLibraryAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + // Parse package-lock.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkgLock } - return res, nil + + var apps []types.Application + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, d fs.DirEntry, r dio.ReadSeekerAt) error { + // Find all licenses from package.json files under node_modules dirs + licenses, err := a.findLicenses(input.FS, filePath) + if err != nil { + log.Logger.Errorf("Unable to collect licenses: %s", err) + licenses = map[string]string{} + } + + app, err := a.parseNpmPkgLock(input.FS, filePath) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Fill licenses + for i, lib := range app.Libraries { + if license, ok := licenses[lib.ID]; ok { + app.Libraries[i].Licenses = []string{license} + } + } + + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("package-lock.json/package.json walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil } func (a npmLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { fileName := filepath.Base(filePath) - return utils.StringInSlice(fileName, requiredFiles) + if fileName == types.NpmPkgLock { + return true + } + // The file path to package.json - */node_modules//package.json + // The path is slashed in analyzers. + dirs := strings.Split(path.Dir(filePath), "/") + if len(dirs) > 1 && dirs[len(dirs)-2] == "node_modules" && fileName == types.NpmPkg { + return true + } + return false } func (a npmLibraryAnalyzer) Type() analyzer.Type { @@ -45,3 +104,59 @@ func (a npmLibraryAnalyzer) Type() analyzer.Type { func (a npmLibraryAnalyzer) Version() int { return version } + +func (a npmLibraryAnalyzer) parseNpmPkgLock(fsys fs.FS, path string) (*types.Application, error) { + f, err := fsys.Open(path) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + file, ok := f.(dio.ReadSeekCloserAt) + if !ok { + return nil, xerrors.Errorf("type assertion error: %w", err) + } + + // parse package-lock.json file + libs, deps, err := a.lockParser.Parse(file) + if err != nil { + return nil, xerrors.Errorf("unable to parse package-lock.json: %w", err) + } + return language.ToApplication(types.Npm, path, "", libs, deps), nil +} + +func (a npmLibraryAnalyzer) findLicenses(fsys fs.FS, lockPath string) (map[string]string, error) { + dir := filepath.Dir(lockPath) + root := path.Join(dir, "node_modules") + if _, err := fs.Stat(fsys, root); errors.Is(err, fs.ErrNotExist) { + log.Logger.Infof(`To collect the license information of packages in %q, "npm install" needs to be performed beforehand`, lockPath) + return nil, nil + } + + // Parse package.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkg + } + + // Traverse node_modules dir and find licenses + // Note that fs.FS is always slashed regardless of the platform, + // and path.Join should be used rather than filepath.Join. + licenses := map[string]string{} + err := fsutils.WalkDir(fsys, root, required, func(filePath string, d fs.DirEntry, r dio.ReadSeekerAt) error { + lib, _, err := a.packageParser.Parse(r) + // package.json always contains only 1 library. + // https://github.com/aquasecurity/go-dep-parser/blob/63a15cdc6bc3aaeb58c4172b275deadde4d55928/pkg/nodejs/packagejson/parse.go#L33-L37 + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", filePath, err) + } else if len(lib) != 1 { + return xerrors.Errorf("unable to parse %q", filePath) + } + + licenses[lib[0].ID] = lib[0].License + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + return licenses, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go b/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go index 77456c67e0..d16961f736 100644 --- a/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go +++ b/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go @@ -12,23 +12,29 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" ) +func TestMain(m *testing.M) { + _ = log.InitLogger(false, true) + os.Exit(m.Run()) +} + func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { tests := []struct { - name string - inputFile string - want *analyzer.AnalysisResult - wantErr string + name string + dir string + want *analyzer.AnalysisResult + wantErr string }{ { - name: "happy path", - inputFile: "testdata/package-lock.json", + name: "with node_modules", + dir: "testdata/happy", want: &analyzer.AnalysisResult{ Applications: []types.Application{ { Type: types.Npm, - FilePath: "testdata/package-lock.json", + FilePath: "package-lock.json", Libraries: []types.Package{ { ID: "array-flatten@1.1.1", @@ -48,6 +54,7 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { Version: "1.18.3", Indirect: true, DependsOn: []string{"debug@2.6.9"}, + Licenses: []string{"MIT"}, Locations: []types.Location{ { StartLine: 17, @@ -61,6 +68,7 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { Version: "2.6.9", Indirect: true, DependsOn: []string{"ms@2.0.0"}, + Licenses: []string{"MIT"}, Locations: []types.Location{ { StartLine: 25, @@ -78,6 +86,7 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { Version: "4.16.4", Indirect: true, DependsOn: []string{"debug@2.6.9"}, + Licenses: []string{"MIT"}, Locations: []types.Location{ { StartLine: 40, @@ -90,6 +99,7 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { Name: "ms", Version: "2.0.0", Indirect: true, + Licenses: []string{"MIT"}, Locations: []types.Location{ { StartLine: 33, @@ -106,6 +116,7 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { Name: "ms", Version: "2.1.1", Indirect: true, + Licenses: []string{"MIT"}, Locations: []types.Location{ { StartLine: 63, @@ -119,26 +130,48 @@ func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { }, }, { - name: "sad path", - inputFile: "testdata/wrong.json", - wantErr: "unable to parse", + name: "without node_modules", + dir: "testdata/no-node_modules", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Npm, + FilePath: "package-lock.json", + Libraries: []types.Package{ + { + ID: "ms@2.1.1", + Name: "ms", + Version: "2.1.1", + Indirect: true, + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 10, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + dir: "testdata/sad", + wantErr: "unable to parse", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f, err := os.Open(tt.inputFile) + a, err := newNpmLibraryAnalyzer(analyzer.AnalyzerOptions{}) require.NoError(t, err) - defer func() { _ = f.Close() }() - a := npmLibraryAnalyzer{} - got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{ - FilePath: tt.inputFile, - Content: f, + got, err := a.PostAnalyze(context.Background(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), }) if tt.wantErr != "" { - require.NotNil(t, err) - assert.Contains(t, err.Error(), tt.wantErr) + assert.ErrorContains(t, err, tt.wantErr) return } @@ -175,13 +208,18 @@ func Test_nodePkgLibraryAnalyzer_Required(t *testing.T) { want bool }{ { - name: "happy path", + name: "lock file", filePath: "npm/package-lock.json", want: true, }, + { + name: "package.json", + filePath: "npm/node_modules/ms/package.json", + want: true, + }, { name: "sad path", - filePath: "npm/package.json", + filePath: "npm/node_modules/package.json", want: false, }, } diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json new file mode 100644 index 0000000000..dc787ba767 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json @@ -0,0 +1,49 @@ +{ + "name": "debug", + "version": "2.6.9", + "repository": { + "type": "git", + "url": "git://github.com/visionmedia/debug.git" + }, + "description": "small debugging utility", + "keywords": [ + "debug", + "log", + "debugger" + ], + "author": "TJ Holowaychuk ", + "contributors": [ + "Nathan Rajlich (http://n8.io)", + "Andrew Rhyne " + ], + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + }, + "devDependencies": { + "browserify": "9.0.3", + "chai": "^3.5.0", + "concurrently": "^3.1.0", + "coveralls": "^2.11.15", + "eslint": "^3.12.1", + "istanbul": "^0.4.5", + "karma": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon": "^1.0.5", + "mocha": "^3.2.0", + "mocha-lcov-reporter": "^1.2.0", + "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "sinon-chai": "^2.8.0" + }, + "main": "./src/index.js", + "browser": "./src/browser.js", + "component": { + "scripts": { + "debug/index.js": "browser.js", + "debug/debug.js": "debug.js" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json new file mode 100644 index 0000000000..6a31c81fac --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.0.0", + "description": "Tiny milisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "3.19.0", + "expect.js": "0.3.1", + "husky": "0.13.3", + "lint-staged": "3.4.1", + "mocha": "3.4.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json new file mode 100644 index 0000000000..db78b735ba --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json @@ -0,0 +1,52 @@ +{ + "name": "body-parser", + "description": "Node.js body parsing middleware", + "version": "1.18.3", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "expressjs/body-parser", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "devDependencies": { + "eslint": "4.19.1", + "eslint-config-standard": "11.0.0", + "eslint-plugin-import": "2.11.0", + "eslint-plugin-markdown": "1.0.0-beta.6", + "eslint-plugin-node": "6.0.1", + "eslint-plugin-promise": "3.7.0", + "eslint-plugin-standard": "3.1.0", + "istanbul": "0.4.5", + "methods": "1.1.2", + "mocha": "2.5.3", + "safe-buffer": "5.1.2", + "supertest": "1.1.0" + }, + "files": [ + "lib/", + "LICENSE", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --require test/support/env --reporter spec --check-leaks --bail test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json new file mode 100644 index 0000000000..dc787ba767 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json @@ -0,0 +1,49 @@ +{ + "name": "debug", + "version": "2.6.9", + "repository": { + "type": "git", + "url": "git://github.com/visionmedia/debug.git" + }, + "description": "small debugging utility", + "keywords": [ + "debug", + "log", + "debugger" + ], + "author": "TJ Holowaychuk ", + "contributors": [ + "Nathan Rajlich (http://n8.io)", + "Andrew Rhyne " + ], + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + }, + "devDependencies": { + "browserify": "9.0.3", + "chai": "^3.5.0", + "concurrently": "^3.1.0", + "coveralls": "^2.11.15", + "eslint": "^3.12.1", + "istanbul": "^0.4.5", + "karma": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon": "^1.0.5", + "mocha": "^3.2.0", + "mocha-lcov-reporter": "^1.2.0", + "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "sinon-chai": "^2.8.0" + }, + "main": "./src/index.js", + "browser": "./src/browser.js", + "component": { + "scripts": { + "debug/index.js": "browser.js", + "debug/debug.js": "debug.js" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json new file mode 100644 index 0000000000..6a31c81fac --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.0.0", + "description": "Tiny milisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "3.19.0", + "expect.js": "0.3.1", + "husky": "0.13.3", + "lint-staged": "3.4.1", + "mocha": "3.4.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json new file mode 100644 index 0000000000..74196ad68e --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json @@ -0,0 +1,98 @@ +{ + "name": "express", + "description": "Fast, unopinionated, minimalist web framework", + "version": "4.16.4", + "author": "TJ Holowaychuk ", + "contributors": [ + "Aaron Heckmann ", + "Ciaran Jessup ", + "Douglas Christopher Wilson ", + "Guillermo Rauch ", + "Jonathan Ong ", + "Roman Shtylman ", + "Young Jae Sim " + ], + "license": "MIT", + "repository": "expressjs/express", + "homepage": "http://expressjs.com/", + "keywords": [ + "express", + "framework", + "sinatra", + "web", + "rest", + "restful", + "router", + "app", + "api" + ], + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "devDependencies": { + "after": "0.8.2", + "connect-redis": "3.4.0", + "cookie-parser": "~1.4.3", + "cookie-session": "1.3.2", + "ejs": "2.6.1", + "eslint": "2.13.1", + "express-session": "1.15.6", + "hbs": "4.0.1", + "istanbul": "0.4.5", + "marked": "0.5.1", + "method-override": "3.0.0", + "mocha": "5.2.0", + "morgan": "1.9.1", + "multiparty": "4.2.1", + "pbkdf2-password": "1.2.1", + "should": "13.2.3", + "supertest": "3.3.0", + "vhost": "~3.0.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "files": [ + "LICENSE", + "History.md", + "Readme.md", + "index.js", + "lib/" + ], + "scripts": { + "lint": "eslint .", + "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", + "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/", + "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json new file mode 100644 index 0000000000..fc28cb39d3 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.1.1", + "description": "Tiny millisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "4.12.1", + "expect.js": "0.3.1", + "husky": "0.14.3", + "lint-staged": "5.0.0", + "mocha": "4.0.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/package-lock.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/package-lock.json similarity index 100% rename from pkg/fanal/analyzer/language/nodejs/npm/testdata/package-lock.json rename to pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/package-lock.json diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json new file mode 100644 index 0000000000..78bd837867 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/wrong.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/sad/package-lock.json similarity index 100% rename from pkg/fanal/analyzer/language/nodejs/npm/testdata/wrong.json rename to pkg/fanal/analyzer/language/nodejs/npm/testdata/sad/package-lock.json diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index 7065b1335c..0e8c3d77cc 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -58,6 +58,7 @@ const ( MavenPom = "pom.xml" + NpmPkg = "package.json" NpmPkgLock = "package-lock.json" YarnLock = "yarn.lock" PnpmLock = "pnpm-lock.yaml"