mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-12 06:56:24 +00:00
refactor(vulnerabilities): return interface for Vulnerabilities (#21310)
This commit is contained in:
parent
2a72f85cc2
commit
75a1ab04eb
3 changed files with 201 additions and 61 deletions
19
lib/workers/repository/process/types.ts
Normal file
19
lib/workers/repository/process/types.ts
Normal 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[];
|
||||||
|
}
|
|
@ -36,6 +36,89 @@ describe('workers/repository/process/vulnerabilities', () => {
|
||||||
describe('fetchVulnerabilities()', () => {
|
describe('fetchVulnerabilities()', () => {
|
||||||
let config: RenovateConfig;
|
let config: RenovateConfig;
|
||||||
let vulnerabilities: Vulnerabilities;
|
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 = {
|
const lodashVulnerability: Osv.Vulnerability = {
|
||||||
id: 'GHSA-x5rq-j2xg-h7qm',
|
id: 'GHSA-x5rq-j2xg-h7qm',
|
||||||
modified: '',
|
modified: '',
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { sanitizeMarkdown } from '../../../util/markdown';
|
import { sanitizeMarkdown } from '../../../util/markdown';
|
||||||
import * as p from '../../../util/promises';
|
import * as p from '../../../util/promises';
|
||||||
import { regEx } from '../../../util/regex';
|
import { regEx } from '../../../util/regex';
|
||||||
|
import type { DependencyVulnerabilities, Vulnerability } from './types';
|
||||||
|
|
||||||
export class Vulnerabilities {
|
export class Vulnerabilities {
|
||||||
private osvOffline: OsvOffline | undefined;
|
private osvOffline: OsvOffline | undefined;
|
||||||
|
@ -54,67 +55,104 @@ export class Vulnerabilities {
|
||||||
config: RenovateConfig,
|
config: RenovateConfig,
|
||||||
packageFiles: Record<string, PackageFile[]>
|
packageFiles: Record<string, PackageFile[]>
|
||||||
): Promise<void> {
|
): 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 managers = Object.keys(packageFiles);
|
||||||
const allManagerJobs = managers.map((manager) =>
|
const allManagerJobs = managers.map((manager) =>
|
||||||
this.fetchManagerVulnerabilities(config, packageFiles, manager)
|
this.fetchManagerVulnerabilities(config, packageFiles, manager)
|
||||||
);
|
);
|
||||||
await Promise.all(allManagerJobs);
|
return (await Promise.all(allManagerJobs)).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchManagerVulnerabilities(
|
private async fetchManagerVulnerabilities(
|
||||||
config: RenovateConfig,
|
config: RenovateConfig,
|
||||||
packageFiles: Record<string, PackageFile[]>,
|
packageFiles: Record<string, PackageFile[]>,
|
||||||
manager: string
|
manager: string
|
||||||
): Promise<void> {
|
): Promise<DependencyVulnerabilities[]> {
|
||||||
const managerConfig = getManagerConfig(config, manager);
|
const managerConfig = getManagerConfig(config, manager);
|
||||||
const queue = packageFiles[manager].map(
|
const queue = packageFiles[manager].map(
|
||||||
(pFile) => (): Promise<void> =>
|
(pFile) => (): Promise<DependencyVulnerabilities[]> =>
|
||||||
this.fetchManagerPackageFileVulnerabilities(
|
this.fetchManagerPackageFileVulnerabilities(managerConfig, pFile)
|
||||||
config,
|
|
||||||
managerConfig,
|
|
||||||
pFile
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
logger.trace(
|
logger.trace(
|
||||||
{ manager, queueLength: queue.length },
|
{ manager, queueLength: queue.length },
|
||||||
'fetchManagerUpdates starting'
|
'fetchManagerVulnerabilities starting'
|
||||||
);
|
);
|
||||||
await p.all(queue);
|
const result = (await p.all(queue)).flat();
|
||||||
logger.trace({ manager }, 'fetchManagerUpdates finished');
|
logger.trace({ manager }, 'fetchManagerVulnerabilities finished');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchManagerPackageFileVulnerabilities(
|
private async fetchManagerPackageFileVulnerabilities(
|
||||||
config: RenovateConfig,
|
|
||||||
managerConfig: RenovateConfig,
|
managerConfig: RenovateConfig,
|
||||||
pFile: PackageFile
|
pFile: PackageFile
|
||||||
): Promise<void> {
|
): Promise<DependencyVulnerabilities[]> {
|
||||||
const { packageFile } = pFile;
|
const { packageFile } = pFile;
|
||||||
const packageFileConfig = mergeChildConfig(managerConfig, pFile);
|
const packageFileConfig = mergeChildConfig(managerConfig, pFile);
|
||||||
const { manager } = packageFileConfig;
|
const { manager } = packageFileConfig;
|
||||||
const queue = pFile.deps.map(
|
const queue = pFile.deps.map(
|
||||||
(dep) => (): Promise<PackageRule[]> =>
|
(dep) => (): Promise<DependencyVulnerabilities | null> =>
|
||||||
this.fetchDependencyVulnerabilities(packageFileConfig, dep)
|
this.fetchDependencyVulnerability(packageFileConfig, dep)
|
||||||
);
|
);
|
||||||
logger.trace(
|
logger.trace(
|
||||||
{ manager, packageFile, queueLength: queue.length },
|
{ manager, packageFile, queueLength: queue.length },
|
||||||
'fetchManagerPackageFileVulnerabilities starting with concurrency'
|
'fetchManagerPackageFileVulnerabilities starting with concurrency'
|
||||||
);
|
);
|
||||||
|
|
||||||
config.packageRules?.push(...(await p.all(queue)).flat());
|
const result = await p.all(queue);
|
||||||
logger.trace(
|
logger.trace(
|
||||||
{ packageFile },
|
{ packageFile },
|
||||||
'fetchManagerPackageFileVulnerabilities finished'
|
'fetchManagerPackageFileVulnerabilities finished'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return result.filter(is.truthy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchDependencyVulnerabilities(
|
private async fetchDependencyVulnerability(
|
||||||
packageFileConfig: RenovateConfig & PackageFile,
|
packageFileConfig: RenovateConfig & PackageFile,
|
||||||
dep: PackageDependency
|
dep: PackageDependency
|
||||||
): Promise<PackageRule[]> {
|
): Promise<DependencyVulnerabilities | null> {
|
||||||
const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource!];
|
const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource!];
|
||||||
if (!ecosystem) {
|
if (!ecosystem) {
|
||||||
logger.trace(`Cannot map datasource ${dep.datasource!} to OSV ecosystem`);
|
logger.trace(`Cannot map datasource ${dep.datasource!} to OSV ecosystem`);
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let packageName = dep.packageName ?? dep.depName!;
|
let packageName = dep.packageName ?? dep.depName!;
|
||||||
|
@ -123,20 +161,19 @@ export class Vulnerabilities {
|
||||||
packageName = packageName.toLowerCase().replace(regEx(/[_.-]+/g), '-');
|
packageName = packageName.toLowerCase().replace(regEx(/[_.-]+/g), '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageRules: PackageRule[] = [];
|
|
||||||
try {
|
try {
|
||||||
const vulnerabilities = await this.osvOffline?.getVulnerabilities(
|
const osvVulnerabilities = await this.osvOffline?.getVulnerabilities(
|
||||||
ecosystem,
|
ecosystem,
|
||||||
packageName
|
packageName
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
is.nullOrUndefined(vulnerabilities) ||
|
is.nullOrUndefined(osvVulnerabilities) ||
|
||||||
is.emptyArray(vulnerabilities)
|
is.emptyArray(osvVulnerabilities)
|
||||||
) {
|
) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`No vulnerabilities found in OSV database for ${packageName}`
|
`No vulnerabilities found in OSV database for ${packageName}`
|
||||||
);
|
);
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const depVersion =
|
const depVersion =
|
||||||
|
@ -149,16 +186,19 @@ export class Vulnerabilities {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`
|
`Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`
|
||||||
);
|
);
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const vulnerability of vulnerabilities) {
|
const vulnerabilities: Vulnerability[] = [];
|
||||||
if (vulnerability.withdrawn) {
|
for (const osvVulnerability of osvVulnerabilities) {
|
||||||
logger.trace(`Skipping withdrawn vulnerability ${vulnerability.id}`);
|
if (osvVulnerability.withdrawn) {
|
||||||
|
logger.trace(
|
||||||
|
`Skipping withdrawn vulnerability ${osvVulnerability.id}`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const affected of vulnerability.affected ?? []) {
|
for (const affected of osvVulnerability.affected ?? []) {
|
||||||
const isVulnerable = this.isPackageVulnerable(
|
const isVulnerable = this.isPackageVulnerable(
|
||||||
ecosystem,
|
ecosystem,
|
||||||
packageName,
|
packageName,
|
||||||
|
@ -171,7 +211,7 @@ export class Vulnerabilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Vulnerability ${vulnerability.id} affects ${packageName} ${depVersion}`
|
`Vulnerability ${osvVulnerability.id} affects ${packageName} ${depVersion}`
|
||||||
);
|
);
|
||||||
const fixedVersion = this.getFixedVersion(
|
const fixedVersion = this.getFixedVersion(
|
||||||
ecosystem,
|
ecosystem,
|
||||||
|
@ -179,39 +219,27 @@ export class Vulnerabilities {
|
||||||
affected,
|
affected,
|
||||||
versioningApi
|
versioningApi
|
||||||
);
|
);
|
||||||
if (is.nullOrUndefined(fixedVersion)) {
|
|
||||||
logger.info(
|
|
||||||
`No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
vulnerabilities.push({
|
||||||
`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`
|
|
||||||
);
|
|
||||||
const rule = this.convertToPackageRule(
|
|
||||||
packageFileConfig,
|
|
||||||
dep,
|
|
||||||
packageName,
|
packageName,
|
||||||
|
vulnerability: osvVulnerability,
|
||||||
|
affected,
|
||||||
depVersion,
|
depVersion,
|
||||||
fixedVersion,
|
fixedVersion,
|
||||||
vulnerability,
|
datasource: dep.datasource!,
|
||||||
affected
|
packageFileConfig,
|
||||||
);
|
});
|
||||||
packageRules.push(rule);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortByFixedVersion(packageRules, versioningApi);
|
return { vulnerabilities, versioningApi };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ err },
|
{ err },
|
||||||
`Error fetching vulnerability information for ${packageName}`
|
`Error fetching vulnerability information for ${packageName}`
|
||||||
);
|
);
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return packageRules;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortByFixedVersion(
|
private sortByFixedVersion(
|
||||||
|
@ -223,7 +251,6 @@ export class Vulnerabilities {
|
||||||
const version = rule.allowedVersions as string;
|
const version = rule.allowedVersions as string;
|
||||||
versionsCleaned[version] = version.replace(regEx(/[=> ]+/g), '');
|
versionsCleaned[version] = version.replace(regEx(/[=> ]+/g), '');
|
||||||
}
|
}
|
||||||
|
|
||||||
packageRules.sort((a, b) =>
|
packageRules.sort((a, b) =>
|
||||||
versioningApi.sortVersions(
|
versioningApi.sortVersions(
|
||||||
versionsCleaned[a.allowedVersions as string],
|
versionsCleaned[a.allowedVersions as string],
|
||||||
|
@ -407,17 +434,28 @@ export class Vulnerabilities {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertToPackageRule(
|
private vulnerabilityToPackageRules(vul: Vulnerability): PackageRule | null {
|
||||||
packageFileConfig: RenovateConfig & PackageFile,
|
const {
|
||||||
dep: PackageDependency,
|
vulnerability,
|
||||||
packageName: string,
|
affected,
|
||||||
depVersion: string,
|
packageName,
|
||||||
fixedVersion: string,
|
depVersion,
|
||||||
vulnerability: Osv.Vulnerability,
|
fixedVersion,
|
||||||
affected: Osv.Affected
|
datasource,
|
||||||
): PackageRule {
|
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 {
|
return {
|
||||||
matchDatasources: [dep.datasource!],
|
matchDatasources: [datasource],
|
||||||
matchPackageNames: [packageName],
|
matchPackageNames: [packageName],
|
||||||
matchCurrentVersion: depVersion,
|
matchCurrentVersion: depVersion,
|
||||||
allowedVersions: fixedVersion,
|
allowedVersions: fixedVersion,
|
||||||
|
|
Loading…
Reference in a new issue