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 20 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('setHighestVulnerabilitySeverity 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
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
119 changes: 119 additions & 0 deletions lib/util/vulnerability/utils.spec.ts
@@ -0,0 +1,119 @@
import { setHighestVulnerabilitySeverity } from './utils';

const config: Record<string, any> = {
vulnerabilitySeverity: undefined,
};

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

const childConfig = {
vulnerabilitySeverity: undefined,
};

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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

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

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

setHighestVulnerabilitySeverity(config, parentConfig, childConfig);

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