Comparing version
@@ -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 @@ [](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
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
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
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
12
9.09%188
1.62%1
-50%42161
-2.42%650
-1.81%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated