Comparing version 2.5.1 to 3.0.0
@@ -1,10 +0,15 @@ | ||
const { version: auditCiVersion } = require('../package.json'); | ||
const { bugs, version: auditCiVersion } = require("../package.json"); | ||
const { yellow } = require("./colors"); | ||
if (!auditCiVersion) { | ||
console.log( | ||
'\x1b[33m%s\x1b[0m', | ||
'Could not identify audit-ci version. Please report this issue to https://github.com/IBM/audit-ci/issues.' | ||
yellow, | ||
`Could not identify audit-ci version. Please report this issue to ${bugs}.` | ||
); | ||
} | ||
module.exports = { auditCiVersion }; | ||
function printAuditCiVersion() { | ||
console.log(`audit-ci version: ${auditCiVersion}`); | ||
} | ||
module.exports = { auditCiVersion, printAuditCiVersion }; |
@@ -6,104 +6,109 @@ /* | ||
*/ | ||
const yargs = require('yargs'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const yargs = require("yargs"); | ||
const audit = require("./audit"); | ||
const { printAuditCiVersion } = require("./audit-ci-version"); | ||
const { green, red } = require("./colors"); | ||
printAuditCiVersion(); | ||
const { argv } = yargs | ||
.config('config') | ||
.config("config") | ||
.options({ | ||
l: { | ||
alias: 'low', | ||
alias: "low", | ||
default: false, | ||
describe: 'Exit for low vulnerabilities or higher', | ||
type: 'boolean', | ||
describe: "Exit for low vulnerabilities or higher", | ||
type: "boolean", | ||
}, | ||
m: { | ||
alias: 'moderate', | ||
alias: "moderate", | ||
default: false, | ||
describe: 'Exit for moderate vulnerabilities or higher', | ||
type: 'boolean', | ||
describe: "Exit for moderate vulnerabilities or higher", | ||
type: "boolean", | ||
}, | ||
h: { | ||
alias: 'high', | ||
alias: "high", | ||
default: false, | ||
describe: 'Exit for high vulnerabilities or higher', | ||
type: 'boolean', | ||
describe: "Exit for high vulnerabilities or higher", | ||
type: "boolean", | ||
}, | ||
c: { | ||
alias: 'critical', | ||
alias: "critical", | ||
default: false, | ||
describe: 'Exit for critical vulnerabilities', | ||
type: 'boolean', | ||
describe: "Exit for critical vulnerabilities", | ||
type: "boolean", | ||
}, | ||
p: { | ||
alias: 'package-manager', | ||
default: 'auto', | ||
describe: 'Choose a package manager', | ||
choices: ['auto', 'npm', 'yarn'], | ||
alias: "package-manager", | ||
default: "auto", | ||
describe: "Choose a package manager", | ||
choices: ["auto", "npm", "yarn"], | ||
}, | ||
r: { | ||
alias: 'report', | ||
alias: "report", | ||
default: false, | ||
describe: 'Show a full audit report', | ||
type: 'boolean', | ||
describe: "Show a full audit report", | ||
type: "boolean", | ||
}, | ||
s: { | ||
alias: 'summary', | ||
alias: "summary", | ||
default: false, | ||
describe: 'Show a summary audit report', | ||
type: 'boolean', | ||
describe: "Show a summary audit report", | ||
type: "boolean", | ||
}, | ||
a: { | ||
alias: 'advisories', | ||
alias: "advisories", | ||
default: [], | ||
describe: 'Whitelisted advisory ids', | ||
type: 'array', | ||
describe: "Whitelisted advisory ids", | ||
type: "array", | ||
}, | ||
w: { | ||
alias: 'whitelist', | ||
alias: "whitelist", | ||
default: [], | ||
describe: 'Whitelisted module names', | ||
type: 'array', | ||
describe: "Whitelisted module names", | ||
type: "array", | ||
}, | ||
d: { | ||
alias: 'directory', | ||
default: './', | ||
describe: 'The directory containing the package.json to audit', | ||
type: 'string', | ||
alias: "directory", | ||
default: "./", | ||
describe: "The directory containing the package.json to audit", | ||
type: "string", | ||
}, | ||
'show-not-found': { | ||
"show-not-found": { | ||
default: true, | ||
describe: 'Show whitelisted advisories that are not found', | ||
type: 'boolean', | ||
describe: "Show whitelisted advisories that are not found", | ||
type: "boolean", | ||
}, | ||
registry: { | ||
default: undefined, | ||
describe: 'The registry to resolve packages by name and version', | ||
type: 'string', | ||
describe: "The registry to resolve packages by name and version", | ||
type: "string", | ||
}, | ||
'report-type': { | ||
default: 'important', | ||
describe: 'Format for the audit report results', | ||
type: 'string', | ||
choices: ['important', 'summary', 'full'], | ||
"report-type": { | ||
default: "important", | ||
describe: "Format for the audit report results", | ||
type: "string", | ||
choices: ["important", "summary", "full"], | ||
}, | ||
'retry-count': { | ||
"retry-count": { | ||
default: 5, | ||
describe: | ||
'The number of attempts audit-ci calls an unavailable registry before failing', | ||
type: 'number', | ||
"The number of attempts audit-ci calls an unavailable registry before failing", | ||
type: "number", | ||
}, | ||
'pass-enoaudit': { | ||
"pass-enoaudit": { | ||
default: false, | ||
describe: | ||
'Pass if no audit is performed due to the registry returning ENOAUDIT', | ||
type: 'boolean', | ||
"Pass if no audit is performed due to the registry returning ENOAUDIT", | ||
type: "boolean", | ||
}, | ||
'path-whitelist': { | ||
"path-whitelist": { | ||
default: [], | ||
describe: 'Whitelisted vulnerability paths', | ||
type: 'array', | ||
describe: "Whitelisted vulnerability paths", | ||
type: "array", | ||
}, | ||
}) | ||
.help('help'); | ||
.help("help"); | ||
@@ -127,21 +132,7 @@ function mapVulnerabilityLevelInput(config) { | ||
function mapReportTypeInput(config) { | ||
const { 'report-type': reportType, report, summary } = config; | ||
if (report) { | ||
console.warn( | ||
'\x1b[33m%s\x1b[0m', | ||
"[DEPRECATED] The 'r' and 'report' options have been deprecated and may be removed in the future. Use `--report-type important` [default] to show only relevant information or `--report-type full` to show the full audit report." | ||
); | ||
return 'full'; | ||
} | ||
if (summary) { | ||
console.warn( | ||
'\x1b[33m%s\x1b[0m', | ||
"[DEPRECATED] The 's' and 'summary' options have been deprecated and may be removed in the future. Use `--report-type important` [default] to show only relevant information or `--report-type summary` to show only the audit metadata." | ||
); | ||
return 'summary'; | ||
} | ||
const { "report-type": reportType } = config; | ||
switch (reportType) { | ||
case 'full': | ||
case 'important': | ||
case 'summary': | ||
case "full": | ||
case "important": | ||
case "summary": | ||
return reportType; | ||
@@ -156,3 +147,3 @@ default: | ||
argv.levels = mapVulnerabilityLevelInput(argv); | ||
argv['report-type'] = mapReportTypeInput(argv); | ||
argv["report-type"] = mapReportTypeInput(argv); | ||
@@ -166,16 +157,15 @@ /** | ||
switch (pmArg) { | ||
case 'npm': | ||
return 'npm'; | ||
case 'yarn': | ||
return 'yarn'; | ||
case 'auto': { | ||
const getPath = file => path.resolve(directory, file); | ||
const packageLockExists = fs.existsSync(getPath('package-lock.json')); | ||
if (packageLockExists) return 'npm'; | ||
const shrinkwrapExists = fs.existsSync(getPath('npm-shrinkwrap.json')); | ||
if (shrinkwrapExists) return 'npm'; | ||
const yarnLockExists = fs.existsSync(getPath('yarn.lock')); | ||
if (yarnLockExists) return 'yarn'; | ||
case "npm": | ||
case "yarn": | ||
return pmArg; | ||
case "auto": { | ||
const getPath = (file) => path.resolve(directory, file); | ||
const packageLockExists = fs.existsSync(getPath("package-lock.json")); | ||
if (packageLockExists) return "npm"; | ||
const shrinkwrapExists = fs.existsSync(getPath("npm-shrinkwrap.json")); | ||
if (shrinkwrapExists) return "npm"; | ||
const yarnLockExists = fs.existsSync(getPath("yarn.lock")); | ||
if (yarnLockExists) return "yarn"; | ||
throw Error( | ||
'Cannot establish package-manager type, missing package-lock.json and yarn.lock.' | ||
"Cannot establish package-manager type, missing package-lock.json and yarn.lock." | ||
); | ||
@@ -190,11 +180,12 @@ } | ||
require('./audit')(pm, argv) | ||
.then(() => { | ||
console.log('\x1b[32m%s\x1b[0m', `Passed ${pm} security audit.`); | ||
}) | ||
.catch(err => { | ||
(async () => { | ||
try { | ||
await audit(pm, argv); | ||
console.log(green, `Passed ${pm} security audit.`); | ||
} catch (err) { | ||
const message = err.message || err; | ||
console.error('\x1b[31m%s\x1b[0m', message); | ||
console.error('\x1b[31m%s\x1b[0m', 'Exiting...'); | ||
console.error(red, message); | ||
console.error(red, "Exiting..."); | ||
process.exitCode = 1; | ||
}); | ||
} | ||
})(); |
@@ -1,36 +0,41 @@ | ||
const npmAuditer = require('./npm-auditer'); | ||
const yarnAuditer = require('./yarn-auditer'); | ||
const { yellow } = require("./colors"); | ||
const npmAuditer = require("./npm-auditer"); | ||
const yarnAuditer = require("./yarn-auditer"); | ||
const PARTIAL_RETRY_ERROR_MSG = { | ||
// The three ENOAUDIT error messages for NPM are: | ||
// `Either your login credentials are invalid or your registry (${opts.registry}) does not support audit.` | ||
// `Your configured registry (${opts.registry}) does not support audit requests.` | ||
// `Your configured registry (${opts.registry}) may not support audit requests, or the audit endpoint may be temporarily unavailable.` | ||
// Between them, all three use the phrasing 'not support audit'. | ||
npm: `not support audit`, | ||
yarn: "503 Service Unavailable", | ||
}; | ||
function audit(pm, config, reporter) { | ||
const auditor = pm === 'npm' ? npmAuditer : yarnAuditer; | ||
const PARTIAL_RETRY_ERROR_MSG = { | ||
// The three ENOAUDIT error messages for NPM are: | ||
// `Either your login credentials are invalid or your registry (${opts.registry}) does not support audit.` | ||
// `Your configured registry (${opts.registry}) does not support audit requests.` | ||
// `Your configured registry (${opts.registry}) may not support audit requests, or the audit endpoint may be temporarily unavailable.` | ||
// Between them, all three use the phrasing 'not support audit'. | ||
npm: `not support audit`, | ||
yarn: '503 Service Unavailable', | ||
}; | ||
const auditor = pm === "npm" ? npmAuditer : yarnAuditer; | ||
const { | ||
"pass-enoaudit": passENoAudit, | ||
"retry-count": maxRetryCount, | ||
} = config; | ||
function run(attempt = 0) { | ||
return auditor.audit(config, reporter).catch(err => { | ||
async function run(attempt = 0) { | ||
try { | ||
const result = await auditor.audit(config, reporter); | ||
return result; | ||
} catch (err) { | ||
const message = err.message || err; | ||
if ( | ||
attempt < config['retry-count'] && | ||
message && | ||
message.includes(PARTIAL_RETRY_ERROR_MSG[pm]) | ||
) { | ||
console.log('RETRY-RETRY'); | ||
const isRetryableMessage = | ||
typeof message === "string" && | ||
message.includes(PARTIAL_RETRY_ERROR_MSG[pm]); | ||
const shouldRetry = attempt < maxRetryCount && isRetryableMessage; | ||
if (shouldRetry) { | ||
console.log("RETRY-RETRY"); | ||
return run(attempt + 1); | ||
} | ||
if ( | ||
config['pass-enoaudit'] && | ||
message.includes(PARTIAL_RETRY_ERROR_MSG[pm]) | ||
) { | ||
const shouldPassWithoutAuditing = passENoAudit && isRetryableMessage; | ||
if (shouldPassWithoutAuditing) { | ||
console.warn( | ||
'\x1b[33m%s\x1b[0m', | ||
`ACTION RECOMMENDED: An audit could not performed due to ${ | ||
config['retry-count'] | ||
} audits that resulted in ENOAUDIT. Perform an audit manually and verify that no significant vulnerabilities exist before merging.` | ||
yellow, | ||
`ACTION RECOMMENDED: An audit could not performed due to ${maxRetryCount} audits that resulted in ENOAUDIT. Perform an audit manually and verify that no significant vulnerabilities exist before merging.` | ||
); | ||
@@ -40,3 +45,3 @@ return Promise.resolve(); | ||
throw err; | ||
}); | ||
} | ||
} | ||
@@ -43,0 +48,0 @@ |
@@ -6,25 +6,23 @@ /* | ||
*/ | ||
const { spawn } = require('cross-spawn'); | ||
const eventStream = require('event-stream'); | ||
const JSONStream = require('JSONStream'); | ||
const ReadlineTransform = require('readline-transform'); | ||
const { spawn } = require("cross-spawn"); | ||
const eventStream = require("event-stream"); | ||
const JSONStream = require("JSONStream"); | ||
const ReadlineTransform = require("readline-transform"); | ||
const { blue, yellow } = require("./colors"); | ||
function reportAudit(summary, config) { | ||
const { whitelist, 'show-not-found': showNotFound } = config; | ||
const { whitelist, "show-not-found": showNotFound } = config; | ||
if (whitelist.length) { | ||
console.log( | ||
'\x1b[36m%s\x1b[0m', | ||
'Modules to whitelist: '.concat(whitelist.join(', '), '.') | ||
); | ||
console.log(blue, `Modules to whitelist: ${whitelist.join(", ")}.`); | ||
} | ||
if (summary.whitelistedModulesFound.length) { | ||
const found = summary.whitelistedModulesFound.join(', '); | ||
const found = summary.whitelistedModulesFound.join(", "); | ||
const msg = `Vulnerable whitelisted modules found: ${found}.`; | ||
console.warn('\x1b[33m%s\x1b[0m', msg); | ||
console.warn(yellow, msg); | ||
} | ||
if (summary.whitelistedAdvisoriesFound.length) { | ||
const found = summary.whitelistedAdvisoriesFound.join(', '); | ||
const found = summary.whitelistedAdvisoriesFound.join(", "); | ||
const msg = `Vulnerable whitelisted advisories found: ${found}.`; | ||
console.warn('\x1b[33m%s\x1b[0m', msg); | ||
console.warn(yellow, msg); | ||
} | ||
@@ -34,5 +32,5 @@ if (showNotFound && summary.whitelistedAdvisoriesNotFound.length) { | ||
.sort((a, b) => a - b) | ||
.join(', '); | ||
.join(", "); | ||
const msg = `Vulnerable whitelisted advisories not found: ${found}.\nConsider not whitelisting them.`; | ||
console.warn('\x1b[33m%s\x1b[0m', msg); | ||
console.warn(yellow, msg); | ||
} | ||
@@ -43,5 +41,5 @@ | ||
const err = `Failed security audit due to ${summary.failedLevelsFound.join( | ||
', ' | ||
", " | ||
)} vulnerabilities.\nVulnerable advisories are: ${summary.advisoriesFound.join( | ||
', ' | ||
", " | ||
)}`; | ||
@@ -54,21 +52,20 @@ throw new Error(err); | ||
function runProgram(command, args, options, stdoutListener, stderrListener) { | ||
return new Promise(resolve => { | ||
const proc = spawn(command, args, options); | ||
const transform = new ReadlineTransform({ skipEmpty: true }); | ||
proc.stdout.setEncoding('utf8'); | ||
proc.stdout | ||
.pipe(transform) | ||
.pipe(JSONStream.parse()) | ||
.pipe( | ||
eventStream.mapSync(data => { | ||
if (!data) return; | ||
try { | ||
stdoutListener(data); | ||
} catch (error) { | ||
stderrListener(error); | ||
} | ||
}) | ||
); | ||
proc.on('close', () => resolve()); | ||
const transform = new ReadlineTransform({ skipEmpty: true }); | ||
const proc = spawn(command, args, options); | ||
proc.stdout.setEncoding("utf8"); | ||
proc.stdout | ||
.pipe(transform) | ||
.pipe(JSONStream.parse()) | ||
.pipe( | ||
eventStream.mapSync((data) => { | ||
if (!data) return; | ||
try { | ||
stdoutListener(data); | ||
} catch (error) { | ||
stderrListener(error); | ||
} | ||
}) | ||
); | ||
return new Promise((resolve) => { | ||
proc.on("close", () => resolve()); | ||
}); | ||
@@ -75,0 +72,0 @@ } |
@@ -6,7 +6,8 @@ /* | ||
*/ | ||
const SUPPORTED_SEVERITY_LEVELS = new Set() | ||
.add('critical') | ||
.add('high') | ||
.add('moderate') | ||
.add('low'); | ||
const SUPPORTED_SEVERITY_LEVELS = new Set([ | ||
"critical", | ||
"high", | ||
"moderate", | ||
"low", | ||
]); | ||
@@ -16,8 +17,7 @@ class Model { | ||
const unsupported = Object.keys(config.levels).filter( | ||
curr => !SUPPORTED_SEVERITY_LEVELS.has(curr) | ||
(curr) => !SUPPORTED_SEVERITY_LEVELS.has(curr) | ||
); | ||
unsupported.sort(); | ||
if (unsupported.length) { | ||
throw new Error( | ||
`Unsupported severity levels found: ${unsupported.join(', ')}` | ||
`Unsupported severity levels found: ${unsupported.sort().join(", ")}` | ||
); | ||
@@ -28,3 +28,3 @@ } | ||
this.whitelistedModuleNames = config.whitelist; | ||
this.whitelistedPaths = config['path-whitelist'] || []; | ||
this.whitelistedPaths = config["path-whitelist"] || []; | ||
this.whitelistedAdvisoryIds = config.advisories; | ||
@@ -43,3 +43,3 @@ | ||
if (this.whitelistedModuleNames.some(m => m === advisory.module_name)) { | ||
if (this.whitelistedModuleNames.some((m) => m === advisory.module_name)) { | ||
if (!this.whitelistedModulesFound.includes(advisory.module_name)) { | ||
@@ -51,3 +51,3 @@ this.whitelistedModulesFound.push(advisory.module_name); | ||
if (this.whitelistedAdvisoryIds.some(a => Number(a) === advisory.id)) { | ||
if (this.whitelistedAdvisoryIds.some((a) => Number(a) === advisory.id)) { | ||
if (!this.whitelistedAdvisoriesFound.includes(advisory.id)) { | ||
@@ -59,4 +59,4 @@ this.whitelistedAdvisoriesFound.push(advisory.id); | ||
advisory.findings.forEach(finding => | ||
finding.paths.forEach(path => { | ||
advisory.findings.forEach((finding) => | ||
finding.paths.forEach((path) => { | ||
if (this.whitelistedPaths.includes(`${advisory.id}|${path}`)) { | ||
@@ -69,4 +69,4 @@ this.whitelistedPathsFound.push(`${advisory.id}|${path}`); | ||
if ( | ||
advisory.findings.every(finding => | ||
finding.paths.every(path => | ||
advisory.findings.every((finding) => | ||
finding.paths.every((path) => | ||
this.whitelistedPaths.includes(`${advisory.id}|${path}`) | ||
@@ -83,6 +83,3 @@ ) | ||
load(parsedOutput) { | ||
Object.keys(parsedOutput.advisories) | ||
// Get the advisories values (Object.values not supported in Node 6) | ||
.map(k => parsedOutput.advisories[k]) | ||
.forEach(a => this.process(a)); | ||
Object.values(parsedOutput.advisories).forEach((a) => this.process(a)); | ||
@@ -92,5 +89,5 @@ return this.getSummary(); | ||
getSummary(advisoryMapper = a => a.id) { | ||
getSummary(advisoryMapper = (a) => a.id) { | ||
const foundSeverities = new Set(); | ||
this.advisoriesFound.forEach(curr => foundSeverities.add(curr.severity)); | ||
this.advisoriesFound.forEach((curr) => foundSeverities.add(curr.severity)); | ||
const failedLevelsFound = [...foundSeverities.values()]; | ||
@@ -102,3 +99,3 @@ failedLevelsFound.sort(); | ||
const whitelistedAdvisoriesNotFound = this.whitelistedAdvisoryIds.filter( | ||
id => !this.whitelistedAdvisoriesFound.includes(id) | ||
(id) => !this.whitelistedAdvisoriesFound.includes(id) | ||
); | ||
@@ -105,0 +102,0 @@ |
@@ -6,13 +6,13 @@ /* | ||
*/ | ||
const { auditCiVersion } = require('./audit-ci-version'); | ||
const { runProgram, reportAudit } = require('./common'); | ||
const Model = require('./Model'); | ||
const { blue } = require("./colors"); | ||
const { runProgram, reportAudit } = require("./common"); | ||
const Model = require("./Model"); | ||
function runNpmAudit(config) { | ||
async function runNpmAudit(config) { | ||
const { directory, registry, _npm } = config; | ||
const npmExec = _npm || 'npm'; | ||
const npmExec = _npm || "npm"; | ||
let stdoutBuffer = {}; | ||
function outListener(data) { | ||
stdoutBuffer = Object.assign({}, stdoutBuffer, data); | ||
stdoutBuffer = { ...stdoutBuffer, ...data }; | ||
} | ||
@@ -25,40 +25,33 @@ | ||
const args = ['audit', '--json']; | ||
const args = ["audit", "--json"]; | ||
if (registry) { | ||
args.push('--registry', registry); | ||
args.push("--registry", registry); | ||
} | ||
const options = { cwd: directory }; | ||
return Promise.resolve() | ||
.then(() => runProgram(npmExec, args, options, outListener, errListener)) | ||
.then(() => { | ||
if (stderrBuffer.length) { | ||
throw new Error( | ||
`Invocation of npm audit failed:\n${stderrBuffer.join('\n')}` | ||
); | ||
} | ||
return stdoutBuffer; | ||
}); | ||
await runProgram(npmExec, args, options, outListener, errListener); | ||
if (stderrBuffer.length) { | ||
throw new Error( | ||
`Invocation of npm audit failed:\n${stderrBuffer.join("\n")}` | ||
); | ||
} | ||
return stdoutBuffer; | ||
} | ||
function printReport(parsedOutput, levels, reportType) { | ||
function printReportObj(text, obj) { | ||
console.log('\x1b[36m%s\x1b[0m', text); | ||
const printReportObj = (text, obj) => { | ||
console.log(blue, text); | ||
console.log(JSON.stringify(obj, null, 2)); | ||
} | ||
console.log(`audit-ci version: ${auditCiVersion}`); | ||
}; | ||
switch (reportType) { | ||
case 'full': | ||
printReportObj('Yarn audit report JSON:', parsedOutput); | ||
case "full": | ||
printReportObj("Yarn audit report JSON:", parsedOutput); | ||
break; | ||
case 'important': { | ||
case "important": { | ||
const relevantAdvisories = Object.keys(parsedOutput.advisories).reduce( | ||
(acc, advisory) => | ||
levels[parsedOutput.advisories[advisory].severity] | ||
? Object.assign( | ||
{ [advisory]: parsedOutput.advisories[advisory] }, | ||
acc | ||
) | ||
? { | ||
[advisory]: parsedOutput.advisories[advisory], | ||
...acc, | ||
} | ||
: acc, | ||
@@ -71,7 +64,7 @@ {} | ||
}; | ||
printReportObj('NPM audit report results:', keyFindings); | ||
printReportObj("NPM audit report results:", keyFindings); | ||
break; | ||
} | ||
case 'summary': | ||
printReportObj('NPM audit report summary:', parsedOutput.metadata); | ||
case "summary": | ||
printReportObj("NPM audit report summary:", parsedOutput.metadata); | ||
break; | ||
@@ -83,3 +76,2 @@ default: | ||
} | ||
return parsedOutput; | ||
} | ||
@@ -101,20 +93,14 @@ | ||
*/ | ||
function audit(config, reporter = reportAudit) { | ||
return Promise.resolve() | ||
.then(() => runNpmAudit(config)) | ||
.then(parsedOutput => { | ||
if (parsedOutput.error) { | ||
const { code, summary } = parsedOutput.error; | ||
if (code !== 'ENOAUDIT' || !config['pass-enoaudit']) { | ||
throw new Error(`code ${code}: ${summary}`); | ||
} | ||
} | ||
return printReport(parsedOutput, config.levels, config['report-type']); | ||
}) | ||
.then(parsedOutput => | ||
reporter(new Model(config).load(parsedOutput), config, parsedOutput) | ||
); | ||
async function audit(config, reporter = reportAudit) { | ||
const parsedOutput = await runNpmAudit(config); | ||
if (parsedOutput.error) { | ||
const { code, summary } = parsedOutput.error; | ||
throw new Error(`code ${code}: ${summary}`); | ||
} | ||
printReport(parsedOutput, config.levels, config["report-type"]); | ||
const model = new Model(config); | ||
const summary = model.load(parsedOutput); | ||
return reporter(summary, config, parsedOutput); | ||
} | ||
module.exports = { audit }; |
@@ -6,9 +6,9 @@ /* | ||
*/ | ||
const childProcess = require('child_process'); | ||
const semver = require('semver'); | ||
const { auditCiVersion } = require('./audit-ci-version'); | ||
const { reportAudit, runProgram } = require('./common'); | ||
const Model = require('./Model'); | ||
const childProcess = require("child_process"); | ||
const semver = require("semver"); | ||
const { blue, red, yellow } = require("./colors"); | ||
const { reportAudit, runProgram } = require("./common"); | ||
const Model = require("./Model"); | ||
const MINIMUM_YARN_VERSION = '1.12.3'; | ||
const MINIMUM_YARN_VERSION = "1.12.3"; | ||
/** | ||
@@ -19,9 +19,6 @@ * Change this to the appropriate version when | ||
*/ | ||
const MINIMUM_YARN_AUDIT_REGISTRY_VERSION = '99.99.99'; | ||
const MINIMUM_YARN_AUDIT_REGISTRY_VERSION = "99.99.99"; | ||
function getYarnVersion() { | ||
const version = childProcess | ||
.execSync('yarn -v') | ||
.toString() | ||
.replace('\n', ''); | ||
const version = childProcess.execSync("yarn -v").toString().replace("\n", ""); | ||
return version; | ||
@@ -52,129 +49,130 @@ } | ||
*/ | ||
function audit(config, reporter = reportAudit) { | ||
return Promise.resolve().then(() => { | ||
const { | ||
levels, | ||
registry, | ||
'report-type': reportType, | ||
whitelist, | ||
_yarn, | ||
} = config; | ||
const yarnExec = _yarn || 'yarn'; | ||
let missingLockFile = false; | ||
const model = new Model(config); | ||
async function audit(config, reporter = reportAudit) { | ||
const { | ||
levels, | ||
registry, | ||
"report-type": reportType, | ||
whitelist, | ||
_yarn, | ||
} = config; | ||
const yarnExec = _yarn || "yarn"; | ||
let missingLockFile = false; | ||
const model = new Model(config); | ||
const yarnVersion = getYarnVersion(); | ||
const isYarnVersionSupported = yarnSupportsAudit(yarnVersion); | ||
if (!isYarnVersionSupported) { | ||
const yarnVersion = getYarnVersion(); | ||
const isYarnVersionSupported = yarnSupportsAudit(yarnVersion); | ||
if (!isYarnVersionSupported) { | ||
throw new Error( | ||
`Yarn ${yarnVersion} not supported, must be >=${MINIMUM_YARN_VERSION}` | ||
); | ||
} | ||
if (whitelist.length) { | ||
console.log(`Modules to whitelist: ${whitelist.join(", ")}.`); | ||
} | ||
switch (reportType) { | ||
case "full": | ||
console.log(blue, "Yarn audit report JSON:"); | ||
break; | ||
case "important": | ||
console.log(blue, "Yarn audit report results:"); | ||
break; | ||
case "summary": | ||
console.log(blue, "Yarn audit report summary:"); | ||
break; | ||
default: | ||
throw new Error( | ||
`Yarn ${yarnVersion} not supported, must be >=${MINIMUM_YARN_VERSION}` | ||
`Invalid report type: ${reportType}. Should be \`['important', 'full', 'summary']\`.` | ||
); | ||
} | ||
} | ||
console.log(`audit-ci version: ${auditCiVersion}`); | ||
if (whitelist.length) { | ||
console.log(`Modules to whitelist: ${whitelist.join(', ')}.`); | ||
} | ||
switch (reportType) { | ||
case 'full': | ||
console.log('\x1b[36m%s\x1b[0m', 'Yarn audit report JSON:'); | ||
break; | ||
case 'important': | ||
console.log('\x1b[36m%s\x1b[0m', 'Yarn audit report results:'); | ||
break; | ||
case 'summary': | ||
console.log('\x1b[36m%s\x1b[0m', 'Yarn audit report summary:'); | ||
break; | ||
default: | ||
throw new Error( | ||
`Invalid report type: ${reportType}. Should be \`['important', 'full', 'summary']\`.` | ||
); | ||
} | ||
function outListener(line) { | ||
try { | ||
const { type, data } = line; | ||
switch (reportType) { | ||
case 'full': | ||
console.log(JSON.stringify(line, null, 2)); | ||
break; | ||
case 'important': | ||
if ( | ||
(type === 'auditAdvisory' && levels[data.advisory.severity]) || | ||
type === 'auditSummary' | ||
) { | ||
console.log(JSON.stringify(data, null, 2)); | ||
} | ||
break; | ||
case 'summary': | ||
if (type === 'auditSummary') { | ||
console.log(JSON.stringify(data, null, 2)); | ||
} | ||
break; | ||
default: | ||
throw new Error( | ||
`Invalid report type: ${reportType}. Should be \`['important', 'full', 'summary']\`.` | ||
); | ||
const printJson = (data) => { | ||
console.log(JSON.stringify(data, null, 2)); | ||
}; | ||
// Define a function to print based on the report type. | ||
let printAuditData; | ||
switch (reportType) { | ||
case "full": | ||
printAuditData = (line) => { | ||
printJson(line); | ||
}; | ||
break; | ||
case "important": | ||
printAuditData = ({ type, data }) => { | ||
if ( | ||
(type === "auditAdvisory" && levels[data.advisory.severity]) || | ||
type === "auditSummary" | ||
) { | ||
printJson(data); | ||
} | ||
if (type === 'info' && data === 'No lockfile found.') { | ||
missingLockFile = true; | ||
return; | ||
}; | ||
break; | ||
case "summary": | ||
printAuditData = ({ type, data }) => { | ||
if (type === "auditSummary") { | ||
printJson(data); | ||
} | ||
}; | ||
break; | ||
default: | ||
throw new Error( | ||
`Invalid report type: ${reportType}. Should be \`['important', 'full', 'summary']\`.` | ||
); | ||
} | ||
if (type !== 'auditAdvisory') { | ||
return; | ||
} | ||
function outListener(line) { | ||
try { | ||
const { type, data } = line; | ||
printAuditData(line); | ||
model.process(data.advisory); | ||
} catch (err) { | ||
console.error( | ||
'\x1b[31m%s\x1b[0m', | ||
`ERROR: Cannot JSONStream.parse response:` | ||
); | ||
console.error(line); | ||
throw err; | ||
if (type === "info" && data === "No lockfile found.") { | ||
missingLockFile = true; | ||
return; | ||
} | ||
if (type !== "auditAdvisory") { | ||
return; | ||
} | ||
model.process(data.advisory); | ||
} catch (err) { | ||
console.error(red, `ERROR: Cannot JSONStream.parse response:`); | ||
console.error(line); | ||
throw err; | ||
} | ||
} | ||
const stderrBuffer = []; | ||
function errListener(line) { | ||
stderrBuffer.push(line); | ||
const stderrBuffer = []; | ||
function errListener(line) { | ||
stderrBuffer.push(line); | ||
if (line.type === 'error') { | ||
throw new Error(line.data); | ||
} | ||
if (line.type === "error") { | ||
throw new Error(line.data); | ||
} | ||
const options = { cwd: config.directory }; | ||
const args = ['audit', '--json']; | ||
if (registry) { | ||
const auditRegistrySupported = yarnAuditSupportsRegistry(yarnVersion); | ||
if (auditRegistrySupported) { | ||
args.push('--registry', registry); | ||
} else { | ||
console.warn( | ||
'\x1b[33m%s\x1b[0m', | ||
'Yarn audit does not support the registry flag yet.' | ||
); | ||
} | ||
} | ||
const options = { cwd: config.directory }; | ||
const args = ["audit", "--json"]; | ||
if (registry) { | ||
const auditRegistrySupported = yarnAuditSupportsRegistry(yarnVersion); | ||
if (auditRegistrySupported) { | ||
args.push("--registry", registry); | ||
} else { | ||
console.warn( | ||
yellow, | ||
"Yarn audit does not support the registry flag yet." | ||
); | ||
} | ||
return runProgram(yarnExec, args, options, outListener, errListener).then( | ||
() => { | ||
if (missingLockFile) { | ||
console.warn( | ||
'\x1b[33m%s\x1b[0m', | ||
'No yarn.lock file. This does not affect auditing, but it may be a mistake.' | ||
); | ||
} | ||
} | ||
await runProgram(yarnExec, args, options, outListener, errListener); | ||
if (missingLockFile) { | ||
console.warn( | ||
yellow, | ||
"No yarn.lock file. This does not affect auditing, but it may be a mistake." | ||
); | ||
} | ||
const summary = model.getSummary(a => a.id); | ||
return reporter(summary, config); | ||
} | ||
); | ||
}); | ||
const summary = model.getSummary((a) => a.id); | ||
return reporter(summary, config); | ||
} | ||
module.exports = { audit }; |
{ | ||
"name": "audit-ci", | ||
"version": "2.5.1", | ||
"version": "3.0.0", | ||
"description": "Audits npm and yarn projects in CI environments", | ||
@@ -33,27 +33,28 @@ "license": "Apache-2.0", | ||
"lint": "eslint -c ./.eslintrc.json **/*.js", | ||
"lint:fix": "eslint -c ./.eslintrc.json **/*.js --fix", | ||
"test": "mocha --exit --timeout 40000 --recursive --reporter spec test/*.js" | ||
}, | ||
"engines": { | ||
"node": ">=8" | ||
}, | ||
"dependencies": { | ||
"JSONStream": "^1.3.5", | ||
"cross-spawn": "6.0.5", | ||
"cross-spawn": "^7.0.2", | ||
"event-stream": "4.0.1", | ||
"readline-transform": "0.9.0", | ||
"semver": "^6.0.0", | ||
"yargs": "12.0.5" | ||
"readline-transform": "1.0.0", | ||
"semver": "^7.0.0", | ||
"yargs": "^15.0.0" | ||
}, | ||
"devDependencies": { | ||
"chai": "^4.2.0", | ||
"eslint": "^5.16.0", | ||
"eslint-config-airbnb-base": "13.1.0", | ||
"eslint-config-prettier": "4.1.0", | ||
"eslint-plugin-import": "2.16.0", | ||
"eslint-plugin-prettier": "3.0.1", | ||
"husky": "1.3.1", | ||
"mocha": "^6.1.4", | ||
"prettier": "1.16.4", | ||
"pretty-quick": "1.10.0" | ||
"eslint": "^6.8.0", | ||
"eslint-config-airbnb-base": "^14.1.0", | ||
"eslint-config-prettier": "^6.10.1", | ||
"eslint-plugin-import": "^2.20.2", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"husky": "^4.0.0", | ||
"mocha": "^7.0.0", | ||
"prettier": "^2.0.2", | ||
"pretty-quick": "^2.0.1" | ||
}, | ||
"resolutions": { | ||
"eslint-utils": "^1.4.1" | ||
}, | ||
"husky": { | ||
@@ -60,0 +61,0 @@ "hooks": { |
@@ -5,3 +5,3 @@ [![Build Status](https://travis-ci.com/IBM/audit-ci.svg?branch=master)](https://travis-ci.com/IBM/audit-ci) | ||
# Overview | ||
# audit-ci | ||
@@ -11,4 +11,9 @@ This module is intended to be consumed by your favourite continuous integration tool to | ||
# Set up | ||
## Requirements | ||
- Node >= 8 | ||
- _(Optional)_ Yarn >= 1.12.3 && Yarn < 2 | ||
## Set up | ||
> `npm install --save-dev audit-ci` | ||
@@ -54,3 +59,3 @@ | ||
name: update-npm | ||
command: 'sudo npm install -g npm' | ||
command: "sudo npm install -g npm" | ||
- restore_cache: | ||
@@ -60,3 +65,3 @@ key: dependency-cache-{{ checksum "package.json" }} | ||
name: install-npm | ||
command: 'npm install --no-audit' | ||
command: "npm install --no-audit" | ||
# This should run immediately after installation to reduce | ||
@@ -104,4 +109,2 @@ # the risk of executing a script from a compromised NPM package. | ||
| | --config | Path to JSON config file | | ||
| -r | --report | [_DEPRECATED_] (Use `--report-type full`) Shows the full audit report (default `false`) | | ||
| -s | --summary | [_DEPRECATED_] (Use `--report-type summary`) Shows the summary audit report (default `false`) | | ||
@@ -108,0 +111,0 @@ ### (_Optional_) Config file specification |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
12
188
42161
650
+ Addedansi-regex@5.0.1(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedcliui@6.0.0(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addedemoji-regex@8.0.0(transitive)
+ Addedfind-up@4.1.0(transitive)
+ Addedget-caller-file@2.0.5(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedlocate-path@5.0.0(transitive)
+ Addedp-locate@4.1.0(transitive)
+ Addedpath-exists@4.0.0(transitive)
+ Addedpath-key@3.1.1(transitive)
+ Addedreadline-transform@1.0.0(transitive)
+ Addedrequire-main-filename@2.0.0(transitive)
+ Addedsemver@7.6.3(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedstring-width@4.2.3(transitive)
+ Addedstrip-ansi@6.0.1(transitive)
+ Addedwhich@2.0.2(transitive)
+ Addedwrap-ansi@6.2.0(transitive)
+ Addedyargs@15.4.1(transitive)
+ Addedyargs-parser@18.1.3(transitive)
- Removedansi-regex@2.1.13.0.1(transitive)
- Removedcliui@4.1.0(transitive)
- Removedcode-point-at@1.1.0(transitive)
- Removedcross-spawn@6.0.5(transitive)
- Removedend-of-stream@1.4.4(transitive)
- Removedexeca@1.0.0(transitive)
- Removedfind-up@3.0.0(transitive)
- Removedget-caller-file@1.0.3(transitive)
- Removedget-stream@4.1.0(transitive)
- Removedinvert-kv@2.0.0(transitive)
- Removedis-fullwidth-code-point@1.0.02.0.0(transitive)
- Removedis-stream@1.1.0(transitive)
- Removedlcid@2.0.0(transitive)
- Removedlocate-path@3.0.0(transitive)
- Removedmap-age-cleaner@0.1.3(transitive)
- Removedmem@4.3.0(transitive)
- Removedmimic-fn@2.1.0(transitive)
- Removednice-try@1.0.5(transitive)
- Removednpm-run-path@2.0.2(transitive)
- Removednumber-is-nan@1.0.1(transitive)
- Removedonce@1.4.0(transitive)
- Removedos-locale@3.1.0(transitive)
- Removedp-defer@1.0.0(transitive)
- Removedp-finally@1.0.0(transitive)
- Removedp-is-promise@2.1.0(transitive)
- Removedp-locate@3.0.0(transitive)
- Removedpath-exists@3.0.0(transitive)
- Removedpath-key@2.0.1(transitive)
- Removedpump@3.0.2(transitive)
- Removedreadline-transform@0.9.0(transitive)
- Removedrequire-main-filename@1.0.1(transitive)
- Removedsemver@5.7.26.3.1(transitive)
- Removedshebang-command@1.2.0(transitive)
- Removedshebang-regex@1.0.0(transitive)
- Removedsignal-exit@3.0.7(transitive)
- Removedstring-width@1.0.22.1.1(transitive)
- Removedstrip-ansi@3.0.14.0.0(transitive)
- Removedstrip-eof@1.0.0(transitive)
- Removedwhich@1.3.1(transitive)
- Removedwrap-ansi@2.1.0(transitive)
- Removedwrappy@1.0.2(transitive)
- Removedyargs@12.0.5(transitive)
- Removedyargs-parser@11.1.1(transitive)
Updatedcross-spawn@^7.0.2
Updatedreadline-transform@1.0.0
Updatedsemver@^7.0.0
Updatedyargs@^15.0.0