From 129adaecf2b92647177bdd5bb03c65e27143be44 Mon Sep 17 00:00:00 2001 From: barslev Date: Sat, 23 May 2026 20:49:32 +0200 Subject: [PATCH 1/5] fix(scan): finalize tier1 reachability scan from `socket scan reach` `socket scan reach` invokes Coana which registers a tier1 reachability scan row on the backend. Until now socket-cli never followed up with a finalize call from this flow because there was no full-scan id to bind to, so every standalone reachability run left the row at an intermediate post-Coana state indistinguishable from a stuck run. Now that the backend's `tier1-reachability-scan/finalize` endpoint accepts a null `report_run_id` for flows that have no full scan, call it from `handle-scan-reach.mts` once Coana has emitted the tier1 reachability scan id. The standalone reachability row reaches its DONE terminal state, and "stuck at the intermediate state" becomes an unambiguous signal of a real problem rather than a normal `scan reach` outcome. Broaden the `finalizeTier1Scan` wrapper signature so the second argument is `string | null`. Best-effort: a finalize failure logs a warning but does not block the user-visible reachability output. --- src/commands/scan/finalize-tier1-scan.mts | 10 ++- src/commands/scan/handle-scan-reach.mts | 17 ++++ src/commands/scan/handle-scan-reach.test.mts | 87 ++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/src/commands/scan/finalize-tier1-scan.mts b/src/commands/scan/finalize-tier1-scan.mts index 4ff9730d6..b74b0f43f 100644 --- a/src/commands/scan/finalize-tier1-scan.mts +++ b/src/commands/scan/finalize-tier1-scan.mts @@ -4,17 +4,19 @@ import type { CResult } from '../../types.mts' export type FinalizeTier1ScanOptions = { tier1_reachability_scan_id: string - report_run_id: string + report_run_id: string | null } /** * Finalize a tier1 reachability scan. - * - Associates the tier1 reachability scan metadata with the full scan. - * - Sets the tier1 reachability scan to "finalized" state. + * - Associates the tier1 reachability scan metadata with the full scan + * (or with `null` when called from a standalone reachability flow that + * has no full scan to bind to). + * - Transitions the tier1 reachability scan to its DONE terminal state. */ export async function finalizeTier1Scan( tier1ReachabilityScanId: string, - scanId: string, + scanId: string | null, ): Promise> { // we do not use the SDK here because the tier1-reachability-scan/finalize is a hidden // endpoint that is not part of the OpenAPI specification. diff --git a/src/commands/scan/handle-scan-reach.mts b/src/commands/scan/handle-scan-reach.mts index 9df5c2a17..359f2d4c9 100644 --- a/src/commands/scan/handle-scan-reach.mts +++ b/src/commands/scan/handle-scan-reach.mts @@ -3,6 +3,7 @@ import { pluralize } from '@socketsecurity/registry/lib/words' import { applyFullExcludePaths } from './exclude-paths.mts' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' +import { finalizeTier1Scan } from './finalize-tier1-scan.mts' import { outputScanReach } from './output-scan-reach.mts' import { performReachabilityAnalysis } from './perform-reachability-analysis.mts' import constants from '../../constants.mts' @@ -103,5 +104,21 @@ export async function handleScanReach({ spinner.stop() + // Standalone reachability has no full scan to bind to, but the tier1 + // reachability scan row still needs to transition to its DONE terminal + // state — otherwise it sits at the post-Coana intermediate state forever + // and looks indistinguishable from a stuck run. Pass `null` as the full + // scan id; the endpoint accepts it for this flow. Best-effort: never + // block the user-visible output on this. + const tier1Id = result.ok ? result.data?.tier1ReachabilityScanId : undefined + if (tier1Id) { + const finalizeResult = await finalizeTier1Scan(tier1Id, null) + if (!finalizeResult.ok) { + logger.warn( + `Failed to finalize tier1 reachability scan: ${finalizeResult.message}${finalizeResult.cause ? ` — ${finalizeResult.cause}` : ''}`, + ) + } + } + await outputScanReach(result, { outputKind, outputPath }) } diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index 6d94d9a21..c5894b196 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -5,6 +5,7 @@ import { handleScanReach } from './handle-scan-reach.mts' const { mockCheckCommandInput, mockFetchSupportedScanFileNames, + mockFinalizeTier1Scan, mockFindSocketYmlSync, mockGetPackageFilesForScan, mockOutputScanReach, @@ -13,6 +14,7 @@ const { } = vi.hoisted(() => ({ mockCheckCommandInput: vi.fn(), mockFetchSupportedScanFileNames: vi.fn(), + mockFinalizeTier1Scan: vi.fn(), mockFindSocketYmlSync: vi.fn(), mockGetPackageFilesForScan: vi.fn(), mockOutputScanReach: vi.fn(), @@ -24,6 +26,10 @@ vi.mock('./fetch-supported-scan-file-names.mts', () => ({ fetchSupportedScanFileNames: mockFetchSupportedScanFileNames, })) +vi.mock('./finalize-tier1-scan.mts', () => ({ + finalizeTier1Scan: mockFinalizeTier1Scan, +})) + vi.mock('./output-scan-reach.mts', () => ({ outputScanReach: mockOutputScanReach, })) @@ -65,6 +71,7 @@ vi.mock('../../utils/path-resolve.mts', () => ({ vi.mock('@socketsecurity/registry/lib/logger', () => ({ logger: { success: vi.fn(), + warn: vi.fn(), }, })) @@ -76,6 +83,7 @@ describe('handleScanReach', () => { ok: true, data: { npm: { packageJson: { pattern: 'package.json' } } }, }) + mockFinalizeTier1Scan.mockResolvedValue({ data: undefined, ok: true }) mockFindSocketYmlSync.mockReturnValue({ ok: true, data: { parsed: { projectIgnorePaths: ['vendor/**'] } }, @@ -292,4 +300,83 @@ describe('handleScanReach', () => { }, ) }) + + it('finalizes the tier1 reachability scan with a null report_run_id when Coana returned a scan id', async () => { + mockPerformReachabilityAnalysis.mockResolvedValueOnce({ + ok: true, + data: { + reachabilityReport: '.socket.facts.json', + tier1ReachabilityScanId: 'tier1-id', + }, + }) + const reachabilityOptions = { + excludePaths: [], + reachAnalysisMemoryLimit: 8192, + reachAnalysisTimeout: 0, + reachConcurrency: 1, + reachContinueOnAnalysisErrors: false, + reachContinueOnInstallErrors: false, + reachContinueOnMissingLockFiles: false, + reachContinueOnNoSourceFiles: false, + reachDebug: false, + reachDetailedAnalysisLogFile: false, + reachDisableAnalytics: false, + reachDisableExternalToolChecks: false, + reachEcosystems: [], + reachEnableAnalysisSplitting: false, + reachExcludePaths: [], + reachLazyMode: false, + reachSkipCache: false, + reachUseOnlyPregeneratedSboms: false, + reachVersion: undefined, + } + + await handleScanReach({ + cwd: '/repo', + interactive: false, + orgSlug: 'fakeOrg', + outputKind: 'text', + outputPath: '', + reachabilityOptions, + targets: ['.'], + }) + + expect(mockFinalizeTier1Scan).toHaveBeenCalledWith('tier1-id', null) + }) + + it('does not call finalize when Coana did not return a tier1 reachability scan id', async () => { + const reachabilityOptions = { + excludePaths: [], + reachAnalysisMemoryLimit: 8192, + reachAnalysisTimeout: 0, + reachConcurrency: 1, + reachContinueOnAnalysisErrors: false, + reachContinueOnInstallErrors: false, + reachContinueOnMissingLockFiles: false, + reachContinueOnNoSourceFiles: false, + reachDebug: false, + reachDetailedAnalysisLogFile: false, + reachDisableAnalytics: false, + reachDisableExternalToolChecks: false, + reachEcosystems: [], + reachEnableAnalysisSplitting: false, + reachExcludePaths: [], + reachLazyMode: false, + reachSkipCache: false, + reachUseOnlyPregeneratedSboms: false, + reachVersion: undefined, + } + + await handleScanReach({ + cwd: '/repo', + interactive: false, + orgSlug: 'fakeOrg', + outputKind: 'text', + outputPath: '', + reachabilityOptions, + targets: ['.'], + }) + + expect(mockFinalizeTier1Scan).not.toHaveBeenCalled() + }) }) From a0f710d8895ce02a8332dbd13c53b0b40c0e6c72 Mon Sep 17 00:00:00 2001 From: barslev Date: Sun, 31 May 2026 05:20:15 +0200 Subject: [PATCH 2/5] chore: bump version to 1.1.109 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36e647b0a..5f83f65df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.108", + "version": "1.1.109", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", From 7171f271bb8a0a1e9a3e2ab440df181aec5a2877 Mon Sep 17 00:00:00 2001 From: barslev Date: Wed, 3 Jun 2026 17:30:56 +0200 Subject: [PATCH 3/5] upgrading coana to version 15.3.20 --- CHANGELOG.md | 1 + package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3642eaf5e..fabcfd09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - **Bazel diagnostics** — `socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster. +- Updated the Coana CLI to v `15.3.20`. ## [1.1.108](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.108) - 2026-05-28 diff --git a/package.json b/package.json index 5f83f65df..104470f66 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.3.12", + "@coana-tech/cli": "15.3.20", "@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 bffc7d40f..4fb75b049 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: 15.3.12 - version: 15.3.12 + specifier: 15.3.20 + version: 15.3.20 '@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@15.3.12': - resolution: {integrity: sha512-cxv/DBmQm9a+nUk/vjV/vgi4g4YnuGpoMhSTFVfmqmiR5M9XYZ1raLoVZl0x0P53Ux8qZbz0Wmr0+WqziznoNw==} + '@coana-tech/cli@15.3.20': + resolution: {integrity: sha512-awiO8Mtdwd64aE3n9vNAQSRYRc0dNzPVEQAqUrq3a430II/SQrAokl9C32mYw7+TaNic/rEcmzsZzClY6YeTLA==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.3.12': {} + '@coana-tech/cli@15.3.20': {} '@colors/colors@1.5.0': optional: true From 23795c0800c89a50010b9fd00ce7e338b79ae996 Mon Sep 17 00:00:00 2001 From: barslev Date: Wed, 3 Jun 2026 19:57:12 +0200 Subject: [PATCH 4/5] test(scan): cover tier1 finalize failure path in scan reach Add a third handleScanReach case for when finalizeTier1Scan returns the non-ok CResult shape: assert a single warning is logged (carrying the message and cause) and that outputScanReach still runs and the handler resolves normally, so a finalize failure never blocks the user-visible scan output. --- src/commands/scan/handle-scan-reach.test.mts | 72 +++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index c5894b196..4251f299e 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -8,6 +8,8 @@ const { mockFinalizeTier1Scan, mockFindSocketYmlSync, mockGetPackageFilesForScan, + mockLoggerSuccess, + mockLoggerWarn, mockOutputScanReach, mockPerformReachabilityAnalysis, mockSentryInternalsSymbol, @@ -17,6 +19,8 @@ const { mockFinalizeTier1Scan: vi.fn(), mockFindSocketYmlSync: vi.fn(), mockGetPackageFilesForScan: vi.fn(), + mockLoggerSuccess: vi.fn(), + mockLoggerWarn: vi.fn(), mockOutputScanReach: vi.fn(), mockPerformReachabilityAnalysis: vi.fn(), mockSentryInternalsSymbol: Symbol('kInternalsSymbol'), @@ -70,8 +74,8 @@ vi.mock('../../utils/path-resolve.mts', () => ({ vi.mock('@socketsecurity/registry/lib/logger', () => ({ logger: { - success: vi.fn(), - warn: vi.fn(), + success: mockLoggerSuccess, + warn: mockLoggerWarn, }, })) @@ -379,4 +383,68 @@ describe('handleScanReach', () => { expect(mockFinalizeTier1Scan).not.toHaveBeenCalled() }) + + it('warns but still produces scan output when tier1 finalize fails', async () => { + mockPerformReachabilityAnalysis.mockResolvedValueOnce({ + ok: true, + data: { + reachabilityReport: '.socket.facts.json', + tier1ReachabilityScanId: 'tier1-id', + }, + }) + // Finalize fails with the CResult error shape; the command must not abort. + mockFinalizeTier1Scan.mockResolvedValueOnce({ + ok: false, + message: 'Finalize request failed', + cause: 'Socket API server error (503)', + }) + const reachabilityOptions = { + excludePaths: [], + reachAnalysisMemoryLimit: 8192, + reachAnalysisTimeout: 0, + reachConcurrency: 1, + reachContinueOnAnalysisErrors: false, + reachContinueOnInstallErrors: false, + reachContinueOnMissingLockFiles: false, + reachContinueOnNoSourceFiles: false, + reachDebug: false, + reachDetailedAnalysisLogFile: false, + reachDisableAnalytics: false, + reachDisableExternalToolChecks: false, + reachEcosystems: [], + reachEnableAnalysisSplitting: false, + reachExcludePaths: [], + reachLazyMode: false, + reachSkipCache: false, + reachUseOnlyPregeneratedSboms: false, + reachVersion: undefined, + } + + // The handler resolves normally (no throw, returns undefined) so the + // command proceeds and exits 0 rather than being blocked by the failure. + await expect( + handleScanReach({ + cwd: '/repo', + interactive: false, + orgSlug: 'fakeOrg', + outputKind: 'text', + outputPath: '', + reachabilityOptions, + targets: ['.'], + }), + ).resolves.toBeUndefined() + + expect(mockFinalizeTier1Scan).toHaveBeenCalledWith('tier1-id', null) + // The failure is surfaced as a single warning carrying message and cause. + expect(mockLoggerWarn).toHaveBeenCalledTimes(1) + const { 0: warnMessage } = mockLoggerWarn.mock.calls[0] + expect(warnMessage).toContain('Failed to finalize tier1 reachability scan') + expect(warnMessage).toContain('Finalize request failed') + expect(warnMessage).toContain('Socket API server error (503)') + // Normal scan output is still produced; the command is not blocked. + expect(mockOutputScanReach).toHaveBeenCalledWith( + expect.objectContaining({ ok: true }), + { cwd: '/repo', outputKind: 'text', outputPath: '' }, + ) + }) }) From f3ebba2fb7ec5a39519ce3f37d8cf5e240742133 Mon Sep 17 00:00:00 2001 From: barslev Date: Wed, 3 Jun 2026 21:16:13 +0200 Subject: [PATCH 5/5] chore: bump version to 1.1.113 --- CHANGELOG.md | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6e55eb8..771dbcb91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ 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] +## [1.1.113](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.113) - 2026-06-03 + +### Added - **`socket manifest bazel [beta]`** — Generate Bazel JVM SBOM manifests by running `bazel query` against discovered Maven repos in a Bazel workspace. Closes the inline-Maven-declaration gap that lockfile-only parsing misses for repos like envoy, ray, tensorflow, tink-java, and or-tools. Auto-detects Bzlmod and legacy `WORKSPACE`. - **`socket scan create --auto-manifest`** now covers Bazel workspaces in addition to Gradle/Scala/Kotlin/Conda. Repos with `MODULE.bazel`, `WORKSPACE`, or `WORKSPACE.bazel` are detected automatically and their Maven dependencies extracted as part of the standard scan-create flow. - **Bazel PyPI extraction** — `socket manifest bazel --ecosystem pypi` now generates `requirements.txt` for Python Bazel workspaces. Discovers custom `rules_python` pip hub names with Bazel command output first, queries `py_library` / `py_binary` / `py_test` dependencies, resolves canonical pinned versions from `requirements_lock.txt`, and emits PEP 503-normalized `name==version` lines. Supports both Bzlmod (`pip.parse`) and legacy `WORKSPACE` (`pip_parse` / `pip_install`) configurations. PyPI remains explicit opt-in for `socket scan create --auto-manifest` until real-world no-lockfile recovery is validated. diff --git a/package.json b/package.json index 45db6cbbc..0f902d184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.112", + "version": "1.1.113", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1",