From dce69127d261f369af1b1d14ca6b8058042e41b7 Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Wed, 18 Sep 2024 12:10:21 +0200 Subject: [PATCH] feat: remediate to greater than or equal versions for github alerts (#31393) Co-authored-by: Michael Kriese --- lib/config/types.ts | 1 + lib/config/validation.ts | 1 + .../__snapshots__/vulnerability.spec.ts.snap | 6 +- lib/workers/repository/init/vulnerability.ts | 6 +- .../repository/process/lookup/index.spec.ts | 119 ++++++++++++++++++ .../repository/process/lookup/index.ts | 45 +++++++ .../repository/process/lookup/types.ts | 2 + 7 files changed, 172 insertions(+), 8 deletions(-) diff --git a/lib/config/types.ts b/lib/config/types.ts index fc26ae82b380de..2575c6f91b25f7 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -383,6 +383,7 @@ export interface PackageRule matchUpdateTypes?: UpdateType[]; registryUrls?: string[] | null; vulnerabilitySeverity?: string; + vulnerabilityFixVersion?: string; } export interface ValidationMessage { diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 620cbe190d5242..fdcf487a20a52f 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -64,6 +64,7 @@ const ignoredNodes = [ 'vulnerabilityAlertsOnly', 'vulnerabilityAlert', 'isVulnerabilityAlert', + 'vulnerabilityFixVersion', // not intended to be used by end users but may be by Mend apps 'copyLocalLibs', // deprecated - functionality is now enabled by default 'prBody', // deprecated 'minimumConfidence', // undocumented feature flag diff --git a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap index ae46fd5e4c4f06..d42cf0cd1f76ab 100644 --- a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap +++ b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap @@ -3,7 +3,6 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() returns go alerts 1`] = ` [ { - "allowedVersions": "1.8.3", "force": { "branchTopic": "{{{datasource}}}-{{{depNameSanitized}}}-vulnerability", "commitMessageSuffix": "[SECURITY]", @@ -30,6 +29,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur go", ], + "vulnerabilityFixVersion": "1.8.3", }, ] `; @@ -37,7 +37,6 @@ go", exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() returns maven alerts 1`] = ` [ { - "allowedVersions": "2.7.9.4", "force": { "branchTopic": "{{{datasource}}}-{{{depNameSanitized}}}-vulnerability", "commitMessageSuffix": "[SECURITY]", @@ -64,6 +63,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur An issue was discovered in FasterXML jackson-databind prior to 2.7.9.4, 2.8.11.2, and 2.9.6. When Default Typing is enabled (either globally or for a specific property), the service has the Jodd-db jar (for database access for the Jodd framework) in the classpath, and an attacker can provide an LDAP service to access, it is possible to make the service execute a malicious payload.", ], + "vulnerabilityFixVersion": "2.7.9.4", }, ] `; @@ -71,7 +71,6 @@ An issue was discovered in FasterXML jackson-databind prior to 2.7.9.4, 2.8.11.2 exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() returns pip alerts 1`] = ` [ { - "allowedVersions": "==2.2.1.0", "force": { "branchTopic": "{{{datasource}}}-{{{depNameSanitized}}}-vulnerability", "commitMessageSuffix": "[SECURITY]", @@ -113,6 +112,7 @@ Ansible before version 2.2.0 fails to properly sanitize fact variables sent from Ansible before versions 2.1.4, 2.2.1 is vulnerable to an improper input validation in Ansible's handling of data sent from client systems. An attacker with control over a client system being managed by Ansible and the ability to send facts back to the Ansible server could use this flaw to execute arbitrary code on the Ansible server using the Ansible server privileges.", ], + "vulnerabilityFixVersion": "2.2.1.0", }, ] `; diff --git a/lib/workers/repository/init/vulnerability.ts b/lib/workers/repository/init/vulnerability.ts index dbf8b04c6a7689..13f796020c9fe6 100644 --- a/lib/workers/repository/init/vulnerability.ts +++ b/lib/workers/repository/init/vulnerability.ts @@ -174,10 +174,6 @@ export async function detectVulnerabilityAlerts( logger.warn({ err }, 'Error generating vulnerability PR notes'); } // TODO: types (#22198) - const allowedVersions = - datasource === PypiDatasource.id - ? `==${val.firstPatchedVersion!}` - : val.firstPatchedVersion; const matchFileNames = datasource === GoDatasource.id ? [fileName.replace('go.sum', 'go.mod')] @@ -191,7 +187,7 @@ export async function detectVulnerabilityAlerts( // Remediate only direct dependencies matchRule = { ...matchRule, - allowedVersions, + vulnerabilityFixVersion: val.firstPatchedVersion, prBodyNotes, isVulnerabilityAlert: true, force: { diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index 3c67849975f522..b129d8a4b89c29 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -818,6 +818,125 @@ describe('workers/repository/process/lookup/index', () => { ]); }); + it('uses vulnerabilityFixVersion when a version', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = '1.1.0'; + config.packageName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + + expect(updates).toEqual([ + { + bucket: 'non-major', + newMajor: 1, + newMinor: 1, + newPatch: 0, + newValue: '1.1.0', + newVersion: '1.1.0', + releaseTimestamp: expect.any(String), + updateType: 'minor', + }, + ]); + }); + + it('takes a later release when vulnerabilityFixVersion does not exist', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = '1.0.2'; + config.packageName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + + expect(updates).toEqual([ + { + bucket: 'non-major', + newMajor: 1, + newMinor: 1, + newPatch: 0, + newValue: '1.1.0', + newVersion: '1.1.0', + releaseTimestamp: expect.any(String), + updateType: 'minor', + }, + ]); + }); + + it('uses vulnerabilityFixVersion when a range', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = '>= 1.1.0'; + config.packageName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + + expect(updates).toEqual([ + { + bucket: 'non-major', + newMajor: 1, + newMinor: 1, + newPatch: 0, + newValue: '1.1.0', + newVersion: '1.1.0', + releaseTimestamp: expect.any(String), + updateType: 'minor', + }, + ]); + }); + + it('ignores vulnerabilityFixVersion if not a version', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = 'abc'; + config.packageName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + + expect(updates).toEqual([ + { + bucket: 'non-major', + newMajor: 1, + newMinor: 0, + newPatch: 1, + newValue: '1.0.1', + newVersion: '1.0.1', + releaseTimestamp: expect.any(String), + updateType: 'patch', + }, + ]); + }); + + it('returns no results if vulnerabilityFixVersion is too high', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = '5.1.0'; + config.packageName = 'q'; + config.datasource = NpmDatasource.id; + httpMock.scope('https://registry.npmjs.org').get('/q').reply(200, qJson); + + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + + expect(updates).toBeEmptyArray(); + }); + it('supports minor and major upgrades for ranged versions', async () => { config.currentValue = '~0.4.0'; config.rangeStrategy = 'pin'; diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts index c4167937ac1fd1..a76163eeeda2be 100644 --- a/lib/workers/repository/process/lookup/index.ts +++ b/lib/workers/repository/process/lookup/index.ts @@ -372,6 +372,51 @@ export async function lookupUpdates( ); let shrinkedViaVulnerability = false; if (config.isVulnerabilityAlert) { + if (config.vulnerabilityFixVersion) { + res.vulnerabilityFixVersion = config.vulnerabilityFixVersion; + if (versioning.isValid(config.vulnerabilityFixVersion)) { + let fixedFilteredReleases; + if (versioning.isVersion(config.vulnerabilityFixVersion)) { + // Retain only releases greater than or equal to the fix version + fixedFilteredReleases = filteredReleases.filter( + (release) => + !versioning.isGreaterThan( + config.vulnerabilityFixVersion!, + release.version, + ), + ); + } else { + // Retain only releases which max the fix constraint + fixedFilteredReleases = filteredReleases.filter((release) => + versioning.matches( + release.version, + config.vulnerabilityFixVersion!, + ), + ); + } + // Warn if this filtering results caused zero releases + if (fixedFilteredReleases.length === 0 && filteredReleases.length) { + logger.warn( + { + releases: filteredReleases, + vulnerabilityFixVersion: config.vulnerabilityFixVersion, + packageName: config.packageName, + }, + 'No releases satisfy vulnerabilityFixVersion', + ); + } + // Use the additionally filtered releases + filteredReleases = fixedFilteredReleases; + } else { + logger.warn( + { + vulnerabilityFixVersion: config.vulnerabilityFixVersion, + packageName: config.packageName, + }, + 'vulnerabilityFixVersion is not valid', + ); + } + } filteredReleases = filteredReleases.slice(0, 1); shrinkedViaVulnerability = true; logger.debug( diff --git a/lib/workers/repository/process/lookup/types.ts b/lib/workers/repository/process/lookup/types.ts index ed944ea253d5e9..aaec2a405d532e 100644 --- a/lib/workers/repository/process/lookup/types.ts +++ b/lib/workers/repository/process/lookup/types.ts @@ -49,6 +49,7 @@ export interface LookupUpdateConfig replacementNameTemplate?: string; replacementVersion?: string; extractVersion?: string; + vulnerabilityFixVersion?: string; } export interface UpdateResult { @@ -68,4 +69,5 @@ export interface UpdateResult { warnings: ValidationMessage[]; versioning?: string; currentVersionTimestamp?: string; + vulnerabilityFixVersion?: string; }