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 |
6 | login handler saves token when login succeeds | 5 |
7 | login handler exits with error when login fails | 6 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |