From 7fb7d1c0fa4c3e25e08711978a6d10e0473311f8 Mon Sep 17 00:00:00 2001 From: sverweij Date: Wed, 22 Nov 2017 19:46:22 +0100 Subject: [PATCH 1/7] build(npm): :arrow_up: ajv, commander, js-makedepend --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ce2e91d8e..d2d02ec47 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "eslint-plugin-security": "1.4.0", "intercept-stdout": "0.1.2", "istanbul": "0.4.5", - "js-makedepend": "2.4.0", + "js-makedepend": "2.4.1", "mocha": "4.0.1", "npm-check-updates": "2.13.0", "nsp": "3.1.0", @@ -61,9 +61,9 @@ "homepage": "https://github.com/sverweij/dependency-cruiser", "dependencies": { "acorn": "5.2.1", - "ajv": "5.3.0", + "ajv": "5.5.0", "chalk": "2.3.0", - "commander": "2.11.0", + "commander": "2.12.1", "figures": "2.0.0", "handlebars": "4.0.11", "lodash": "4.17.4", From e0139f94e62cb7055ea2c0b4e9b98c78a8df31d5 Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 11:17:33 +0100 Subject: [PATCH 2/7] feat(extract): adds a 'license' field for dependencies --- Makefile | 2 + src/extract/jsonschema.json | 8 ++ .../resolve/determineDependencyTypes.js | 19 +---- src/extract/resolve/localNpmHelpers.js | 54 ++++++++++++- src/extract/resolve/resolve-commonJS.js | 7 ++ .../fixtures/bundled-dependencies.json | 2 + test/extract/fixtures/cjs.json | 1 + test/extract/fixtures/es6.json | 4 + .../node_modules/GPL-license/index.js | 2 + .../node_modules/GPL-license/package.json | 77 +++++++++++++++++++ .../node_modules/array-licenses/package.json | 14 ++++ .../node_modules/boolean-license/package.json | 5 ++ .../node_modules/no-license/package.json | 4 + .../node_modules/object-license/package.json | 8 ++ .../licenses/require-something-deprecated.js | 1 + test/extract/resolve/localNpmHelpers.spec.js | 59 ++++++++++++++ 16 files changed, 245 insertions(+), 22 deletions(-) create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/GPL-license/index.js create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/GPL-license/package.json create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/array-licenses/package.json create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/boolean-license/package.json create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/no-license/package.json create mode 100644 test/extract/resolve/fixtures/licenses/node_modules/object-license/package.json create mode 100644 test/extract/resolve/fixtures/licenses/require-something-deprecated.js diff --git a/Makefile b/Makefile index 1a98c6c1a..65602cb7f 100644 --- a/Makefile +++ b/Makefile @@ -172,6 +172,7 @@ src/extract/resolve/determineDependencyTypes.js: \ src/extract/resolve/resolve-commonJS.js: \ src/extract/resolve/determineDependencyTypes.js \ + src/extract/resolve/localNpmHelpers.js \ src/extract/resolve/readPackageDeps.js \ src/extract/transpile/meta.js @@ -314,6 +315,7 @@ src/extract/resolve/determineDependencyTypes.js: \ src/extract/resolve/resolve-commonJS.js: \ src/extract/resolve/determineDependencyTypes.js \ + src/extract/resolve/localNpmHelpers.js \ src/extract/resolve/readPackageDeps.js \ src/extract/transpile/meta.js diff --git a/src/extract/jsonschema.json b/src/extract/jsonschema.json index 15416bb78..69dbfe329 100644 --- a/src/extract/jsonschema.json +++ b/src/extract/jsonschema.json @@ -68,6 +68,10 @@ "items": { "$ref": "#/definitions/DependencyType" }, "description": "the type of inclusion - local, core, unknown (= we honestly don't know), undetermined (= we didn't bother determining it) or one of the npm dependencies defined in a package.jsom ('npm' for 'depenencies', 'npm-dev', 'npm-optional', 'npm-peer', 'npm-no-pkg' for development, optional, peer dependencies and dependencies in node_modules but not in package.json respectively)" }, + "license": { + "type": "string", + "description": "the license, if known (usually known for modules pulled from npm, not for local ones)" + }, "dependencies": { "type": "array", "items": { @@ -101,6 +105,10 @@ "items": { "$ref": "#/definitions/DependencyType" }, "description": "the type of inclusion - local, core, unknown (= we honestly don't know), undetermined (= we didn't bother determining it) or one of the npm dependencies defined in a package.jsom ('npm' for 'depenencies', 'npm-dev', 'npm-optional', 'npm-peer', 'npm-no-pkg' for development, optional, peer dependencies and dependencies in node_modules but not in package.json respectively)" }, + "license": { + "type": "string", + "description": "the license, if known (usually known for modules pulled from npm, not for local ones)" + }, "followable": { "type": "boolean", "description": "Whether or not this is a dependency that can be followed any further. This will be 'false' for for core modules, json, modules that could not be resolved to a file and modules that weren't followed because it matches the doNotFollow expression." diff --git a/src/extract/resolve/determineDependencyTypes.js b/src/extract/resolve/determineDependencyTypes.js index 68da85feb..4cb478dc6 100644 --- a/src/extract/resolve/determineDependencyTypes.js +++ b/src/extract/resolve/determineDependencyTypes.js @@ -26,16 +26,6 @@ function determineNpmDependencyTypes(pModuleName, pPackageDeps) { return lRetval; } -function dependencyIsDeprecated (pModule, pBaseDir) { - let lRetval = false; - let lPackageJson = localNpmHelpers.getPackageJson(pModule, pBaseDir); - - if (Boolean(lPackageJson)){ - lRetval = lPackageJson.hasOwnProperty("deprecated") && lPackageJson.deprecated; - } - return lRetval; -} - function dependencyIsBundled(pModule, pPackageDeps) { let lRetval = false; @@ -64,19 +54,12 @@ module.exports = (pDependency, pModuleName, pPackageDeps, pBaseDir) => { } else if (pModuleName.startsWith(".")) { lRetval = ["local"]; } else if (pDependency.resolved.includes("node_modules")) { - // probably a node_module - let's see if we can find it in the package - // deps - but we're only interested in anything up till the first - // '/' (if any) - because e.g. 'lodash/fp' is ultimately the 'lodash' - // package... - // - // unless the package is 'scoped (@organization/coolpackage), - // in which case we'd need it until the second '/' lRetval = determineNpmDependencyTypes( localNpmHelpers.getPackageRoot(pModuleName), pPackageDeps ); - if (dependencyIsDeprecated(pModuleName, pBaseDir)) { + if (localNpmHelpers.dependencyIsDeprecated(pModuleName, pBaseDir)) { lRetval.push("deprecated"); } if (dependencyIsBundled(pModuleName, pPackageDeps)) { diff --git a/src/extract/resolve/localNpmHelpers.js b/src/extract/resolve/localNpmHelpers.js index 22bed13bb..a00d7eec4 100644 --- a/src/extract/resolve/localNpmHelpers.js +++ b/src/extract/resolve/localNpmHelpers.js @@ -34,7 +34,7 @@ const isScoped = (pModule) => pModule.startsWith('@'); * @param {string} pModule a module name * @return {string} the module name root */ -module.exports.getPackageRoot = (pModule) => { +function getPackageRoot (pModule) { if (!Boolean(pModule) || isLocal(pModule)) { return pModule; } @@ -55,7 +55,7 @@ module.exports.getPackageRoot = (pModule) => { // lodash // lodash/fp return lPathElements[0]; -}; +} /** * returns the contents of the package.json of the given pModule as it would @@ -74,12 +74,12 @@ module.exports.getPackageRoot = (pModule) => { * null if either module or package.json could * not be found */ -module.exports.getPackageJson = (pModule, pBaseDir) => { +function getPackageJson (pModule, pBaseDir) { let lRetval = null; try { let lPackageJsonFilename = resolve.sync( - path.join(module.exports.getPackageRoot(pModule), "package.json"), + path.join(getPackageRoot(pModule), "package.json"), { basedir: pBaseDir ? pBaseDir : "." } @@ -93,4 +93,50 @@ module.exports.getPackageJson = (pModule, pBaseDir) => { // left empty on purpose } return lRetval; +} + +/** + * Tells whether the pModule as resolved to pBaseDir is deprecated + * + * @param {string} pModule The module to get the deprecation status of + * @param {string} pBaseDir The base dir. Defaults to '.' + * @return {boolean} true if depcrecated, false in all other cases + */ +function dependencyIsDeprecated (pModule, pBaseDir) { + let lRetval = false; + let lPackageJson = getPackageJson(pModule, pBaseDir); + + if (Boolean(lPackageJson)){ + lRetval = lPackageJson.hasOwnProperty("deprecated") && lPackageJson.deprecated; + } + return lRetval; +} + +/** + * Returns the license of pModule as resolved to pBaseDir - if any + * + * @param {string} pModule The module to get the deprecation status of + * @param {string} pBaseDir The base dir. Defaults to '.' + * @return {string} The module's license string, or '' in case + * there is no package.json or no license field + */ +function getLicense (pModule, pBaseDir) { + let lRetval = ""; + let lPackageJson = getPackageJson(pModule, pBaseDir); + + if ( + Boolean(lPackageJson) && + lPackageJson.hasOwnProperty("license") && + typeof lPackageJson.license === "string" + ){ + lRetval = lPackageJson.license; + } + return lRetval; +} + +module.exports = { + getPackageRoot, + getPackageJson, + dependencyIsDeprecated, + getLicense }; diff --git a/src/extract/resolve/resolve-commonJS.js b/src/extract/resolve/resolve-commonJS.js index 2535887b4..8240f7db3 100644 --- a/src/extract/resolve/resolve-commonJS.js +++ b/src/extract/resolve/resolve-commonJS.js @@ -5,6 +5,7 @@ const resolve = require('resolve'); const transpileMeta = require('../transpile/meta'); const determineDependencyTypes = require('./determineDependencyTypes'); const readPackageDeps = require('./readPackageDeps'); +const localNpmHelpers = require('./localNpmHelpers'); const SUPPORTED_EXTENSIONS = transpileMeta.scannableExtensions; @@ -40,6 +41,12 @@ module.exports = (pModuleName, pBaseDir, pFileDir) => { } } + const lLicense = localNpmHelpers.getLicense(pModuleName, pBaseDir); + + if (Boolean(lLicense)) { + lRetval.license = lLicense; + } + return Object.assign( lRetval, { diff --git a/test/extract/fixtures/bundled-dependencies.json b/test/extract/fixtures/bundled-dependencies.json index b08d7500a..f999fb6e0 100644 --- a/test/extract/fixtures/bundled-dependencies.json +++ b/test/extract/fixtures/bundled-dependencies.json @@ -21,6 +21,7 @@ "dependencyTypes": [ "npm" ], + "license": "MIT", "module": "idontgetbundled", "moduleSystem": "cjs", "valid": true @@ -35,6 +36,7 @@ "npm", "npm-bundled" ], + "license": "MIT", "module": "igetbundled", "moduleSystem": "cjs", "valid": true diff --git a/test/extract/fixtures/cjs.json b/test/extract/fixtures/cjs.json index 708caaf04..89bdc1aa6 100644 --- a/test/extract/fixtures/cjs.json +++ b/test/extract/fixtures/cjs.json @@ -63,6 +63,7 @@ "dependencyTypes": [ "npm" ], + "license": "MIT", "followable": true, "matchesDoNotFollow": false, "couldNotResolve": false diff --git a/test/extract/fixtures/es6.json b/test/extract/fixtures/es6.json index 4d1f156db..e02bf9b45 100644 --- a/test/extract/fixtures/es6.json +++ b/test/extract/fixtures/es6.json @@ -13,6 +13,7 @@ "dependencyTypes": [ "npm" ], + "license": "MIT", "followable": true, "matchesDoNotFollow": false, "couldNotResolve": false @@ -25,6 +26,7 @@ "dependencyTypes": [ "npm-dev" ], + "license": "MIT", "followable": true, "matchesDoNotFollow": false, "couldNotResolve": false @@ -37,6 +39,7 @@ "dependencyTypes": [ "npm" ], + "license": "MIT", "followable": true, "matchesDoNotFollow": false, "couldNotResolve": false @@ -49,6 +52,7 @@ "dependencyTypes": [ "npm-dev" ], + "license": "MIT", "followable": true, "matchesDoNotFollow": false, "couldNotResolve": false diff --git a/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/index.js b/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/index.js new file mode 100644 index 000000000..7c8dd2521 --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/index.js @@ -0,0 +1,2 @@ +const $package = require('./package.json'); +module.exports = () => process.stderr.write(`\nYou've required the '${$package.name}' module. It's deprecated. It really is. Don't use it. Ever. It's true. \n\n`); diff --git a/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/package.json b/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/package.json new file mode 100644 index 000000000..c6572b8bc --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/GPL-license/package.json @@ -0,0 +1,77 @@ +{ + "_args": [ + [ + { + "raw": "GPL-license", + "scope": null, + "escapedName": "GPL-license", + "name": "GPL-license", + "rawSpec": "", + "spec": "latest", + "type": "tag" + }, + "/Users/koos/things/include-deprecated-on-purpose" + ] + ], + "_from": "GPL-license@latest", + "_id": "GPL-license@0.0.0", + "_inCache": true, + "_location": "/GPL-license", + "_nodeVersion": "7.6.0", + "_npmOperationalInternal": { + "host": "packages-12-west.internal.npmjs.com", + "tmp": "tmp/GPL-license-0.0.0.tgz_1489608739604_0.4536006406415254" + }, + "_npmUser": { + "name": "kkoets", + "email": "koos.koets@mozes.kriebel.simplistiesverbond.com" + }, + "_npmVersion": "4.1.2", + "_phantomChildren": {}, + "_requested": { + "raw": "GPL-license", + "scope": null, + "escapedName": "GPL-license", + "name": "GPL-license", + "rawSpec": "", + "spec": "latest", + "type": "tag" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/GPL-license/-/GPL-license-0.0.0.tgz", + "_shasum": "00054f29790a4965d476c772f6c678914e7f837f", + "_shrinkwrap": null, + "_spec": "GPL-license", + "_where": "/Users/koos/things/include-deprecated-on-purpose", + "author": "Koos Koets", + "dependencies": {}, + "deprecated": "as the name of the package implies: this package is just to see what happens when you deprecate a package. Do not use. Will probably be _unpublished_ shortly", + "description": "do not use this - is deprecated at the start", + "devDependencies": {}, + "directories": {}, + "dist": { + "shasum": "00054f29790a4965d476c772f6c678914e7f837f", + "tarball": "https://registry.npmjs.org/GPL-license/-/GPL-license-0.0.0.tgz" + }, + "keywords": [], + "license": "GPL-3.0", + "main": "index.js", + "maintainers": [ + { + "name": "kkoets", + "email": "koos.koets@mozes.kriebel.simplistiesverbond.com" + } + ], + "name": "GPL-license", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "/dev/null" + }, + "scripts": {}, + "version": "0.0.0" +} diff --git a/test/extract/resolve/fixtures/licenses/node_modules/array-licenses/package.json b/test/extract/resolve/fixtures/licenses/node_modules/array-licenses/package.json new file mode 100644 index 000000000..ebc04e9ba --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/array-licenses/package.json @@ -0,0 +1,14 @@ +{ + "name": "array-license", + "version": "481.0.0", + "licenses": [ + { + "type": "MIT", + "url": "https://www.opensource.org/licenses/mit-license.php" + }, + { + "type": "Apache-2.0", + "url": "https://opensource.org/licenses/apache2.0.php" + } + ] +} diff --git a/test/extract/resolve/fixtures/licenses/node_modules/boolean-license/package.json b/test/extract/resolve/fixtures/licenses/node_modules/boolean-license/package.json new file mode 100644 index 000000000..72c03565a --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/boolean-license/package.json @@ -0,0 +1,5 @@ +{ + "name": "boolean-license", + "version": "481.0.0", + "license": true +} diff --git a/test/extract/resolve/fixtures/licenses/node_modules/no-license/package.json b/test/extract/resolve/fixtures/licenses/node_modules/no-license/package.json new file mode 100644 index 000000000..706d1a24e --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/no-license/package.json @@ -0,0 +1,4 @@ +{ + "name": "no-license", + "version": "481.0.0" +} diff --git a/test/extract/resolve/fixtures/licenses/node_modules/object-license/package.json b/test/extract/resolve/fixtures/licenses/node_modules/object-license/package.json new file mode 100644 index 000000000..022850eee --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/node_modules/object-license/package.json @@ -0,0 +1,8 @@ +{ + "name": "object-license", + "version": "481.0.0", + "license": { + "type" : "ISC", + "url" : "https://opensource.org/licenses/ISC" + } +} diff --git a/test/extract/resolve/fixtures/licenses/require-something-deprecated.js b/test/extract/resolve/fixtures/licenses/require-something-deprecated.js new file mode 100644 index 000000000..ac126dccc --- /dev/null +++ b/test/extract/resolve/fixtures/licenses/require-something-deprecated.js @@ -0,0 +1 @@ +const weShouldNotRequireThis = require('GPL-license'); diff --git a/test/extract/resolve/localNpmHelpers.spec.js b/test/extract/resolve/localNpmHelpers.spec.js index 411044511..b267863a7 100644 --- a/test/extract/resolve/localNpmHelpers.spec.js +++ b/test/extract/resolve/localNpmHelpers.spec.js @@ -105,4 +105,63 @@ describe("localNpmHelpers.getPackageRoot", () => { }); }); +describe("localNpmHelpers.getLicense", () => { + it("returns '' if the module does not exist", () => { + expect( + localNpmHelpers.getLicense('this-module-does-not-exist') + ).to.equal(''); + }); + + it("returns '' if the module does exist but has no associated package.json", () => { + expect( + localNpmHelpers.getLicense('./test/extract/resolve/fixtures/no-package-json') + ).to.equal(''); + }); + + it("returns '' if the module does exist, has a package.json, but no license field", () => { + expect( + localNpmHelpers.getLicense( + 'no-license', + './test/extract/resolve/fixtures/licenses/' + ) + ).to.equal(''); + }); + + it("returns '' if the module exists, has a package.json, and a license field that is a boolean", () => { + expect( + localNpmHelpers.getLicense( + 'boolean-license', + './test/extract/resolve/fixtures/licenses/' + ) + ).to.equal(''); + }); + + it("returns '' if the module exists, has a package.json, and a license field that is an object", () => { + expect( + localNpmHelpers.getLicense( + 'object-license', + './test/extract/resolve/fixtures/licenses/' + ) + ).to.equal(''); + }); + + it("returns '' package.json has a licenses field that is an array (and no license field)", () => { + expect( + localNpmHelpers.getLicense( + 'array-license', + './test/extract/resolve/fixtures/licenses/' + ) + ).to.equal(''); + }); + + it("returns the license if the module exists, has a package.json, and a string license field", () => { + expect( + localNpmHelpers.getLicense( + 'GPL-license', + './test/extract/resolve/fixtures/licenses/' + ) + ).to.equal('GPL-3.0'); + }); + +}); /* eslint no-unused-expressions: 0 */ From 940805f6284a9294882e683d0c991fb5fb58d186 Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 13:13:06 +0100 Subject: [PATCH 3/7] feat(rules): adds license and licenseNot rule types --- .dependency-cruiser-custom.json | 6 ++ src/main/ruleSet/jsonschema.json | 9 ++ src/main/ruleSet/validate.js | 5 +- src/validate/index.js | 6 +- test/main/ruleSet/validate.spec.js | 31 +++++++ test/validate/fixtures/rules.license.json | 9 ++ test/validate/fixtures/rules.licensenot.json | 9 ++ .../rules.scary-regex-in-license.json | 11 +++ .../rules.scary-regex-in-licensenot.json | 11 +++ test/validate/index.spec.js | 83 +++++++++++++++++++ 10 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 test/validate/fixtures/rules.license.json create mode 100644 test/validate/fixtures/rules.licensenot.json create mode 100644 test/validate/fixtures/rules.scary-regex-in-license.json create mode 100644 test/validate/fixtures/rules.scary-regex-in-licensenot.json diff --git a/.dependency-cruiser-custom.json b/.dependency-cruiser-custom.json index 1dd189337..9432bc27d 100644 --- a/.dependency-cruiser-custom.json +++ b/.dependency-cruiser-custom.json @@ -90,5 +90,11 @@ "severity": "error", "from": { "pathNot": "^(node_modules)"}, "to": { "circular": true } + },{ + "name": "no-GPL-license", + "comment": "Our license (MIT) is not compatible with any form of GPL, so we are not allowed to use GPL licensed modules", + "severity": "error", + "from": {}, + "to": { "license": "GPL" } }] } diff --git a/src/main/ruleSet/jsonschema.json b/src/main/ruleSet/jsonschema.json index 112233385..82fb6c4b0 100644 --- a/src/main/ruleSet/jsonschema.json +++ b/src/main/ruleSet/jsonschema.json @@ -103,7 +103,16 @@ "moreThanOneDependencyType": { "type": "boolean", "description": "If true matches dependencies with more than one dependency type (e.g. defined in _both_ npm and npm-dev)" + }, + "license": { + "type": "string", + "description": "Whether or not to match modules that were released under one of the mentioned licenses. E.g. to flag GPL-1.0, GPL-2.0 licensed modules (e.g. because your app is not compatible with the GPL) use \"GPL\"" + }, + "licenseNot": { + "type": "string", + "description": "Whether or not to match modules that were NOT released under one of the mentioned licenses. E.g. to flag everyting non MIT use \"MIT\" here" } + } }, "SeverityType": { diff --git a/src/main/ruleSet/validate.js b/src/main/ruleSet/validate.js index 10b46eb1c..173f27a56 100644 --- a/src/main/ruleSet/validate.js +++ b/src/main/ruleSet/validate.js @@ -19,13 +19,16 @@ function hasPath(pObject, pPath) { pObject[pPath[0]].hasOwnProperty(pPath[1]); } + function checkRuleSafety(pRule) { if ( !( (!hasPath(pRule, ["from", "path"]) || safeRegex(pRule.from.path)) && (!hasPath(pRule, ["to", "path"]) || safeRegex(pRule.to.path)) && (!hasPath(pRule, ["from", "pathNot"]) || safeRegex(pRule.from.pathNot)) && - (!hasPath(pRule, ["to", "pathNot"]) || safeRegex(pRule.to.pathNot)) + (!hasPath(pRule, ["to", "pathNot"]) || safeRegex(pRule.to.pathNot)) && + (!hasPath(pRule, ["to", "license"]) || safeRegex(pRule.to.license)) && + (!hasPath(pRule, ["to", "licenseNot"]) || safeRegex(pRule.to.licenseNot)) ) ){ throw new Error( diff --git a/src/validate/index.js b/src/validate/index.js index deef8e240..42a1b2c7d 100644 --- a/src/validate/index.js +++ b/src/validate/index.js @@ -70,6 +70,10 @@ function matchRule(pFrom, pTo) { intersects(pTo.dependencyTypes, pRule.to.dependencyTypes) ) && (!pRule.to.hasOwnProperty("moreThanOneDependencyType") || pTo.dependencyTypes.length > 1 + ) && (!pRule.to.hasOwnProperty("license") || + pTo.license && pTo.license.match(pRule.to.license) + ) && (!pRule.to.hasOwnProperty("licenseNot") || + pTo.license && !pTo.license.match(pRule.to.licenseNot) ) && propertyEquals(pTo, pRule, "couldNotResolve") && propertyEquals(pTo, pRule, "circular"); }; @@ -137,4 +141,4 @@ module.exports = (pValidate, pRuleSet, pFrom, pTo) => { - we only use it from within the module with two fixed values - the propertyEquals function is not exposed externaly */ -/* eslint security/detect-object-injection: 0 */ +/* eslint security/detect-object-injection: 0, complexity: 0 */ diff --git a/test/main/ruleSet/validate.spec.js b/test/main/ruleSet/validate.spec.js index 42fe6ee06..c771c0402 100644 --- a/test/main/ruleSet/validate.spec.js +++ b/test/main/ruleSet/validate.spec.js @@ -34,6 +34,37 @@ describe("ruleSetReader", () => { } }); + it("bails out on scary regexps in licenses", () => { + try { + validate( + JSON.parse( + fs.readFileSync("./test/validate/fixtures/rules.scary-regex-in-license.json", 'utf8') + ) + ); + expect("not to be here").to.equal("still here, though"); + } catch (e) { + expect(e.message).to.contain( + 'rule {"from":{},"to":{"license":"(.+)*"}} has an unsafe regular expression. Bailing out.\n' + ); + } + }); + + + it("bails out on scary regexps in licenseNots", () => { + try { + validate( + JSON.parse( + fs.readFileSync("./test/validate/fixtures/rules.scary-regex-in-licensenot.json", 'utf8') + ) + ); + expect("not to be here").to.equal("still here, though"); + } catch (e) { + expect(e.message).to.contain( + 'rule {"from":{},"to":{"licenseNot":"(.+)*"}} has an unsafe regular expression. Bailing out.\n' + ); + } + }); + it("barfs on an invalid rules file", () => { try { validate( diff --git a/test/validate/fixtures/rules.license.json b/test/validate/fixtures/rules.license.json new file mode 100644 index 000000000..a3aad0b08 --- /dev/null +++ b/test/validate/fixtures/rules.license.json @@ -0,0 +1,9 @@ +{ + "forbidden": [ + { + "name": "no-somepl-license", + "from": {}, + "to": {"license": "SomePL"} + } + ] +} diff --git a/test/validate/fixtures/rules.licensenot.json b/test/validate/fixtures/rules.licensenot.json new file mode 100644 index 000000000..cf696bf59 --- /dev/null +++ b/test/validate/fixtures/rules.licensenot.json @@ -0,0 +1,9 @@ +{ + "forbidden": [ + { + "name": "only-somepl-license", + "from": {}, + "to": {"licenseNot": "SomePL"} + } + ] +} diff --git a/test/validate/fixtures/rules.scary-regex-in-license.json b/test/validate/fixtures/rules.scary-regex-in-license.json new file mode 100644 index 000000000..de2ac7a32 --- /dev/null +++ b/test/validate/fixtures/rules.scary-regex-in-license.json @@ -0,0 +1,11 @@ +{ + "forbidden": [ + { + "from": { + }, + "to": { + "license": "(.+)*" + } + } + ] +} diff --git a/test/validate/fixtures/rules.scary-regex-in-licensenot.json b/test/validate/fixtures/rules.scary-regex-in-licensenot.json new file mode 100644 index 000000000..0329d2a76 --- /dev/null +++ b/test/validate/fixtures/rules.scary-regex-in-licensenot.json @@ -0,0 +1,11 @@ +{ + "forbidden": [ + { + "from": { + }, + "to": { + "licenseNot": "(.+)*" + } + } + ] +} diff --git a/test/validate/index.spec.js b/test/validate/index.spec.js index 0163010f7..a32f77a5e 100644 --- a/test/validate/index.spec.js +++ b/test/validate/index.spec.js @@ -452,3 +452,86 @@ describe("group matching - path group matched in a pathnot", () => { ).to.deep.equal({valid: true}); }); }); + + +describe("validate - license", () => { + it("Skips dependencies that have no license attached", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.license.json"), + "something", + {"resolved": "src/aap/speeltuigen/autoband.ts"} + ) + ).to.deep.equal({valid: true}); + }); + + it("does not flag dependencies that do not match the license expression", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.license.json"), + "something", + { + "resolved": "src/aap/speeltuigen/autoband.ts", + "license": "Monkey-PL" + } + ) + ).to.deep.equal({valid: true}); + }); + + it("flags dependencies that match the license expression", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.license.json"), + "something", + { + "resolved": "src/aap/speeltuigen/autoband.ts", + "license": "SomePL-3.1" + } + ) + ).to.deep.equal({valid: false, rule: {name: "no-somepl-license", severity: "warn"}}); + }); +}); + +describe("validate - licenseNot", () => { + it("Skips dependencies that have no license attached", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.licensenot.json"), + "something", + {"resolved": "src/aap/speeltuigen/autoband.ts"} + ) + ).to.deep.equal({valid: true}); + }); + + it("does not flag dependencies that do match the license expression", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.licensenot.json"), + "something", + { + "resolved": "src/aap/speeltuigen/autoband.ts", + "license": "SomePL-3.1" + } + ) + ).to.deep.equal({valid: true}); + }); + + it("flags dependencies that do not match the license expression", () => { + expect( + validate( + true, + _readRuleSet("./test/validate/fixtures/rules.licensenot.json"), + "something", + { + "resolved": "src/aap/speeltuigen/autoband.ts", + "license": "Monkey-PL" + } + ) + ).to.deep.equal({valid: false, rule: {name: "only-somepl-license", severity: "warn"}}); + }); +}); From 71f0dfcc2fd90cec09e95d073858eaf75ea762ee Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 13:44:56 +0100 Subject: [PATCH 4/7] doc(rules): describes the license and licenseNot rules --- doc/rules-reference.md | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/doc/rules-reference.md b/doc/rules-reference.md index 20b3c08ec..f9200457b 100644 --- a/doc/rules-reference.md +++ b/doc/rules-reference.md @@ -189,6 +189,49 @@ up at itself. } ``` +### _license_ and _licenseNot_ +You can flag dependent modules that have licenses that are e.g. not compatible with your own license or with the policies within your company with +`license` and `licenseNot`. Both take a regular expression that matches +against the license string that goes with the dependency. + +E.g. to forbid GPL and APL licenses (which require you to publish your source +code - which will not always be what you want): + +```json +{ + "name": "no-gpl-apl-licenses", + "severity": "error", + "from": {}, + "to": { "license": "GPL|APL" } +} +``` +This raise an error when you use a dependency that has a string with GPL or +APL in the "license" attribute of its package.json (e.g. +[SPDX](https://spdx.org) compatible expressions like `GPL-3.0`, `APL-1.0` and +`MIT OR GPL-3.0` but also on non SPDX compatible) + +To only allow licenses from an approved list (e.g. a whitelist provided by your +legal department): +```json +{ + "name": "only-licenses-approved-by-legal", + "severity": "warn", + "from": {}, + "to": { "licenseNot": "MIT|ISC" } +} +``` + +Note: dependency-cruiser can help out a bit here, but you remain responsible +for managing your own legal stuff. To re-iterate what is in the + [LICENSE](../LICENSE) to dependency-cruiser: +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ## dependencyTypes You might have spent some time wondering why something works on your machine, From 2340cb8332ac7deb320e6afd5c0fb1e1f2b1c2fe Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 15:08:54 +0100 Subject: [PATCH 5/7] perf(extract): memoize module package.json retrieval --- src/extract/resolve/localNpmHelpers.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/extract/resolve/localNpmHelpers.js b/src/extract/resolve/localNpmHelpers.js index a00d7eec4..4638b7980 100644 --- a/src/extract/resolve/localNpmHelpers.js +++ b/src/extract/resolve/localNpmHelpers.js @@ -1,8 +1,9 @@ "use strict"; -const fs = require('fs'); -const path = require('path'); -const resolve = require('resolve'); +const fs = require('fs'); +const path = require('path'); +const resolve = require('resolve'); +const _memoize = require('lodash/memoize'); const isLocal = (pModule) => pModule.startsWith('.'); const isScoped = (pModule) => pModule.startsWith('@'); @@ -74,7 +75,7 @@ function getPackageRoot (pModule) { * null if either module or package.json could * not be found */ -function getPackageJson (pModule, pBaseDir) { +function bareGetPackageJson (pModule, pBaseDir) { let lRetval = null; try { @@ -95,6 +96,11 @@ function getPackageJson (pModule, pBaseDir) { return lRetval; } +const getPackageJson = _memoize( + bareGetPackageJson, + (pModule, pBaseDir) => `${pBaseDir}||${pModule}` +); + /** * Tells whether the pModule as resolved to pBaseDir is deprecated * From 61bcf2442f8112c74699eacb111b97ccc3c1c1ae Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 15:26:58 +0100 Subject: [PATCH 6/7] feat(extract): also extract license for amd dependencies --- src/extract/resolve/resolve-AMD.js | 7 +++++++ test/extract/fixtures/amd-recursive.json | 1 + test/extract/fixtures/amd.json | 1 + 3 files changed, 9 insertions(+) diff --git a/src/extract/resolve/resolve-AMD.js b/src/extract/resolve/resolve-AMD.js index 63d4e8eb8..4517393f8 100644 --- a/src/extract/resolve/resolve-AMD.js +++ b/src/extract/resolve/resolve-AMD.js @@ -6,6 +6,7 @@ const fs = require('fs'); const memoize = require('lodash/memoize'); const determineDependencyTypes = require('./determineDependencyTypes'); const readPackageDeps = require('./readPackageDeps'); +const localNpmHelpers = require('./localNpmHelpers'); const fileExists = memoize(pFile => { try { @@ -35,6 +36,12 @@ module.exports = (pModuleName, pBaseDir, pFileDir) => { couldNotResolve: !Boolean(resolve.isCore(pModuleName)) && !fileExists(lProbablePath) }; + const lLicense = localNpmHelpers.getLicense(pModuleName, pBaseDir); + + if (Boolean(lLicense)) { + lDependency.license = lLicense; + } + return Object.assign( lDependency, { diff --git a/test/extract/fixtures/amd-recursive.json b/test/extract/fixtures/amd-recursive.json index ecb2c8c19..213f32ddc 100644 --- a/test/extract/fixtures/amd-recursive.json +++ b/test/extract/fixtures/amd-recursive.json @@ -16,6 +16,7 @@ "dependencyTypes": [ "unknown" ], + "license": "MIT", "followable": false, "matchesDoNotFollow": false, "couldNotResolve": true, diff --git a/test/extract/fixtures/amd.json b/test/extract/fixtures/amd.json index 8249816d4..ca8444600 100644 --- a/test/extract/fixtures/amd.json +++ b/test/extract/fixtures/amd.json @@ -210,6 +210,7 @@ "dependencyTypes": [ "unknown" ], + "license": "MIT", "followable": false, "matchesDoNotFollow": false, "couldNotResolve": true From b70c049387ce68882f555e787e3cb4f336f1cb76 Mon Sep 17 00:00:00 2001 From: sverweij Date: Sat, 25 Nov 2017 17:01:32 +0100 Subject: [PATCH 7/7] style(extract): it just looks nicer this way (space police) --- src/extract/resolve/localNpmHelpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extract/resolve/localNpmHelpers.js b/src/extract/resolve/localNpmHelpers.js index 4638b7980..c1e2c62e7 100644 --- a/src/extract/resolve/localNpmHelpers.js +++ b/src/extract/resolve/localNpmHelpers.js @@ -96,10 +96,11 @@ function bareGetPackageJson (pModule, pBaseDir) { return lRetval; } -const getPackageJson = _memoize( - bareGetPackageJson, - (pModule, pBaseDir) => `${pBaseDir}||${pModule}` -); +const getPackageJson = + _memoize( + bareGetPackageJson, + (pModule, pBaseDir) => `${pBaseDir}|${pModule}` + ); /** * Tells whether the pModule as resolved to pBaseDir is deprecated