UNPKG

is-my-node-vulnerable

Version:

package that checks if your Node.js installation is vulnerable to known security vulnerabilities

214 lines (183 loc) 6.18 kB
const { danger, allGood, bold, vulnerableWarning, separator } = require('./ascii') const { request } = require('https') const fs = require('fs') const path = require('path') const satisfies = require('semver/functions/satisfies') const STORE = { security: { url: 'https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/core/index.json', jsonFile: path.join(__dirname, 'security.json'), etagFile: path.join(__dirname, 'security.etag'), etagValue: '' }, schedule: { url: 'https://raw.githubusercontent.com/nodejs/Release/main/schedule.json', jsonFile: path.join(__dirname, 'schedule.json'), etagFile: path.join(__dirname, 'schedule.etag'), etagValue: '' } } async function readLocal (file) { return require(file) } function debug (msg) { if (process.env.DEBUG) { console.debug(msg) } } function loadETag () { for (const [key, obj] of Object.entries(STORE)) { if (fs.existsSync(obj.etagFile)) { debug(`Loading local ETag for '${key}'`) obj.etagValue = fs.readFileSync(obj.etagFile).toString() } } } async function fetchJson (obj) { await new Promise((resolve) => { request(obj.url, (res) => { if (res.statusCode !== 200) { console.error(`Request to Github returned http status ${res.statusCode}. Aborting...`) process.nextTick(() => { process.exit(1) }) } const fileStream = fs.createWriteStream(obj.jsonFile) res.pipe(fileStream) fileStream.on('finish', () => { fileStream.close() resolve() }) fileStream.on('error', (err) => { console.error(`Error ${err.message} while writing to '${obj.jsonFile}'. Aborting...`) process.nextTick(() => { process.exit(1) }) }) }).on('error', (err) => { console.error(`Request to Github returned error ${err.message}. Aborting...`) process.nextTick(() => { process.exit(1) }) }).end() }) return readLocal(obj.jsonFile) } async function getJson (obj) { return new Promise((resolve) => { request(obj.url, { method: 'HEAD' }, (res) => { if (res.statusCode !== 200) { console.error(`Request to Github returned http status ${res.statusCode}. Aborting...`) process.nextTick(() => { process.exit(1) }) } res.on('data', () => {}) const { etag } = res.headers if (!obj.etagValue || obj.eTagValue !== etag || !fs.existsSync(obj.jsonFile)) { obj.etagValue = etag fs.writeFileSync(obj.etagFile, etag) debug('Creating local core.json') resolve(fetchJson(obj)) } else { debug(`No updates from upstream. Getting a cached version: ${obj.jsonFile}`) resolve(readLocal(obj.jsonFile)) } }).on('error', (err) => { console.error(`Request to Github returned error ${err.message}. Aborting...`) process.nextTick(() => { process.exit(1) }) }).end() }) } const checkPlatform = platform => { const availablePlatforms = ['aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32', 'android'] if (platform && !availablePlatforms.includes(platform)) { throw new Error(`platform ${platform} is not valid. Please use ${availablePlatforms.join(',')}.`) } } const isSystemAffected = (platform, affectedEnvironments) => { // No platform specified (legacy mode) if (!platform || !Array.isArray(affectedEnvironments)) { return true } // If the environment is matching or all the environments are affected if (affectedEnvironments.includes(platform) || affectedEnvironments.includes('all')) { return true } // Default to false return false } function getVulnerabilityList (currentVersion, data, platform) { const list = [] for (const key in data) { const vuln = data[key] if ( ( satisfies(currentVersion, vuln.vulnerable) && !satisfies(currentVersion, vuln.patched) ) && isSystemAffected(platform, vuln.affectedEnvironments) ) { const severity = vuln.severity === 'unknown' ? '' : `(${vuln.severity})` list.push(`${bold(vuln.cve)}${severity}: ${vuln.overview}\n${bold('Patched versions')}: ${vuln.patched}`) } } return list } async function cli (currentVersion, platform) { checkPlatform(platform) const isEOL = await isNodeEOL(currentVersion) if (isEOL) { console.error(danger) console.error(`${currentVersion} is end-of-life. There are high chances of being vulnerable. Please upgrade it.`) process.exit(1) } const securityJson = await getJson(STORE.security) const list = getVulnerabilityList(currentVersion, securityJson, platform) if (list.length) { console.error(danger) console.error(vulnerableWarning + '\n') console.error(`${list.join(`\n${separator}\n\n`)}\n${separator}`) process.exit(1) } else { console.info(allGood) } } async function getVersionInfo (version) { const scheduleJson = await getJson(STORE.schedule) if (scheduleJson[version.toLowerCase()]) { return scheduleJson[version.toLowerCase()] } for (const [key, value] of Object.entries(scheduleJson)) { if (satisfies(version, key)) { return value } } return null } /** * @param {string} version * @returns {Promise<boolean>} true if the version is end-of-life */ async function isNodeEOL (version) { const myVersionInfo = await getVersionInfo(version) if (!myVersionInfo) { // i.e. isNodeEOL('abcd') or isNodeEOL('lts') or isNodeEOL('99') throw Error(`Could not fetch version information for ${version}`) } else if (!myVersionInfo.end) { // We got a record, but.. // v0.12.18 etc does not have an EOL date, which probably means too old. return true } const now = new Date() const end = new Date(myVersionInfo.end) return now > end } async function isNodeVulnerable (version, platform) { checkPlatform(platform) const isEOL = await isNodeEOL(version) if (isEOL) { return true } const coreIndex = await getJson(STORE.security) const list = getVulnerabilityList(version, coreIndex, platform) return list.length > 0 } if (process.argv[2] !== '-r') { loadETag() } module.exports = { isNodeVulnerable, cli }