diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ff8b791..52b719856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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.10.13`. + +### Fixed +- `socket scan reach` again names the workspace a reachability error came from, instead of reporting every error against the repository root. + ## [1.1.156](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.156) - 2026-08-10 ### Changed diff --git a/package.json b/package.json index 8f27e357f..d20d77001 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.10.10", + "@coana-tech/cli": "15.10.13", "@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 b791af898..f289a4d28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,8 +135,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.10.10 - version: 15.10.10 + specifier: 15.10.13 + version: 15.10.13 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -824,8 +824,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.10.10': - resolution: {integrity: sha512-OSyoiAdv4i/yk/wSWh1oZmKXA7LZRnt62oudq1lxZMiBjsSPg384PAwyFJGXlYk/a9FsMiqEOGXZd6eb2lpFrw==} + '@coana-tech/cli@15.10.13': + resolution: {integrity: sha512-PTZ0nhh+afFqxV33++b3Zo17v2VepTbu7rx0h4Z8056c1p/J7J+YzfyNuv3FZDTEQhIJWLV0mXdi1NLzbswsrg==} hasBin: true '@colors/colors@1.5.0': @@ -5698,7 +5698,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.10.10': {} + '@coana-tech/cli@15.10.13': {} '@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 46b84ecb6..3ca0ced0a 100644 --- a/src/commands/scan/cmd-scan-reach.e2e.test.mts +++ b/src/commands/scan/cmd-scan-reach.e2e.test.mts @@ -327,14 +327,21 @@ describe('socket scan reach (E2E tests)', async () => { expect(Array.isArray(facts.components)).toBe(true) expect(Array.isArray(facts.workspaceDiagnostics)).toBe(true) - // Verify workspace diagnostics includes all 3 subprojects. - const subprojectPaths = facts.workspaceDiagnostics.map( - d => d.subprojectPath, + // Verify workspace diagnostics includes all 3 workspaces. Since + // Coana v15.10.8, subprojectPath identifies the build root and + // workspacePath the workspace within it, so this single-root npm + // monorepo reports one `.` subproject holding three workspaces. + const workspacePaths = facts.workspaceDiagnostics.map( + d => d.workspacePath, ) - expect(subprojectPaths).toContain('.') - expect(subprojectPaths).toContain('packages/package-a') - expect(subprojectPaths).toContain('packages/package-b') + expect(workspacePaths).toContain('.') + expect(workspacePaths).toContain('packages/package-a') + expect(workspacePaths).toContain('packages/package-b') expect(facts.workspaceDiagnostics).toHaveLength(3) + expect( + facts.workspaceDiagnostics.every(d => d.subprojectPath === '.'), + 'all workspaces should belong to the single `.` build root', + ).toBe(true) // Verify components count is reasonable (should be > 100 for this workspace). expect(facts.components.length).toBeGreaterThan(100) @@ -694,13 +701,13 @@ describe('socket scan reach (E2E tests)', async () => { // Read and validate the facts file structure. const facts = await readSocketFactsJson(tempFixture.path) - // Verify all workspace subprojects are found when using --cwd. - const subprojectPaths = facts.workspaceDiagnostics.map( - d => d.subprojectPath, + // Verify all workspaces are found when using --cwd. + const workspacePaths = facts.workspaceDiagnostics.map( + d => d.workspacePath, ) - expect(subprojectPaths).toContain('.') - expect(subprojectPaths).toContain('packages/package-a') - expect(subprojectPaths).toContain('packages/package-b') + expect(workspacePaths).toContain('.') + expect(workspacePaths).toContain('packages/package-a') + expect(workspacePaths).toContain('packages/package-b') // Verify we have components. expect(facts.components.length).toBeGreaterThan(100) diff --git a/src/commands/scan/output-scan-reach.mts b/src/commands/scan/output-scan-reach.mts index 652392c7f..5b39eaa72 100644 --- a/src/commands/scan/output-scan-reach.mts +++ b/src/commands/scan/output-scan-reach.mts @@ -4,7 +4,10 @@ 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 { + extractReachabilityErrors, + getFullWorkspacePath, +} from '../../utils/coana.mts' import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' @@ -53,7 +56,7 @@ export async function outputScanReach( ) for (const err of errors) { logger.warn( - ` - ${err.ghsaId} in ${err.componentName}@${err.componentVersion} (${err.subprojectPath})`, + ` - ${err.ghsaId} in ${err.componentName}@${err.componentVersion} (${getFullWorkspacePath(err.subprojectPath, err.workspacePath)})`, ) } } diff --git a/src/commands/scan/output-scan-reach.test.mts b/src/commands/scan/output-scan-reach.test.mts index 3ddb7548c..fcf98fe0d 100644 --- a/src/commands/scan/output-scan-reach.test.mts +++ b/src/commands/scan/output-scan-reach.test.mts @@ -39,7 +39,13 @@ const errorComponentsBody = { reachability: [ { ghsa_id: 'GHSA-aaaa-bbbb-cccc', - reachability: [{ type: 'error', subprojectPath: 'packages/web' }], + reachability: [ + { + type: 'error', + subprojectPath: '.', + workspacePath: 'packages/web', + }, + ], }, ], }, @@ -91,5 +97,7 @@ describe('outputScanReach facts-file resolution', () => { const warned = warnSpy.mock.calls.map(c => String(c[0])).join('\n') expect(warned).toContain('GHSA-aaaa-bbbb-cccc') expect(warned).toContain('lodash@4.17.21') + // The workspace, not the `.` build root Coana reports as subprojectPath. + expect(warned).toContain('packages/web') }) }) diff --git a/src/utils/coana.mts b/src/utils/coana.mts index 195a36a05..f484fbb34 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -9,6 +9,7 @@ * to depscan is brotli (api-v0 decodes at the multipart boundary). * - extractReachabilityErrors: Extract per-component reachability errors * - extractTier1ReachabilityScanId: Extract scan ID from socket facts file + * - getFullWorkspacePath: Label a build-root/workspace pair the way Coana does * * Integration: * - Works with @coana-tech/cli for reachability analysis @@ -120,6 +121,7 @@ export type ReachabilityError = { componentVersion: string ghsaId: string subprojectPath: string + workspacePath: string } export function extractReachabilityErrors( @@ -134,6 +136,7 @@ export function extractReachabilityErrors( reachability?: Array<{ subprojectPath?: string type?: string + workspacePath?: string }> }> version?: string @@ -160,6 +163,7 @@ export function extractReachabilityErrors( componentVersion: String(component.version ?? ''), ghsaId: String(ghsaEntry.ghsa_id ?? ''), subprojectPath: String(entry.subprojectPath ?? ''), + workspacePath: String(entry.workspacePath ?? ''), }) } } @@ -179,3 +183,20 @@ export function extractTier1ReachabilityScanId( ? tier1ReachabilityScanId : undefined } + +// Label a reachability location the way Coana's own `getFullWorkspacePath` +// does. Since @coana-tech/cli v15.10.8 `subprojectPath` names the build root +// and `workspacePath` the workspace within it, so a single-root repo reports +// `.` as its subproject and must be labelled by its workspace alone. +export function getFullWorkspacePath( + subprojectPath: string, + workspacePath: string, +): string { + const subproject = + subprojectPath && subprojectPath !== '.' ? subprojectPath : '' + const workspace = workspacePath && workspacePath !== '.' ? workspacePath : '' + if (subproject && workspace) { + return `${subproject}/${workspace}` + } + return subproject || workspace || '.' +} diff --git a/src/utils/coana.test.mts b/src/utils/coana.test.mts index 528d84dbc..9babc6692 100644 --- a/src/utils/coana.test.mts +++ b/src/utils/coana.test.mts @@ -6,6 +6,8 @@ * brotli-compressed .br temps, leaves other paths alone, cleans up. * - extractTier1ReachabilityScanId: plain JSON + edge cases. * - extractReachabilityErrors: plain JSON + missing + malformed. + * - getFullWorkspacePath: build-root/workspace labelling, including the + * single-root and absent-path cases. * * Related Files: * - utils/coana.mts (implementation) @@ -28,6 +30,7 @@ import { compressSocketFactsForUpload, extractReachabilityErrors, extractTier1ReachabilityScanId, + getFullWorkspacePath, } from './coana.mts' describe('coana facts-file utils', () => { @@ -233,8 +236,16 @@ describe('coana facts-file utils', () => { { ghsa_id: 'GHSA-aaaa-bbbb-cccc', reachability: [ - { type: 'error', subprojectPath: 'packages/web' }, - { type: 'reachable', subprojectPath: 'packages/api' }, + { + type: 'error', + subprojectPath: '.', + workspacePath: 'packages/web', + }, + { + type: 'reachable', + subprojectPath: '.', + workspacePath: 'packages/api', + }, ], }, ], @@ -245,7 +256,13 @@ describe('coana facts-file utils', () => { reachability: [ { ghsa_id: 'GHSA-xxxx-yyyy-zzzz', - reachability: [{ type: 'error', subprojectPath: 'packages/api' }], + reachability: [ + { + type: 'error', + subprojectPath: 'services/gateway', + workspacePath: 'packages/api', + }, + ], }, ], }, @@ -257,13 +274,15 @@ describe('coana facts-file utils', () => { componentName: 'lodash', componentVersion: '4.17.21', ghsaId: 'GHSA-aaaa-bbbb-cccc', - subprojectPath: 'packages/web', + subprojectPath: '.', + workspacePath: 'packages/web', }, { componentName: 'axios', componentVersion: '1.4.0', ghsaId: 'GHSA-xxxx-yyyy-zzzz', - subprojectPath: 'packages/api', + subprojectPath: 'services/gateway', + workspacePath: 'packages/api', }, ] @@ -300,4 +319,34 @@ describe('coana facts-file utils', () => { expect(extractReachabilityErrors(file)).toEqual([]) }) }) + + describe('getFullWorkspacePath', () => { + it('joins a build root with the workspace inside it', () => { + expect(getFullWorkspacePath('services/gateway', 'packages/api')).toBe( + 'services/gateway/packages/api', + ) + }) + + it('uses the workspace alone for a single-root repo', () => { + expect(getFullWorkspacePath('.', 'packages/api')).toBe('packages/api') + }) + + it('uses the build root alone when it holds no nested workspace', () => { + expect(getFullWorkspacePath('services/gateway', '.')).toBe( + 'services/gateway', + ) + }) + + it('falls back to the root when neither path is meaningful', () => { + expect(getFullWorkspacePath('.', '.')).toBe('.') + expect(getFullWorkspacePath('', '')).toBe('.') + }) + + it('ignores an absent path from an older Coana release', () => { + expect(getFullWorkspacePath('', 'packages/api')).toBe('packages/api') + expect(getFullWorkspacePath('services/gateway', '')).toBe( + 'services/gateway', + ) + }) + }) })