renovate/lib/workers/repository/init/vulnerability.ts

209 lines
7.3 KiB
TypeScript

import type { PackageRule, RenovateConfig } from '../../../config/types';
import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { CrateDatasource } from '../../../modules/datasource/crate';
import { GoDatasource } from '../../../modules/datasource/go';
import { MavenDatasource } from '../../../modules/datasource/maven';
import { NpmDatasource } from '../../../modules/datasource/npm';
import { NugetDatasource } from '../../../modules/datasource/nuget';
import { PackagistDatasource } from '../../../modules/datasource/packagist';
import { PypiDatasource } from '../../../modules/datasource/pypi';
import { RubygemsDatasource } from '../../../modules/datasource/rubygems';
import { platform } from '../../../modules/platform';
import * as allVersioning from '../../../modules/versioning';
import * as composerVersioning from '../../../modules/versioning/composer';
import * as mavenVersioning from '../../../modules/versioning/maven';
import * as npmVersioning from '../../../modules/versioning/npm';
import * as pep440Versioning from '../../../modules/versioning/pep440';
import * as rubyVersioning from '../../../modules/versioning/ruby';
import * as semverVersioning from '../../../modules/versioning/semver';
import type { SecurityAdvisory } from '../../../types';
import { sanitizeMarkdown } from '../../../util/markdown';
type Datasource = string;
type DependencyName = string;
type FileName = string;
type CombinedAlert = Record<
FileName,
Record<
Datasource,
Record<
DependencyName,
{
advisories: SecurityAdvisory[];
fileType?: string;
firstPatchedVersion?: string;
}
>
>
>;
export function getFixedVersionByDatasource(
fixedVersion: string,
datasource: string,
): string {
if (datasource === MavenDatasource.id || datasource === NugetDatasource.id) {
return `[${fixedVersion},)`;
}
// crates.io, Go, Hex, npm, RubyGems, PyPI
return `>= ${fixedVersion}`;
}
// 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 versionings: Record<string, string> = {
'github-tags': semverVersioning.id,
go: semverVersioning.id,
packagist: composerVersioning.id,
maven: mavenVersioning.id,
npm: npmVersioning.id,
nuget: semverVersioning.id,
pypi: pep440Versioning.id,
rubygems: rubyVersioning.id,
};
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 datasourceMapping: Record<string, string> = {
composer: PackagistDatasource.id,
go: GoDatasource.id,
maven: MavenDatasource.id,
npm: NpmDatasource.id,
nuget: NugetDatasource.id,
pip: PypiDatasource.id,
rubygems: RubygemsDatasource.id,
rust: CrateDatasource.id,
};
const datasource =
datasourceMapping[alert.security_vulnerability.package.ecosystem];
const depName = alert.security_vulnerability.package.name;
const fileName = alert.dependency.manifest_path;
const fileType = fileName.split('/').pop();
const firstPatchedVersion =
alert.security_vulnerability.first_patched_version.identifier;
const advisory = alert.security_advisory;
combinedAlerts[fileName] ||= {};
combinedAlerts[fileName][datasource] ||= {};
combinedAlerts[fileName][datasource][depName] ||= {
advisories: [],
};
const alertDetails = combinedAlerts[fileName][datasource][depName];
alertDetails.advisories.push(advisory);
const version = allVersioning.get(versionings[datasource]);
if (version.isVersion(firstPatchedVersion)) {
if (
!alertDetails.firstPatchedVersion ||
version.isGreaterThan(
firstPatchedVersion,
alertDetails.firstPatchedVersion,
)
) {
alertDetails.firstPatchedVersion = firstPatchedVersion;
}
} else {
logger.debug('Invalid firstPatchedVersion: ' + firstPatchedVersion);
}
alertDetails.fileType = fileType;
} catch (err) {
logger.warn({ err }, 'Error parsing vulnerability alert');
}
}
const alertPackageRules: PackageRule[] = [];
config.remediations = {} as never;
for (const [fileName, files] of Object.entries(combinedAlerts)) {
for (const [datasource, dependencies] of Object.entries(files)) {
for (const [depName, val] of Object.entries(dependencies)) {
let prBodyNotes: string[] = [];
try {
prBodyNotes = ['### GitHub Vulnerability Alerts'].concat(
val.advisories.map((advisory) => {
const identifiers = advisory.identifiers;
const description = advisory.description;
let content = '#### ';
let heading: string;
if (identifiers.some((id) => id.type === 'CVE')) {
heading = identifiers
.filter((id) => id.type === 'CVE')
.map((id) => id.value)
.join(' / ');
} else {
heading = identifiers.map((id) => id.value).join(' / ');
}
if (advisory.references?.length) {
heading = `[${heading}](${advisory.references[0].url})`;
}
content += heading;
content += '\n\n';
content += sanitizeMarkdown(description);
return content;
}),
);
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error generating vulnerability PR notes');
}
// TODO: types (#22198)
const allowedVersions =
datasource === PypiDatasource.id
? `==${val.firstPatchedVersion!}`
: val.firstPatchedVersion;
const matchFileNames =
datasource === GoDatasource.id
? [fileName.replace('go.sum', 'go.mod')]
: [fileName];
let matchRule: PackageRule = {
matchDatasources: [datasource],
matchPackageNames: [depName],
matchFileNames,
};
// Remediate only direct dependencies
matchRule = {
...matchRule,
allowedVersions,
prBodyNotes,
isVulnerabilityAlert: true,
force: {
...config.vulnerabilityAlerts,
},
};
alertPackageRules.push(matchRule);
}
}
}
logger.debug({ alertPackageRules }, 'alert package rules');
config.packageRules = (config.packageRules ?? []).concat(alertPackageRules);
return config;
}