mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
c32e2391a0
Refs #45216 The object counterpart to `prefer-coerce-array`: `x ?? {}` becomes `coerceObject(x)`. The rule flags a `??` whose default is a bare `{}` literal, so a populated default or an asserted `({} as Foo)` still passes; a `x ?? y ?? {}` chain maps onto `coerceObject(x, y)`. Every hit across `lib/` converted cleanly, so no disable comments were needed.
220 lines
7.5 KiB
TypeScript
220 lines
7.5 KiB
TypeScript
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<RenovateConfig> {
|
|
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<details>\n<summary>More information</summary>\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 += `</details>`;
|
|
|
|
return [sanitizeMarkdown(content)];
|
|
}
|