Test Report

0
0
7
86

ID Title Duration (ms)
1 check handler can run a clean check 30
2 check handler can run a check with one finding 6
3 check handler can run a check with multiple findings 9
4 check handler can run an offline check 6
5 check handler exits with status 3 when package.json is invalid 2

Code Coverage Report

83.33%
504
420
84

commands/check.js

96.97%
99
96
3
Line Lint Hits Source
1 'use strict';
2
3 1 const Fs = require('fs');
4 1 const Path = require('path');
5
6 1 const API = require('../lib/api');
7 1 const Command = require('../lib/command');
8 1 const Offline = require('../lib/offline');
9 1 const Package = require('../lib/package');
10
11 1 exports.command = 'check [path]';
12 1 exports.description = 'checks a project for known vulnerabilities against the Node Security API';
13
14 1 exports.builder = {
15 'warn-only': {
16 boolean: true,
17 default: false,
18 description: 'display vulnerabilities but do not exit with an error code',
19 group: 'Output:'
20 },
21 offline: {
22 boolean: true,
23 description: 'execute checks without an internet connection',
24 group: 'Offline:'
25 },
26 advisories: {
27 description: 'path to local advisories database used in offline checks',
28 group: 'Offline:'
29 },
30 'cvss-threshold': {
31 alias: 'threshold',
32 description: 'cvss threshold that must be reached in order to exit with an error',
33 type: 'number',
34 group: 'Output:'
35 },
36 'cvss-filter': {
37 alias: 'filter',
38 description: 'cvss score below which findings will be hidden',
39 type: 'number',
40 group: 'Output:'
41 },
42 exceptions: {
43 type: 'array',
44 description: 'advisories to ignore while running this check',
45 default: [],
46 group: 'Project:'
47 },
48 org: {
49 description: 'nodesecurity organization your project is contained in',
50 implies: 'integration',
51 group: 'Project:'
52 },
53 integration: {
54 description: 'your project\'s uuid',
55 implies: 'org',
56 group: 'Project:'
57 }
58 };
59
60 1 exports.handler = Command.wrap('check', (args) => {
61
62 let pkg;
63 5 try {
64 5 pkg = JSON.parse(Fs.readFileSync(Path.join(args.path, 'package.json')));
65 }
66 catch (err) {
67 1 return Promise.reject(new Error(`Unable to load package.json for project: ${Path.basename(args.path)}`));
68 }
69 4 pkg = Package.sanitize(pkg);
70
71 4 let shrinkwrap;
72 4 try {
73 4 shrinkwrap = JSON.parse(Fs.readFileSync(Path.join(args.path, 'npm-shrinkwrap.json')));
74 }
75 catch (err) {}
76
77 4 let packagelock;
78 4 try {
79 4 packagelock = JSON.parse(Fs.readFileSync(Path.join(args.path, 'package-lock.json')));
80 }
81 catch (err) {}
82
83 4 let check;
84 4 if (!args.offline) {
85 3 const api = new API(args);
86 3 check = api.check(args, { package: pkg, shrinkwrap, packagelock, exceptions: args.exceptions });
87 }
88 else {
89 1 let advisories;
90 1 try {
91
if (
args.advisories
) {
92 advisories = JSON.parse(Fs.readFileSync(Path.resolve(process.cwd(), args.advisories)));
93 }
94 else {
95 1 advisories = JSON.parse(Fs.readFileSync(Path.join(args.path, 'advisories.json')));
96 }
97 }
98 catch (err) {
99 return Promise.reject(new Error('Unable to load local advisories database'));
100 }
101
102 1 check = Offline.check({ package: pkg, shrinkwrap, packagelock, advisories, exceptions: args.exceptions });
103 }
104
105 4 return check.then((results) => {
106
107 results.message = results.data.length ? `${results.data.length} ${results.data.length === 1 ? 'vulnerability' : 'vulnerabilities'} found` : 'No known vulnerabilities found';
108 4 results.data = results.data.sort((a, b) => b.cvss_score - a.cvss_score);
109 4 return results;
110 });
111 });
112

commands/login.js

100%
59
59
0
Line Lint Hits Source
1 'use strict';
2
3 1 const Inquirer = require('inquirer');
4
5 1 const Command = require('../lib/command');
6 1 const Config = require('../lib/config');
7 1 const API = require('../lib/api');
8
9 1 exports.command = 'login';
10 1 exports.description = 'login to the node security project';
11
12 1 exports.builder = {
13 email: {
14 description: 'your email address',
15 group: 'User info:'
16 },
17 password: {
18 description: 'your password',
19 group: 'User info:'
20 }
21 };
22
23 1 exports.handler = Command.wrap('login', (args) => {
24
25 // $lab:coverage:off$
26 let input = Promise.resolve();
27 if (process.stdout.isTTY) {
28 if (!args.email) {
29 input = input.then(() => {
30
31 return Inquirer.prompt({
32 name: 'email',
33 message: 'Email address'
34 }).then((answer) => {
35
36 args.email = answer.email;
37 });
38 });
39 }
40
41 if (!args.password) {
42 input = input.then(() => {
43
44 return Inquirer.prompt({
45 name: 'password',
46 message: 'Password',
47 type: 'password'
48 }).then((answer) => {
49
50 args.password = answer.password;
51 });
52 });
53 }
54 }
55 else {
56 if (!args.email ||
57 !args.password) {
58
59 return Promise.reject(new Error('Email and password required for non-interactive logins'));
60 }
61 }
62 // $lab:coverage:on$
63
64 2 return input.then(() => {
65
66 const api = new API(args);
67 2 return api.login({ email: args.email, password: args.password });
68 }).then((res) => {
69
70 return Config.update(res.data);
71 }).then(() => {
72
73 return {
74 message: `logged in as ${args.email}`
75 };
76 });
77 });
78

lib/api.js

72.73%
44
32
12
Line Lint Hits Source
1 'use strict';
2
3 1 const Pkg = require('../package.json');
4 1 const ProxyAgent = require('https-proxy-agent');
5 1 const Wreck = require('./wreck');
6
7 class API {
8 constructor(options) {
9
10 this._options = {
11 baseUrl: options.baseUrl,
12 json: true,
13 headers: {
14 'X-NSP-VERSION': Pkg.version
15 }
16 };
17
18
if (
options.proxy
) {
19 this._options.agent = new ProxyAgent(options.proxy);
20 }
21
22
if (
options.token
) {
23 this.authed = true;
24 this._options.headers.authorization = options.token;
25 }
26 }
27
28 check(params, payload) {
29
30 let pre = Promise.resolve();
31
if (
params.org
&&
32
params.integration
) {
33
34 pre = pre.then(() => {
35
36
return this.getIntegration(params);
37 }).then((integration) => {
38
39
payload.exceptions = integration.data.settings.exceptions;
40 return Promise.resolve();
41 });
42 }
43
44 3 return pre.then(() => {
45
46 return Wreck.post('/check', Object.assign({}, this._options, { payload }));
47 });
48 }
49
50 getIntegration(params) {
51
52
return Wreck.get(`/integrations/${params.org}/github/${params.integration}`, this._options);
53 }
54
55 login(payload) {
56
57 return Wreck.post('/user/login', Object.assign({}, this._options, { payload }));
58 }
59 }
60
61 1 module.exports = API;
62

lib/command.js

67.92%
106
72
34
Line Lint Hits Source
1 'use strict';
2
3 1 const Fs = require('fs');
4 1 const Os = require('os');
5 1 const Path = require('path');
6
7 1 const Reporters = require('../reporters');
8
9 1 const internals = {};
10 1 internals.wrapReporter = function (name, fn, ...args) {
11
12
return new Promise((resolve, reject) => {
13
14
try {
15 return resolve(fn(...args));
16 }
17 catch (err) {
18 return reject(err);
19
}
20 }).catch((err) => {
21
22
console.error(`Error in reporter: ${name}`);
23 console.error(err.stack);
24 process.exit(4);
25
});
26 };
27
28 1 exports.wrap = function (name, handler) {
29
30 return function (args) {
31
32 const reporter = Reporters.load(args.reporter);
33
34 // we set the default here because if you use yargs to do it
35 // it shows up in the output of --help and we don't want that
36 7 if (!args.baseUrl) {
37 1 args.baseUrl = 'https://api.nodesecurity.io';
38 }
39
40 7 args.path = args.path ? Path.resolve(args.path) : process.cwd();
41
42 7 let userConfig;
43 7 try {
44 7 userConfig = JSON.parse(Fs.readFileSync(Path.join(Os.homedir(), '.nsprc')));
45 }
46 catch (err) {}
47
48
if (
userConfig
) {
49 Object.assign(args, userConfig);
50 }
51
52 7 let config;
53 7 try {
54 7 config = JSON.parse(Fs.readFileSync(Path.join(args.path, '.nsprc')));
55 }
56 catch (err) {}
57
58
if (
config
) {
59 Object.assign(args, config);
60 }
61
62 7 return handler(args).then((result) => {
63
64 let maxCvss;
65
if (
args.filter
||
66 args.threshold) {
67
68 maxCvss = Math.max(...result.data.map((item) => item.cvss_score));
69 }
70
71 5 if (name === 'check' &&
72
args.filter
&&
73
result.data.length
) {
74
75 result.data = result.data.filter((item) => item.cvss_score > args.filter);
76 }
77
78 5 let output;
79
if (
args.quiet
) {
80 5 output = Promise.resolve();
81 }
82 else if (reporter.hasOwnProperty(name) &&
83 reporter[name].hasOwnProperty('success')) {
84
85 output = internals.wrapReporter(args.reporter, reporter[name].success, result, args);
86 }
87 else {
88 output = internals.wrapReporter(args.reporter, reporter.success, result, args);
89 }
90
91 5 return output.then(() => {
92
93 if (name === 'check' &&
94 result.data.length > 0 &&
95 !args['warn-only']) {
96
97
if (
!args.threshold
||
98
(
args.threshold && maxCvss > args.threshold
)) {
99
100 3 process.exit(1);
101 }
102 }
103
104 5 process.exit(0);
105 });
106 }).catch((err) => {
107
108
if (
err.statusCode === 400
) {
109 err.message += ` ${err.data.message}`;
110 }
111
112 2 let output;
113
if (
args.quiet
) {
114 2 output = Promise.resolve();
115 }
116 else if (reporter.hasOwnProperty(name) &&
117 reporter[name].hasOwnProperty('error')) {
118
119 output = internals.wrapReporter(args.reporter, reporter[name].error, err, args);
120 }
121 else {
122 output = internals.wrapReporter(args.reporter, reporter.error, err, args);
123 }
124
125 2 return output.then(() => {
126
127
if (
!args['warn-only']
) {
128
if (
err.isServer
) {
129 if (!args['ignore-server-errors']) {
130 process.exit(2);
131 }
132 }
133 else {
134 2 process.exit(3);
135 }
136 }
137 });
138 });
139 };
140 };
141

lib/config.js

91.67%
24
22
2
Line Lint Hits Source
1 'use strict';
2
3 1 const Fs = require('fs');
4 1 const Os = require('os');
5 1 const Path = require('path');
6
7 1 exports.update = function (settings) {
8
9 const path = Path.join(Os.homedir(), '.nsprc');
10 1 let current;
11 1 try {
12 1 current = JSON.parse(Fs.readFileSync(path));
13 }
14 catch (err) {
15 1 current = {};
16 }
17
18 1 const updated = Object.assign(current, settings);
19 1 for (const key in updated) {
20
if (
updated[key] === undefined
) {
21 delete updated[key];
22 }
23 }
24
25 1 try {
26 1 Fs.writeFileSync(path, JSON.stringify(updated, null, 2));
27 }
28 catch (err) {}
29 };
30

lib/offline.js

87.69%
65
57
8
Line Lint Hits Source
1 'use strict';
2
3 1 const NPMUtils = require('nodesecurity-npm-utils');
4 1 const Semver = require('semver');
5
6 1 const internals = {};
7 1 internals.exceptionRegex = /^https\:\/\/nodesecurity\.io\/advisories\/([0-9]+)$/;
8
9 1 exports.check = function (options) {
10
11
if (
!options.shrinkwrap
&&
12
!options.packagelock
) {
13
14 return Promise.reject(new Error('npm-shrinkwrap.json or package-lock.json is required for offline checks'));
15 }
16
17 1 const exceptions = options.exceptions.filter((exception) => {
18
19
return internals.exceptionRegex.test(exception);
20 }).map((exception) => {
21
22
return Number(internals.exceptionRegex.exec(exception)[1]);
23 });
24
25
return NPMUtils.getShrinkwrapDependencies(
options.shrinkwrap
||
options.packagelock
, options.package).then((tree) => {
26
27 const result = [];
28 1 Object.keys(tree).map((child) => {
29
30 const mod = tree[child];
31 3 const matches = [];
32
33 for (const advisory of options.advisories) {
34 3 if (mod.name === advisory.module_name &&
35
!exceptions.includes(advisory.id)
&&
36 Semver.satisfies(mod.version, advisory.vulnerable_versions)) {
37
38 1 matches.push(advisory);
39 }
40 }
41
42 3 return {
43 module: mod.name,
44 version: mod.version,
45 vulnerabilities: matches
46 };
47 }).filter((mod) => {
48
49
50 return mod.vulnerabilities.length > 0;
51 }).forEach((finding) => {
52
53 for (const vuln of finding.vulnerabilities) {
54 1 const paths = tree[`${finding.module}@${finding.version}`].paths;
55
56 for (const path of paths) {
57 1 result.push({
58 id: vuln.id,
59 module: finding.module,
60 version: finding.version,
61 vulnerable_versions: vuln.vulnerable_versions,
62 patched_versions: vuln.patched_versions,
63 title: vuln.title,
64 advisory: `https://nodesecurity.io/advisories/${vuln.id}`,
65 updated_at: vuln.updated_at,
66 created_at: vuln.created_at,
67 publish_date: vuln.publish_date,
68 overview: vuln.overview,
69 recommendation: vuln.recommendation,
70 cvss_vector: vuln.cvss_vector,
71 cvss_score: vuln.cvss_score,
72 path
73 });
74 }
75 }
76 });
77
78 1 return {
79 data: result
80 };
81 }).catch((err) => {
82
83
throw Object.assign(new Error('Unable to parse dependency tree'), { error: err });
84 });
85 };
86

lib/package.js

95.45%
22
21
1
Line Lint Hits Source
1 'use strict';
2
3 1 const internals = {};
4 1 internals.whitelist = [
5 'name',
6 'version',
7 'engine',
8 'dependencies',
9 'devDependencies',
10 'optionalDependencies',
11 'peerDependencies',
12 'bundleDependencies',
13 'bundledDependencies'
14 ];
15
16 1 exports.sanitize = function (pkg) {
17
18 const result = {};
19 4 for (const key in pkg) {
20
if (
internals.whitelist.includes(key)
) {
21 12 result[key] = pkg[key];
22 }
23 }
24
25 4 return result;
26 };
27

lib/wreck.js

85.71%
35
30
5
Line Lint Hits Source
1 'use strict';
2
3 1 const Wreck = require('wreck');
4
5 1 const internals = {};
6 1 internals.request = function (method, uri, options) {
7
8 return new Promise((resolve, reject) => {
9
10 Wreck[method](uri, options, (err, res, data) => {
11
12 if (err) {
13 1 const e = new Error(err.message);
14 1 return reject(Object.assign(e, {
15 statusCode: err.output.statusCode,
16
headers:
err.data
? err.data.headers :
err.output.headers
,
17
data:
err.data
? err.data.payload :
err.output.payload
,
18 isServer: err.isServer
19 }));
20 }
21
22 4 return resolve({
23 statusCode: res.statusCode,
24 headers: res.headers,
25 data
26 });
27 });
28 });
29 };
30
31 1 exports.delete = function (uri, options) {
32
33
return internals.request('delete', uri, options);
34 };
35
36 1 exports.get = function (uri, options) {
37
38
return internals.request('get', uri, options);
39 };
40
41 1 exports.post = function (uri, options) {
42
43 return internals.request('post', uri, options);
44 };
45
46 1 exports.put = function (uri, options) {
47
48
return internals.request('put', uri, options);
49 };
50

reporters/index.js

94.12%
17
16
1
Line Lint Hits Source
1 'use strict';
2
3 1 const Path = require('path');
4
5 1 const loadInternalReporter = function (name) {
6
7 try {
8 14 return require(Path.join(__dirname, name));
9 }
10 catch (err) {}
11 };
12
13 1 const loadExternalReporter = function (name) {
14
15 try {
16 7 return require(`nsp-reporter-${name}`);
17 }
18 catch (err) {}
19 };
20
21 1 exports.load = function (name) {
22
23
return
loadInternalReporter(name)
||
loadExternalReporter(name)
|| loadInternalReporter('table');
24 };
25

reporters/table.js

45.45%
33
15
18
Line Lint Hits Source
1 'use strict';
2
3 1 const Chalk = require('chalk');
4 1 const Table = require('cli-table2');
5 1 const Cvss = require('cvss');
6
7 1 exports.error = function (err) {
8
9
console.error(Chalk.yellow('(+)'), err.message);
10 };
11
12 1 exports.success = function (result) {
13
14
console.log(Chalk.green('(+)'), result.message);
15 };
16
17 1 exports.check = {};
18 1 exports.check.success = function (result) {
19
20
if (!result.data.length) {
21 return console.log(Chalk.green('(+)'), result.message);
22
}
23
24 console.log(Chalk.red('(+)'), result.message);
25
26 result.data.forEach((finding) => {
27
28
const table = new Table({
29 head: ['', finding.title],
30 colWidths: [12, 68],
31 wordWrap: true
32
});
33
34 table.push(['Name', finding.module]);
35 table.push(['CVSS', `${finding.cvss_score} (${Cvss.getRating(finding.cvss_score)})`]);
36 table.push(['Installed', finding.version]);
37 table.push(['Vulnerable', finding.vulnerable_versions === '<=99.999.99999' ? 'All' : finding.vulnerable_versions]);
38 table.push(['Patched', finding.patched_versions === '<0.0.0' ? 'None' : finding.patched_versions]);
39 table.push(['Path', finding.path.join(' > ')]);
40 table.push(['More Info', finding.advisory]);
41
42 console.log(table.toString());
43 console.log();
44 });
45 };
46

Linting Report

0 0