From bd1d76141c7685e0506218e3146390b384c94d95 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Thu, 18 Jun 2026 13:16:17 +0200 Subject: [PATCH 1/3] feat(scan): unit suffixes for reachability timeout/memory limits (1.1.123, Coana 15.5.0) --reach-analysis-timeout and --reach-analysis-memory-limit now accept unit suffixes (s/m/h for duration, MB/GB for memory, case-insensitive). Coana owns the canonical parsing, so the CLI forwards the raw string verbatim instead of coercing to a number. A thin local validator gives fast errors before the Coana binary is spawned. Empty or zero-magnitude values are omitted when forwarding so Coana applies its own defaults, preserving the prior numeric-0 sentinel. Bare numbers keep working but are no longer documented. Bumps the bundled Coana CLI to 15.5.0, whose parser handles these units. --- CHANGELOG.md | 8 +++ package.json | 4 +- pnpm-lock.yaml | 10 +-- src/commands/ci/handle-ci.mts | 4 +- src/commands/scan/cmd-scan-create.mts | 26 ++++++-- src/commands/scan/cmd-scan-create.test.mts | 4 +- src/commands/scan/cmd-scan-reach.mts | 26 ++++++-- src/commands/scan/cmd-scan-reach.test.mts | 30 +++++---- src/commands/scan/create-scan-from-github.mts | 4 +- src/commands/scan/exclude-paths.test.mts | 4 +- .../scan/handle-create-new-scan.test.mts | 24 +++---- src/commands/scan/handle-scan-reach.test.mts | 28 ++++----- .../scan/perform-reachability-analysis.mts | 17 ++--- .../perform-reachability-analysis.test.mts | 63 ++++++++++++++++++- src/commands/scan/reachability-flags.mts | 12 ++-- src/commands/scan/reachability-units.mts | 38 +++++++++++ src/commands/scan/reachability-units.test.mts | 55 ++++++++++++++++ 17 files changed, 280 insertions(+), 77 deletions(-) create mode 100644 src/commands/scan/reachability-units.mts create mode 100644 src/commands/scan/reachability-units.test.mts 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..34a2828d6 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -8,6 +8,12 @@ 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 { + REACH_ANALYSIS_MEMORY_LIMIT_HELP, + REACH_ANALYSIS_TIMEOUT_HELP, + isValidReachAnalysisMemoryLimit, + isValidReachAnalysisTimeout, +} 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 +290,8 @@ async function run( tmp: boolean // Reachability flags. reach: boolean - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -572,6 +578,18 @@ async function run( message: 'Reachability analysis flags require --reach to be enabled', fail: 'add --reach flag to use --reach-* options', }, + { + nook: true, + test: !reach || isValidReachAnalysisTimeout(reachAnalysisTimeout), + message: `The --reach-analysis-timeout must be ${REACH_ANALYSIS_TIMEOUT_HELP}`, + fail: `invalid value "${reachAnalysisTimeout}"`, + }, + { + nook: true, + test: !reach || isValidReachAnalysisMemoryLimit(reachAnalysisMemoryLimit), + message: `The --reach-analysis-memory-limit must be ${REACH_ANALYSIS_MEMORY_LIMIT_HELP}`, + fail: `invalid value "${reachAnalysisMemoryLimit}"`, + }, { nook: true, test: !reach || reachTargetValidation.isValid, @@ -633,8 +651,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..6b01681bb 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. diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 9c366259f..42914968b 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -6,6 +6,12 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { assertValidExcludePaths } from './exclude-paths.mts' import { handleScanReach } from './handle-scan-reach.mts' import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts' +import { + REACH_ANALYSIS_MEMORY_LIMIT_HELP, + REACH_ANALYSIS_TIMEOUT_HELP, + isValidReachAnalysisMemoryLimit, + isValidReachAnalysisTimeout, +} from './reachability-units.mts' import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants from '../../constants.mts' @@ -147,8 +153,8 @@ async function run( markdown: boolean org: string output: string - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -259,6 +265,18 @@ async function run( message: 'Target directory must be inside the current working directory', fail: 'provide a path inside the working directory', }, + { + nook: true, + test: isValidReachAnalysisTimeout(reachAnalysisTimeout), + message: `The --reach-analysis-timeout must be ${REACH_ANALYSIS_TIMEOUT_HELP}`, + fail: `invalid value "${reachAnalysisTimeout}"`, + }, + { + nook: true, + test: isValidReachAnalysisMemoryLimit(reachAnalysisMemoryLimit), + message: `The --reach-analysis-memory-limit must be ${REACH_ANALYSIS_MEMORY_LIMIT_HELP}`, + fail: `invalid value "${reachAnalysisMemoryLimit}"`, + }, ) if (!wasValidInput) { return @@ -277,8 +295,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..f4923613d 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. @@ -1054,8 +1054,10 @@ describe('socket scan reach', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('[DryRun]: Bailing now') - expect(code).toBe(0) + expect(output).toContain( + 'The --reach-analysis-memory-limit must be a whole number optionally followed by MB or GB', + ) + expect(code).not.toBe(0) }, ) @@ -1065,18 +1067,20 @@ 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 show clear error for unsupported memory unit', async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('[DryRun]: Bailing now') - expect(code).toBe(0) + expect(output).toContain( + 'The --reach-analysis-memory-limit must be a whole number optionally followed by MB or GB', + ) + expect(code).not.toBe(0) }, ) @@ -1096,8 +1100,10 @@ describe('socket scan reach', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr - expect(output).toContain('[DryRun]: Bailing now') - expect(code).toBe(0) + expect(output).toContain( + 'The --reach-analysis-timeout must be a whole number optionally followed by s, m or h', + ) + expect(code).not.toBe(0) }, ) @@ -1107,13 +1113,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 623826ac0..789a73e01 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' @@ -23,8 +24,8 @@ import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type ReachabilityOptions = { autoManifestConfig?: AutoManifestConfig | undefined excludePaths: string[] - reachAnalysisMemoryLimit: number - reachAnalysisTimeout: number + reachAnalysisMemoryLimit: string + reachAnalysisTimeout: string reachConcurrency: number reachContinueOnAnalysisErrors: boolean reachContinueOnInstallErrors: boolean @@ -203,12 +204,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 d144c9fdd..d26f9b5ed 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, @@ -162,3 +162,62 @@ describe('performReachabilityAnalysis facts-file resolution', () => { expect(result.ok && result.data.tier1ReachabilityScanId).toBeUndefined() }) }) + +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') + }) +}) 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..5425f5fe4 --- /dev/null +++ b/src/commands/scan/reachability-units.mts @@ -0,0 +1,38 @@ +// Thin local mirror of the reachability unit grammar. Coana (@coana-tech/cli) +// is the canonical parser for these values; the Socket CLI forwards the raw +// string through verbatim and only performs this lightweight check so obvious +// mistakes fail fast before the Coana binary is spawned. Keep the patterns and +// help text in sync with Coana's grammar. + +// --reach-analysis-timeout: a whole number optionally followed by s, m or h. +// Units are case-insensitive (matching Coana). A bare number is treated as +// seconds (back-compat, no longer documented). +const REACH_ANALYSIS_TIMEOUT_PATTERN = /^\d+(?:s|m|h)?$/i + +// --reach-analysis-memory-limit: a whole number optionally followed by MB or GB. +// Units are case-insensitive (matching Coana). A bare number is treated as MB +// (back-compat, no longer documented). +const REACH_ANALYSIS_MEMORY_LIMIT_PATTERN = /^\d+(?:mb|gb)?$/i + +export const REACH_ANALYSIS_MEMORY_LIMIT_HELP = + 'a whole number optionally followed by MB or GB (e.g. 512MB, 8GB)' + +export const REACH_ANALYSIS_TIMEOUT_HELP = + 'a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h)' + +// 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 +} + +export function isValidReachAnalysisMemoryLimit(value: string): boolean { + return value === '' || REACH_ANALYSIS_MEMORY_LIMIT_PATTERN.test(value) +} + +export function isValidReachAnalysisTimeout(value: string): boolean { + return value === '' || REACH_ANALYSIS_TIMEOUT_PATTERN.test(value) +} diff --git a/src/commands/scan/reachability-units.test.mts b/src/commands/scan/reachability-units.test.mts new file mode 100644 index 000000000..0295bc0e1 --- /dev/null +++ b/src/commands/scan/reachability-units.test.mts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest' + +import { + isOmittedReachValue, + isValidReachAnalysisMemoryLimit, + isValidReachAnalysisTimeout, +} from './reachability-units.mts' + +describe('isValidReachAnalysisTimeout', () => { + // Units are case-insensitive, matching Coana. + it.each(['', '0', '90', '90s', '10m', '1h', '600', '10M', '1H', '30S'])( + 'accepts %j', + value => { + expect(isValidReachAnalysisTimeout(value)).toBe(true) + }, + ) + + it.each(['90ms', '1.5h', '10 m', 'm', '-1', 'invalid', '10mb'])( + 'rejects %j', + value => { + expect(isValidReachAnalysisTimeout(value)).toBe(false) + }, + ) +}) + +describe('isValidReachAnalysisMemoryLimit', () => { + // Units are case-insensitive, matching Coana. + it.each(['', '0', '8192', '512MB', '512mb', '8GB', '8gb', '8Gb'])( + 'accepts %j', + value => { + expect(isValidReachAnalysisMemoryLimit(value)).toBe(true) + }, + ) + + it.each(['512kb', '1TB', '1.5GB', '8 GB', 'GB', '-1', 'invalid'])( + 'rejects %j', + value => { + expect(isValidReachAnalysisMemoryLimit(value)).toBe(false) + }, + ) +}) + +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) + }) +}) From 6d0ed47c6871f389712218c318b3e5154f7578bc Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Thu, 18 Jun 2026 13:35:49 +0200 Subject: [PATCH 2/3] fix(scan): treat default-equivalent reach unit values as default in --reach guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "reachability flags require --reach" guard compared the raw flag strings to the default string, so unit-equivalent inputs were wrongly flagged as non-default and rejected without --reach: 8GB / 8192MB (= the 8192MB default) and the zero/omit timeout sentinel 0 / 0s. The latter was a regression from the number→string change (numeric 0 used to equal the numeric default). Compare by resolved magnitude instead: reachMemoryLimitToMb normalizes 8192/8192MB/8GB to 8192, and the timeout uses the omit sentinel so any zero counts as default. --- src/commands/scan/cmd-scan-create.mts | 17 +++++-- src/commands/scan/cmd-scan-create.test.mts | 50 +++++++++++++++++++ src/commands/scan/reachability-units.mts | 16 ++++++ src/commands/scan/reachability-units.test.mts | 22 ++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 34a2828d6..2b617c2a4 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -11,8 +11,10 @@ import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts' import { REACH_ANALYSIS_MEMORY_LIMIT_HELP, REACH_ANALYSIS_TIMEOUT_HELP, + isOmittedReachValue, isValidReachAnalysisMemoryLimit, isValidReachAnalysisTimeout, + reachMemoryLimitToMb, } from './reachability-units.mts' import { suggestOrgSlug } from './suggest-org-slug.mts' import { suggestTarget } from './suggest_target.mts' @@ -491,12 +493,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 diff --git a/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index 6b01681bb..43453c39c 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -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/reachability-units.mts b/src/commands/scan/reachability-units.mts index 5425f5fe4..89f59be24 100644 --- a/src/commands/scan/reachability-units.mts +++ b/src/commands/scan/reachability-units.mts @@ -36,3 +36,19 @@ export function isValidReachAnalysisMemoryLimit(value: string): boolean { export function isValidReachAnalysisTimeout(value: string): boolean { return value === '' || REACH_ANALYSIS_TIMEOUT_PATTERN.test(value) } + +// 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). +// Lets callers compare a value against the default regardless of how the unit +// is written: 8192, 8192MB and 8GB all resolve to 8192. +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 index 0295bc0e1..8a9e570bd 100644 --- a/src/commands/scan/reachability-units.test.mts +++ b/src/commands/scan/reachability-units.test.mts @@ -4,6 +4,7 @@ import { isOmittedReachValue, isValidReachAnalysisMemoryLimit, isValidReachAnalysisTimeout, + reachMemoryLimitToMb, } from './reachability-units.mts' describe('isValidReachAnalysisTimeout', () => { @@ -53,3 +54,24 @@ describe('isOmittedReachValue', () => { 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() + }, + ) +}) From 0fbb1b0a608a69945288da9fec24d6c4dfd568e5 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Fri, 19 Jun 2026 08:59:26 +0200 Subject: [PATCH 3/3] refactor(scan): drop local reach unit validation, defer to Coana Coana (@coana-tech/cli) is now the sole validator/parser of the --reach-analysis-timeout and --reach-analysis-memory-limit values, matching the Python CLI. Removes the local grammar mirror (isValid* regex fast-fail) that had already drifted from Coana twice (unit case-sensitivity, and a whitespace gap where Coana trims but the mirror did not). An invalid unit now surfaces as Coana's error instead of a fast local one. Kept the non-validation helpers, which Coana does not model: isOmittedReachValue (empty/zero -> omit the flag so Coana applies its default) and reachMemoryLimitToMb (unit-agnostic default-equivalence for the "requires --reach" guard). The raw string is still forwarded to Coana verbatim. --- src/commands/scan/cmd-scan-create.mts | 16 -------- src/commands/scan/cmd-scan-reach.mts | 18 --------- src/commands/scan/cmd-scan-reach.test.mts | 24 +++++------ src/commands/scan/reachability-units.mts | 40 +++++-------------- src/commands/scan/reachability-units.test.mts | 36 ----------------- 5 files changed, 18 insertions(+), 116 deletions(-) diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 2b617c2a4..36d04280c 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -9,11 +9,7 @@ import { handleCreateNewScan } from './handle-create-new-scan.mts' import { outputCreateNewScan } from './output-create-new-scan.mts' import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts' import { - REACH_ANALYSIS_MEMORY_LIMIT_HELP, - REACH_ANALYSIS_TIMEOUT_HELP, isOmittedReachValue, - isValidReachAnalysisMemoryLimit, - isValidReachAnalysisTimeout, reachMemoryLimitToMb, } from './reachability-units.mts' import { suggestOrgSlug } from './suggest-org-slug.mts' @@ -587,18 +583,6 @@ async function run( message: 'Reachability analysis flags require --reach to be enabled', fail: 'add --reach flag to use --reach-* options', }, - { - nook: true, - test: !reach || isValidReachAnalysisTimeout(reachAnalysisTimeout), - message: `The --reach-analysis-timeout must be ${REACH_ANALYSIS_TIMEOUT_HELP}`, - fail: `invalid value "${reachAnalysisTimeout}"`, - }, - { - nook: true, - test: !reach || isValidReachAnalysisMemoryLimit(reachAnalysisMemoryLimit), - message: `The --reach-analysis-memory-limit must be ${REACH_ANALYSIS_MEMORY_LIMIT_HELP}`, - fail: `invalid value "${reachAnalysisMemoryLimit}"`, - }, { nook: true, test: !reach || reachTargetValidation.isValid, diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 42914968b..ac6f9f265 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -6,12 +6,6 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { assertValidExcludePaths } from './exclude-paths.mts' import { handleScanReach } from './handle-scan-reach.mts' import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts' -import { - REACH_ANALYSIS_MEMORY_LIMIT_HELP, - REACH_ANALYSIS_TIMEOUT_HELP, - isValidReachAnalysisMemoryLimit, - isValidReachAnalysisTimeout, -} from './reachability-units.mts' import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants from '../../constants.mts' @@ -265,18 +259,6 @@ async function run( message: 'Target directory must be inside the current working directory', fail: 'provide a path inside the working directory', }, - { - nook: true, - test: isValidReachAnalysisTimeout(reachAnalysisTimeout), - message: `The --reach-analysis-timeout must be ${REACH_ANALYSIS_TIMEOUT_HELP}`, - fail: `invalid value "${reachAnalysisTimeout}"`, - }, - { - nook: true, - test: isValidReachAnalysisMemoryLimit(reachAnalysisMemoryLimit), - message: `The --reach-analysis-memory-limit must be ${REACH_ANALYSIS_MEMORY_LIMIT_HELP}`, - fail: `invalid value "${reachAnalysisMemoryLimit}"`, - }, ) if (!wasValidInput) { return diff --git a/src/commands/scan/cmd-scan-reach.test.mts b/src/commands/scan/cmd-scan-reach.test.mts index f4923613d..206337051 100644 --- a/src/commands/scan/cmd-scan-reach.test.mts +++ b/src/commands/scan/cmd-scan-reach.test.mts @@ -1050,14 +1050,12 @@ 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 - expect(output).toContain( - 'The --reach-analysis-memory-limit must be a whole number optionally followed by MB or GB', - ) - expect(code).not.toBe(0) + expect(output).toContain('[DryRun]: Bailing now') + expect(code).toBe(0) }, ) @@ -1073,14 +1071,12 @@ describe('socket scan reach', async () => { FLAG_CONFIG, '{"apiToken":"fake-token"}', ], - 'should show clear error for unsupported memory unit', + '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 - expect(output).toContain( - 'The --reach-analysis-memory-limit must be a whole number optionally followed by MB or GB', - ) - expect(code).not.toBe(0) + expect(output).toContain('[DryRun]: Bailing now') + expect(code).toBe(0) }, ) @@ -1096,14 +1092,12 @@ 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 - expect(output).toContain( - 'The --reach-analysis-timeout must be a whole number optionally followed by s, m or h', - ) - expect(code).not.toBe(0) + expect(output).toContain('[DryRun]: Bailing now') + expect(code).toBe(0) }, ) diff --git a/src/commands/scan/reachability-units.mts b/src/commands/scan/reachability-units.mts index 89f59be24..52a7b6e76 100644 --- a/src/commands/scan/reachability-units.mts +++ b/src/commands/scan/reachability-units.mts @@ -1,24 +1,8 @@ -// Thin local mirror of the reachability unit grammar. Coana (@coana-tech/cli) -// is the canonical parser for these values; the Socket CLI forwards the raw -// string through verbatim and only performs this lightweight check so obvious -// mistakes fail fast before the Coana binary is spawned. Keep the patterns and -// help text in sync with Coana's grammar. - -// --reach-analysis-timeout: a whole number optionally followed by s, m or h. -// Units are case-insensitive (matching Coana). A bare number is treated as -// seconds (back-compat, no longer documented). -const REACH_ANALYSIS_TIMEOUT_PATTERN = /^\d+(?:s|m|h)?$/i - -// --reach-analysis-memory-limit: a whole number optionally followed by MB or GB. -// Units are case-insensitive (matching Coana). A bare number is treated as MB -// (back-compat, no longer documented). -const REACH_ANALYSIS_MEMORY_LIMIT_PATTERN = /^\d+(?:mb|gb)?$/i - -export const REACH_ANALYSIS_MEMORY_LIMIT_HELP = - 'a whole number optionally followed by MB or GB (e.g. 512MB, 8GB)' - -export const REACH_ANALYSIS_TIMEOUT_HELP = - 'a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h)' +// 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 @@ -29,18 +13,12 @@ export function isOmittedReachValue(value: string): boolean { return !match || Number(match[0]) === 0 } -export function isValidReachAnalysisMemoryLimit(value: string): boolean { - return value === '' || REACH_ANALYSIS_MEMORY_LIMIT_PATTERN.test(value) -} - -export function isValidReachAnalysisTimeout(value: string): boolean { - return value === '' || REACH_ANALYSIS_TIMEOUT_PATTERN.test(value) -} - // 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). -// Lets callers compare a value against the default regardless of how the unit -// is written: 8192, 8192MB and 8GB all resolve to 8192. +// 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 diff --git a/src/commands/scan/reachability-units.test.mts b/src/commands/scan/reachability-units.test.mts index 8a9e570bd..e32e9f766 100644 --- a/src/commands/scan/reachability-units.test.mts +++ b/src/commands/scan/reachability-units.test.mts @@ -2,45 +2,9 @@ import { describe, expect, it } from 'vitest' import { isOmittedReachValue, - isValidReachAnalysisMemoryLimit, - isValidReachAnalysisTimeout, reachMemoryLimitToMb, } from './reachability-units.mts' -describe('isValidReachAnalysisTimeout', () => { - // Units are case-insensitive, matching Coana. - it.each(['', '0', '90', '90s', '10m', '1h', '600', '10M', '1H', '30S'])( - 'accepts %j', - value => { - expect(isValidReachAnalysisTimeout(value)).toBe(true) - }, - ) - - it.each(['90ms', '1.5h', '10 m', 'm', '-1', 'invalid', '10mb'])( - 'rejects %j', - value => { - expect(isValidReachAnalysisTimeout(value)).toBe(false) - }, - ) -}) - -describe('isValidReachAnalysisMemoryLimit', () => { - // Units are case-insensitive, matching Coana. - it.each(['', '0', '8192', '512MB', '512mb', '8GB', '8gb', '8Gb'])( - 'accepts %j', - value => { - expect(isValidReachAnalysisMemoryLimit(value)).toBe(true) - }, - ) - - it.each(['512kb', '1TB', '1.5GB', '8 GB', 'GB', '-1', 'invalid'])( - 'rejects %j', - value => { - expect(isValidReachAnalysisMemoryLimit(value)).toBe(false) - }, - ) -}) - describe('isOmittedReachValue', () => { // Empty or any zero-magnitude value means "use the default" (flag omitted). it.each(['', '0', '00', '0s', '0m', '0h', '0mb', '0gb'])(