From 5e28c4f713dac05bca4c40338bc15c130abc0660 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Wed, 7 Oct 2026 11:45:09 +0200 Subject: [PATCH 1/2] fix(fix): fail when Coana leaves computed fixes unapplied socket fix reported success whenever Coana exited 0, and hardcoded fixedAll: true. Coana now lists fixes it could not apply under notApplied in its --output-file and exits 5. socket fix treats that exit as a result rather than a crash: it still copies the output file, then fails with each unapplied fix and its reason, and sets fixedAll to false. In PR mode it warns and opens a PR for whatever was applied. Refs ENG-5437 --- src/commands/fix/coana-fix.mts | 60 ++++++++-- src/commands/fix/handle-fix-limit.test.mts | 123 +++++++++++++++++++++ 2 files changed, 176 insertions(+), 7 deletions(-) diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index 6b9191490..7d1ec971e 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -251,6 +251,35 @@ function readWrittenFiles(outputFile: string): Set | undefined { type CoanaFixResult = CResult<{ fixedAll: boolean; ghsaDetails: unknown[] }> +// Coana exits with this code after writing an --output-file that lists computed fixes it left unapplied. +const COANA_FIXES_NOT_APPLIED_EXIT_CODE = 5 + +type NotAppliedFix = { + ghsa: string + purl: string + fixedVersion: string + reason: string +} + +function isFixesNotAppliedExit(result: CResult): boolean { + return ( + !result.ok && + (result.data as any)?.code === COANA_FIXES_NOT_APPLIED_EXIT_CODE + ) +} + +function readNotAppliedFixes(fixesResultJson: unknown): NotAppliedFix[] { + const notApplied = (fixesResultJson as { notApplied?: unknown } | null) + ?.notApplied + return Array.isArray(notApplied) ? (notApplied as NotAppliedFix[]) : [] +} + +function formatNotAppliedFixes(notApplied: NotAppliedFix[]): string { + return notApplied + .map(f => ` ${f.ghsa}: ${f.purl} -> ${f.fixedVersion}: ${f.reason}`) + .join('\n') +} + type GeneratedSocketFactsSlot = { generated?: GeneratedSocketFacts | undefined tmpDir: string @@ -542,13 +571,13 @@ async function coanaFixWithFacts( spinner?.stop() } - if (!fixCResult.ok) { + if (!fixCResult.ok && !isFixesNotAppliedExit(fixCResult)) { return fixCResult } // Read the temporary file to get the actual fixes result. const fixesResultJson = readJsonSync(tmpFile, { throws: false }) as - | { fixes?: Record } + | { type?: string; fixes?: Record } | null | undefined @@ -561,12 +590,23 @@ async function coanaFixWithFacts( await fs.writeFile(outputFile, tmpContent, 'utf8') } + const ghsaDetails = fixesResultJson ? [fixesResultJson] : [] + const notApplied = readNotAppliedFixes(fixesResultJson) + if (!fixCResult.ok || notApplied.length) { + return { + ok: false, + message: + fixesResultJson?.type === 'no-fixes-applied' + ? 'No computed fixes were applied' + : 'Some computed fixes were not applied', + cause: formatNotAppliedFixes(notApplied), + data: { fixedAll: false, ghsaDetails }, + } + } + return { ok: true, - data: { - fixedAll: true, - ghsaDetails: fixesResultJson ? [fixesResultJson] : [], - }, + data: { fixedAll: true, ghsaDetails }, } } finally { // Clean up the temporary file. @@ -718,7 +758,13 @@ async function coanaFixWithFacts( }, ) - if (!fixCResult.ok) { + if (isFixesNotAppliedExit(fixCResult)) { + if (!silence) { + logger.warn( + `Not every fix for ${ghsaId} was applied:\n${formatNotAppliedFixes(readNotAppliedFixes(readJsonSync(tmpFile, { throws: false })))}`, + ) + } + } else if (!fixCResult.ok) { if (!silence) { logger.error( `Update failed for ${ghsaId}: ${getErrorCause(fixCResult)}`, diff --git a/src/commands/fix/handle-fix-limit.test.mts b/src/commands/fix/handle-fix-limit.test.mts index 19da2b943..3f3803051 100644 --- a/src/commands/fix/handle-fix-limit.test.mts +++ b/src/commands/fix/handle-fix-limit.test.mts @@ -1,4 +1,6 @@ import { promises as fs } from 'node:fs' +import os from 'node:os' +import path from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -104,6 +106,25 @@ function mockDiscoveryEnvelope(envelope: { }) } +function mockFixOutput( + output: Record, + exitCode: number | undefined, +) { + mockSpawnCoanaDlx.mockImplementation(async (args: string[]) => { + await fs.writeFile( + args[args.indexOf('--output-file') + 1]!, + JSON.stringify(output), + ) + return exitCode === undefined + ? { ok: true, data: '' } + : { + ok: false, + data: { code: exitCode }, + message: `Coana command failed (exit code ${exitCode})`, + } + }) +} + describe('socket fix --pr-limit behavior verification', () => { const baseConfig: FixConfig = { all: false, @@ -567,6 +588,108 @@ describe('socket fix --pr-limit behavior verification', () => { }) }) + describe('fixes Coana did not apply', () => { + const notApplied = [ + { + ghsa: 'GHSA-1111-1111-1111', + purl: 'pkg:npm/lodash@4.17.20', + fixedVersion: '4.17.21', + reason: + 'Skipping upgrade for this directory: no supported lockfile found (.)', + }, + ] + + it('fails with the not-applied fixes when Coana applied none', async () => { + mockFixOutput({ type: 'no-fixes-applied', notApplied }, 5) + + const result = await coanaFix({ + ...baseConfig, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(result).toEqual({ + ok: false, + message: 'No computed fixes were applied', + cause: expect.stringContaining('no supported lockfile found'), + data: { + fixedAll: false, + ghsaDetails: [{ type: 'no-fixes-applied', notApplied }], + }, + }) + }) + + it('fails and still writes --output-file when only some fixes were applied', async () => { + const output = { + type: 'applied-fixes', + fixes: { + 'GHSA-2222-2222-2222': [ + { purl: 'pkg:npm/qs@6.5.2', fixedVersion: '6.5.3' }, + ], + }, + notApplied, + modifiedFiles: ['package-lock.json'], + } + mockFixOutput(output, 5) + const outputFile = path.join( + os.tmpdir(), + `socket-fix-not-applied-${Date.now()}.json`, + ) + + const result = await coanaFix({ + ...baseConfig, + ghsas: ['GHSA-1111-1111-1111', 'GHSA-2222-2222-2222'], + outputFile, + }) + + expect(result.ok).toBe(false) + expect(result.message).toBe('Some computed fixes were not applied') + expect(result.data).toEqual({ fixedAll: false, ghsaDetails: [output] }) + expect(JSON.parse(await fs.readFile(outputFile, 'utf8'))).toEqual(output) + await fs.rm(outputFile, { force: true }) + }) + + it('reports fixedAll when every computed fix was applied', async () => { + const output = { + type: 'applied-fixes', + fixes: { + 'GHSA-1111-1111-1111': [ + { purl: 'pkg:npm/lodash@4.17.20', fixedVersion: '4.17.21' }, + ], + }, + modifiedFiles: ['package-lock.json'], + } + mockFixOutput(output, undefined) + + const result = await coanaFix({ + ...baseConfig, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(result).toEqual({ + ok: true, + data: { fixedAll: true, ghsaDetails: [output] }, + }) + }) + + it('keeps other Coana failures as errors', async () => { + mockSpawnCoanaDlx.mockResolvedValue({ + ok: false, + data: { code: 2 }, + message: 'Coana command failed (exit code 2)', + }) + + const result = await coanaFix({ + ...baseConfig, + ghsas: ['GHSA-1111-1111-1111'], + }) + + expect(result).toMatchObject({ + ok: false, + message: 'Coana command failed (exit code 2)', + }) + }) + }) + describe('--id filtering in local mode', () => { it('should process all provided GHSA IDs in local mode (prLimit ignored)', async () => { const ghsas = [ From ba035369b5a3179a992144d436a74f546ad65be8 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Thu, 8 Oct 2026 07:47:56 +0200 Subject: [PATCH 2/2] chore(deps): update the Coana CLI to 15.12.2 15.12.2 is the first Coana release that reports unapplied fixes under notApplied and exits 5, so this turns on the new socket fix handling. Refs ENG-5437 --- CHANGELOG.md | 5 +++++ package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9bd82e0e..8a3dfaeee 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] + +### Changed +- Updated the Coana CLI to v `15.12.2`. + ## [1.5.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.5.0) - 2026-10-06 ### Added diff --git a/package.json b/package.json index 9065272d0..680971eca 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.12.1", + "@coana-tech/cli": "15.12.2", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14f7a1f4e..f7f0a7975 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,8 +138,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.12.1 - version: 15.12.1 + specifier: 15.12.2 + version: 15.12.2 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -827,8 +827,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.12.1': - resolution: {integrity: sha512-91NnY5K+6FT65S5wqteiLOY19vUqdYLcnndt6sVkMTvHbU9h+7Aq7uBx/lGSoYkhF7QN+7SqXGLJVlM//IN4tQ==} + '@coana-tech/cli@15.12.2': + resolution: {integrity: sha512-z7PwQbg6xggw3oFIZWcgsSo37VV57WrWkHqMZ/zvj2Qgw40D4uDIXSsPrU9f2DXgmz+eDEKnLl/lxfaYcB/q+w==} hasBin: true '@colors/colors@1.5.0': @@ -5702,7 +5702,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.12.1': {} + '@coana-tech/cli@15.12.2': {} '@colors/colors@1.5.0': optional: true