From 1fd87c4d4ca36345b8929e8e0dc300aefdf90879 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Fri, 24 Oct 2025 09:31:28 +0200 Subject: [PATCH] ensure the target directory is handled correctly by the reachability analysis --- .../cli/src/commands/scan/cmd-scan-create.mts | 55 +++++++++++++++++++ .../cli/src/commands/scan/cmd-scan-reach.mts | 42 ++++++++++++++ .../commands/scan/handle-create-new-scan.mts | 1 + .../src/commands/scan/handle-scan-reach.mts | 1 + .../scan/perform-reachability-analysis.mts | 10 +++- 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/scan/cmd-scan-create.mts b/packages/cli/src/commands/scan/cmd-scan-create.mts index 4437b4c11..69af1b675 100644 --- a/packages/cli/src/commands/scan/cmd-scan-create.mts +++ b/packages/cli/src/commands/scan/cmd-scan-create.mts @@ -1,5 +1,7 @@ import path from 'node:path' +import { existsSync, promises as fs } from 'node:fs' + import { joinAnd } from '@socketsecurity/lib/arrays' import { logger } from '@socketsecurity/lib/logger' @@ -444,6 +446,32 @@ async function run( hasReachExcludePaths || reachSkipCache + // Validate target constraints when --reach is enabled. + let reachTargetValid = true + let reachTargetIsDirectory = false + let reachTargetExists = false + let reachTargetInsideCwd = false + + if (reach) { + // Resolve target path to absolute for validation. + const targetPath = path.isAbsolute(targets[0]!) + ? targets[0]! + : path.resolve(cwd, targets[0]!) + + // Check if target is inside cwd. + const relativePath = path.relative(cwd, targetPath) + reachTargetInsideCwd = + !relativePath.startsWith('..') && !path.isAbsolute(relativePath) + + reachTargetExists = existsSync(targetPath) + if (reachTargetExists) { + const targetStat = await fs.stat(targetPath) + reachTargetIsDirectory = targetStat.isDirectory() + } + + reachTargetValid = targets.length === 1 + } + const wasValidInput = checkCommandInput( outputKind, { @@ -487,6 +515,33 @@ async function run( message: 'Reachability analysis flags require --reach to be enabled', fail: 'add --reach flag to use --reach-* options', }, + { + nook: true, + test: !reach || reachTargetValid, + message: + 'Reachability analysis requires exactly one target directory when --reach is enabled', + fail: 'provide exactly one directory path', + }, + { + nook: true, + test: !reach || reachTargetIsDirectory, + message: + 'Reachability analysis target must be a directory when --reach is enabled', + fail: 'provide a directory path, not a file', + }, + { + nook: true, + test: !reach || reachTargetExists, + message: 'Target directory must exist when --reach is enabled', + fail: 'provide an existing directory path', + }, + { + nook: true, + test: !reach || reachTargetInsideCwd, + message: + 'Target directory must be inside the current working directory when --reach is enabled', + fail: 'provide a path inside the working directory', + }, ) if (!wasValidInput) { return diff --git a/packages/cli/src/commands/scan/cmd-scan-reach.mts b/packages/cli/src/commands/scan/cmd-scan-reach.mts index 8b117950a..33b6bb408 100644 --- a/packages/cli/src/commands/scan/cmd-scan-reach.mts +++ b/packages/cli/src/commands/scan/cmd-scan-reach.mts @@ -1,5 +1,7 @@ import path from 'node:path' +import { existsSync, promises as fs } from 'node:fs' + import { joinAnd } from '@socketsecurity/lib/arrays' import { logger } from '@socketsecurity/lib/logger' @@ -175,6 +177,22 @@ async function run( const outputKind = getOutputKind(json, markdown) + // Resolve target path to absolute for validation. + const targetPath = path.isAbsolute(targets[0]!) + ? targets[0]! + : path.resolve(cwd, targets[0]!) + + // Check if target is inside cwd. + const relativePath = path.relative(cwd, targetPath) + const isInsideCwd = + !relativePath.startsWith('..') && !path.isAbsolute(relativePath) + + let isDirectory = false + if (existsSync(targetPath)) { + const targetStat = await fs.stat(targetPath) + isDirectory = targetStat.isDirectory() + } + const wasValidInput = checkCommandInput( outputKind, { @@ -201,6 +219,30 @@ async function run( message: 'The --output path must end with .json', fail: 'use a path ending with .json', }, + { + nook: true, + test: targets.length === 1, + message: 'Reachability analysis requires exactly one target directory', + fail: 'provide exactly one directory path', + }, + { + nook: true, + test: isDirectory, + message: 'Reachability analysis target must be a directory', + fail: 'provide a directory path, not a file', + }, + { + nook: true, + test: existsSync(targetPath), + message: 'Target directory must exist', + fail: 'provide an existing directory path', + }, + { + nook: true, + test: isInsideCwd, + message: 'Target directory must be inside the current working directory', + fail: 'provide a path inside the working directory', + }, ) if (!wasValidInput) { return diff --git a/packages/cli/src/commands/scan/handle-create-new-scan.mts b/packages/cli/src/commands/scan/handle-create-new-scan.mts index 33a4ca3dc..ce2969afe 100644 --- a/packages/cli/src/commands/scan/handle-create-new-scan.mts +++ b/packages/cli/src/commands/scan/handle-create-new-scan.mts @@ -165,6 +165,7 @@ export async function handleCreateNewScan({ reachabilityOptions: reach, repoName, spinner, + target: targets[0]!, }) spinner.stop() diff --git a/packages/cli/src/commands/scan/handle-scan-reach.mts b/packages/cli/src/commands/scan/handle-scan-reach.mts index a866f5b4b..314da5ff0 100644 --- a/packages/cli/src/commands/scan/handle-scan-reach.mts +++ b/packages/cli/src/commands/scan/handle-scan-reach.mts @@ -80,6 +80,7 @@ export async function handleScanReach({ packagePaths, reachabilityOptions, spinner, + target: targets[0]!, uploadManifests: true, }) diff --git a/packages/cli/src/commands/scan/perform-reachability-analysis.mts b/packages/cli/src/commands/scan/perform-reachability-analysis.mts index 1bc808fe0..142d9abde 100644 --- a/packages/cli/src/commands/scan/perform-reachability-analysis.mts +++ b/packages/cli/src/commands/scan/perform-reachability-analysis.mts @@ -35,6 +35,7 @@ export type ReachabilityAnalysisOptions = { reachabilityOptions: ReachabilityOptions repoName?: string | undefined spinner?: Spinner | undefined + target: string uploadManifests?: boolean | undefined } @@ -55,9 +56,16 @@ export async function performReachabilityAnalysis( reachabilityOptions, repoName, spinner, + target, uploadManifests = true, } = { __proto__: null, ...options } as ReachabilityAnalysisOptions + // Determine the analysis target - make it relative to cwd if absolute. + let analysisTarget = target + if (path.isAbsolute(analysisTarget)) { + analysisTarget = path.relative(cwd, analysisTarget) || '.' + } + // Check if user has enterprise plan for reachability analysis. const orgsCResult = await fetchOrganization() if (!orgsCResult.ok) { @@ -140,7 +148,7 @@ export async function performReachabilityAnalysis( // Build Coana arguments. const coanaArgs = [ 'run', - cwd, + analysisTarget, '--output-dir', path.dirname(outputFilePath), '--socket-mode',