diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 6b85afa5b1131f..dc10180b133e20 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -4062,3 +4062,18 @@ To disable the vulnerability alerts feature, set `enabled=false` in a `vulnerabi !!! note If you want to raise only vulnerability fix PRs, you may use the `security:only-security-updates` preset. + +### vulnerabilityFixStrategy + +When a vulnerability fix is available, Renovate will default to picking the lowest fixed version (`vulnerabilityFixStrategy=lowest`). +For example, if the current version is `1.0.0`, and a vulnerability is fixed in `1.1.0`, while the latest version is `1.2.0`, then Renovate will propose an update to `1.1.0` as the vulnerability fix. + +If `vulnerabilityFixStrategy=highest` is configured then Renovate will use its normal strategy for picking upgrades, e.g. in the above example it will propose an update to `1.2.0` to fix the vulnerability. + +```json title="Setting vulnerabilityFixStrategy to highest" +{ + "vulnerabilityAlerts": { + "vulnerabilityFixStrategy": "highest" + } +} +``` diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index aa3751a231d670..38b31a45c5b3b1 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1968,12 +1968,22 @@ const options: RenovateOptions[] = [ commitMessageSuffix: '[SECURITY]', branchTopic: `{{{datasource}}}-{{{depNameSanitized}}}-vulnerability`, prCreation: 'immediate', + vulnerabilityFixStrategy: 'lowest', }, mergeable: true, cli: false, env: false, supportedPlatforms: ['github'], }, + { + name: 'vulnerabilityFixStrategy', + description: + 'Strategy to use when fixing vulnerabilities. `lowest` will use the lowest fixed version, `highest` will use the highest fixed version.', + type: 'string', + allowedValues: ['lowest', 'highest'], + default: 'lowest', + parents: ['vulnerabilityAlerts'], + }, { name: 'osvVulnerabilityAlerts', description: 'Use vulnerability alerts from `osv.dev`.', diff --git a/lib/config/types.ts b/lib/config/types.ts index fc26ae82b380de..6fccc8f885eb32 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -391,12 +391,13 @@ export interface ValidationMessage { } export type AllowedParents = - | 'customManagers' | 'customDatasources' + | 'customManagers' | 'hostRules' - | 'postUpgradeTasks' + | 'logLevelRemap' | 'packageRules' - | 'logLevelRemap'; + | 'postUpgradeTasks' + | 'vulnerabilityAlerts'; export interface RenovateOptionBase { /** * If true, the option can only be configured by people with access to the Renovate instance. diff --git a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap index d42cf0cd1f76ab..1d7868498bb0b0 100644 --- a/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap +++ b/lib/workers/repository/init/__snapshots__/vulnerability.spec.ts.snap @@ -12,6 +12,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], + "vulnerabilityFixStrategy": "lowest", }, "isVulnerabilityAlert": true, "matchDatasources": [ @@ -46,6 +47,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], + "vulnerabilityFixStrategy": "lowest", }, "isVulnerabilityAlert": true, "matchDatasources": [ @@ -80,6 +82,7 @@ exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() retur "prCreation": "immediate", "rangeStrategy": "update-lockfile", "schedule": [], + "vulnerabilityFixStrategy": "lowest", }, "isVulnerabilityAlert": true, "matchDatasources": [ diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index 56d4b740c693c3..11841cec4b9914 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -793,7 +793,7 @@ describe('workers/repository/process/lookup/index', () => { ]); }); - it('uses minimum version for vulnerabilityAlerts', async () => { + it('uses lowest version by default for vulnerabilityAlerts', async () => { config.currentValue = '1.0.0'; config.isVulnerabilityAlert = true; config.packageName = 'q'; @@ -818,6 +818,32 @@ describe('workers/repository/process/lookup/index', () => { ]); }); + it('uses highest version for vulnerabilityAlerts when vulnerabilityFixStrategy=highest', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixStrategy = 'highest'; + 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: 4, + newPatch: 1, + newValue: '1.4.1', + newVersion: '1.4.1', + releaseTimestamp: expect.any(String), + updateType: 'minor', + }, + ]); + }); + it('uses vulnerabilityFixVersion', async () => { config.currentValue = '1.0.0'; config.isVulnerabilityAlert = true; @@ -844,6 +870,33 @@ describe('workers/repository/process/lookup/index', () => { ]); }); + it('takes highest verion when using vulnerabilityFixStrategy=highest with vulnerabilityFixVersion', async () => { + config.currentValue = '1.0.0'; + config.isVulnerabilityAlert = true; + config.vulnerabilityFixVersion = '1.1.0'; + config.vulnerabilityFixStrategy = 'highest'; + 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: 4, + newPatch: 1, + newValue: '1.4.1', + newVersion: '1.4.1', + releaseTimestamp: expect.any(String), + updateType: 'minor', + }, + ]); + }); + it('ignores vulnerabilityFixVersion if not a version', async () => { config.currentValue = '1.0.0'; config.isVulnerabilityAlert = true; diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts index 9b4ecc3a24a81d..f567e4ed5b78af 100644 --- a/lib/workers/repository/process/lookup/index.ts +++ b/lib/workers/repository/process/lookup/index.ts @@ -373,6 +373,7 @@ export async function lookupUpdates( let shrinkedViaVulnerability = false; if (config.isVulnerabilityAlert) { if (config.vulnerabilityFixVersion) { + res.vulnerabilityFixStrategy = config.vulnerabilityFixStrategy; res.vulnerabilityFixVersion = config.vulnerabilityFixVersion; if (versioning.isVersion(config.vulnerabilityFixVersion)) { // Filter out versions if the vulnerabilityFixVersion is higher @@ -406,12 +407,19 @@ export async function lookupUpdates( ); } } - filteredReleases = filteredReleases.slice(0, 1); - shrinkedViaVulnerability = true; - logger.debug( - { filteredReleases }, - 'Vulnerability alert found: limiting results to a single release', - ); + if (config.vulnerabilityFixStrategy === 'highest') { + // Don't shrink the list of releases - let Renovate use its normal logic + logger.once.debug( + `Using vulnerabilityFixStrategy=highest for ${config.packageName}`, + ); + } else { + // Shrink the list of releases to the lowest fixed version + logger.once.debug( + `Using vulnerabilityFixStrategy=lowest for ${config.packageName}`, + ); + filteredReleases = filteredReleases.slice(0, 1); + shrinkedViaVulnerability = true; + } } const buckets: Record = {}; for (const release of filteredReleases) { diff --git a/lib/workers/repository/process/lookup/types.ts b/lib/workers/repository/process/lookup/types.ts index aaec2a405d532e..03ad7f94942f39 100644 --- a/lib/workers/repository/process/lookup/types.ts +++ b/lib/workers/repository/process/lookup/types.ts @@ -50,6 +50,7 @@ export interface LookupUpdateConfig replacementVersion?: string; extractVersion?: string; vulnerabilityFixVersion?: string; + vulnerabilityFixStrategy?: string; } export interface UpdateResult { @@ -70,4 +71,5 @@ export interface UpdateResult { versioning?: string; currentVersionTimestamp?: string; vulnerabilityFixVersion?: string; + vulnerabilityFixStrategy?: string; }