import is from '@sindresorhus/is'; import type { PackageRule, RenovateConfig } from '../../../config/types.ts'; import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages.ts'; import { logger } from '../../../logger/index.ts'; import { getDefaultVersioning } from '../../../modules/datasource/common.ts'; import { GithubTagsDatasource } from '../../../modules/datasource/github-tags/index.ts'; import { MavenDatasource } from '../../../modules/datasource/maven/index.ts'; import { NugetDatasource } from '../../../modules/datasource/nuget/index.ts'; import type { Ecosystem, SecurityAdvisory, } from '../../../modules/platform/github/schema.ts'; import { platform } from '../../../modules/platform/index.ts'; import * as allVersioning from '../../../modules/versioning/index.ts'; import { coerceArray } from '../../../util/array.ts'; import { sanitizeMarkdown } from '../../../util/markdown.ts'; import { coerceObject } from '../../../util/object.ts'; import { regEx } from '../../../util/regex.ts'; import { titleCase } from '../../../util/string.ts'; import { githubEcosystemToDatasource } from '../../../util/vulnerability/ecosystem.ts'; import { getFixedVersionConstraint, getHighestVulnerabilitySeverity, } from '../../../util/vulnerability/utils.ts'; type DependencyName = string; type CombinedAlert = Partial< Record< Ecosystem, Record< DependencyName, { advisories: SecurityAdvisory[]; firstPatchedVersion?: string; severity?: string; } > > >; export function getFixedVersionByDatasource( fixedVersion: string, datasource: string, ): string { return getFixedVersionConstraint(fixedVersion, datasource); } // TODO can return `null` and `undefined` (#22198) export async function detectVulnerabilityAlerts( input: RenovateConfig, ): Promise { if (!input?.vulnerabilityAlerts) { return input; } if (input.vulnerabilityAlerts.enabled === false) { logger.debug('Vulnerability alerts are disabled'); return input; } const alerts = await platform.getVulnerabilityAlerts?.(); if (!alerts?.length) { logger.debug('No vulnerability alerts found'); if (input.vulnerabilityAlertsOnly) { throw new Error(NO_VULNERABILITY_ALERTS); } return input; } const config = { ...input }; const combinedAlerts: CombinedAlert = {}; for (const alert of alerts) { try { if (alert.dismissed_reason) { continue; } if (!alert.security_vulnerability?.first_patched_version) { logger.debug( { alert }, 'Vulnerability alert has no firstPatchedVersion - skipping', ); continue; } const ecosystem = alert.security_vulnerability.package.ecosystem; const datasources = githubEcosystemToDatasource[ecosystem]; const depName = alert.security_vulnerability.package.name; const firstPatchedVersion = alert.security_vulnerability.first_patched_version.identifier; const advisory = alert.security_advisory; // Keying off of ecosystem instead of datasource because now a single ecosystem (such as maven) could have // multiple datasources. combinedAlerts[ecosystem] ??= {}; combinedAlerts[ecosystem][depName] ??= { advisories: [], }; const alertDetails = combinedAlerts[ecosystem][depName]; alertDetails.advisories.push(advisory); alertDetails.severity = getHighestVulnerabilitySeverity( { vulnerabilitySeverity: alertDetails.severity }, { vulnerabilitySeverity: alert.security_vulnerability.severity }, ); const versioningApi = allVersioning.get( getDefaultVersioning(datasources[0]), ); if (versioningApi.isVersion(firstPatchedVersion)) { if ( !alertDetails.firstPatchedVersion || versioningApi.isGreaterThan( firstPatchedVersion, alertDetails.firstPatchedVersion, ) ) { alertDetails.firstPatchedVersion = firstPatchedVersion; } } else { logger.debug(`Invalid firstPatchedVersion: ${firstPatchedVersion}`); } } catch (err) { logger.warn({ err }, 'Error parsing vulnerability alert'); } } const alertPackageRules: PackageRule[] = []; config.remediations = {}; for (const [ecosystem, dependencies] of Object.entries(combinedAlerts)) { const matchDatasources = githubEcosystemToDatasource[ecosystem as Ecosystem]; const primaryDatasource = matchDatasources[0]; for (const [depName, val] of Object.entries(dependencies)) { if (!val.firstPatchedVersion) { continue; } let prBodyNotes: string[] = []; try { prBodyNotes = val.advisories.flatMap((advisory) => generatePrBodyNotes(advisory), ); } catch (err) /* v8 ignore next -- defensive: note generation from already-parsed advisories does not throw in specs */ { logger.warn({ err }, 'Error generating vulnerability PR notes'); } let matchRule: PackageRule = { matchDatasources, matchPackageNames: [depName], }; let matchCurrentVersion = `< ${val.firstPatchedVersion}`; if ( primaryDatasource === MavenDatasource.id || primaryDatasource === NugetDatasource.id ) { matchCurrentVersion = `(,${val.firstPatchedVersion})`; } else if (primaryDatasource === GithubTagsDatasource.id) { matchCurrentVersion = `!/^${RegExp.escape(val.firstPatchedVersion)}$/`; } matchRule = { ...matchRule, matchCurrentVersion, vulnerabilityFixVersion: val.firstPatchedVersion, vulnerabilitySeverity: val.severity, prBodyNotes, isVulnerabilityAlert: true, force: { ...config.vulnerabilityAlerts, }, }; alertPackageRules.push(matchRule); } } logger.debug({ alertPackageRules }, 'alert package rules'); config.packageRules = coerceArray(config.packageRules).concat( alertPackageRules, ); return config; } function generatePrBodyNotes(advisory: SecurityAdvisory): string[] { const aliases = advisory.identifiers .map((id) => id.value) .sort() .map((id) => { if (id.startsWith('CVE-')) { return `[${id}](https://nvd.nist.gov/vuln/detail/${id})`; } if (id.startsWith('GHSA-')) { return `[${id}](https://github.com/advisories/${id})`; } return id; }); let content = '\n\n---\n\n### '; content += `${advisory.summary}\n`; content += `${aliases.join(' / ')}\n`; content += `\n
\nMore information\n`; const details = advisory.description.replace(regEx(/^#{1,4} /gm), '##### '); content += `#### Details\n${details}\n`; content += '#### Severity\n'; const { cvss_v4, cvss_v3 } = coerceObject(advisory.cvss_severities); const cvss = cvss_v4?.vector_string ? cvss_v4 : cvss_v3; if (is.number(cvss?.score) && cvss?.vector_string) { content += `- CVSS Score: ${cvss.score.toFixed(1)} / 10 (${titleCase(advisory.severity)})\n`; content += `- Vector String: \`${cvss.vector_string}\`\n`; } else { content += `${titleCase(advisory.severity)}\n`; } content += `\n#### References\n${ advisory.references ?.map((ref) => `- [${ref.url}](${ref.url})`) .join('\n') ?? 'No references.' }`; content += `\n\nThis data is provided by the [GitHub Advisory Database](https://github.com/advisories/${advisory.ghsa_id}) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md)).\n`; content += `
`; return [sanitizeMarkdown(content)]; }