diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c86f24e2..771dbcb91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,16 @@ 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. ### 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.112](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.112) - 2026-05-29 diff --git a/package.json b/package.json index 862d49138..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", @@ -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.15", + "@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 930ab0914..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.15 - version: 15.3.15 + 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.15': - resolution: {integrity: sha512-AH6my6LNU61JpP52NAdiYYRlZFGEnRngDAt8yKrLDgnPeuiVWkWnYequcEiPX9RFvhxKO/eW9PN14K/ixdsfkQ==} + '@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.15': {} + '@coana-tech/cli@15.3.20': {} '@colors/colors@1.5.0': optional: true 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 e16e4cc37..22537041b 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' @@ -104,5 +105,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, { cwd, outputKind, outputPath }) } diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index 6d94d9a21..4251f299e 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -5,16 +5,22 @@ import { handleScanReach } from './handle-scan-reach.mts' const { mockCheckCommandInput, mockFetchSupportedScanFileNames, + mockFinalizeTier1Scan, mockFindSocketYmlSync, mockGetPackageFilesForScan, + mockLoggerSuccess, + mockLoggerWarn, mockOutputScanReach, mockPerformReachabilityAnalysis, mockSentryInternalsSymbol, } = vi.hoisted(() => ({ mockCheckCommandInput: vi.fn(), mockFetchSupportedScanFileNames: vi.fn(), + mockFinalizeTier1Scan: vi.fn(), mockFindSocketYmlSync: vi.fn(), mockGetPackageFilesForScan: vi.fn(), + mockLoggerSuccess: vi.fn(), + mockLoggerWarn: vi.fn(), mockOutputScanReach: vi.fn(), mockPerformReachabilityAnalysis: vi.fn(), mockSentryInternalsSymbol: Symbol('kInternalsSymbol'), @@ -24,6 +30,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, })) @@ -64,7 +74,8 @@ vi.mock('../../utils/path-resolve.mts', () => ({ vi.mock('@socketsecurity/registry/lib/logger', () => ({ logger: { - success: vi.fn(), + success: mockLoggerSuccess, + warn: mockLoggerWarn, }, })) @@ -76,6 +87,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 +304,147 @@ 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() + }) + + 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: '' }, + ) + }) })