mirror of https://github.com/renovatebot/renovate
209 lines
7.3 KiB
TypeScript
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;
|
|
}
|