Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vulnerabilities): set, maintain and expose vulnerabilitySeverity for templated fields #21939

Merged
merged 22 commits into from May 11, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/config/index.spec.ts
Expand Up @@ -109,5 +109,19 @@ describe('config/index', () => {
const config = configParser.filterConfig(parentConfig, 'pr');
expect(config).toBeObject();
});

it('setHighestVulnerabilitySeverity when config is vulnerability alert', async () => {
const parentConfig = { ...defaultConfig };
Object.assign(parentConfig, {
isVulnerabilityAlert: true,
vulnerabilitySeverity: 'HIGH',
});
const childConfig = {
vulnerabilitySeverity: 'CRITICAL',
};
const configParser = await import('./index');
setchy marked this conversation as resolved.
Show resolved Hide resolved
const config = configParser.mergeChildConfig(parentConfig, childConfig);
expect(config.vulnerabilitySeverity).toBe('CRITICAL');
});
});
});
3 changes: 3 additions & 0 deletions lib/config/types.ts
Expand Up @@ -251,6 +251,7 @@ export interface RenovateConfig
warnings?: ValidationMessage[];
vulnerabilityAlerts?: RenovateSharedConfig;
osvVulnerabilityAlerts?: boolean;
vulnerabilitySeverity?: string;
regexManagers?: RegExManager[];

fetchReleaseNotes?: boolean;
Expand Down Expand Up @@ -309,6 +310,7 @@ export interface PackageRule
UpdateConfig,
Record<string, unknown> {
description?: string | string[];
isVulnerabilityAlert?: boolean;
setchy marked this conversation as resolved.
Show resolved Hide resolved
matchFiles?: string[];
matchPaths?: string[];
matchLanguages?: string[];
Expand All @@ -333,6 +335,7 @@ export interface PackageRule
matchUpdateTypes?: UpdateType[];
matchConfidence?: MergeConfidence[];
registryUrls?: string[] | null;
vulnerabilitySeverity?: string;
}

export interface ValidationMessage {
Expand Down
8 changes: 8 additions & 0 deletions lib/config/utils.ts
@@ -1,4 +1,5 @@
import { logger } from '../logger';
import { setHighestVulnerabilitySeverity } from '../util/vulnerability/utils';
import * as options from './options';
import type { RenovateConfig } from './types';

Expand All @@ -13,13 +14,20 @@ export function mergeChildConfig<
const parentConfig = structuredClone(parent);
const childConfig = structuredClone(child);
const config: Record<string, any> = { ...parentConfig, ...childConfig };

// Ensure highest severity survives parent / child merge
if (config?.isVulnerabilityAlert) {
setHighestVulnerabilitySeverity(config, parent, child);
}

for (const option of options.getOptions()) {
if (
option.mergeable &&
childConfig[option.name] &&
parentConfig[option.name]
) {
logger.trace(`mergeable option: ${option.name}`);

if (option.name === 'constraints') {
config[option.name] = {
...parentConfig[option.name],
Expand Down
1 change: 1 addition & 0 deletions lib/modules/manager/types.ts
Expand Up @@ -174,6 +174,7 @@ export interface Upgrade<T = Record<string, any>> extends PackageDependency<T> {
isLockFileMaintenance?: boolean;
isRemediation?: boolean;
isVulnerabilityAlert?: boolean;
vulnerabilitySeverity?: string;
registryUrls?: string[] | null;
currentVersion?: string;
replaceString?: string;
Expand Down
3 changes: 3 additions & 0 deletions lib/util/template/index.ts
Expand Up @@ -100,6 +100,7 @@ export const allowedFields = {
isRange: 'true if the new value is a range',
isSingleVersion:
'true if the upgrade is to a single version rather than a range',
isVulnerabilityAlert: 'true if the upgrade is a vulnerability alert',
logJSON: 'ChangeLogResult object for the upgrade',
manager: 'The (package) manager which detected the dependency',
newDigest: 'The new digest value',
Expand Down Expand Up @@ -143,6 +144,8 @@ export const allowedFields = {
version: 'The version number of the changelog',
versioning: 'The versioning scheme in use',
versions: 'An array of ChangeLogRelease objects in the upgrade',
vulnerabilitySeverity:
'The vulnerability severity for a vulnerability alert upgrade',
secustor marked this conversation as resolved.
Show resolved Hide resolved
};

const prBodyFields = [
Expand Down
126 changes: 126 additions & 0 deletions lib/util/vulnerability/utils.spec.ts
@@ -0,0 +1,126 @@
import { setHighestVulnerabilitySeverity } from './utils';

let config: Record<string, any>;
let parentConfig: Record<string, any>;
let childConfig: Record<string, any>;

beforeEach(() => {
jest.resetAllMocks();
config = {
vulnerabilitySeverity: undefined,
};
});

setchy marked this conversation as resolved.
Show resolved Hide resolved
describe('util/vulnerability/utils', () => {
it('parent CRITICAL vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'CRITICAL',
};

childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('CRITICAL');
});

it('child CRITICAL vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'MODERATE',
};

childConfig = {
vulnerabilitySeverity: 'CRITICAL',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('CRITICAL');
});

it('parent HIGH vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'HIGH',
};

childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('HIGH');
});

it('child HIGH vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'MODERATE',
};

childConfig = {
vulnerabilitySeverity: 'HIGH',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('HIGH');
});

it('parent MODERATE vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'MODERATE',
};

childConfig = {
vulnerabilitySeverity: 'LOW',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('MODERATE');
});

it('child MODERATE vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'LOW',
};

childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('MODERATE');
});

it('parent LOW vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: 'LOW',
};

childConfig = {
vulnerabilitySeverity: undefined,
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('LOW');
});

it('child LOW vulnerability severity rating is maintained', () => {
parentConfig = {
vulnerabilitySeverity: undefined,
};

childConfig = {
vulnerabilitySeverity: 'LOW',
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

expect(config.vulnerabilitySeverity).toBe('LOW');
});
});
31 changes: 31 additions & 0 deletions lib/util/vulnerability/utils.ts
@@ -0,0 +1,31 @@
export function setHighestVulnerabilitySeverity(
config: Record<string, any>,
parent: Record<string, any>,
child: Record<string, any>
setchy marked this conversation as resolved.
Show resolved Hide resolved
): void {
let mostSevere: string | undefined;

const parentVulSeverity = parent.vulnerabilitySeverity?.toUpperCase();

const childVulSeverity = child.vulnerabilitySeverity?.toUpperCase();

if (!parentVulSeverity && childVulSeverity) {
mostSevere = childVulSeverity;
} else if (parentVulSeverity && !childVulSeverity) {
mostSevere = parentVulSeverity;
} else if (parentVulSeverity === 'CRITICAL') {
mostSevere = 'CRITICAL';
} else if (parentVulSeverity === 'HIGH' && childVulSeverity !== 'CRITICAL') {
mostSevere = 'HIGH';
} else if (
parentVulSeverity === 'MODERATE' &&
childVulSeverity !== 'CRITICAL' &&
childVulSeverity !== 'HIGH'
) {
mostSevere = 'MODERATE';
} else {
mostSevere = childVulSeverity;
}
setchy marked this conversation as resolved.
Show resolved Hide resolved

config.vulnerabilitySeverity = mostSevere;
}
57 changes: 42 additions & 15 deletions lib/workers/repository/process/vulnerabilities.ts
Expand Up @@ -454,12 +454,19 @@ export class Vulnerabilities {
logger.debug(
`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
);

const [, , severityLevel] = this.extractSeverityDetails(
vulnerability,
affected
);

return {
matchDatasources: [datasource],
matchPackageNames: [packageName],
matchCurrentVersion: depVersion,
allowedVersions: fixedVersion,
isVulnerabilityAlert: true,
vulnerabilitySeverity: severityLevel?.toUpperCase(),
prBodyNotes: this.generatePrBodyNotes(vulnerability, affected),
force: {
...packageFileConfig.vulnerabilityAlerts,
Expand Down Expand Up @@ -513,24 +520,16 @@ export class Vulnerabilities {
content += `#### Details\n${details ?? 'No details.'}\n`;

content += '#### Severity\n';
const cvssVector =
vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ??
vulnerability.severity?.[0]?.score ??
(affected.database_specific?.cvss as string); // RUSTSEC
const [cvssVector, score, severityLevel] = this.extractSeverityDetails(
vulnerability,
affected
);

if (cvssVector) {
const [baseScore, severity] = this.evaluateCvssVector(cvssVector);
const score = baseScore ? `${baseScore} / 10 (${severity})` : 'Unknown';
content += `- CVSS Score: ${score}\n`;
content += `- Vector String: \`${cvssVector}\`\n`;
} else if (
vulnerability.id.startsWith('GHSA-') &&
vulnerability.database_specific?.severity
) {
const severity = vulnerability.database_specific.severity as string;
content +=
severity.charAt(0).toUpperCase() +
severity.slice(1).toLowerCase() +
'\n';
} else if (severityLevel) {
content += `${severityLevel}\n`;
} else {
content += 'Unknown severity.\n';
}
Expand Down Expand Up @@ -558,4 +557,32 @@ export class Vulnerabilities {

return [sanitizeMarkdown(content)];
}

private extractSeverityDetails(
vulnerability: Osv.Vulnerability,
affected: Osv.Affected
): [string, string, string | undefined] {
let severityLevel: string | undefined;
let score = 'Unknown';

const cvssVector =
vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ??
vulnerability.severity?.[0]?.score ??
(affected.database_specific?.cvss as string); // RUSTSEC
setchy marked this conversation as resolved.
Show resolved Hide resolved

if (cvssVector) {
const [baseScore, severity] = this.evaluateCvssVector(cvssVector);
severityLevel = severity;
score = baseScore ? `${baseScore} / 10 (${severityLevel})` : 'Unknown';
} else if (
vulnerability.id.startsWith('GHSA-') &&
vulnerability.database_specific?.severity
) {
const severity = vulnerability.database_specific.severity as string;
setchy marked this conversation as resolved.
Show resolved Hide resolved
severityLevel =
severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase();
}

return [cvssVector, score, severityLevel];
setchy marked this conversation as resolved.
Show resolved Hide resolved
}
}