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 all 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
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;
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
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;
}