UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

378 lines • 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Vulnerabilities = void 0; const tslib_1 = require("tslib"); const osv_offline_1 = require("@renovatebot/osv-offline"); const is_1 = tslib_1.__importDefault(require("@sindresorhus/is")); const vuln_vects_1 = require("vuln-vects"); const config_1 = require("../../../config"); const logger_1 = require("../../../logger"); const common_1 = require("../../../modules/datasource/common"); const versioning_1 = require("../../../modules/versioning"); const markdown_1 = require("../../../util/markdown"); const p = tslib_1.__importStar(require("../../../util/promises")); const regex_1 = require("../../../util/regex"); const string_1 = require("../../../util/string"); class Vulnerabilities { osvOffline; static datasourceEcosystemMap = { crate: 'crates.io', go: 'Go', hackage: 'Hackage', hex: 'Hex', maven: 'Maven', npm: 'npm', nuget: 'NuGet', packagist: 'Packagist', pypi: 'PyPI', rubygems: 'RubyGems', }; constructor() { // private constructor } async initialize() { this.osvOffline = await osv_offline_1.OsvOffline.create(); } static async create() { const instance = new Vulnerabilities(); await instance.initialize(); return instance; } async appendVulnerabilityPackageRules(config, packageFiles) { const dependencyVulnerabilities = await this.fetchDependencyVulnerabilities(config, packageFiles); config.packageRules ??= []; for (const { vulnerabilities, versioningApi, } of dependencyVulnerabilities) { const groupPackageRules = []; for (const vulnerability of vulnerabilities) { const rule = this.vulnerabilityToPackageRules(vulnerability); if (is_1.default.nullOrUndefined(rule)) { continue; } groupPackageRules.push(rule); } this.sortByFixedVersion(groupPackageRules, versioningApi); config.packageRules.push(...groupPackageRules); } } async fetchVulnerabilities(config, packageFiles) { const groups = await this.fetchDependencyVulnerabilities(config, packageFiles); return groups.flatMap((group) => group.vulnerabilities); } async fetchDependencyVulnerabilities(config, packageFiles) { const managers = Object.keys(packageFiles); const allManagerJobs = managers.map((manager) => this.fetchManagerVulnerabilities(config, packageFiles, manager)); return (await Promise.all(allManagerJobs)).flat(); } async fetchManagerVulnerabilities(config, packageFiles, manager) { const managerConfig = (0, config_1.getManagerConfig)(config, manager); const queue = packageFiles[manager].map((pFile) => () => this.fetchManagerPackageFileVulnerabilities(managerConfig, pFile)); logger_1.logger.trace({ manager, queueLength: queue.length }, 'fetchManagerVulnerabilities starting'); const result = (await p.all(queue)).flat(); logger_1.logger.trace({ manager }, 'fetchManagerVulnerabilities finished'); return result; } async fetchManagerPackageFileVulnerabilities(managerConfig, pFile) { const { packageFile } = pFile; const packageFileConfig = (0, config_1.mergeChildConfig)(managerConfig, pFile); const { manager } = packageFileConfig; const queue = pFile.deps.map((dep) => () => this.fetchDependencyVulnerability(packageFileConfig, dep)); logger_1.logger.trace({ manager, packageFile, queueLength: queue.length }, 'fetchManagerPackageFileVulnerabilities starting with concurrency'); const result = await p.all(queue); logger_1.logger.trace({ packageFile }, 'fetchManagerPackageFileVulnerabilities finished'); return result.filter(is_1.default.truthy); } async fetchDependencyVulnerability(packageFileConfig, dep) { const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource]; if (!ecosystem) { logger_1.logger.trace(`Cannot map datasource ${dep.datasource} to OSV ecosystem`); return null; } let packageName = dep.packageName ?? dep.depName; if (ecosystem === 'PyPI') { // https://peps.python.org/pep-0503/#normalized-names packageName = packageName.toLowerCase().replace((0, regex_1.regEx)(/[_.-]+/g), '-'); } try { const osvVulnerabilities = await this.osvOffline?.getVulnerabilities(ecosystem, packageName); if (is_1.default.nullOrUndefined(osvVulnerabilities) || is_1.default.emptyArray(osvVulnerabilities)) { logger_1.logger.trace(`No vulnerabilities found in OSV database for ${packageName}`); return null; } const depVersion = dep.lockedVersion ?? dep.currentVersion ?? dep.currentValue; const versioning = dep.versioning ?? (0, common_1.getDefaultVersioning)(dep.datasource); const versioningApi = (0, versioning_1.get)(versioning); if (!versioningApi.isVersion(depVersion)) { logger_1.logger.debug(`Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`); return null; } const vulnerabilities = []; for (const osvVulnerability of osvVulnerabilities) { if (osvVulnerability.withdrawn) { logger_1.logger.trace(`Skipping withdrawn vulnerability ${osvVulnerability.id}`); continue; } for (const affected of osvVulnerability.affected ?? []) { const isVulnerable = this.isPackageVulnerable(ecosystem, packageName, depVersion, affected, versioningApi); if (!isVulnerable) { continue; } logger_1.logger.debug(`Vulnerability ${osvVulnerability.id} affects ${packageName} ${depVersion}`); const fixedVersion = this.getFixedVersion(ecosystem, depVersion, affected, versioningApi); vulnerabilities.push({ packageName, vulnerability: osvVulnerability, affected, depVersion, fixedVersion, datasource: dep.datasource, packageFileConfig, }); } } return { vulnerabilities, versioningApi }; } catch (err) { logger_1.logger.warn({ err, packageName }, 'Error fetching vulnerability information for package'); return null; } } sortByFixedVersion(packageRules, versioningApi) { const versionsCleaned = {}; for (const rule of packageRules) { const version = rule.allowedVersions; versionsCleaned[version] = version.replace((0, regex_1.regEx)(/[(),=> ]+/g), ''); } packageRules.sort((a, b) => versioningApi.sortVersions(versionsCleaned[a.allowedVersions], versionsCleaned[b.allowedVersions])); } // https://ossf.github.io/osv-schema/#affectedrangesevents-fields sortEvents(events, versioningApi) { const sortedCopy = []; let zeroEvent = null; for (const event of events) { if (event.introduced === '0') { zeroEvent = event; } else if (versioningApi.isVersion(Object.values(event)[0])) { sortedCopy.push(event); } else { logger_1.logger.debug({ event }, 'Skipping OSV event with invalid version'); } } sortedCopy.sort((a, b) => // no pre-processing, as there are only very few values to sort versioningApi.sortVersions(Object.values(a)[0], Object.values(b)[0])); if (zeroEvent) { sortedCopy.unshift(zeroEvent); } return sortedCopy; } isPackageAffected(ecosystem, packageName, affected) { return (affected.package?.name === packageName && affected.package?.ecosystem === ecosystem); } includedInVersions(depVersion, affected) { return !!affected.versions?.includes(depVersion); } includedInRanges(depVersion, affected, versioningApi) { for (const range of affected.ranges ?? []) { if (range.type === 'GIT') { continue; } let vulnerable = false; for (const event of this.sortEvents(range.events, versioningApi)) { if (is_1.default.nonEmptyString(event.introduced) && (event.introduced === '0' || this.isVersionGtOrEq(depVersion, event.introduced, versioningApi))) { vulnerable = true; } else if (is_1.default.nonEmptyString(event.fixed) && this.isVersionGtOrEq(depVersion, event.fixed, versioningApi)) { vulnerable = false; } else if (is_1.default.nonEmptyString(event.last_affected) && this.isVersionGt(depVersion, event.last_affected, versioningApi)) { vulnerable = false; } } if (vulnerable) { return true; } } return false; } // https://ossf.github.io/osv-schema/#evaluation isPackageVulnerable(ecosystem, packageName, depVersion, affected, versioningApi) { return (this.isPackageAffected(ecosystem, packageName, affected) && (this.includedInVersions(depVersion, affected) || this.includedInRanges(depVersion, affected, versioningApi))); } getFixedVersion(ecosystem, depVersion, affected, versioningApi) { const fixedVersions = []; const lastAffectedVersions = []; for (const range of affected.ranges ?? []) { if (range.type === 'GIT') { continue; } for (const event of range.events) { if (is_1.default.nonEmptyString(event.fixed) && versioningApi.isVersion(event.fixed)) { fixedVersions.push(event.fixed); } else if (is_1.default.nonEmptyString(event.last_affected) && versioningApi.isVersion(event.last_affected)) { lastAffectedVersions.push(event.last_affected); } } } fixedVersions.sort((a, b) => versioningApi.sortVersions(a, b)); const fixedVersion = fixedVersions.find((version) => this.isVersionGt(version, depVersion, versioningApi)); if (fixedVersion) { return this.getFixedVersionByEcosystem(fixedVersion, ecosystem); } lastAffectedVersions.sort((a, b) => versioningApi.sortVersions(a, b)); const lastAffected = lastAffectedVersions.find((version) => this.isVersionGtOrEq(version, depVersion, versioningApi)); if (lastAffected) { return this.getLastAffectedByEcosystem(lastAffected, ecosystem); } return null; } getFixedVersionByEcosystem(fixedVersion, ecosystem) { if (ecosystem === 'Maven' || ecosystem === 'NuGet') { return `[${fixedVersion},)`; } // crates.io, Go, Hex, npm, RubyGems, PyPI return `>= ${fixedVersion}`; } getLastAffectedByEcosystem(lastAffected, ecosystem) { if (ecosystem === 'Maven') { return `(${lastAffected},)`; } // crates.io, Go, Hex, npm, RubyGems, PyPI return `> ${lastAffected}`; } isVersionGt(version, other, versioningApi) { return (versioningApi.isVersion(version) && versioningApi.isVersion(other) && versioningApi.isGreaterThan(version, other)); } isVersionGtOrEq(version, other, versioningApi) { return (versioningApi.isVersion(version) && versioningApi.isVersion(other) && (versioningApi.equals(version, other) || versioningApi.isGreaterThan(version, other))); } vulnerabilityToPackageRules(vul) { const { vulnerability, affected, packageName, depVersion, fixedVersion, datasource, packageFileConfig, } = vul; if (is_1.default.nullOrUndefined(fixedVersion)) { logger_1.logger.debug(`No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`); return null; } logger_1.logger.debug(`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`); const severityDetails = this.extractSeverityDetails(vulnerability, affected); return { matchDatasources: [datasource], matchPackageNames: [packageName], matchCurrentVersion: depVersion, allowedVersions: fixedVersion, isVulnerabilityAlert: true, vulnerabilitySeverity: severityDetails.severityLevel, prBodyNotes: this.generatePrBodyNotes(vulnerability, affected), force: { ...packageFileConfig.vulnerabilityAlerts, }, }; } evaluateCvssVector(vector) { try { const parsedCvss = (0, vuln_vects_1.parseCvssVector)(vector); const severityLevel = parsedCvss.cvss3OverallSeverityText; return [parsedCvss.baseScore.toFixed(1), severityLevel]; } catch { logger_1.logger.debug(`Error processing CVSS vector ${vector}`); } return ['', '']; } generatePrBodyNotes(vulnerability, affected) { let aliases = [vulnerability.id].concat(vulnerability.aliases ?? []).sort(); aliases = aliases.map((id) => { if (id.startsWith('CVE-')) { return `[${id}](https://nvd.nist.gov/vuln/detail/${id})`; } else if (id.startsWith('GHSA-')) { return `[${id}](https://github.com/advisories/${id})`; } else if (id.startsWith('GO-')) { return `[${id}](https://pkg.go.dev/vuln/${id})`; } else if (id.startsWith('RUSTSEC-')) { return `[${id}](https://rustsec.org/advisories/${id}.html)`; } return id; }); let content = '\n\n---\n\n### '; content += vulnerability.summary ? `${vulnerability.summary}\n` : ''; content += `${aliases.join(' / ')}\n`; content += `\n<details>\n<summary>More information</summary>\n`; const details = vulnerability.details?.replace((0, regex_1.regEx)(/^#{1,4} /gm), '##### '); content += `#### Details\n${details ?? 'No details.'}\n`; content += '#### Severity\n'; const severityDetails = this.extractSeverityDetails(vulnerability, affected); if (severityDetails.cvssVector) { content += `- CVSS Score: ${severityDetails.score}\n`; content += `- Vector String: \`${severityDetails.cvssVector}\`\n`; } else { content += `${(0, string_1.titleCase)(severityDetails.severityLevel)}\n`; } content += `\n#### References\n${vulnerability.references ?.map((ref) => { return `- [${ref.url}](${ref.url})`; }) .join('\n') ?? 'No references.'}`; let attribution = ''; if (vulnerability.id.startsWith('GHSA-')) { attribution = ` and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md))`; } else if (vulnerability.id.startsWith('GO-')) { attribution = ` and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license))`; } else if (vulnerability.id.startsWith('PYSEC-')) { attribution = ` and the [PyPI Advisory Database](https://github.com/pypa/advisory-database) ([CC-BY 4.0](https://github.com/pypa/advisory-database/blob/main/LICENSE))`; } else if (vulnerability.id.startsWith('RUSTSEC-')) { attribution = ` and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt))`; } content += `\n\nThis data is provided by [OSV](https://osv.dev/vulnerability/${vulnerability.id})${attribution}.\n`; content += `</details>`; return [(0, markdown_1.sanitizeMarkdown)(content)]; } extractSeverityDetails(vulnerability, affected) { let severityLevel = 'UNKNOWN'; let score = 'Unknown'; const cvssVector = vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ?? vulnerability.severity?.[0]?.score ?? affected.database_specific?.cvss; // RUSTSEC if (cvssVector) { const [baseScore, severity] = this.evaluateCvssVector(cvssVector); severityLevel = severity ? severity.toUpperCase() : 'UNKNOWN'; score = baseScore ? `${baseScore} / 10 (${(0, string_1.titleCase)(severityLevel)})` : 'Unknown'; } else if (vulnerability.id.startsWith('GHSA-') && vulnerability.database_specific?.severity) { const severity = vulnerability.database_specific.severity; severityLevel = severity.toUpperCase(); } return { cvssVector, score, severityLevel, }; } } exports.Vulnerabilities = Vulnerabilities; //# sourceMappingURL=vulnerabilities.js.map