From f7b0579ab5d89359206bed389d2a83a5166abed7 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Fri, 2 Oct 2026 06:49:25 +0200 Subject: [PATCH] feat(fix): add opt-in --allow-overrides flag socket fix can now write a package manager override when that is the only way to fix a vulnerability. This happens when a parent package declares a version range that rules out every fixed version of the vulnerable dependency. The flag is off by default. When it is set, socket fix passes --allow-overrides to Coana in both local and CI mode. Coana then writes an npm overrides, pnpm overrides, Yarn Berry resolutions or Rush globalOverrides entry scoped to the blocking parent. The forced version can sit outside the range the parent declares, so the parent should be tested afterwards. Coana only gets the flag when it is set, so runs keep working with Coana versions that do not know it yet. --- CHANGELOG.md | 5 ++ src/commands/fix/cmd-fix.integration.test.mts | 17 ++++++ src/commands/fix/cmd-fix.mts | 9 +++ .../coana-fix-dynamic-sbom-inference.test.mts | 1 + src/commands/fix/coana-fix-pr-files.test.mts | 1 + src/commands/fix/coana-fix.mts | 3 + src/commands/fix/handle-fix-limit.test.mts | 61 +++++++++++++++++++ src/commands/fix/handle-fix.mts | 3 + src/commands/fix/types.mts | 1 + 9 files changed, 101 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7178c76..95ae6c5dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased] + +### Added +- `socket fix --allow-overrides` fixes a vulnerability that a parent package's version range blocks by writing an override or resolution that forces the fixed version under that parent, in npm, pnpm, Yarn Berry and Rush projects. + ## [1.4.1](https://github.com/SocketDev/socket-cli/releases/tag/v1.4.1) - 2026-09-30 ### Changed diff --git a/src/commands/fix/cmd-fix.integration.test.mts b/src/commands/fix/cmd-fix.integration.test.mts index 0e364c379..03c64713c 100644 --- a/src/commands/fix/cmd-fix.integration.test.mts +++ b/src/commands/fix/cmd-fix.integration.test.mts @@ -164,6 +164,7 @@ describe('socket fix', async () => { Options --all Process all discovered vulnerabilities in local mode. Cannot be used with --id. + --allow-overrides When the only fix for a vulnerability is blocked by a parent package's declared version range, write an override or resolution that forces the fixed version under that parent. This can install a version outside the range the parent declares, so test the parent afterwards. Works for npm, pnpm, Yarn Berry and Rush projects. --autopilot Enable auto-merge for pull requests that Socket opens. See GitHub documentation (https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository) for managing auto-merge for pull requests in your repository. --debug Enable debug logging in the Coana-based Socket Fix CLI invocation. @@ -398,6 +399,22 @@ describe('socket fix', async () => { }, ) + cmdit( + [ + 'fix', + FLAG_DRY_RUN, + '--allow-overrides', + FLAG_CONFIG, + '{"apiToken":"fakeToken"}', + ], + 'should accept --allow-overrides flag', + async cmd => { + const { code, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Not saving"`) + expect(code, 'should exit with code 0').toBe(0) + }, + ) + cmdit( [ 'fix', diff --git a/src/commands/fix/cmd-fix.mts b/src/commands/fix/cmd-fix.mts index 684b8cde0..6ff0a6f7e 100644 --- a/src/commands/fix/cmd-fix.mts +++ b/src/commands/fix/cmd-fix.mts @@ -57,6 +57,12 @@ export const cmdFix = { } const generalFlags: MeowFlags = { + allowOverrides: { + type: 'boolean', + default: false, + description: + "When the only fix for a vulnerability is blocked by a parent package's declared version range, write an override or resolution that forces the fixed version under that parent. This can install a version outside the range the parent declares, so test the parent afterwards. Works for npm, pnpm, Yarn Berry and Rush projects.", + }, autopilot: { type: 'boolean', default: false, @@ -327,6 +333,7 @@ async function run( const { all, + allowOverrides, applyFixes, autopilot, debug, @@ -354,6 +361,7 @@ async function run( unknownFlags = [], } = cli.flags as { all: boolean + allowOverrides: boolean applyFixes: boolean autopilot: boolean debug: boolean @@ -520,6 +528,7 @@ async function run( await handleFix({ all, + allowOverrides, applyFixes, autopilot, coanaVersion: fixVersion, diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts index 7a5fddbae..19f001d0a 100644 --- a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -94,6 +94,7 @@ function coanaCalls(command: string): string[][] { describe('socket fix --dynamic-sbom-inference', () => { const baseConfig: FixConfig = { all: false, + allowOverrides: false, applyFixes: true, autopilot: false, coanaVersion: undefined, diff --git a/src/commands/fix/coana-fix-pr-files.test.mts b/src/commands/fix/coana-fix-pr-files.test.mts index 633610469..ae26aaa91 100644 --- a/src/commands/fix/coana-fix-pr-files.test.mts +++ b/src/commands/fix/coana-fix-pr-files.test.mts @@ -84,6 +84,7 @@ function committedFiles(): string[] { describe('socket fix PR mode commits', () => { const baseConfig: FixConfig = { all: false, + allowOverrides: false, applyFixes: true, autopilot: false, coanaVersion: undefined, diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index e76a91e33..6b9191490 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -276,6 +276,7 @@ async function coanaFixWithFacts( ): Promise { const { all, + allowOverrides, applyFixes, autopilot, coanaVersion, @@ -522,6 +523,7 @@ async function coanaFixWithFacts( ? ['--disable-external-tool-checks'] : []), ...(disableMajorUpdates ? ['--disable-major-updates'] : []), + ...(allowOverrides ? ['--allow-overrides'] : []), ...(showAffectedDirectDependencies ? ['--show-affected-direct-dependencies'] : []), @@ -699,6 +701,7 @@ async function coanaFixWithFacts( ? ['--disable-external-tool-checks'] : []), ...(disableMajorUpdates ? ['--disable-major-updates'] : []), + ...(allowOverrides ? ['--allow-overrides'] : []), ...(showAffectedDirectDependencies ? ['--show-affected-direct-dependencies'] : []), diff --git a/src/commands/fix/handle-fix-limit.test.mts b/src/commands/fix/handle-fix-limit.test.mts index bf83ecf21..19da2b943 100644 --- a/src/commands/fix/handle-fix-limit.test.mts +++ b/src/commands/fix/handle-fix-limit.test.mts @@ -107,6 +107,7 @@ function mockDiscoveryEnvelope(envelope: { describe('socket fix --pr-limit behavior verification', () => { const baseConfig: FixConfig = { all: false, + allowOverrides: false, applyFixes: true, autopilot: false, coanaVersion: undefined, @@ -691,4 +692,64 @@ describe('socket fix --pr-limit behavior verification', () => { ]) }) }) + + describe('--allow-overrides flag', () => { + it('forwards --allow-overrides to coana in local mode', async () => { + mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: 'fix applied' }) + + await coanaFix({ + ...baseConfig, + allowOverrides: true, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(mockSpawnCoanaDlx).toHaveBeenCalledTimes(1) + const callArgs = mockSpawnCoanaDlx.mock.calls[0]?.[0] as string[] + expect(callArgs[0]).toBe('compute-fixes-and-upgrade-purls') + expect(callArgs).toContain('--allow-overrides') + }) + + it('omits --allow-overrides when the flag is not set', async () => { + mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: 'fix applied' }) + + await coanaFix({ + ...baseConfig, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(mockSpawnCoanaDlx).toHaveBeenCalledTimes(1) + const callArgs = mockSpawnCoanaDlx.mock.calls[0]?.[0] as string[] + expect(callArgs).not.toContain('--allow-overrides') + }) + + it('forwards --allow-overrides to coana in PR mode', async () => { + mockGetFixEnv.mockResolvedValue({ + baseBranch: 'main', + githubToken: 'test-token', + gitEmail: 'test@example.com', + gitUser: 'test-user', + isCi: true, + repoInfo: { + defaultBranch: 'main', + owner: 'test-owner', + repo: 'test-repo', + }, + }) + mockGetSocketFixPrs.mockResolvedValue([]) + mockFetchGhsaDetails.mockResolvedValue(new Map()) + mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: 'fix applied' }) + + await coanaFix({ + ...baseConfig, + allowOverrides: true, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(mockSpawnCoanaDlx).toHaveBeenCalledTimes(1) + const callArgs = mockSpawnCoanaDlx.mock.calls[0]?.[0] as string[] + expect(callArgs[0]).toBe('compute-fixes-and-upgrade-purls') + expect(callArgs).toContain('GHSA-1111-1111-1111') + expect(callArgs).toContain('--allow-overrides') + }) + }) }) diff --git a/src/commands/fix/handle-fix.mts b/src/commands/fix/handle-fix.mts index dbf83ba9b..65f0617f6 100644 --- a/src/commands/fix/handle-fix.mts +++ b/src/commands/fix/handle-fix.mts @@ -115,6 +115,7 @@ export async function convertIdsToGhsas( export async function handleFix({ all, + allowOverrides, applyFixes, autopilot, coanaVersion, @@ -145,6 +146,7 @@ export async function handleFix({ debugFn('notice', `Starting fix command for ${orgSlug}`) debugDir('inspect', { all, + allowOverrides, applyFixes, autopilot, coanaVersion, @@ -174,6 +176,7 @@ export async function handleFix({ await outputFixResult( await coanaFix({ all, + allowOverrides, applyFixes, autopilot, coanaVersion, diff --git a/src/commands/fix/types.mts b/src/commands/fix/types.mts index a14500fc8..e3a70d79e 100644 --- a/src/commands/fix/types.mts +++ b/src/commands/fix/types.mts @@ -4,6 +4,7 @@ import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type FixConfig = { all: boolean + allowOverrides: boolean applyFixes: boolean autopilot: boolean coanaVersion: string | undefined