Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(depcruise): add license check rules #24

Merged
merged 7 commits into from
Nov 25, 2017
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
6 changes: 6 additions & 0 deletions .dependency-cruiser-custom.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}]
}
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions doc/rules-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/extract/jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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."
Expand Down
19 changes: 1 addition & 18 deletions src/extract/resolve/determineDependencyTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
67 changes: 60 additions & 7 deletions src/extract/resolve/localNpmHelpers.js
Original file line number Diff line number Diff line change
@@ -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('@');
Expand Down Expand Up @@ -34,7 +35,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;
}
Expand All @@ -55,7 +56,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
Expand All @@ -74,12 +75,12 @@ module.exports.getPackageRoot = (pModule) => {
* null if either module or package.json could
* not be found
*/
module.exports.getPackageJson = (pModule, pBaseDir) => {
function bareGetPackageJson (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 : "."
}
Expand All @@ -93,4 +94,56 @@ module.exports.getPackageJson = (pModule, pBaseDir) => {
// left empty on purpose
}
return lRetval;
}

const getPackageJson =
_memoize(
bareGetPackageJson,
(pModule, pBaseDir) => `${pBaseDir}|${pModule}`
);

/**
* 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
};
7 changes: 7 additions & 0 deletions src/extract/resolve/resolve-AMD.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
{
Expand Down
7 changes: 7 additions & 0 deletions src/extract/resolve/resolve-commonJS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
{
Expand Down
9 changes: 9 additions & 0 deletions src/main/ruleSet/jsonschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
5 changes: 4 additions & 1 deletion src/main/ruleSet/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion src/validate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
};
Expand Down Expand Up @@ -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 */
1 change: 1 addition & 0 deletions test/extract/fixtures/amd-recursive.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencyTypes": [
"unknown"
],
"license": "MIT",
"followable": false,
"matchesDoNotFollow": false,
"couldNotResolve": true,
Expand Down
1 change: 1 addition & 0 deletions test/extract/fixtures/amd.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"dependencyTypes": [
"unknown"
],
"license": "MIT",
"followable": false,
"matchesDoNotFollow": false,
"couldNotResolve": true
Expand Down
2 changes: 2 additions & 0 deletions test/extract/fixtures/bundled-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencyTypes": [
"npm"
],
"license": "MIT",
"module": "idontgetbundled",
"moduleSystem": "cjs",
"valid": true
Expand All @@ -35,6 +36,7 @@
"npm",
"npm-bundled"
],
"license": "MIT",
"module": "igetbundled",
"moduleSystem": "cjs",
"valid": true
Expand Down
1 change: 1 addition & 0 deletions test/extract/fixtures/cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"dependencyTypes": [
"npm"
],
"license": "MIT",
"followable": true,
"matchesDoNotFollow": false,
"couldNotResolve": false
Expand Down
Loading