diff --git a/CHANGELOG.md b/CHANGELOG.md index 79184a1e8..58866309c 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/). +## [1.1.123](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.123) - 2026-06-18 + +### Added +- `socket scan create --reach` and `socket scan reach` now accept unit suffixes on `--reach-analysis-timeout` (`s`, `m`, `h` — e.g. `90s`, `10m`, `1h`) and `--reach-analysis-memory-limit` (`MB`, `GB` — e.g. `512MB`, `8GB`). Plain numbers keep working as before. + +### Changed +- Updated the Coana CLI to v `15.5.0`. + ## [1.1.122](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.122) - 2026-06-17 ### Changed diff --git a/package.json b/package.json index b23af77eb..bb4cd8449 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.122", + "version": "1.1.123", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.4.6", + "@coana-tech/cli": "15.5.0", "@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 c2999fdfd..08379840d 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.4.6 - version: 15.4.6 + specifier: 15.5.0 + version: 15.5.0 '@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.4.6': - resolution: {integrity: sha512-U++rLRiCOxsiYoGlxDVH0clF2eaqBcAnxvQmIxBtcPpq0CvJVWyJ8+JYODo31RMst8dDfPtnE/h9ONHcuVc5hA==} + '@coana-tech/cli@15.5.0': + resolution: {integrity: sha512-CpFOZ2I5Fb/GM5YPqBwlIN5YjwNBlHPAVpDfO9E6rEk12fTdJ1tVH6brOPmzrpuuRvxQx97VXFpYwjrKCSuHNA==} 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.4.6': {} + '@coana-tech/cli@15.5.0': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/ci/handle-ci.mts b/src/commands/ci/handle-ci.mts index d959e7988..52251ed50 100644 --- a/src/commands/ci/handle-ci.mts +++ b/src/commands/ci/handle-ci.mts @@ -52,8 +52,8 @@ export async function handleCi(autoManifest: boolean): Promise { pullRequest: 0, reach: { excludePaths: [], - reachAnalysisMemoryLimit: 0, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 4221a52b6..36d04280c 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -8,6 +8,10 @@ import { assertValidExcludePaths } from './exclude-paths.mts' import { handleCreateNewScan } from './handle-create-new-scan.mts' import { outputCreateNewScan } from './output-create-new-scan.mts' import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts' +import { + isOmittedReachValue, + reachMemoryLimitToMb, +} from './reachability-units.mts' import { suggestOrgSlug } from './suggest-org-slug.mts' import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' @@ -284,8 +288,8 @@ async function run( tmp: boolean // Reachability flags. reach: boolean - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -485,12 +489,19 @@ async function run( const hasReachExcludePaths = reachExcludePaths.length > 0 + // Compare by resolved magnitude, not string identity: 8192, 8192MB and 8GB + // all mean the default, and an omitted/zero timeout means "use the default". + // A naive string compare would flag those equivalents as non-default and + // wrongly require --reach. + const memoryLimitMb = reachMemoryLimitToMb(reachAnalysisMemoryLimit) const isUsingNonDefaultMemoryLimit = - reachAnalysisMemoryLimit !== - reachabilityFlags['reachAnalysisMemoryLimit']?.default + memoryLimitMb !== null && + memoryLimitMb !== + reachMemoryLimitToMb( + String(reachabilityFlags['reachAnalysisMemoryLimit']?.default ?? ''), + ) - const isUsingNonDefaultTimeout = - reachAnalysisTimeout !== reachabilityFlags['reachAnalysisTimeout']?.default + const isUsingNonDefaultTimeout = !isOmittedReachValue(reachAnalysisTimeout) const isUsingNonDefaultConcurrency = reachConcurrency !== reachabilityFlags['reachConcurrency']?.default @@ -633,8 +644,8 @@ async function run( }) : undefined, excludePaths, - reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit), - reachAnalysisTimeout: Number(reachAnalysisTimeout), + reachAnalysisMemoryLimit, + reachAnalysisTimeout, reachConcurrency: Number(reachConcurrency), reachContinueOnAnalysisErrors: Boolean(reachContinueOnAnalysisErrors), reachContinueOnInstallErrors: Boolean(reachContinueOnInstallErrors), diff --git a/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index 8a4275190..43453c39c 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -56,8 +56,8 @@ describe('socket scan create', async () => { --workspace The workspace in the Socket Organization that the repository is in to associate with the full scan. Reachability Options (when --reach is used) - --reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB. - --reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly. + --reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB. + --reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly. --reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. --reach-continue-on-analysis-errors Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back to precomputed (Tier 2) results. By default, the CLI halts on analysis errors. --reach-continue-on-install-errors Continue reachability analysis when package installation fails, falling back to precomputed (Tier 2) results. By default, the CLI halts on installation errors. @@ -266,6 +266,56 @@ describe('socket scan create', async () => { }, ) + cmdit( + [ + 'scan', + 'create', + FLAG_ORG, + 'fakeOrg', + 'target', + FLAG_DRY_RUN, + '--repo', + 'xyz', + '--branch', + 'abc', + '--reach-analysis-memory-limit', + '8GB', + FLAG_CONFIG, + '{"apiToken":"fakeToken"}', + ], + 'should succeed when --reach-analysis-memory-limit equals the default in a different unit (8GB) without --reach', + async cmd => { + const { code, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(code, 'should treat 8GB as the default 8192MB').toBe(0) + }, + ) + + cmdit( + [ + 'scan', + 'create', + FLAG_ORG, + 'fakeOrg', + 'target', + FLAG_DRY_RUN, + '--repo', + 'xyz', + '--branch', + 'abc', + '--reach-analysis-timeout', + '0', + FLAG_CONFIG, + '{"apiToken":"fakeToken"}', + ], + 'should succeed when --reach-analysis-timeout is the zero/omit sentinel without --reach', + async cmd => { + const { code, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(code, 'should treat 0 as the default (omit) timeout').toBe(0) + }, + ) + cmdit( [ 'scan', diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 9c366259f..ac6f9f265 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -147,8 +147,8 @@ async function run( markdown: boolean org: string output: string - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -277,8 +277,8 @@ async function run( outputPath: outputPath || '', reachabilityOptions: { excludePaths, - reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit), - reachAnalysisTimeout: Number(reachAnalysisTimeout), + reachAnalysisMemoryLimit, + reachAnalysisTimeout, reachConcurrency: Number(reachConcurrency), reachContinueOnAnalysisErrors: Boolean(reachContinueOnAnalysisErrors), reachContinueOnInstallErrors: Boolean(reachContinueOnInstallErrors), diff --git a/src/commands/scan/cmd-scan-reach.test.mts b/src/commands/scan/cmd-scan-reach.test.mts index 055c2e0c4..206337051 100644 --- a/src/commands/scan/cmd-scan-reach.test.mts +++ b/src/commands/scan/cmd-scan-reach.test.mts @@ -38,8 +38,8 @@ describe('socket scan reach', async () => { Reachability Options --exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) Tier 1 reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (\`--cwd\` if set), not the reachability target: \`tests\` matches only \`/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags. - --reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB. - --reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly. + --reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB. + --reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly. --reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. --reach-continue-on-analysis-errors Continue reachability analysis when errors occur (timeouts, OOM, parse errors, etc.), falling back to precomputed (Tier 2) results. By default, the CLI halts on analysis errors. --reach-continue-on-install-errors Continue reachability analysis when package installation fails, falling back to precomputed (Tier 2) results. By default, the CLI halts on installation errors. @@ -1050,7 +1050,7 @@ describe('socket scan reach', async () => { FLAG_CONFIG, '{"apiToken":"fake-token"}', ], - 'should show clear error for invalid memory limit', + 'should forward an unrecognized memory value to Coana without locally rejecting it', async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr @@ -1065,13 +1065,13 @@ describe('socket scan reach', async () => { 'reach', FLAG_DRY_RUN, '--reach-analysis-memory-limit', - '-1', + '512kb', '--org', 'fakeOrg', FLAG_CONFIG, '{"apiToken":"fake-token"}', ], - 'should show clear error for negative memory limit', + 'should forward an unsupported memory unit to Coana without locally rejecting it', async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr @@ -1092,7 +1092,7 @@ describe('socket scan reach', async () => { FLAG_CONFIG, '{"apiToken":"fake-token"}', ], - 'should show clear error for invalid timeout value', + 'should forward an unrecognized timeout value to Coana without locally rejecting it', async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr @@ -1107,13 +1107,13 @@ describe('socket scan reach', async () => { 'reach', FLAG_DRY_RUN, '--reach-analysis-timeout', - '0', + '10m', '--org', 'fakeOrg', FLAG_CONFIG, '{"apiToken":"fake-token"}', ], - 'should show clear error for zero timeout', + 'should accept a timeout value with a unit suffix', async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr diff --git a/src/commands/scan/create-scan-from-github.mts b/src/commands/scan/create-scan-from-github.mts index 759a9e6a2..4f53e86fc 100644 --- a/src/commands/scan/create-scan-from-github.mts +++ b/src/commands/scan/create-scan-from-github.mts @@ -251,8 +251,8 @@ async function scanOneRepo( pullRequest: 0, reach: { excludePaths: [], - reachAnalysisMemoryLimit: 0, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, diff --git a/src/commands/scan/exclude-paths.test.mts b/src/commands/scan/exclude-paths.test.mts index 517773649..488c8944c 100644 --- a/src/commands/scan/exclude-paths.test.mts +++ b/src/commands/scan/exclude-paths.test.mts @@ -15,8 +15,8 @@ function makeReachOptions( ): ReachabilityOptions { return { excludePaths: [], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, diff --git a/src/commands/scan/handle-create-new-scan.test.mts b/src/commands/scan/handle-create-new-scan.test.mts index 0b702f9f4..3c03048ee 100644 --- a/src/commands/scan/handle-create-new-scan.test.mts +++ b/src/commands/scan/handle-create-new-scan.test.mts @@ -87,8 +87,8 @@ function createConfig( pullRequest: 0, reach: { excludePaths: [], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -198,8 +198,8 @@ describe('handleCreateNewScan excludePaths', () => { pullRequest: 0, reach: { excludePaths: ['tests', 'packages/*'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -260,8 +260,8 @@ describe('handleCreateNewScan excludePaths', () => { pullRequest: 0, reach: { excludePaths: ['apps/api/tests', '**/dist'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -328,8 +328,8 @@ describe('handleCreateNewScan excludePaths', () => { pullRequest: 0, reach: { excludePaths: ['tests'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -386,8 +386,8 @@ describe('handleCreateNewScan excludePaths', () => { pullRequest: 0, reach: { excludePaths: ['apps/api'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -444,8 +444,8 @@ describe('handleCreateNewScan excludePaths', () => { pullRequest: 0, reach: { excludePaths: ['tests'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index 4251f299e..f845247aa 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -105,8 +105,8 @@ describe('handleScanReach', () => { it('applies excludePaths to manifest discovery and reachability analysis', async () => { const reachabilityOptions = { excludePaths: ['tests', 'packages/*'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -156,8 +156,8 @@ describe('handleScanReach', () => { it('translates excludePaths from the scan root for nested targets', async () => { const reachabilityOptions = { excludePaths: ['apps/api/tests', '**/dist'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -217,8 +217,8 @@ describe('handleScanReach', () => { ) const reachabilityOptions = { excludePaths: ['apps/api'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -264,8 +264,8 @@ describe('handleScanReach', () => { const reachabilityOptions = { excludePaths: ['tests'], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -315,8 +315,8 @@ describe('handleScanReach', () => { }) const reachabilityOptions = { excludePaths: [], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -351,8 +351,8 @@ describe('handleScanReach', () => { it('does not call finalize when Coana did not return a tier1 reachability scan id', async () => { const reachabilityOptions = { excludePaths: [], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -400,8 +400,8 @@ describe('handleScanReach', () => { }) const reachabilityOptions = { excludePaths: [], - reachAnalysisMemoryLimit: 8192, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '8192', + reachAnalysisTimeout: '', reachConcurrency: 1, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 69a445bce..c95fdd93a 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -5,6 +5,7 @@ import path from 'node:path' import { logger } from '@socketsecurity/registry/lib/logger' +import { isOmittedReachValue } from './reachability-units.mts' import constants from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts' @@ -24,8 +25,8 @@ import type { StdioOptions } from 'node:child_process' export type ReachabilityOptions = { autoManifestConfig?: AutoManifestConfig | undefined excludePaths: string[] - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -206,12 +207,12 @@ export async function performReachabilityAnalysis( '--socket-mode', outputFilePath, '--disable-report-submission', - ...(reachabilityOptions.reachAnalysisTimeout - ? ['--analysis-timeout', `${reachabilityOptions.reachAnalysisTimeout}`] - : []), - ...(reachabilityOptions.reachAnalysisMemoryLimit - ? ['--memory-limit', `${reachabilityOptions.reachAnalysisMemoryLimit}`] - : []), + ...(isOmittedReachValue(reachabilityOptions.reachAnalysisTimeout) + ? [] + : ['--analysis-timeout', reachabilityOptions.reachAnalysisTimeout]), + ...(isOmittedReachValue(reachabilityOptions.reachAnalysisMemoryLimit) + ? [] + : ['--memory-limit', reachabilityOptions.reachAnalysisMemoryLimit]), ...(reachabilityOptions.reachConcurrency ? ['--concurrency', `${reachabilityOptions.reachConcurrency}`] : []), diff --git a/src/commands/scan/perform-reachability-analysis.test.mts b/src/commands/scan/perform-reachability-analysis.test.mts index 054478a38..16d4af9dc 100644 --- a/src/commands/scan/perform-reachability-analysis.test.mts +++ b/src/commands/scan/perform-reachability-analysis.test.mts @@ -76,8 +76,8 @@ vi.mock('@socketsecurity/registry/lib/logger', () => ({ function makeReachabilityOptions(): ReachabilityOptions { return { excludePaths: [], - reachAnalysisMemoryLimit: 0, - reachAnalysisTimeout: 0, + reachAnalysisMemoryLimit: '', + reachAnalysisTimeout: '', reachConcurrency: 0, reachContinueOnAnalysisErrors: false, reachContinueOnInstallErrors: false, @@ -163,6 +163,65 @@ describe('performReachabilityAnalysis facts-file resolution', () => { }) }) +describe('performReachabilityAnalysis timeout/memory forwarding', () => { + let scanCwd: string + + beforeEach(() => { + vi.clearAllMocks() + mockFetchOrganization.mockResolvedValue({ + ok: true, + data: { organizations: {} }, + }) + mockHasEnterpriseOrgPlan.mockReturnValue(true) + mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: '' }) + scanCwd = mkdtempSync(path.join(tmpdir(), 'socket-reaf-')) + }) + + afterEach(() => { + rmSync(scanCwd, { force: true, recursive: true }) + }) + + async function coanaArgsFor( + overrides: Partial, + ): Promise { + await performReachabilityAnalysis({ + cwd: scanCwd, + reachabilityOptions: { ...makeReachabilityOptions(), ...overrides }, + target: scanCwd, + }) + return mockSpawnCoanaDlx.mock.calls[0]![0] as string[] + } + + it('forwards unit-bearing values to Coana verbatim', async () => { + const args = await coanaArgsFor({ + reachAnalysisTimeout: '90s', + reachAnalysisMemoryLimit: '8GB', + }) + expect(args).toContain('--analysis-timeout') + expect(args[args.indexOf('--analysis-timeout') + 1]).toBe('90s') + expect(args).toContain('--memory-limit') + expect(args[args.indexOf('--memory-limit') + 1]).toBe('8GB') + }) + + it('omits both flags for empty values so Coana applies its defaults', async () => { + const args = await coanaArgsFor({ + reachAnalysisTimeout: '', + reachAnalysisMemoryLimit: '', + }) + expect(args).not.toContain('--analysis-timeout') + expect(args).not.toContain('--memory-limit') + }) + + it('omits flags for zero-magnitude values (back-compat sentinel)', async () => { + const args = await coanaArgsFor({ + reachAnalysisTimeout: '0', + reachAnalysisMemoryLimit: '0', + }) + expect(args).not.toContain('--analysis-timeout') + expect(args).not.toContain('--memory-limit') + }) +}) + describe('performReachabilityAnalysis stdio routing by output kind', () => { let scanCwd: string diff --git a/src/commands/scan/reachability-flags.mts b/src/commands/scan/reachability-flags.mts index f852b4f8a..bdc8931a3 100644 --- a/src/commands/scan/reachability-flags.mts +++ b/src/commands/scan/reachability-flags.mts @@ -8,16 +8,16 @@ export const reachabilityFlags: MeowFlags = { description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION}.`, }, reachAnalysisMemoryLimit: { - type: 'number', - default: 8192, + type: 'string', + default: '8192', description: - 'The maximum memory in MB to use for the reachability analysis. The default is 8192MB.', + 'The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB.', }, reachAnalysisTimeout: { - type: 'number', - default: 0, + type: 'string', + default: '', description: - 'Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly.', + 'Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly.', }, reachConcurrency: { type: 'number', diff --git a/src/commands/scan/reachability-units.mts b/src/commands/scan/reachability-units.mts new file mode 100644 index 000000000..52a7b6e76 --- /dev/null +++ b/src/commands/scan/reachability-units.mts @@ -0,0 +1,32 @@ +// Helpers for the reachability unit values. Coana (@coana-tech/cli) is the sole +// validator/parser of these values; the Socket CLI forwards the raw string +// through verbatim. These helpers do NOT validate grammar (that would duplicate +// Coana's and drift): they only handle the meow-default sentinel and detect +// whether a value differs from the default, neither of which Coana models. + +// A zero-magnitude or empty value (e.g. "", "0", "0s", "0gb") means "use the +// default": the flag is omitted when forwarding and Coana applies its own +// default. This preserves the historical sentinel where a numeric 0 dropped the +// flag, and avoids Coana's undefined zero (0ms / 0MB) path. +export function isOmittedReachValue(value: string): boolean { + const match = /^\d+/.exec(value) + return !match || Number(match[0]) === 0 +} + +// Resolve a memory-limit value to its magnitude in MB (the unit Coana uses), or +// null when the value is omitted/zero (Coana then applies its own default). +// Used only to compare a value against the default regardless of how the unit +// is written: 8192, 8192MB and 8GB all resolve to 8192. This is default +// detection, not validation, so an unrecognized value resolves to null and is +// simply treated as "not a non-default value". +export function reachMemoryLimitToMb(value: string): number | null { + if (isOmittedReachValue(value)) { + return null + } + const match = /^(\d+)(mb|gb)?$/i.exec(value) + if (!match) { + return null + } + const amount = Number(match[1]) + return match[2]?.toLowerCase() === 'gb' ? amount * 1024 : amount +} diff --git a/src/commands/scan/reachability-units.test.mts b/src/commands/scan/reachability-units.test.mts new file mode 100644 index 000000000..e32e9f766 --- /dev/null +++ b/src/commands/scan/reachability-units.test.mts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' + +import { + isOmittedReachValue, + reachMemoryLimitToMb, +} from './reachability-units.mts' + +describe('isOmittedReachValue', () => { + // Empty or any zero-magnitude value means "use the default" (flag omitted). + it.each(['', '0', '00', '0s', '0m', '0h', '0mb', '0gb'])( + 'treats %j as omitted', + value => { + expect(isOmittedReachValue(value)).toBe(true) + }, + ) + + it.each(['90s', '10m', '8192', '8GB', '1'])('forwards %j', value => { + expect(isOmittedReachValue(value)).toBe(false) + }) +}) + +describe('reachMemoryLimitToMb', () => { + it.each([ + { 0: '8192', 1: 8192 }, + { 0: '8192MB', 1: 8192 }, + { 0: '8192mb', 1: 8192 }, + { 0: '8GB', 1: 8192 }, + { 0: '8gb', 1: 8192 }, + { 0: '512MB', 1: 512 }, + { 0: '1', 1: 1 }, + ])('resolves $0 to $1 MB', ({ 0: value, 1: expected }) => { + expect(reachMemoryLimitToMb(value)).toBe(expected) + }) + + it.each(['', '0', '0mb', '0gb', 'invalid'])( + 'returns null for omitted/unparseable %j', + value => { + expect(reachMemoryLimitToMb(value)).toBeNull() + }, + ) +})