Skip to content

Commit

Permalink
feat(vulnerabilities): set, maintain and expose vulnerabilitySeverity…
Browse files Browse the repository at this point in the history
… for templated fields (#21939)
  • Loading branch information
setchy committed May 11, 2023
1 parent ebf064e commit a2e036e
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 43 deletions.
55 changes: 31 additions & 24 deletions lib/config/index.spec.ts
@@ -1,4 +1,5 @@
import { getConfig } from './defaults';
import { filterConfig, getManagerConfig, mergeChildConfig } from './index';

jest.mock('../modules/datasource/npm');
try {
Expand All @@ -11,7 +12,7 @@ const defaultConfig = getConfig();

describe('config/index', () => {
describe('mergeChildConfig(parentConfig, childConfig)', () => {
it('merges', async () => {
it('merges', () => {
const parentConfig = { ...defaultConfig };
const childConfig = {
foo: 'bar',
Expand All @@ -20,30 +21,28 @@ describe('config/index', () => {
schedule: ['on monday'],
},
};
const configParser = await import('./index');
const config = configParser.mergeChildConfig(parentConfig, childConfig);
const config = mergeChildConfig(parentConfig, childConfig);
expect(config.foo).toBe('bar');
expect(config.rangeStrategy).toBe('replace');
expect(config.lockFileMaintenance.schedule).toEqual(['on monday']);
expect(config.lockFileMaintenance).toMatchSnapshot();
});

it('merges packageRules', async () => {
it('merges packageRules', () => {
const parentConfig = { ...defaultConfig };
Object.assign(parentConfig, {
packageRules: [{ a: 1 }, { a: 2 }],
});
const childConfig = {
packageRules: [{ a: 3 }, { a: 4 }],
};
const configParser = await import('./index');
const config = configParser.mergeChildConfig(parentConfig, childConfig);
const config = mergeChildConfig(parentConfig, childConfig);
expect(config.packageRules.map((rule) => rule.a)).toMatchObject([
1, 2, 3, 4,
]);
});

it('merges constraints', async () => {
it('merges constraints', () => {
const parentConfig = { ...defaultConfig };
Object.assign(parentConfig, {
constraints: {
Expand All @@ -56,8 +55,7 @@ describe('config/index', () => {
node: '<15',
},
};
const configParser = await import('./index');
const config = configParser.mergeChildConfig(parentConfig, childConfig);
const config = mergeChildConfig(parentConfig, childConfig);
expect(config.constraints).toMatchSnapshot();
expect(config.constraints.node).toBe('<15');
});
Expand All @@ -75,39 +73,48 @@ describe('config/index', () => {
expect(config.packageRules).toHaveLength(2);
});

it('handles null child packageRules', async () => {
it('handles null child packageRules', () => {
const parentConfig = { ...defaultConfig };
parentConfig.packageRules = [{ a: 3 }, { a: 4 }];
const configParser = await import('./index');
const config = configParser.mergeChildConfig(parentConfig, {});
const config = mergeChildConfig(parentConfig, {});
expect(config.packageRules).toHaveLength(2);
});

it('handles undefined childConfig', async () => {
it('handles undefined childConfig', () => {
const parentConfig = { ...defaultConfig };
const configParser = await import('./index');
const config = configParser.mergeChildConfig(parentConfig, undefined);
const config = mergeChildConfig(parentConfig, undefined);
expect(config).toMatchObject(parentConfig);
});

it('getManagerConfig()', async () => {
it('getManagerConfig()', () => {
const parentConfig = { ...defaultConfig };
const configParser = await import('./index');
const config = configParser.getManagerConfig(parentConfig, 'npm');
const config = getManagerConfig(parentConfig, 'npm');
expect(config).toContainEntries([
['fileMatch', ['(^|/)package\\.json$']],
['rollbackPrs', true],
]);
expect(
configParser.getManagerConfig(parentConfig, 'html')
).toContainEntries([['fileMatch', ['\\.html?$']]]);
expect(getManagerConfig(parentConfig, 'html')).toContainEntries([
['fileMatch', ['\\.html?$']],
]);
});

it('filterConfig()', async () => {
it('filterConfig()', () => {
const parentConfig = { ...defaultConfig };
const configParser = await import('./index');
const config = configParser.filterConfig(parentConfig, 'pr');
const config = filterConfig(parentConfig, 'pr');
expect(config).toBeObject();
});

it('highest vulnerabilitySeverity maintained when config is vulnerability alert', () => {
const parentConfig = { ...defaultConfig };
Object.assign(parentConfig, {
isVulnerabilityAlert: true,
vulnerabilitySeverity: 'HIGH',
});
const childConfig = {
vulnerabilitySeverity: 'CRITICAL',
};
const config = 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;
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
11 changes: 11 additions & 0 deletions lib/config/utils.ts
@@ -1,4 +1,5 @@
import { logger } from '../logger';
import { getHighestVulnerabilitySeverity } from '../util/vulnerability/utils';
import * as options from './options';
import type { RenovateConfig } from './types';

Expand All @@ -13,13 +14,23 @@ 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) {
config.vulnerabilitySeverity = getHighestVulnerabilitySeverity(
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 severity for a vulnerability alert upgrade (eg: LOW, MODERATE, HIGH, CRITICAL)',
};

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

describe('util/vulnerability/utils', () => {
it('parent CRITICAL vulnerability severity rating is maintained', () => {
const parentConfig = {
vulnerabilitySeverity: 'CRITICAL',
};

const childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('CRITICAL');
});

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

const childConfig = {
vulnerabilitySeverity: 'CRITICAL',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('CRITICAL');
});

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

const childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('HIGH');
});

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

const childConfig = {
vulnerabilitySeverity: 'HIGH',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('HIGH');
});

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

const childConfig = {
vulnerabilitySeverity: 'LOW',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('MODERATE');
});

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

const childConfig = {
vulnerabilitySeverity: 'MODERATE',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('MODERATE');
});

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

const childConfig = {
vulnerabilitySeverity: undefined,
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('LOW');
});

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

const childConfig = {
vulnerabilitySeverity: 'LOW',
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBe('LOW');
});

it('handled undefined parent and child vulnerability severity', () => {
const parentConfig = {
vulnerabilitySeverity: undefined,
};

const childConfig = {
vulnerabilitySeverity: undefined,
};

const severity = getHighestVulnerabilitySeverity(parentConfig, childConfig);

expect(severity).toBeUndefined();
});
});
26 changes: 26 additions & 0 deletions lib/util/vulnerability/utils.ts
@@ -0,0 +1,26 @@
const severityOrder: Record<string, number> = {
LOW: 1,
MODERATE: 2,
HIGH: 3,
CRITICAL: 4,
};

export function getHighestVulnerabilitySeverity<
T extends Record<string, any>,
TChild extends Record<string, any> | undefined
>(parent: T, child: TChild): string {
const parentVulSeverity = parent.vulnerabilitySeverity?.toUpperCase();
const childVulSeverity = child?.vulnerabilitySeverity?.toUpperCase();

if (childVulSeverity === undefined) {
return parentVulSeverity;
}

if (parentVulSeverity === undefined) {
return childVulSeverity;
}

return severityOrder[parentVulSeverity] >= severityOrder[childVulSeverity]
? parentVulSeverity
: childVulSeverity;
}
6 changes: 6 additions & 0 deletions lib/workers/repository/process/types.ts
Expand Up @@ -17,3 +17,9 @@ export interface DependencyVulnerabilities {
versioningApi: VersioningApi;
vulnerabilities: Vulnerability[];
}

export interface SeverityDetails {
cvssVector: string;
score: string;
severityLevel?: string;
}

0 comments on commit a2e036e

Please sign in to comment.