Skip to content

Commit

Permalink
feat: per-file vulnerability alerts (#8770)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Feb 20, 2021
1 parent e68e3c6 commit 2c9a172
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Array [
"matchDatasources": Array [
"npm",
],
"matchFiles": Array [
"backend/package-lock.json",
"backend/package.json",
],
"matchPackageNames": Array [
"electron",
],
Expand All @@ -44,6 +48,9 @@ Electron version 1.7 up to 1.7.12; 1.8 up to 1.8.3 and 2.0.0 up to 2.0.0-beta.3
"matchDatasources": Array [
"pypi",
],
"matchFiles": Array [
"requirements.txt",
],
"matchPackageNames": Array [
"ansible",
],
Expand Down Expand Up @@ -85,6 +92,9 @@ Ansible before versions 2.1.4, 2.2.1 is vulnerable to an improper input validati
"matchDatasources": Array [
"maven",
],
"matchFiles": Array [
"pom.xml",
],
"matchPackageNames": Array [
"com.fasterxml.jackson.core:jackson-databind",
],
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/repository/init/vulnerability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('workers/repository/init/vulnerability', () => {
{
dismissReason: null,
vulnerableManifestFilename: 'package-lock.json',
vulnerableManifestPath: 'package-lock.json',
vulnerableManifestPath: 'backend/package-lock.json',
vulnerableRequirements: '= 1.8.2',
securityAdvisory: {
description:
Expand Down
185 changes: 102 additions & 83 deletions lib/workers/repository/init/vulnerability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RenovateConfig } from '../../../config';
import { PackageRule, RenovateConfig } from '../../../config';
import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
import * as datasourceMaven from '../../../datasource/maven';
import * as datasourceNpm from '../../../datasource/npm';
Expand All @@ -9,23 +9,30 @@ import { logger } from '../../../logger';
import { platform } from '../../../platform';
import { SecurityAdvisory } from '../../../types';
import { sanitizeMarkdown } from '../../../util/markdown';
import { regEx } from '../../../util/regex';
import * as allVersioning from '../../../versioning';
import * as mavenVersioning from '../../../versioning/maven';
import * as npmVersioning from '../../../versioning/npm';
import * as pep440Versioning from '../../../versioning/pep440';
import * as rubyVersioning from '../../../versioning/ruby';
import * as semverVersioning from '../../../versioning/semver';

type Datasource = string;
type DependencyName = string;
type FileName = string;

type CombinedAlert = Record<
string,
Datasource,
Record<
string,
{
advisories: SecurityAdvisory[];
fileNames: string[];
firstPatchedVersion?: string;
vulnerableRequirements?: string;
}
DependencyName,
Record<
FileName,
{
advisories: SecurityAdvisory[];
firstPatchedVersion?: string;
vulnerableRequirements?: string;
}
>
>
>;

Expand Down Expand Up @@ -75,18 +82,16 @@ export async function detectVulnerabilityAlerts(
}
const depName = alert.securityVulnerability.package.name;
if (!combinedAlerts[datasource][depName]) {
combinedAlerts[datasource][depName] = {
combinedAlerts[datasource][depName] = {};
}
const fileName = alert.vulnerableManifestPath;
if (!combinedAlerts[datasource][depName][fileName]) {
combinedAlerts[datasource][depName][fileName] = {
advisories: [],
fileNames: [],
};
}
combinedAlerts[datasource][depName].advisories.push(
alert.securityAdvisory
);
const fileName = alert.vulnerableManifestFilename;
if (!combinedAlerts[datasource][depName].fileNames.includes(fileName)) {
combinedAlerts[datasource][depName].fileNames.push(fileName);
}
const alertDetails = combinedAlerts[datasource][depName][fileName];
alertDetails.advisories.push(alert.securityAdvisory);
const firstPatchedVersion =
alert.securityVulnerability.firstPatchedVersion.identifier;
const versionings: Record<string, string> = {
Expand All @@ -98,25 +103,19 @@ export async function detectVulnerabilityAlerts(
};
const version = allVersioning.get(versionings[datasource]);
if (version.isVersion(firstPatchedVersion)) {
if (combinedAlerts[datasource][depName].firstPatchedVersion) {
if (alertDetails.firstPatchedVersion) {
if (
version.isGreaterThan(
firstPatchedVersion,
combinedAlerts[datasource][depName].firstPatchedVersion
alertDetails.firstPatchedVersion
)
) {
combinedAlerts[datasource][
depName
].firstPatchedVersion = firstPatchedVersion;
combinedAlerts[datasource][depName].vulnerableRequirements =
alert.vulnerableRequirements;
alertDetails.firstPatchedVersion = firstPatchedVersion;
alertDetails.vulnerableRequirements = alert.vulnerableRequirements;
}
} else {
combinedAlerts[datasource][
depName
].firstPatchedVersion = firstPatchedVersion;
combinedAlerts[datasource][depName].vulnerableRequirements =
alert.vulnerableRequirements;
alertDetails.firstPatchedVersion = firstPatchedVersion;
alertDetails.vulnerableRequirements = alert.vulnerableRequirements;
}
} else {
logger.debug('Invalid firstPatchedVersion: ' + firstPatchedVersion);
Expand All @@ -127,61 +126,81 @@ export async function detectVulnerabilityAlerts(
}
const alertPackageRules = [];
for (const [datasource, dependencies] of Object.entries(combinedAlerts)) {
for (const [depName, val] of Object.entries(dependencies)) {
let prBodyNotes: string[] = [];
try {
prBodyNotes = ['### GitHub Vulnerability Alerts'].concat(
val.advisories.map((advisory) => {
let content = '#### ';
let heading: string;
if (advisory.identifiers.some((id) => id.type === 'CVE')) {
heading = advisory.identifiers
.filter((id) => id.type === 'CVE')
.map((id) => id.value)
.join(' / ');
} else {
heading = advisory.identifiers.map((id) => id.value).join(' / ');
}
if (advisory.references.length) {
heading = `[${heading}](${advisory.references[0].url})`;
}
content += heading;
content += '\n\n';
// eslint-disable-next-line no-loop-func
content += sanitizeMarkdown(advisory.description);
return content;
})
);
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error generating vulnerability PR notes');
}
let matchCurrentVersion = val.vulnerableRequirements;
// istanbul ignore if
if (!matchCurrentVersion) {
if (datasource === datasourceMaven.id) {
matchCurrentVersion = `(,${val.firstPatchedVersion})`;
} else {
matchCurrentVersion = `< ${val.firstPatchedVersion}`;
for (const [depName, files] of Object.entries(dependencies)) {
for (const [fileName, val] of Object.entries(files)) {
let prBodyNotes: string[] = [];
try {
prBodyNotes = ['### GitHub Vulnerability Alerts'].concat(
val.advisories.map((advisory) => {
let content = '#### ';
let heading: string;
if (advisory.identifiers.some((id) => id.type === 'CVE')) {
heading = advisory.identifiers
.filter((id) => id.type === 'CVE')
.map((id) => id.value)
.join(' / ');
} else {
heading = advisory.identifiers
.map((id) => id.value)
.join(' / ');
}
if (advisory.references.length) {
heading = `[${heading}](${advisory.references[0].url})`;
}
content += heading;
content += '\n\n';
// eslint-disable-next-line no-loop-func
content += sanitizeMarkdown(advisory.description);
return content;
})
);
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error generating vulnerability PR notes');
}
let matchCurrentVersion = val.vulnerableRequirements;
// istanbul ignore if
if (!matchCurrentVersion) {
if (datasource === datasourceMaven.id) {
matchCurrentVersion = `(,${val.firstPatchedVersion})`;
} else {
matchCurrentVersion = `< ${val.firstPatchedVersion}`;
}
}
const allowedVersions =
datasource === datasourcePypi.id
? `==${val.firstPatchedVersion}`
: val.firstPatchedVersion;
const matchRule: PackageRule = {
matchDatasources: [datasource],
matchPackageNames: [depName],
matchCurrentVersion,
allowedVersions,
prBodyNotes,
force: {
...config.vulnerabilityAlerts,
vulnerabilityAlert: true,
branchTopic: `${datasource}-${depName}-vulnerability`,
prCreation: 'immediate',
},
};
matchRule.matchFiles = [fileName];
// The following list based off https://docs.github.com/en/github/visualizing-repository-data-with-graphs/about-the-dependency-graph#supported-package-ecosystems
const lockToPackageFile = {
'package-lock.json': 'package.json',
'composer.lock': 'composer.json',
'pipfile.lock': 'Pipfile',
'Gemfile.lock': 'Gemfile',
'yarn.lock': 'package.json',
};
for (const [lock, packageFile] of Object.entries(lockToPackageFile)) {
if (fileName.endsWith(lock)) {
matchRule.matchFiles.push(
fileName.replace(regEx(`${lock}$`), packageFile)
);
}
}
alertPackageRules.push(matchRule);
}
const allowedVersions =
datasource === datasourcePypi.id
? `==${val.firstPatchedVersion}`
: val.firstPatchedVersion;
const matchRule = {
matchDatasources: [datasource],
matchPackageNames: [depName],
matchCurrentVersion,
allowedVersions,
prBodyNotes,
force: {
...config.vulnerabilityAlerts,
vulnerabilityAlert: true,
branchTopic: `${datasource}-${depName}-vulnerability`,
prCreation: 'immediate',
},
};
alertPackageRules.push(matchRule);
}
}
logger.debug({ alertPackageRules }, 'alert package rules');
Expand Down

0 comments on commit 2c9a172

Please sign in to comment.