diff --git a/CHANGELOG.md b/CHANGELOG.md index c22c0cda3..68791342b 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/). +## [1.1.79](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.79) - 2026-04-08 + +### Changed +- Updated the Coana CLI to v `14.12.205`. + ## [1.1.78](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.78) - 2026-04-01 ### Fixed diff --git a/package.json b/package.json index 37e709c4d..69fd7bad5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.78", + "version": "1.1.79", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", @@ -97,7 +97,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "14.12.201", + "@coana-tech/cli": "14.12.205", "@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 294a09192..ddc7538ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 14.12.201 - version: 14.12.201 + specifier: 14.12.205 + version: 14.12.205 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -749,8 +749,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@14.12.201': - resolution: {integrity: sha512-uX6zaR+mHh6Gkg4ssNEVGq9N8deh/KPLsB1hr5Po9DKnZ+adB5WWYdJuFIOTWK+N0k5fcHSGP9pGje9sIPQ6ow==} + '@coana-tech/cli@14.12.205': + resolution: {integrity: sha512-+iprCFeKFpD3kOkPd9Xje568XVzmAPYOFqiGqfYnPcd0PiaeD/yvPix1qDXhImaFhEX3J2vH8eZmDM3hz0jXpA==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@14.12.201': {} + '@coana-tech/cli@14.12.205': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/scan/cmd-scan-reach.e2e.test.mts b/src/commands/scan/cmd-scan-reach.e2e.test.mts index 220f409e2..8abb519b8 100644 --- a/src/commands/scan/cmd-scan-reach.e2e.test.mts +++ b/src/commands/scan/cmd-scan-reach.e2e.test.mts @@ -226,11 +226,33 @@ function findReachabilityForGhsa( * Logs stdout and stderr to help diagnose test failures. */ function logCommandOutput(code: number, stdout: string, stderr: string): void { - logger.error(`Command failed with code ${code}`) + logger.error(`Command exited with code ${code}`) logger.error('stdout:', stdout) logger.error('stderr:', stderr) } +/** + * Log reachability entries that have type "error" for debugging. + * Helps diagnose Coana analysis failures in CI. + */ +function logReachabilityErrors(facts: SocketFactsJson): void { + for (const component of facts.components) { + if (!component.reachability) { + continue + } + for (const ghsaEntry of component.reachability) { + for (const entry of ghsaEntry.reachability) { + if (entry.type === 'error') { + logger.error( + `Reachability error for ${ghsaEntry.ghsa_id} in ${component.name}@${component.version} ` + + `(subproject: ${entry.subprojectPath}): ${JSON.stringify(entry)}`, + ) + } + } + } + } +} + describe('socket scan reach (E2E tests)', async () => { const { binCliPath } = constants // Standard timeout for most tests. @@ -386,8 +408,13 @@ describe('socket scan reach (E2E tests)', async () => { logger.info('\nReachability analysis completed successfully') } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) + logCommandOutput(code, stdout, stderr) + // Log reachability errors from the facts file if it was parsed. + try { + const errorFacts = await readSocketFactsJson(tempFixture.path) + logReachabilityErrors(errorFacts) + } catch { + // Facts file may not exist if the failure was earlier. } throw e } finally { @@ -483,9 +510,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with excluded paths completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -595,9 +620,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with target restriction completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -673,9 +696,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with --cwd flag completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -771,9 +792,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with --cwd and target completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -915,9 +934,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis output location verified successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -1019,9 +1036,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with pypi ecosystem filter completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() @@ -1119,9 +1134,7 @@ describe('socket scan reach (E2E tests)', async () => { '\nReachability analysis with npm ecosystem filter completed successfully', ) } catch (e) { - if (code !== 0) { - logCommandOutput(code, stdout, stderr) - } + logCommandOutput(code, stdout, stderr) throw e } finally { await tempFixture.cleanup() diff --git a/src/commands/scan/output-scan-reach.mts b/src/commands/scan/output-scan-reach.mts index c177ef5b3..4f1bdb860 100644 --- a/src/commands/scan/output-scan-reach.mts +++ b/src/commands/scan/output-scan-reach.mts @@ -1,6 +1,8 @@ import { logger } from '@socketsecurity/registry/lib/logger' +import { pluralize } from '@socketsecurity/registry/lib/words' import constants from '../../constants.mts' +import { extractReachabilityErrors } from '../../utils/coana.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' @@ -29,4 +31,20 @@ export async function outputScanReach( logger.log('') logger.success('Reachability analysis completed successfully!') logger.info(`Reachability report has been written to: ${actualOutputPath}`) + + // Warn about individual vulnerabilities where reachability analysis errored. + const errors = extractReachabilityErrors( + result.data.reachabilityReport, + ) + if (errors.length) { + logger.log('') + logger.warn( + `Reachability analysis returned ${errors.length} ${pluralize('error', errors.length)} for individual ${pluralize('vulnerability', errors.length)}:`, + ) + for (const err of errors) { + logger.warn( + ` - ${err.ghsaId} in ${err.componentName}@${err.componentVersion} (${err.subprojectPath})`, + ) + } + } } diff --git a/src/utils/coana.mts b/src/utils/coana.mts index 42726be2c..c80d66fe4 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -13,6 +13,59 @@ import { readJsonSync } from '@socketsecurity/registry/lib/fs' +export type ReachabilityError = { + componentName: string + componentVersion: string + ghsaId: string + subprojectPath: string +} + +export function extractReachabilityErrors( + socketFactsFile: string, +): ReachabilityError[] { + const json = readJsonSync(socketFactsFile, { throws: false }) as + | { + components?: Array<{ + name?: string + reachability?: Array<{ + ghsa_id?: string + reachability?: Array<{ + subprojectPath?: string + type?: string + }> + }> + version?: string + }> + } + | null + | undefined + if (!json || !Array.isArray(json.components)) { + return [] + } + const errors: ReachabilityError[] = [] + for (const component of json.components) { + if (!Array.isArray(component.reachability)) { + continue + } + for (const ghsaEntry of component.reachability) { + if (!Array.isArray(ghsaEntry.reachability)) { + continue + } + for (const entry of ghsaEntry.reachability) { + if (entry.type === 'error') { + errors.push({ + componentName: String(component.name ?? ''), + componentVersion: String(component.version ?? ''), + ghsaId: String(ghsaEntry.ghsa_id ?? ''), + subprojectPath: String(entry.subprojectPath ?? ''), + }) + } + } + } + } + return errors +} + export function extractTier1ReachabilityScanId( socketFactsFile: string, ): string | undefined {