retire
Version:
Retire is a tool for detecting use of vulnerable libraries
286 lines (285 loc) • 11.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateRepository = validateRepository;
exports.asbowerrepo = asbowerrepo;
exports.loadrepository = loadrepository;
exports.loadrepositoryFromFile = loadrepositoryFromFile;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const retire = __importStar(require("./retire"));
const URL = __importStar(require("url"));
const proxy_agent_1 = require("proxy-agent");
const z = __importStar(require("zod"));
const types_1 = require("./types");
function validateRepository(repo, replacer) {
const keys = Object.keys(types_1.severityLevels);
const versionValidator = z.string().regex(/^[\d.]+([a-zA-Z\d.-]+)?$/);
const numericString = z.string().regex(/^[\d]+$/);
const vulnValidator = z
.object({
below: versionValidator,
atOrAbove: versionValidator.optional(),
severity: z.enum(keys),
cwe: z.array(z.string().regex(/^CWE-[0-9]+$/)).min(1),
identifiers: z
.object({
CVE: z.array(z.string().regex(/^CVE-[0-9X-]+$/)).optional(),
bug: z
.string()
.regex(/^[a-z0-9-]+$/i)
.optional(),
issue: numericString.optional(),
summary: z.string().min(3).optional(),
githubID: z
.string()
.regex(/^GHSA[A-Z0-9-]+$/i)
.optional(),
osvdb: z.array(numericString).optional(),
gist: z
.string()
.regex(/^[a-z0-9-]+\/[a-f0-9]+$/i)
.optional(),
tenable: numericString.optional(),
blog: z
.string()
.min(10)
.regex(/^[:a-z0-9/-]+$/)
.optional(),
release: z.string().min(5).optional(),
PR: numericString.optional(),
retid: z
.string()
.regex(/^[\d]+$/)
.optional(),
})
.strict()
.superRefine((o, ctx) => {
if (Object.keys(o).filter((k) => k != 'summary').length == 0)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
const ids = Object.values(o)
.map((x) => (Array.isArray(x) ? x : [x]))
.reduce((a, b) => a.concat(b), []).length;
if (ids == 0)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
}),
info: z.array(z.string().regex(/^https?:\/\/.+/)),
})
.strict();
const regexValidator = z.string().superRefine((s, ctx) => {
if (ctx.path[0] == 'dont check')
return;
try {
new RegExp(s);
}
catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid regex: ' + s,
});
}
if (s.includes('[]'))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain []: ' + s });
if (s.includes('{}'))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain {}: ' + s });
[/.*[^\\]\{[^0-9,\\]\}.*/, /[^,0-9\\]\}/, /[^\\]\{[^,0-9]/].forEach((r) => {
if (r.test(s))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'There is something odd with this regex: ' + s });
});
let versionMatcher = '§§version§§';
if (replacer)
versionMatcher = JSON.parse(`"${replacer(versionMatcher)}"`);
const versionIndex = s.indexOf(versionMatcher);
if (versionIndex == -1 || (versionIndex > 0 && s.substring(versionIndex - 1, versionIndex) == '\\')) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must contain (§§version§§): ' + s });
}
else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1].indexOf(versionMatcher) != 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Regex must contain (§§version§§) as first capture group: ' + s.replace(/\(\?:/g, '').replace(/\\\(/g, ''),
});
}
});
const replaceValidator = z
.string()
.regex(/^\/(.*[^\\])\/([^/]+)\/$/, 'RegExp error - should be on format "/search/replacement/"');
const validator = z.record(z
.object({
bowername: z.array(z.string().regex(/^[a-z0-9.-]+$/i)).optional(),
basePurl: z
.string()
.regex(/^pkg:[a-z0-9%.-/]+$/i)
.optional(),
npmname: z
.string()
.regex(/^[a-z0-9.-]+$/i)
.optional(),
vulnerabilities: z.array(vulnValidator),
extractors: z
.object({
func: z.array(z.string().min(5)).optional(),
uri: z.array(regexValidator).optional(),
filename: z.array(regexValidator).optional(),
filecontent: z.array(regexValidator).optional(),
filecontentreplace: z.array(replaceValidator).optional(),
hashes: z.record(z.string().regex(/^[a-f0-9]+$/i), versionValidator).optional(),
ast: z.array(z.string()).optional(),
})
.strict(),
licenses: z
.array(z
.string()
.regex(/(\([A-Za-z\-0-9.]+( OR [A-Za-z\-0-9.]+)+\)|[A-Za-z\-0-9.]+) >=[0-9.]+( <[0-9.]+)?(;>=[0-9.]+( <[0-9.]+)?)*/))
.optional(),
})
.strict());
return validator.safeParse(repo);
}
function formatValidationError(error) {
return JSON.stringify(error.format(), (key, value) => (Array.isArray(value) && value.length === 0 ? undefined : value), 2);
}
async function loadJson(url, options) {
return new Promise((resolve, reject) => {
options.log.info('Downloading ' + url + ' ...');
const reqOptions = { ...URL.parse(url), method: 'GET' };
const proxyUri = options.proxy || process.env.http_proxy;
if (proxyUri) {
reqOptions.agent = new proxy_agent_1.ProxyAgent({
getProxyForUrl: () => proxyUri,
});
}
if (options.insecure) {
reqOptions.rejectUnauthorized = false;
}
if (options.cacertbuf) {
reqOptions.ca = [options.cacertbuf];
}
const req = (url.startsWith('http:') ? http : https).get(reqOptions, (res) => {
if (res.statusCode != 200)
return reject(`Error downloading: ${url}: HTTP ${res.statusCode} ${res.statusMessage}`);
const data = [];
res.on('data', (c) => data.push(c));
res.on('end', () => {
let d = Buffer.concat(data).toString();
d = options.process ? options.process(d) : d;
const json = JSON.parse(d);
const vresult = validateRepository(json, options.process);
if (vresult.success) {
resolve(json);
}
else {
reject(`Invalid repository from ${url}: ${formatValidationError(vresult.error)}`);
}
});
});
req.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
req.end();
});
}
async function loadJsonFromFile(file, options) {
options.log.debug('Reading ' + file + ' ...');
return new Promise((resolve, reject) => {
fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
if (err) {
return reject(err.toString());
}
data = options.process ? options.process(data) : data;
const json = JSON.parse(data);
const vresult = validateRepository(json, options.process);
if (vresult.success) {
resolve(json);
}
else {
reject(`Invalid repository from ${file}: ${formatValidationError(vresult.error)}`);
}
});
});
}
async function loadFromCache(url, cachedir, options) {
const cacheIndex = path.resolve(cachedir, 'index.json');
if (!fs.existsSync(cachedir))
fs.mkdirSync(cachedir);
const cache = fs.existsSync(cacheIndex) ? JSON.parse(fs.readFileSync(cacheIndex, 'utf-8')) : {};
const now = new Date().getTime();
if (cache[url]) {
if (now - cache[url].date < 60 * 60 * 1000) {
options.log.info('Loading from cache: ' + url);
return loadJsonFromFile(path.resolve(cachedir, cache[url].file), options);
}
else {
if (fs.existsSync(path.resolve(cachedir, cache[url].date + '.json'))) {
try {
fs.unlinkSync(path.resolve(cachedir, cache[url].date + '.json'));
}
catch (error) {
if (error != null && typeof error == 'object' && 'code' in error && error.code !== 'ENOENT') {
throw error;
}
else {
console.warn('Could not delete cache. Ignore this error if you are running multiple retire.js in parallel');
}
}
}
}
}
const data = await loadJson(url, options);
cache[url] = { date: now, file: now + '.json' };
fs.writeFileSync(path.resolve(cachedir, cache[url].file), JSON.stringify(data), { encoding: 'utf8' });
fs.writeFileSync(cacheIndex, JSON.stringify(cache), { encoding: 'utf8' });
return data;
}
function asbowerrepo(jsRepo) {
const result = {};
Object.keys(jsRepo).map((k) => {
(jsRepo[k].bowername || [k]).map((b) => {
result[b] = result[b] || { vulnerabilities: [] };
result[b].vulnerabilities = result[b].vulnerabilities.concat(jsRepo[k].vulnerabilities);
});
});
return result;
}
async function loadrepository(repoUrl, options) {
options = { ...options, process: retire.replaceVersion };
if (options.nocache) {
return await loadJson(repoUrl, options);
}
return await loadFromCache(repoUrl, options.cachedir, options);
}
async function loadrepositoryFromFile(filepath, options) {
options = { ...options, process: retire.replaceVersion };
return await loadJsonFromFile(filepath, options);
}