refactor(vulnerabilities): return interface for Vulnerabilities (#21310)

This commit is contained in:
Sebastian Poxhofer 2023-04-18 11:07:36 +02:00 committed by GitHub
parent 2a72f85cc2
commit 75a1ab04eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 201 additions and 61 deletions

View file

@ -0,0 +1,19 @@
import type { Osv } from '@renovatebot/osv-offline';
import type { RenovateConfig } from '../../../config/types';
import type { PackageFile } from '../../../modules/manager/types';
import type { VersioningApi } from '../../../modules/versioning';
export interface Vulnerability {
packageFileConfig: RenovateConfig & PackageFile;
packageName: string;
depVersion: string;
fixedVersion: string | null;
datasource: string;
vulnerability: Osv.Vulnerability;
affected: Osv.Affected;
}
export interface DependencyVulnerabilities {
versioningApi: VersioningApi;
vulnerabilities: Vulnerability[];
}

View file

@ -36,6 +36,89 @@ describe('workers/repository/process/vulnerabilities', () => {
describe('fetchVulnerabilities()', () => {
let config: RenovateConfig;
let vulnerabilities: Vulnerabilities;
beforeAll(async () => {
createMock.mockResolvedValue({
getVulnerabilities: getVulnerabilitiesMock,
});
vulnerabilities = await Vulnerabilities.create();
});
beforeEach(() => {
config = getConfig();
config.packageRules = [];
});
it('return list of Vulnerabilities', async () => {
const packageFiles: Record<string, PackageFile[]> = {
poetry: [
{
deps: [
{ depName: 'django', currentValue: '3.2', datasource: 'pypi' },
],
packageFile: 'some-file',
},
],
};
getVulnerabilitiesMock.mockResolvedValueOnce([
{
id: 'GHSA-qrw5-5h28-modded',
modified: '',
affected: [
{
package: {
name: 'django',
ecosystem: 'PyPI',
purl: 'pkg:pypi/django',
},
ranges: [
{
type: 'ECOSYSTEM',
events: [{ introduced: '3.0' }, { fixed: '3.3.8' }],
},
],
},
{
package: {
name: 'django',
ecosystem: 'PyPI',
purl: 'pkg:pypi/django',
},
ranges: [
{
type: 'ECOSYSTEM',
events: [{ introduced: '3.2' }, { fixed: '3.2.16' }],
},
],
},
],
},
]);
const vulnerabilityList = await vulnerabilities.fetchVulnerabilities(
config,
packageFiles
);
expect(vulnerabilityList).toMatchObject([
{
packageName: 'django',
depVersion: '3.2',
fixedVersion: '==3.3.8',
datasource: 'pypi',
},
{
packageName: 'django',
depVersion: '3.2',
fixedVersion: '==3.2.16',
datasource: 'pypi',
},
]);
});
});
describe('appendVulnerabilityPackageRules()', () => {
let config: RenovateConfig;
let vulnerabilities: Vulnerabilities;
const lodashVulnerability: Osv.Vulnerability = {
id: 'GHSA-x5rq-j2xg-h7qm',
modified: '',

View file

@ -18,6 +18,7 @@ import {
import { sanitizeMarkdown } from '../../../util/markdown';
import * as p from '../../../util/promises';
import { regEx } from '../../../util/regex';
import type { DependencyVulnerabilities, Vulnerability } from './types';
export class Vulnerabilities {
private osvOffline: OsvOffline | undefined;
@ -54,67 +55,104 @@ export class Vulnerabilities {
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>
): Promise<void> {
const dependencyVulnerabilities = await this.fetchDependencyVulnerabilities(
config,
packageFiles
);
config.packageRules ??= [];
for (const {
vulnerabilities,
versioningApi,
} of dependencyVulnerabilities) {
const groupPackageRules: PackageRule[] = [];
for (const vulnerability of vulnerabilities) {
const rule = this.vulnerabilityToPackageRules(vulnerability);
if (is.nullOrUndefined(rule)) {
continue;
}
groupPackageRules.push(rule);
}
this.sortByFixedVersion(groupPackageRules, versioningApi);
config.packageRules.push(...groupPackageRules);
}
}
async fetchVulnerabilities(
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>
): Promise<Vulnerability[]> {
const groups = await this.fetchDependencyVulnerabilities(
config,
packageFiles
);
return groups.flatMap((group) => group.vulnerabilities);
}
private async fetchDependencyVulnerabilities(
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>
): Promise<DependencyVulnerabilities[]> {
const managers = Object.keys(packageFiles);
const allManagerJobs = managers.map((manager) =>
this.fetchManagerVulnerabilities(config, packageFiles, manager)
);
await Promise.all(allManagerJobs);
return (await Promise.all(allManagerJobs)).flat();
}
private async fetchManagerVulnerabilities(
config: RenovateConfig,
packageFiles: Record<string, PackageFile[]>,
manager: string
): Promise<void> {
): Promise<DependencyVulnerabilities[]> {
const managerConfig = getManagerConfig(config, manager);
const queue = packageFiles[manager].map(
(pFile) => (): Promise<void> =>
this.fetchManagerPackageFileVulnerabilities(
config,
managerConfig,
pFile
)
(pFile) => (): Promise<DependencyVulnerabilities[]> =>
this.fetchManagerPackageFileVulnerabilities(managerConfig, pFile)
);
logger.trace(
{ manager, queueLength: queue.length },
'fetchManagerUpdates starting'
'fetchManagerVulnerabilities starting'
);
await p.all(queue);
logger.trace({ manager }, 'fetchManagerUpdates finished');
const result = (await p.all(queue)).flat();
logger.trace({ manager }, 'fetchManagerVulnerabilities finished');
return result;
}
private async fetchManagerPackageFileVulnerabilities(
config: RenovateConfig,
managerConfig: RenovateConfig,
pFile: PackageFile
): Promise<void> {
): Promise<DependencyVulnerabilities[]> {
const { packageFile } = pFile;
const packageFileConfig = mergeChildConfig(managerConfig, pFile);
const { manager } = packageFileConfig;
const queue = pFile.deps.map(
(dep) => (): Promise<PackageRule[]> =>
this.fetchDependencyVulnerabilities(packageFileConfig, dep)
(dep) => (): Promise<DependencyVulnerabilities | null> =>
this.fetchDependencyVulnerability(packageFileConfig, dep)
);
logger.trace(
{ manager, packageFile, queueLength: queue.length },
'fetchManagerPackageFileVulnerabilities starting with concurrency'
);
config.packageRules?.push(...(await p.all(queue)).flat());
const result = await p.all(queue);
logger.trace(
{ packageFile },
'fetchManagerPackageFileVulnerabilities finished'
);
return result.filter(is.truthy);
}
private async fetchDependencyVulnerabilities(
private async fetchDependencyVulnerability(
packageFileConfig: RenovateConfig & PackageFile,
dep: PackageDependency
): Promise<PackageRule[]> {
): Promise<DependencyVulnerabilities | null> {
const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource!];
if (!ecosystem) {
logger.trace(`Cannot map datasource ${dep.datasource!} to OSV ecosystem`);
return [];
return null;
}
let packageName = dep.packageName ?? dep.depName!;
@ -123,20 +161,19 @@ export class Vulnerabilities {
packageName = packageName.toLowerCase().replace(regEx(/[_.-]+/g), '-');
}
const packageRules: PackageRule[] = [];
try {
const vulnerabilities = await this.osvOffline?.getVulnerabilities(
const osvVulnerabilities = await this.osvOffline?.getVulnerabilities(
ecosystem,
packageName
);
if (
is.nullOrUndefined(vulnerabilities) ||
is.emptyArray(vulnerabilities)
is.nullOrUndefined(osvVulnerabilities) ||
is.emptyArray(osvVulnerabilities)
) {
logger.trace(
`No vulnerabilities found in OSV database for ${packageName}`
);
return [];
return null;
}
const depVersion =
@ -149,16 +186,19 @@ export class Vulnerabilities {
logger.debug(
`Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`
);
return [];
return null;
}
for (const vulnerability of vulnerabilities) {
if (vulnerability.withdrawn) {
logger.trace(`Skipping withdrawn vulnerability ${vulnerability.id}`);
const vulnerabilities: Vulnerability[] = [];
for (const osvVulnerability of osvVulnerabilities) {
if (osvVulnerability.withdrawn) {
logger.trace(
`Skipping withdrawn vulnerability ${osvVulnerability.id}`
);
continue;
}
for (const affected of vulnerability.affected ?? []) {
for (const affected of osvVulnerability.affected ?? []) {
const isVulnerable = this.isPackageVulnerable(
ecosystem,
packageName,
@ -171,7 +211,7 @@ export class Vulnerabilities {
}
logger.debug(
`Vulnerability ${vulnerability.id} affects ${packageName} ${depVersion}`
`Vulnerability ${osvVulnerability.id} affects ${packageName} ${depVersion}`
);
const fixedVersion = this.getFixedVersion(
ecosystem,
@ -179,39 +219,27 @@ export class Vulnerabilities {
affected,
versioningApi
);
if (is.nullOrUndefined(fixedVersion)) {
logger.info(
`No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
);
continue;
}
logger.debug(
`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
);
const rule = this.convertToPackageRule(
packageFileConfig,
dep,
vulnerabilities.push({
packageName,
vulnerability: osvVulnerability,
affected,
depVersion,
fixedVersion,
vulnerability,
affected
);
packageRules.push(rule);
datasource: dep.datasource!,
packageFileConfig,
});
}
}
this.sortByFixedVersion(packageRules, versioningApi);
return { vulnerabilities, versioningApi };
} catch (err) {
logger.warn(
{ err },
`Error fetching vulnerability information for ${packageName}`
);
return [];
return null;
}
return packageRules;
}
private sortByFixedVersion(
@ -223,7 +251,6 @@ export class Vulnerabilities {
const version = rule.allowedVersions as string;
versionsCleaned[version] = version.replace(regEx(/[=> ]+/g), '');
}
packageRules.sort((a, b) =>
versioningApi.sortVersions(
versionsCleaned[a.allowedVersions as string],
@ -407,17 +434,28 @@ export class Vulnerabilities {
);
}
private convertToPackageRule(
packageFileConfig: RenovateConfig & PackageFile,
dep: PackageDependency,
packageName: string,
depVersion: string,
fixedVersion: string,
vulnerability: Osv.Vulnerability,
affected: Osv.Affected
): PackageRule {
private vulnerabilityToPackageRules(vul: Vulnerability): PackageRule | null {
const {
vulnerability,
affected,
packageName,
depVersion,
fixedVersion,
datasource,
packageFileConfig,
} = vul;
if (is.nullOrUndefined(fixedVersion)) {
logger.info(
`No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
);
return null;
}
logger.debug(
`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
);
return {
matchDatasources: [dep.datasource!],
matchDatasources: [datasource],
matchPackageNames: [packageName],
matchCurrentVersion: depVersion,
allowedVersions: fixedVersion,