├── .gitignore ├── config └── config.json ├── .npmignore ├── .travis.yml ├── .editorconfig ├── test ├── exclusion.json ├── inclusion.json ├── list_item.js ├── license.js ├── bin.js └── audit.js ├── lib ├── license.js ├── list_item.js └── audit.js ├── index.js ├── package.json ├── LICENSE.txt ├── docs ├── flowchart.dot └── flowchart.svg ├── bin └── dep-audit └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclusions": {}, 3 | "inclusions": {}, 4 | "spdx": "(MIT OR ISC)" 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .editorconfig 3 | .travix.yml 4 | config 5 | coverage 6 | docs 7 | test 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | - '0.12' 6 | - '4' 7 | - '6' 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | -------------------------------------------------------------------------------- /test/exclusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclusions": { 3 | "aws-sign2": [ 4 | { 5 | "version_range": "0.6.0", 6 | "audit_trail": "test", 7 | "desc": "fizzzzz" 8 | } 9 | ] 10 | }, 11 | "inclusions": {}, 12 | "spdx": "(MIT OR ISC OR BSD-3-Clause OR BSD-2-Clause OR WTFPL OR Apache-2.0 OR CC-BY-3.0 OR Unlicense OR MPL-2.0)" 13 | } -------------------------------------------------------------------------------- /lib/license.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016, Yahoo Inc. 3 | Code licensed under the MIT License. 4 | See LICENSE.txt 5 | */ 6 | var checker = require('license-checker') 7 | var Promise = require('bluebird') 8 | 9 | module.exports = function getModules (opts) { 10 | return new Promise(function (resolve, reject) { 11 | checker.init(opts, function (err, json) { 12 | err ? reject(err) : resolve(json) 13 | }) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /test/inclusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclusions": {}, 3 | "inclusions": { 4 | "tweetnacl": [ 5 | { 6 | "version_range": "0.13.3", 7 | "audit_trail": "foo", 8 | "desc": "bar" 9 | }, 10 | { 11 | "version_range": "0.14.x", 12 | "audit_trail": "foo", 13 | "desc": "has \"SEE LICENSE IN COPYING.txt\"" 14 | } 15 | ] 16 | }, 17 | "spdx": "(MIT OR ISC OR BSD-4-Clause OR BSD-3-Clause OR BSD-2-Clause OR WTFPL OR Apache-2.0 OR CC-BY-3.0 OR Unlicense OR MPL-2.0)" 18 | } 19 | -------------------------------------------------------------------------------- /lib/list_item.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016, Yahoo Inc. 3 | Code licensed under the MIT License. 4 | See LICENSE.txt 5 | */ 6 | var semver = require('semver') 7 | 8 | module.exports = function getListItem (exceptions) { 9 | return function (pkg) { 10 | var components = pkg.split('@') 11 | var name = components[0] 12 | var ver = components[1] 13 | var match = null 14 | 15 | if (exceptions[name]) { 16 | exceptions[name].some(function (item) { 17 | if (semver.satisfies(ver, item.version_range)) { 18 | match = item 19 | return true 20 | } 21 | }) 22 | } 23 | return match 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016, Yahoo Inc. 3 | Code licensed under the MIT License. 4 | See LICENSE.txt 5 | */ 6 | var getLicenses = require('./lib/license') 7 | var audit = require('./lib/audit') 8 | 9 | // The second argument to the callback is an object with the `pass` and `fail` 10 | // keys. Each of these is an object. For each key package@version the value 11 | // is the reason why the package passes or fails the checks. If the reason is 12 | // a string it's the license of the package@version, otherwise the reason is 13 | // the exception object. 14 | module.exports = function (opts, callback) { 15 | getLicenses(opts.checker) 16 | .then(function (json) { 17 | if (typeof opts.hook === 'function') { 18 | return opts.hook(json) 19 | } 20 | return json 21 | }) 22 | .then(function (json) { 23 | var report = audit(opts.fix, json, opts.spdx, opts.include, opts.exclude) 24 | callback(null, report) 25 | }) 26 | .catch(callback) 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dep-audit", 3 | "version": "1.0.1", 4 | "description": "Audits licenses of npm packages installed as dependencies", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && mocha", 8 | "coverage": "istanbul cover _mocha", 9 | "doc": "dot -Tsvg docs/flowchart.dot > docs/flowchart.svg" 10 | }, 11 | "bin": { 12 | "dep-audit": "./bin/dep-audit" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/yahoo/dep-audit" 17 | }, 18 | "keywords": [ 19 | "license" 20 | ], 21 | "author": "jungsoo", 22 | "license": "MIT", 23 | "dependencies": { 24 | "bluebird": "^3.4.1", 25 | "license-checker": "^8.0.2", 26 | "request": "^2.72.0", 27 | "semver": "^5.1.1", 28 | "spdx-correct": "^1.0.2", 29 | "spdx-satisfies": "^0.1.3", 30 | "validate-npm-package-license": "^3.0.1", 31 | "yargs": "^4.7.1" 32 | }, 33 | "devDependencies": { 34 | "istanbul": "^0.4.3", 35 | "mocha": "^2.5.3", 36 | "mockery": "^1.7.0", 37 | "standard": "^7.1.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Yahoo, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/list_item.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe, before, after */ 2 | var assert = require('assert') 3 | var mockery = require('mockery') 4 | 5 | describe('exceptions module', function () { 6 | var getItem 7 | before(function () { 8 | var semverMock = {satisfies: function (version, accepted) { 9 | return version === accepted 10 | }} 11 | 12 | mockery.enable({ 13 | useCleanCache: true, 14 | warnOnReplace: false, 15 | warnOnUnregistered: false 16 | }) 17 | mockery.registerMock('semver', semverMock) 18 | var exceptionList = { 19 | 'pp': [{version_range: '2.9.0'}], 20 | 'x': [{version_range: '1.0.0'}] 21 | } 22 | getItem = require('../lib/list_item.js')(exceptionList) 23 | }) 24 | 25 | it('Returns list item if name is on exception list and version is ok', function () { 26 | assert.deepEqual(getItem('x@1.0.0'), {version_range: '1.0.0'}) 27 | }) 28 | 29 | it('Returns null if name is not on exception list', function () { 30 | assert.equal(getItem('y@1.0.0'), null) 31 | }) 32 | 33 | it('Returns null if version is not in accepted range', function () { 34 | assert.equal(getItem('x@1.0.1'), null) 35 | }) 36 | 37 | after(function () { 38 | mockery.disable() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/license.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe, before, after */ 2 | var assert = require('assert') 3 | var mockery = require('mockery') 4 | 5 | describe('license module', function () { 6 | var getLicenses 7 | before(function () { 8 | var checkerMock = { 9 | init: function (opts, callback) { 10 | if (opts.err) { 11 | callback(new Error('Could not get licenses', null)) 12 | } 13 | callback(null, 'success') 14 | } 15 | } 16 | mockery.enable({ 17 | useCleanCache: true, 18 | warnOnReplace: false, 19 | warnOnUnregistered: false 20 | }) 21 | mockery.registerMock('license-checker', checkerMock) 22 | getLicenses = require('../lib/license.js') 23 | }) 24 | 25 | it('Returns resolved promise on success', function () { 26 | var opt = {} 27 | return getLicenses(opt) 28 | .then(function (data) { 29 | assert.equal(data, 'success') 30 | }) 31 | }) 32 | 33 | it('Rejects promise on error', function () { 34 | var opt = {err: true} 35 | return getLicenses(opt) 36 | .then(function (data) { 37 | assert.fail('No error thrown') 38 | }) 39 | .catch(function (e) { 40 | assert.equal(e.message, 'Could not get licenses') 41 | }) 42 | }) 43 | 44 | after(function () { 45 | mockery.disable() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /docs/flowchart.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | subgraph cluster_logic { 3 | label="Logic"; 4 | node[shape="box", style="rounded"] 5 | start; good; bad; 6 | node[shape="diamond", style=""] 7 | spdx_valid; inclusion_list; exclusion_list; hook; 8 | } 9 | 10 | start -> hook; 11 | hook -> bad[label="no"]; 12 | hook -> spdx_valid[label="yes"]; 13 | spdx_valid -> exclusion_list[label="yes"]; 14 | spdx_valid -> inclusion_list[label="no"]; 15 | exclusion_list -> bad[label="yes"]; 16 | exclusion_list -> good[label="no"]; 17 | inclusion_list -> good[label="yes"]; 18 | inclusion_list -> bad[label="no"]; 19 | 20 | spdx_valid[label="Is 'valid'\nSPDX license?"]; 21 | hook[label="Passes\nuser-supplied\nhook?"]; 22 | inclusion_list[label="Is in the\ninclusion list?"] 23 | exclusion_list[label="Is in the\nexclusion list?"] 24 | 25 | subgraph cluster_data { 26 | label="Data"; 27 | list_item[ 28 | label=< 29 | 30 | 31 | 32 | 33 | 34 |
list_item
name
audit_trail
desc
version_range
> 35 | shape="none" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/bin.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe, before */ 2 | var path = require('path') 3 | var assert = require('assert') 4 | var exec = require('child_process').exec 5 | var bin = path.resolve(__dirname, '..', 'bin', 'dep-audit') 6 | 7 | function config (p) { 8 | return path.resolve(__dirname, p) 9 | } 10 | 11 | describe('If license does not match', function () { 12 | var code 13 | before(function (done) { 14 | this.timeout(10000) 15 | exec(bin + ' --config ' + config('../config/config.json'), 16 | {cwd: process.cwd()}, 17 | function (error) { 18 | code = error.code 19 | done() 20 | }) 21 | }) 22 | 23 | it('Should exit 1', function () { 24 | assert.equal(code, 1) 25 | }) 26 | }) 27 | 28 | describe('There is a dependency on inclusion list', function () { 29 | var code 30 | before(function (done) { 31 | this.timeout(10000) 32 | exec(bin + ' --config ' + config('inclusion.json') + ' --fix --guess', 33 | {cwd: process.cwd()}, 34 | function (error) { 35 | code = error 36 | done() 37 | }) 38 | }) 39 | 40 | it('Should exit 0', function () { 41 | assert.equal(code, null) 42 | }) 43 | }) 44 | 45 | describe('There is a dependency on exclusion list', function () { 46 | var err 47 | before(function (done) { 48 | this.timeout(10000) 49 | exec(bin + ' --config ' + config('exclusion.json') + ' --fix --guess', 50 | {cwd: process.cwd()}, 51 | function (error) { 52 | err = error 53 | done() 54 | }) 55 | }) 56 | 57 | it('Should exit 1', function () { 58 | assert.equal(err.code, 1) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /lib/audit.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016, Yahoo Inc. 3 | Code licensed under the MIT License. 4 | See LICENSE.txt 5 | */ 6 | var licenseSatisfies = require('spdx-satisfies') 7 | var correct = require('spdx-correct') 8 | var valid = require('validate-npm-package-license') 9 | var getItem = require('./list_item.js') 10 | var json = require(process.cwd() + '/package.json') 11 | 12 | function satisfies (license, allowed) { 13 | try { 14 | return licenseSatisfies(license, allowed) 15 | } catch (e) { // catch lexical error if license not recognized 16 | return false 17 | } 18 | } 19 | 20 | function spdxFix (spdx) { 21 | var licenses = spdx.split(' ') 22 | var fixed = licenses.map(function (val, ind) { 23 | if (val.charAt(0) === '(') { 24 | return '(' + (correct(val.slice(1)) || val.slice(1)) 25 | } else if (val.charAt(val.length - 1) === ')') { 26 | return (correct(val.slice(0, -1)) || val.slice(0, -1)) + ')' 27 | } else { 28 | return correct(val) || val 29 | } 30 | }) 31 | return fixed.join(' ') 32 | } 33 | 34 | module.exports = function audit (fix, modules, allowed, inclusions, exclusions) { 35 | if (valid(allowed).spdx !== true) { 36 | throw new Error('Allowed licenses ' + allowed + ' is not a valid spdx expression') 37 | } 38 | 39 | var getInclusion = getItem(inclusions) 40 | var getExclusion = getItem(exclusions) 41 | var report = { pass: {}, fail: {} } 42 | 43 | Object.keys(modules).forEach(function (nameVersion) { 44 | if (json.name + '@' + json.version === nameVersion) { 45 | return 46 | } 47 | var licenses = modules[nameVersion].licenses 48 | if (Array.isArray(licenses)) { 49 | licenses = '(' + licenses.join(' OR ') + ')' 50 | } 51 | licenses = licenses.replace(/[*]/g, '') // in case we are guessing 52 | licenses = fix ? spdxFix(licenses) : licenses 53 | 54 | var exception 55 | exception = getExclusion(nameVersion) 56 | if (exception) { 57 | report.fail[nameVersion] = exception 58 | return 59 | } 60 | exception = getInclusion(nameVersion) 61 | if (exception) { 62 | report.pass[nameVersion] = exception 63 | return 64 | } 65 | if (satisfies(licenses, allowed)) { 66 | report.pass[nameVersion] = licenses 67 | } else { 68 | report.fail[nameVersion] = licenses 69 | } 70 | }) 71 | return report 72 | } 73 | -------------------------------------------------------------------------------- /bin/dep-audit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Copyright 2016, Yahoo Inc. 4 | Code licensed under the MIT License. 5 | See LICENSE.txt 6 | */ 7 | 8 | // support early versions of node 6 9 | if (process.stdout._handle && process.stdout._handle.setBlocking) process.stdout._handle.setBlocking(true) 10 | if (process.stderr._handle && process.stderr._handle.setBlocking) process.stderr._handle.setBlocking(true) 11 | 12 | var path = require('path') 13 | var audit = require('../index') 14 | var request = require('request') 15 | var argv = require('yargs') 16 | .usage('Usage:\n$0 --config [options]' + 17 | '\n$0 --config-url [options]') 18 | .config() 19 | .describe({'config-url': 'URL to JSON config file', 20 | 'hook': 'Path to hook to execute on modules before auditing', 21 | 'fix': 'Attempt to fix incorrect licenses', 22 | 'guess': 'Attempt to guess licenses from files other than package.json'}) 23 | .string(['config', 'config-url', 'hook']) 24 | .boolean(['fix', 'guess', 'version']) 25 | .version() 26 | .help() 27 | .argv 28 | 29 | if (argv['config-url']) { 30 | request.get({ 31 | uri: argv['config-url'], 32 | json: true 33 | }, function (err, res, body) { 34 | if (err) { 35 | console.error('Could not fetch config file') 36 | process.exit(2) 37 | } else if (res.statusCode === 200) { 38 | checkDependencies(body) 39 | } 40 | }) 41 | } else if (argv.config) { 42 | checkDependencies(require(path.resolve(process.cwd(), argv.config))) 43 | } 44 | 45 | function checkDependencies (body) { 46 | var opts = { 47 | checker: 48 | {start: process.cwd(), 49 | production: true, 50 | unknown: !argv.guess}, 51 | fix: argv.fix, 52 | spdx: (body || argv).spdx, 53 | include: (body || argv).inclusions || {}, 54 | exclude: (body || argv).exclusions || {} 55 | } 56 | if (argv.hook) { 57 | opts.hook = require(path.resolve(process.cwd(), argv.hook)) 58 | } 59 | 60 | audit(opts, function (error, report) { 61 | if (error) { 62 | console.error(error) 63 | process.exit(1) 64 | } 65 | 66 | if (Object.keys(report.fail).length) { 67 | console.error('The following packages violate the policy:') 68 | Object.keys(report.fail).forEach(function (nameVersion) { 69 | var reason = report.fail[nameVersion] 70 | if (typeof reason === 'string') { 71 | console.error(' ' + nameVersion + ' license', JSON.stringify(reason), 'not allowed by policy') 72 | } else { 73 | console.error(' ' + nameVersion + ' ' + reason.desc, '(' + JSON.stringify(reason.audit_trail) + ')') 74 | } 75 | }) 76 | process.exit(1) 77 | } 78 | 79 | console.log('All modules are allowed!') 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /test/audit.js: -------------------------------------------------------------------------------- 1 | /* globals it, describe, before, after */ 2 | var assert = require('assert') 3 | var mockery = require('mockery') 4 | 5 | describe('audit module', function () { 6 | var audit, bl, wl 7 | before(function () { 8 | bl = {name: 'x', version_range: '1.0.0', 9 | audit_trail: '1/1/2000', desc: 'blacklisted'} 10 | wl = {name: 'y', version_range: '2.0.0', 11 | audit_trail: '2/2/2000', desc: 'whitelisted'} 12 | var exceptionMock = function (config) { 13 | return function (name) { 14 | var item = null 15 | var split = name.split('@') 16 | if (split[0] === config.name && 17 | split[1] === config.version_range) { 18 | item = config 19 | } 20 | return item 21 | } 22 | } 23 | var satisfiesMock = function (license, allowed) { 24 | if (license === 'err') { 25 | throw new Error() 26 | } 27 | return license === allowed 28 | } 29 | var fixMock = function (spdx) { 30 | if (spdx === 'OR' || spdx === 'unknown') { 31 | return null 32 | } 33 | return 'fix_' + spdx 34 | } 35 | var validMock = function (license) { 36 | if (license === 'invalid') { 37 | return {spdx: false} 38 | } 39 | return {spdx: true} 40 | } 41 | mockery.enable({ 42 | useCleanCache: true, 43 | warnOnReplace: false, 44 | warnOnUnregistered: false 45 | }) 46 | mockery.registerMock('spdx-satisfies', satisfiesMock) 47 | mockery.registerMock('spdx-correct', fixMock) 48 | mockery.registerMock('validate-npm-package-license', validMock) 49 | mockery.registerMock('./list_item.js', exceptionMock) 50 | audit = require('../lib/audit') 51 | }) 52 | 53 | it('reports packages not in inclusion list', function () { 54 | var modules = {'z@1.0.1': {licenses: 'foo'}} 55 | var res = audit(false, modules, 'bar', wl, bl) 56 | assert.equal(Object.keys(res.pass).length, 0, 'no passing modules') 57 | assert.deepEqual(res.fail, { 58 | 'z@1.0.1': 'foo' 59 | }, 'one failing module') 60 | }) 61 | 62 | it('reports packages in exclusion list', function () { 63 | var modules = {'x@1.0.0': {licenses: 'foo'}} 64 | var res = audit(false, modules, 'foo', wl, bl) 65 | assert.equal(Object.keys(res.pass).length, 0, 'no passing modules') 66 | assert.deepEqual(res.fail, { 67 | 'x@1.0.0': { 68 | name: 'x', 69 | version_range: '1.0.0', 70 | audit_trail: '1/1/2000', 71 | desc: 'blacklisted' 72 | } 73 | }, 'one failing module') 74 | }) 75 | 76 | it('reports all passing modules', function () { 77 | var modules = {'z@1.0.0': {licenses: 'foo'}, 'y@2.0.0': {licenses: 'bar'}} 78 | var res = audit(false, modules, 'foo', wl, bl) 79 | assert.deepEqual(res, { 80 | pass: { 81 | 'y@2.0.0': { 82 | name: 'y', 83 | version_range: '2.0.0', 84 | audit_trail: '2/2/2000', 85 | desc: 'whitelisted' 86 | }, 87 | 'z@1.0.0': 'foo' 88 | }, 89 | fail: {} 90 | }) 91 | }) 92 | 93 | it('Removes asterisks from licenses when checking', function () { 94 | var modules = {'z@5.0.0': {licenses: '(foo* OR bar*)'}} 95 | var res = audit(false, modules, '(foo OR bar)', wl, bl) 96 | assert.deepEqual(res.pass, { 97 | 'z@5.0.0': '(foo OR bar)' 98 | }) 99 | }) 100 | 101 | it('Fixes licenses in spdx expression', function () { 102 | var modules = {'m@5.0.0': {licenses: '(foo OR bar OR test)'}} 103 | var res = audit(true, modules, '(fix_foo OR fix_bar OR fix_test)', wl, bl) 104 | assert.deepEqual(res.pass, { 105 | 'm@5.0.0': '(fix_foo OR fix_bar OR fix_test)' 106 | }) 107 | }) 108 | 109 | it('Fixer returns original value if it cannot fix license', function () { 110 | var modules = {'z@2.3.0': {licenses: '(unknown OR unknown)'}} 111 | var res = audit(true, modules, '(unknown OR unknown)', wl, bl) 112 | assert.deepEqual(res.pass, { 113 | 'z@2.3.0': '(unknown OR unknown)' 114 | }) 115 | }) 116 | 117 | it('If license cannot be read, does not satisfy', function () { 118 | var modules = {z: {licenses: 'err'}} 119 | var res = audit(false, modules, 'mit', wl, bl) 120 | assert.deepEqual(res.fail, { 121 | 'z': 'err' 122 | }) 123 | }) 124 | 125 | it('If allowed licenses are invalid, throws error', function () { 126 | try { 127 | var modules = {z: {licenses: 'foo'}} 128 | audit(false, modules, 'invalid', wl, bl) 129 | } catch (e) { 130 | assert.equal(e.message, 'Allowed licenses invalid is not a valid spdx expression') 131 | } 132 | }) 133 | 134 | it('If license field is array, create spdx', function () { 135 | var modules = {'x@2.1.1': {licenses: ['MIT', 'ISC']}} 136 | var res = audit(false, modules, '(MIT OR ISC)', wl, bl) 137 | assert.deepEqual(res.pass, { 138 | 'x@2.1.1': '(MIT OR ISC)' 139 | }) 140 | }) 141 | 142 | after(function () { 143 | mockery.deregisterAll() 144 | mockery.disable() 145 | }) 146 | }) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NPM Dependency Audit Tool [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 2 | ====================== 3 | 4 | ``` 5 | npm install -g dep-audit 6 | ``` 7 | 8 | Given a path to a configuration file, this module will check that each module 9 | in the node_modules tree of the current directory satisfies the requirements 10 | established by the configuration file. 11 | 12 | ``` 13 | dep-audit --config config/config.json 14 | ``` 15 | 16 | Config file should be formatted like so: 17 | 18 | ``` 19 | { 20 | "exclusions":{ 21 | "name":[ 22 | { 23 | "version_range":"1.0.0-2.1.4", // semver range that is not allowed 24 | "audit_trail":"Added by admin @ 2016-1-1", // who added it and when 25 | "desc":"Bad module" // why module isn't allowed 26 | } 27 | ] 28 | }, 29 | "inclusions":{ 30 | "name":[ 31 | { 32 | "version_range":"1.0.0-2.1.4", // semver range that is allowed 33 | "audit_trail":"Added by admin @ 2016-1-1", // who added it and when 34 | "desc":"Bad module" // why module is allowed 35 | } 36 | ] 37 | }, 38 | "spdx":"(MIT OR ISC)" // spdx expression indicating which licenses are ok 39 | } 40 | ``` 41 | If a module is not in `exclusions` or `inclusions`, it will be allowed if 42 | its license satisfies the SPDX expression in `spdx`. 43 | Modules in `exclusions` will not be allowed even if their license 44 | satisfies the spdx expression. 45 | Modules in `inclusions` will be allowed even if their license 46 | does not satisfy the spdx expression. 47 | 48 | If an unacceptable module that does not satisfy the requirements in the config file 49 | is found, dep-audit will log the module as well as its audit trail and description. 50 | The process will then exit with exit code 1. 51 | 52 | Alternatively, the user can provide a URL to fetch a config file from. 53 | 54 | ``` 55 | dep-audit --config-url http://.... 56 | ``` 57 | 58 | Hooks 59 | ----- 60 | A hook can be provided that will be executed before each module is audited. 61 | In order to supply a hook, pass the file path of the module to the hook option. 62 | ``` 63 | --hook /path/to/hook/ 64 | ``` 65 | The file must export a function that takes an object representing the 66 | node_modules tree as an argument and returns an object representing the 67 | modules from the node_modules tree that should be audited. 68 | ``` 69 | module.exports = function (json) { 70 | return json 71 | } 72 | ``` 73 | The node_modules object will be formatted like so using 74 | a separate object for each module. 75 | ``` 76 | { 77 | "name@version": { 78 | "licenses": "ISC", 79 | "repository": "url to repo", 80 | "licenseFile": "/path/to/license/file" 81 | } 82 | } 83 | ``` 84 | 85 | Options 86 | ------- 87 | * `--config [path]` Path to fetch inclusion list, exclusion list, and spdx expression 88 | * `--config-url [url]` URL to fetch inclusion list, exclusion list, and spdx expression 89 | * `--allowed [list]` Audit modules in node_modules tree using list of licenses 90 | * `--hook [path]` Path to hook to execute before modules are audited 91 | * `--fix` Attempt to fix incorrect licenses (implemented using [spdx-correct](https://www.npmjs.com/package/spdx-correct)) 92 | * `--guess` Attempt to guess licenses from files other than package.json (implemented using [license-checker](https://www.npmjs.com/package/license-checker)) 93 | * `--version` Display the current version 94 | * `--help` Get help 95 | 96 | Using dep-audit programmatically 97 | ------------------------------------ 98 | If dep-audit is installed locally, it can be used as a library rather than 99 | a command line tool. 100 | ``` 101 | npm install dep-audit 102 | ``` 103 | 104 | Then, just require the module in your project and you can audit your dependencies programmatically. 105 | ``` 106 | var audit = require('dep-audit') 107 | var opts = { 108 | "hook": function (json) { 109 | console.log(json) 110 | return json 111 | }, 112 | "checker": 113 | { 114 | "start": "/path/to/project/", 115 | "production": true, 116 | // If true, will only audit production dependencies. 117 | // If false, will also audit dev dependencies, 118 | "unknown": false 119 | // If true, will only check package.json for license. 120 | // If false, will guess license from other files 121 | }, 122 | "fix": true, // If true, will fix malformed licenses, 123 | "spdx": "MIT", // spdx expression indicating which licenses are allowed 124 | "include": { 125 | "name":[ 126 | { 127 | "version_range":"1.0.0-2.1.4", // semver range that is allowed 128 | "audit_trail":"Added by admin @ 2016-1-1", // who added it and when 129 | "desc":"Bad module" // why module is allowed 130 | } 131 | ] 132 | }, 133 | "exclude": { 134 | "name":[ 135 | { 136 | "version_range":"1.0.0-2.1.4", // semver range that is not allowed 137 | "audit_trail":"Added by admin @ 2016-1-1", // who added it and when 138 | "desc":"Bad module" // why module isn't allowed 139 | } 140 | ] 141 | } 142 | } 143 | 144 | audit (opts, function (error, report) { 145 | if (error) { 146 | throw error 147 | } 148 | Object.keys(report.fail).forEach(function (nameVersion) { 149 | console.log('FAILED', nameVersion, 'because', report.fail[nameVersion]) 150 | }) 151 | }) 152 | ``` 153 | `opts.include` and `opts.exclude` should be formatted like the `inclusions` and `exclusions` 154 | fields of a config file used in the command line tool. 155 | That is, `opts.include` and `opts.exclude` should be objects where the keys are module names 156 | and the values are lists containing objects with `version_range`, `audit_trail`, and `desc` fields. 157 | 158 | License 159 | ------- 160 | 161 | See LICENSE.txt 162 | -------------------------------------------------------------------------------- /docs/flowchart.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | cluster_logic 13 | 14 | Logic 15 | 16 | cluster_data 17 | 18 | Data 19 | 20 | 21 | start 22 | 23 | start 24 | 25 | 26 | hook 27 | 28 | Passes 29 | user-supplied 30 | hook? 31 | 32 | 33 | start->hook 34 | 35 | 36 | 37 | 38 | good 39 | 40 | good 41 | 42 | 43 | bad 44 | 45 | bad 46 | 47 | 48 | spdx_valid 49 | 50 | Is 'valid' 51 | SPDX license? 52 | 53 | 54 | inclusion_list 55 | 56 | Is in the 57 | inclusion list? 58 | 59 | 60 | spdx_valid->inclusion_list 61 | 62 | 63 | no 64 | 65 | 66 | exclusion_list 67 | 68 | Is in the 69 | exclusion list? 70 | 71 | 72 | spdx_valid->exclusion_list 73 | 74 | 75 | yes 76 | 77 | 78 | inclusion_list->good 79 | 80 | 81 | yes 82 | 83 | 84 | inclusion_list->bad 85 | 86 | 87 | no 88 | 89 | 90 | exclusion_list->good 91 | 92 | 93 | no 94 | 95 | 96 | exclusion_list->bad 97 | 98 | 99 | yes 100 | 101 | 102 | hook->bad 103 | 104 | 105 | no 106 | 107 | 108 | hook->spdx_valid 109 | 110 | 111 | yes 112 | 113 | 114 | list_item 115 | 116 | list_item 117 | 118 | name 119 | 120 | audit_trail 121 | 122 | desc 123 | 124 | version_range 125 | 126 | 127 | 128 | --------------------------------------------------------------------------------