mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 15:06:27 +00:00
265 lines
9.6 KiB
TypeScript
265 lines
9.6 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 { GithubTagsDatasource } from '../../../modules/datasource/github-tags';
|
|
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';
|
|
import { regEx } from '../../../util/regex';
|
|
|
|
type Datasource = string;
|
|
type DependencyName = string;
|
|
type FileName = string;
|
|
type VulnerableRequirements = string;
|
|
|
|
type CombinedAlert = Record<
|
|
FileName,
|
|
Record<
|
|
Datasource,
|
|
Record<
|
|
DependencyName,
|
|
Record<
|
|
VulnerableRequirements,
|
|
{
|
|
advisories: SecurityAdvisory[];
|
|
fileType?: string;
|
|
firstPatchedVersion?: string;
|
|
}
|
|
>
|
|
>
|
|
>
|
|
>;
|
|
|
|
// TODO can return `null` and `undefined` (#7154)
|
|
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) {
|
|
if (
|
|
alert.securityVulnerability?.package?.name === 'yargs-parser' &&
|
|
(alert.vulnerableRequirements === '= 5.0.0-security.0' ||
|
|
alert.vulnerableRequirements === '= 5.0.1')
|
|
) {
|
|
continue;
|
|
}
|
|
try {
|
|
if (alert.dismissReason) {
|
|
continue;
|
|
}
|
|
if (!alert.securityVulnerability.firstPatchedVersion) {
|
|
logger.debug(
|
|
{ alert },
|
|
'Vulnerability alert has no firstPatchedVersion - skipping'
|
|
);
|
|
continue;
|
|
}
|
|
const datasourceMapping: Record<string, string> = {
|
|
ACTIONS: GithubTagsDatasource.id,
|
|
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.securityVulnerability.package.ecosystem];
|
|
const depName = alert.securityVulnerability.package.name;
|
|
const fileName = alert.vulnerableManifestPath;
|
|
const fileType = alert.vulnerableManifestFilename;
|
|
const firstPatchedVersion =
|
|
alert.securityVulnerability.firstPatchedVersion.identifier;
|
|
const advisory = alert.securityAdvisory;
|
|
// TODO #7154
|
|
let vulnerableRequirements = alert.vulnerableRequirements!;
|
|
// istanbul ignore if
|
|
if (!vulnerableRequirements.length) {
|
|
if (datasource === MavenDatasource.id) {
|
|
vulnerableRequirements = `(,${firstPatchedVersion})`;
|
|
} else {
|
|
vulnerableRequirements = `< ${firstPatchedVersion}`;
|
|
}
|
|
}
|
|
if (datasource === PypiDatasource.id) {
|
|
vulnerableRequirements = vulnerableRequirements.replace(
|
|
regEx(/^= /),
|
|
'== '
|
|
);
|
|
}
|
|
if (datasource === GithubTagsDatasource.id) {
|
|
// GitHub Actions uses docker versioning, which doesn't support `= 1.2.3` matching, so we strip the equals
|
|
vulnerableRequirements = vulnerableRequirements.replace(/^=\s*/, '');
|
|
}
|
|
combinedAlerts[fileName] ||= {};
|
|
combinedAlerts[fileName][datasource] ||= {};
|
|
combinedAlerts[fileName][datasource][depName] ||= {};
|
|
combinedAlerts[fileName][datasource][depName][vulnerableRequirements] ||=
|
|
{
|
|
advisories: [],
|
|
};
|
|
const alertDetails =
|
|
combinedAlerts[fileName][datasource][depName][vulnerableRequirements];
|
|
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, currentValues] of Object.entries(dependencies)) {
|
|
for (const [matchCurrentVersion, val] of Object.entries(
|
|
currentValues
|
|
)) {
|
|
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 (#7154)
|
|
const allowedVersions =
|
|
datasource === PypiDatasource.id
|
|
? `==${val.firstPatchedVersion!}`
|
|
: val.firstPatchedVersion;
|
|
const matchFiles =
|
|
datasource === GoDatasource.id
|
|
? [fileName.replace('go.sum', 'go.mod')]
|
|
: [fileName];
|
|
let matchRule: PackageRule = {
|
|
matchDatasources: [datasource],
|
|
matchPackageNames: [depName],
|
|
matchCurrentVersion,
|
|
matchFiles,
|
|
};
|
|
const supportedRemediationFileTypes = ['package-lock.json'];
|
|
if (
|
|
config.transitiveRemediation &&
|
|
supportedRemediationFileTypes.includes(val.fileType!)
|
|
) {
|
|
const remediations = config.remediations as Record<
|
|
string,
|
|
unknown[]
|
|
>;
|
|
remediations[fileName] ??= [];
|
|
const currentVersion = matchCurrentVersion.replace('=', '').trim();
|
|
const newVersion = allowedVersions;
|
|
const remediation = {
|
|
datasource,
|
|
depName,
|
|
currentVersion,
|
|
newVersion,
|
|
prBodyNotes,
|
|
};
|
|
remediations[fileName].push(remediation);
|
|
} else {
|
|
// Remediate only direct dependencies
|
|
matchRule = {
|
|
...matchRule,
|
|
allowedVersions,
|
|
prBodyNotes,
|
|
isVulnerabilityAlert: true,
|
|
force: {
|
|
...config.vulnerabilityAlerts,
|
|
},
|
|
};
|
|
// istanbul ignore if
|
|
if (
|
|
config.transitiveRemediation &&
|
|
matchRule.matchFiles?.[0] === 'package.json'
|
|
) {
|
|
matchRule.force!.rangeStrategy = 'replace';
|
|
}
|
|
}
|
|
alertPackageRules.push(matchRule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
logger.debug({ alertPackageRules }, 'alert package rules');
|
|
config.packageRules = (config.packageRules ?? []).concat(alertPackageRules);
|
|
return config;
|
|
}
|