mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +00:00
feat(vulnerabilities): add option to add summary to dashboard (#21766)
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
parent
cd5abc87b8
commit
3fccfbe927
5 changed files with 313 additions and 0 deletions
|
@ -703,6 +703,19 @@ You can configure this to `true` if you prefer Renovate to close an existing Dep
|
||||||
The labels only get updated when the Dependency Dashboard issue updates its content and/or title.
|
The labels only get updated when the Dependency Dashboard issue updates its content and/or title.
|
||||||
It is pointless to edit the labels, as Renovate bot restores the labels on each run.
|
It is pointless to edit the labels, as Renovate bot restores the labels on each run.
|
||||||
|
|
||||||
|
## dependencyDashboardOSVVulnerabilitySummary
|
||||||
|
|
||||||
|
Use this option to control if the Dependency Dashboard lists the OSV-sourced CVEs for your repository.
|
||||||
|
You can choose from:
|
||||||
|
|
||||||
|
- `none` (default) do not list any CVEs
|
||||||
|
- `unresolved` list CVEs that have no fixes
|
||||||
|
- `all` list all CVEs
|
||||||
|
|
||||||
|
This feature is independent of the `osvVulnerabilityAlerts` option.
|
||||||
|
|
||||||
|
The source of these CVEs is [OSV.dev](https://osv.dev/).
|
||||||
|
|
||||||
## dependencyDashboardTitle
|
## dependencyDashboardTitle
|
||||||
|
|
||||||
Configure this option if you prefer a different title for the Dependency Dashboard.
|
Configure this option if you prefer a different title for the Dependency Dashboard.
|
||||||
|
|
|
@ -520,6 +520,15 @@ const options: RenovateOptions[] = [
|
||||||
subType: 'string',
|
subType: 'string',
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'dependencyDashboardOSVVulnerabilitySummary',
|
||||||
|
description:
|
||||||
|
'Control if the Dependency Dashboard issue lists CVEs supplied by [osv.dev](https://osv.dev).',
|
||||||
|
type: 'string',
|
||||||
|
allowedValues: ['none', 'all', 'unresolved'],
|
||||||
|
default: 'none',
|
||||||
|
experimental: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'configWarningReuseIssue',
|
name: 'configWarningReuseIssue',
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -232,6 +232,7 @@ export interface RenovateConfig
|
||||||
dependencyDashboardHeader?: string;
|
dependencyDashboardHeader?: string;
|
||||||
dependencyDashboardFooter?: string;
|
dependencyDashboardFooter?: string;
|
||||||
dependencyDashboardLabels?: string[];
|
dependencyDashboardLabels?: string[];
|
||||||
|
dependencyDashboardOSVVulnerabilitySummary?: 'none' | 'all' | 'unresolved';
|
||||||
packageFile?: string;
|
packageFile?: string;
|
||||||
packageRules?: PackageRule[];
|
packageRules?: PackageRule[];
|
||||||
postUpdateOptions?: string[];
|
postUpdateOptions?: string[];
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ERROR, WARN } from 'bunyan';
|
import { ERROR, WARN } from 'bunyan';
|
||||||
|
import { codeBlock } from 'common-tags';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { Fixtures } from '../../../test/fixtures';
|
import { Fixtures } from '../../../test/fixtures';
|
||||||
import {
|
import {
|
||||||
|
@ -21,8 +22,21 @@ import {
|
||||||
import { regEx } from '../../util/regex';
|
import { regEx } from '../../util/regex';
|
||||||
import type { BranchConfig, BranchUpgradeConfig } from '../types';
|
import type { BranchConfig, BranchUpgradeConfig } from '../types';
|
||||||
import * as dependencyDashboard from './dependency-dashboard';
|
import * as dependencyDashboard from './dependency-dashboard';
|
||||||
|
import { getDashboardMarkdownVulnerabilities } from './dependency-dashboard';
|
||||||
import { PackageFiles } from './package-files';
|
import { PackageFiles } from './package-files';
|
||||||
|
|
||||||
|
const createVulnerabilitiesMock = jest.fn();
|
||||||
|
jest.mock('./process/vulnerabilities', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
Vulnerabilities: class {
|
||||||
|
static create() {
|
||||||
|
return createVulnerabilitiesMock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
type PrUpgrade = BranchUpgradeConfig;
|
type PrUpgrade = BranchUpgradeConfig;
|
||||||
|
|
||||||
const massageMdSpy = platform.massageMarkdown;
|
const massageMdSpy = platform.massageMarkdown;
|
||||||
|
@ -1019,4 +1033,175 @@ describe('workers/repository/dependency-dashboard', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDashboardMarkdownVulnerabilities()', () => {
|
||||||
|
const packageFiles = Fixtures.getJson<Record<string, PackageFile[]>>(
|
||||||
|
'./package-files.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
it('return empty string if summary is empty', async () => {
|
||||||
|
const result = await getDashboardMarkdownVulnerabilities(
|
||||||
|
config,
|
||||||
|
packageFiles
|
||||||
|
);
|
||||||
|
expect(result).toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return empty string if summary is set to none', async () => {
|
||||||
|
const result = await getDashboardMarkdownVulnerabilities(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
dependencyDashboardOSVVulnerabilitySummary: 'none',
|
||||||
|
},
|
||||||
|
packageFiles
|
||||||
|
);
|
||||||
|
expect(result).toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return no data section if summary is set to all and no vulnerabilities', async () => {
|
||||||
|
const fetchVulnerabilitiesMock = jest.fn();
|
||||||
|
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||||||
|
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchVulnerabilitiesMock.mockResolvedValueOnce([]);
|
||||||
|
const result = await getDashboardMarkdownVulnerabilities(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
dependencyDashboardOSVVulnerabilitySummary: 'all',
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
expect(result).toBe(
|
||||||
|
`## Vulnerabilities\n\nRenovate has not found any CVEs on [osv.dev](https://osv.dev).\n\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return all vulnerabilities if set to all and disabled osvVulnerabilities', async () => {
|
||||||
|
const fetchVulnerabilitiesMock = jest.fn();
|
||||||
|
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||||||
|
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchVulnerabilitiesMock.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
packageName: 'express',
|
||||||
|
depVersion: '4.17.3',
|
||||||
|
fixedVersion: '4.18.1',
|
||||||
|
packageFileConfig: {
|
||||||
|
manager: 'npm',
|
||||||
|
},
|
||||||
|
vulnerability: {
|
||||||
|
id: 'GHSA-29mw-wpgm-hmr9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'cookie-parser',
|
||||||
|
depVersion: '1.4.6',
|
||||||
|
packageFileConfig: {
|
||||||
|
manager: 'npm',
|
||||||
|
},
|
||||||
|
vulnerability: {
|
||||||
|
id: 'GHSA-35jh-r3h4-6jhm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const result = await getDashboardMarkdownVulnerabilities(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
dependencyDashboardOSVVulnerabilitySummary: 'all',
|
||||||
|
osvVulnerabilityAlerts: true,
|
||||||
|
},
|
||||||
|
packageFiles
|
||||||
|
);
|
||||||
|
expect(result.trimEnd()).toBe(codeBlock`## Vulnerabilities
|
||||||
|
|
||||||
|
\`1\`/\`2\` CVEs have Renovate fixes.
|
||||||
|
<details><summary>npm</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
<details><summary>undefined</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
<details><summary>express</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
- [GHSA-29mw-wpgm-hmr9](https://osv.dev/vulnerability/GHSA-29mw-wpgm-hmr9) (fixed in 4.18.1)
|
||||||
|
</blockquote>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>cookie-parser</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
- [GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm)
|
||||||
|
</blockquote>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
</details>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return unresolved vulnerabilities if set to "unresolved"', async () => {
|
||||||
|
const fetchVulnerabilitiesMock = jest.fn();
|
||||||
|
createVulnerabilitiesMock.mockResolvedValueOnce({
|
||||||
|
fetchVulnerabilities: fetchVulnerabilitiesMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchVulnerabilitiesMock.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
packageName: 'express',
|
||||||
|
depVersion: '4.17.3',
|
||||||
|
fixedVersion: '4.18.1',
|
||||||
|
packageFileConfig: {
|
||||||
|
manager: 'npm',
|
||||||
|
},
|
||||||
|
vulnerability: {
|
||||||
|
id: 'GHSA-29mw-wpgm-hmr9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: 'cookie-parser',
|
||||||
|
depVersion: '1.4.6',
|
||||||
|
packageFileConfig: {
|
||||||
|
manager: 'npm',
|
||||||
|
},
|
||||||
|
vulnerability: {
|
||||||
|
id: 'GHSA-35jh-r3h4-6jhm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const result = await getDashboardMarkdownVulnerabilities(
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
dependencyDashboardOSVVulnerabilitySummary: 'unresolved',
|
||||||
|
},
|
||||||
|
packageFiles
|
||||||
|
);
|
||||||
|
expect(result.trimEnd()).toBe(codeBlock`## Vulnerabilities
|
||||||
|
|
||||||
|
\`1\`/\`2\` CVEs have possible Renovate fixes.
|
||||||
|
See [\`osvVulnerabilityAlerts\`](https://docs.renovatebot.com/configuration-options/#osvvulnerabilityalerts) to allow Renovate to supply fixes.
|
||||||
|
<details><summary>npm</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
<details><summary>undefined</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
<details><summary>cookie-parser</summary>
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
|
- [GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm)
|
||||||
|
</blockquote>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
</details>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,8 @@ import * as template from '../../util/template';
|
||||||
import type { BranchConfig, SelectAllConfig } from '../types';
|
import type { BranchConfig, SelectAllConfig } from '../types';
|
||||||
import { getDepWarningsDashboard } from './errors-warnings';
|
import { getDepWarningsDashboard } from './errors-warnings';
|
||||||
import { PackageFiles } from './package-files';
|
import { PackageFiles } from './package-files';
|
||||||
|
import type { Vulnerability } from './process/types';
|
||||||
|
import { Vulnerabilities } from './process/vulnerabilities';
|
||||||
|
|
||||||
interface DependencyDashboard {
|
interface DependencyDashboard {
|
||||||
dependencyDashboardChecks: Record<string, string>;
|
dependencyDashboardChecks: Record<string, string>;
|
||||||
|
@ -411,6 +413,9 @@ export async function ensureDependencyDashboard(
|
||||||
'This repository currently has no open or pending branches.\n\n';
|
'This repository currently has no open or pending branches.\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add CVE section
|
||||||
|
issueBody += await getDashboardMarkdownVulnerabilities(config, packageFiles);
|
||||||
|
|
||||||
// fit the detected dependencies section
|
// fit the detected dependencies section
|
||||||
const footer = getFooter(config);
|
const footer = getFooter(config);
|
||||||
issueBody += PackageFiles.getDashboardMarkdown(
|
issueBody += PackageFiles.getDashboardMarkdown(
|
||||||
|
@ -468,3 +473,103 @@ function getFooter(config: RenovateConfig): string {
|
||||||
|
|
||||||
return footer;
|
return footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDashboardMarkdownVulnerabilities(
|
||||||
|
config: RenovateConfig,
|
||||||
|
packageFiles: Record<string, PackageFile[]>
|
||||||
|
): Promise<string> {
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
is.nullOrUndefined(config.dependencyDashboardOSVVulnerabilitySummary) ||
|
||||||
|
config.dependencyDashboardOSVVulnerabilitySummary === 'none'
|
||||||
|
) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += '## Vulnerabilities\n\n';
|
||||||
|
|
||||||
|
const vulnerabilityFetcher = await Vulnerabilities.create();
|
||||||
|
const vulnerabilities = await vulnerabilityFetcher.fetchVulnerabilities(
|
||||||
|
config,
|
||||||
|
packageFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
if (vulnerabilities.length === 0) {
|
||||||
|
result +=
|
||||||
|
'Renovate has not found any CVEs on [osv.dev](https://osv.dev).\n\n';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unresolvedVulnerabilities = vulnerabilities.filter((value) =>
|
||||||
|
is.nullOrUndefined(value.fixedVersion)
|
||||||
|
);
|
||||||
|
const resolvedVulnerabilitiesLength =
|
||||||
|
vulnerabilities.length - unresolvedVulnerabilities.length;
|
||||||
|
|
||||||
|
result += `\`${resolvedVulnerabilitiesLength}\`/\`${vulnerabilities.length}\``;
|
||||||
|
if (is.truthy(config.osvVulnerabilityAlerts)) {
|
||||||
|
result += ' CVEs have Renovate fixes.\n';
|
||||||
|
} else {
|
||||||
|
result +=
|
||||||
|
' CVEs have possible Renovate fixes.\nSee [`osvVulnerabilityAlerts`](https://docs.renovatebot.com/configuration-options/#osvvulnerabilityalerts) to allow Renovate to supply fixes.\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderedVulnerabilities: Vulnerability[];
|
||||||
|
switch (config.dependencyDashboardOSVVulnerabilitySummary) {
|
||||||
|
// filter vulnerabilities to display based on configuration
|
||||||
|
case 'unresolved':
|
||||||
|
renderedVulnerabilities = unresolvedVulnerabilities;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
renderedVulnerabilities = vulnerabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
const managerRecords: Record<
|
||||||
|
string,
|
||||||
|
Record<string, Record<string, Vulnerability[]>>
|
||||||
|
> = {};
|
||||||
|
for (const vulnerability of renderedVulnerabilities) {
|
||||||
|
const { manager, packageFile } = vulnerability.packageFileConfig;
|
||||||
|
if (is.nullOrUndefined(managerRecords[manager!])) {
|
||||||
|
managerRecords[manager!] = {};
|
||||||
|
}
|
||||||
|
if (is.nullOrUndefined(managerRecords[manager!][packageFile])) {
|
||||||
|
managerRecords[manager!][packageFile] = {};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
is.nullOrUndefined(
|
||||||
|
managerRecords[manager!][packageFile][vulnerability.packageName]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
managerRecords[manager!][packageFile][vulnerability.packageName] = [];
|
||||||
|
}
|
||||||
|
managerRecords[manager!][packageFile][vulnerability.packageName].push(
|
||||||
|
vulnerability
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [manager, packageFileRecords] of Object.entries(managerRecords)) {
|
||||||
|
result += `<details><summary>${manager}</summary>\n<blockquote>\n\n`;
|
||||||
|
for (const [packageFile, packageNameRecords] of Object.entries(
|
||||||
|
packageFileRecords
|
||||||
|
)) {
|
||||||
|
result += `<details><summary>${packageFile}</summary>\n<blockquote>\n\n`;
|
||||||
|
for (const [packageName, cves] of Object.entries(packageNameRecords)) {
|
||||||
|
result += `<details><summary>${packageName}</summary>\n<blockquote>\n\n`;
|
||||||
|
for (const vul of cves) {
|
||||||
|
const id = vul.vulnerability.id;
|
||||||
|
const suffix = is.nonEmptyString(vul.fixedVersion)
|
||||||
|
? ` (fixed in ${vul.fixedVersion})`
|
||||||
|
: '';
|
||||||
|
result += `- [${id}](https://osv.dev/vulnerability/${id})${suffix}\n`;
|
||||||
|
}
|
||||||
|
result += `</blockquote>\n</details>\n\n`;
|
||||||
|
}
|
||||||
|
result += `</blockquote>\n</details>\n\n`;
|
||||||
|
}
|
||||||
|
result += `</blockquote>\n</details>\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue