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.
|
||||
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
|
||||
|
||||
Configure this option if you prefer a different title for the Dependency Dashboard.
|
||||
|
|
|
@ -520,6 +520,15 @@ const options: RenovateOptions[] = [
|
|||
subType: 'string',
|
||||
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',
|
||||
description:
|
||||
|
|
|
@ -232,6 +232,7 @@ export interface RenovateConfig
|
|||
dependencyDashboardHeader?: string;
|
||||
dependencyDashboardFooter?: string;
|
||||
dependencyDashboardLabels?: string[];
|
||||
dependencyDashboardOSVVulnerabilitySummary?: 'none' | 'all' | 'unresolved';
|
||||
packageFile?: string;
|
||||
packageRules?: PackageRule[];
|
||||
postUpdateOptions?: string[];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ERROR, WARN } from 'bunyan';
|
||||
import { codeBlock } from 'common-tags';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { Fixtures } from '../../../test/fixtures';
|
||||
import {
|
||||
|
@ -21,8 +22,21 @@ import {
|
|||
import { regEx } from '../../util/regex';
|
||||
import type { BranchConfig, BranchUpgradeConfig } from '../types';
|
||||
import * as dependencyDashboard from './dependency-dashboard';
|
||||
import { getDashboardMarkdownVulnerabilities } from './dependency-dashboard';
|
||||
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;
|
||||
|
||||
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 { getDepWarningsDashboard } from './errors-warnings';
|
||||
import { PackageFiles } from './package-files';
|
||||
import type { Vulnerability } from './process/types';
|
||||
import { Vulnerabilities } from './process/vulnerabilities';
|
||||
|
||||
interface DependencyDashboard {
|
||||
dependencyDashboardChecks: Record<string, string>;
|
||||
|
@ -411,6 +413,9 @@ export async function ensureDependencyDashboard(
|
|||
'This repository currently has no open or pending branches.\n\n';
|
||||
}
|
||||
|
||||
// add CVE section
|
||||
issueBody += await getDashboardMarkdownVulnerabilities(config, packageFiles);
|
||||
|
||||
// fit the detected dependencies section
|
||||
const footer = getFooter(config);
|
||||
issueBody += PackageFiles.getDashboardMarkdown(
|
||||
|
@ -468,3 +473,103 @@ function getFooter(config: RenovateConfig): string {
|
|||
|
||||
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