Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/config/index.spec.ts
Original file line number Diff line number Diff line change
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');
Comment thread
setchy marked this conversation as resolved.
Outdated
const config = configParser.mergeChildConfig(parentConfig, childConfig);
expect(config.vulnerabilitySeverity).toBe('CRITICAL');
});
});
});
3 changes: 3 additions & 0 deletions lib/config/types.ts
Original file line number Diff line number Diff line change
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;
Comment thread
setchy marked this conversation as 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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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',
Comment thread
secustor marked this conversation as resolved.
Outdated
};

const prBodyFields = [
Expand Down
126 changes: 126 additions & 0 deletions lib/util/vulnerability/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
};
});

Comment thread
setchy marked this conversation as resolved.
Outdated
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function setHighestVulnerabilitySeverity(
config: Record<string, any>,
parent: Record<string, any>,
child: Record<string, any>
Comment thread
setchy marked this conversation as resolved.
Outdated
): 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;
}
Comment thread
setchy marked this conversation as resolved.
Outdated

config.vulnerabilitySeverity = mostSevere;
}
57 changes: 42 additions & 15 deletions lib/workers/repository/process/vulnerabilities.ts
Original file line number Diff line number Diff line change
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
Comment thread
setchy marked this conversation as 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;
Comment thread
setchy marked this conversation as resolved.
severityLevel =
severity.charAt(0).toUpperCase() + severity.slice(1).toLowerCase();
}

return [cvssVector, score, severityLevel];
Comment thread
setchy marked this conversation as resolved.
Outdated
}
}