diff --git a/CHANGELOG.md b/CHANGELOG.md index 55606c119..db963b05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ 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.34](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.33) - 2025-11-21 + +### Fixed +- The target path is now properly considered when conducting reachability analysis: `socket scan reach ` and `socket scan create --reach `. +- Fixed a bug where manifest files `` were not included in a scan when the target was pointing to a directory. + ## [1.1.33](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.33) - 2025-11-20 ### Changed diff --git a/package.json b/package.json index 3440ec1ac..aa0ec7ef5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.33", + "version": "1.1.34", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 6c0188432..659f34f83 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -8,6 +8,7 @@ import { outputCreateNewScan } from './output-create-new-scan.mts' import { reachabilityFlags } from './reachability-flags.mts' import { suggestOrgSlug } from './suggest-org-slug.mts' import { suggestTarget } from './suggest_target.mts' +import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -451,6 +452,16 @@ async function run( reachSkipCache || reachDisableAnalysisSplitting + // Validate target constraints when --reach is enabled. + const reachTargetValidation = reach + ? await validateReachabilityTarget(targets, cwd) + : { + isDirectory: false, + isInsideCwd: false, + isValid: true, + targetExists: false, + } + const wasValidInput = checkCommandInput( outputKind, { @@ -494,6 +505,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 || reachTargetValidation.isValid, + message: + 'Reachability analysis requires exactly one target directory when --reach is enabled', + fail: 'provide exactly one directory path', + }, + { + nook: true, + test: !reach || reachTargetValidation.isDirectory, + message: + 'Reachability analysis target must be a directory when --reach is enabled', + fail: 'provide a directory path, not a file', + }, + { + nook: true, + test: !reach || reachTargetValidation.targetExists, + message: 'Target directory must exist when --reach is enabled', + fail: 'provide an existing directory path', + }, + { + nook: true, + test: !reach || reachTargetValidation.isInsideCwd, + 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/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index cb2742def..b4331f966 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -304,7 +304,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -369,7 +369,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -404,7 +404,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -433,7 +433,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -526,7 +526,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -594,7 +594,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -620,7 +620,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -646,7 +646,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -676,7 +676,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -709,7 +709,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -734,7 +734,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -759,7 +759,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', @@ -789,7 +789,7 @@ describe('socket scan create', async () => { 'create', FLAG_ORG, 'fakeOrg', - 'target', + 'test/fixtures/commands/scan/reach', FLAG_DRY_RUN, '--repo', 'xyz', diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 3bc7fd609..f9f850f86 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -6,6 +6,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { handleScanReach } from './handle-scan-reach.mts' import { reachabilityFlags } from './reachability-flags.mts' import { suggestTarget } from './suggest_target.mts' +import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -154,7 +155,7 @@ async function run( : processCwd // Accept zero or more paths. Default to cwd() if none given. - let targets = cli.input || [cwd] + let targets = cli.input.length ? cli.input : [cwd] // Use suggestTarget if no targets specified and in interactive mode if (!targets.length && !dryRun && interactive) { @@ -167,6 +168,9 @@ async function run( const outputKind = getOutputKind(json, markdown) + // Validate target constraints for reachability analysis. + const targetValidation = await validateReachabilityTarget(targets, cwd) + const wasValidInput = checkCommandInput( outputKind, { @@ -187,6 +191,30 @@ async function run( message: 'The json and markdown flags cannot be both set, pick one', fail: 'omit one', }, + { + nook: true, + test: targetValidation.isValid, + message: 'Reachability analysis requires exactly one target directory', + fail: 'provide exactly one directory path', + }, + { + nook: true, + test: targetValidation.isDirectory, + message: 'Reachability analysis target must be a directory', + fail: 'provide a directory path, not a file', + }, + { + nook: true, + test: targetValidation.targetExists, + message: 'Target directory must exist', + fail: 'provide an existing directory path', + }, + { + nook: true, + test: targetValidation.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/src/commands/scan/cmd-scan-reach.test.mts b/src/commands/scan/cmd-scan-reach.test.mts index 4639d0e41..0ba1711e2 100644 --- a/src/commands/scan/cmd-scan-reach.test.mts +++ b/src/commands/scan/cmd-scan-reach.test.mts @@ -778,7 +778,7 @@ describe('socket scan reach', async () => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr expect(output).toMatch( - /no eligible files|file.*dir.*must contain|not.*found/i, + /Target directory must exist|no eligible files|file.*dir.*must contain|not.*found/i, ) expect(code).toBeGreaterThan(0) }, diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index b36a32fc7..acf550a19 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -173,6 +173,7 @@ export async function handleCreateNewScan({ reachabilityOptions: reach, repoName, spinner, + target: targets[0]!, }) spinner.stop() diff --git a/src/commands/scan/handle-scan-reach.mts b/src/commands/scan/handle-scan-reach.mts index 5997278e6..3d13c76f2 100644 --- a/src/commands/scan/handle-scan-reach.mts +++ b/src/commands/scan/handle-scan-reach.mts @@ -73,6 +73,7 @@ export async function handleScanReach({ packagePaths, reachabilityOptions, spinner, + target: targets[0]!, uploadManifests: true, }) diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 847abce01..99fc579bf 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -32,6 +32,7 @@ export type ReachabilityAnalysisOptions = { reachabilityOptions: ReachabilityOptions repoName?: string | undefined spinner?: Spinner | undefined + target: string uploadManifests?: boolean | undefined } @@ -51,9 +52,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) { @@ -136,7 +144,7 @@ export async function performReachabilityAnalysis( // Build Coana arguments. const coanaArgs = [ 'run', - cwd, + analysisTarget, '--output-dir', cwd, '--socket-mode', diff --git a/src/commands/scan/validate-reachability-target.mts b/src/commands/scan/validate-reachability-target.mts new file mode 100644 index 000000000..e674f1acd --- /dev/null +++ b/src/commands/scan/validate-reachability-target.mts @@ -0,0 +1,53 @@ +import { existsSync, promises as fs } from 'node:fs' +import path from 'node:path' + +export type ReachabilityTargetValidation = { + isDirectory: boolean + isInsideCwd: boolean + isValid: boolean + targetExists: boolean +} + +/** + * Validates that a target directory meets the requirements for reachability analysis. + * + * @param targets - Array of target paths to validate. + * @param cwd - Current working directory. + * @returns Validation result object with boolean flags. + */ +export async function validateReachabilityTarget( + targets: string[], + cwd: string, +): Promise { + const result: ReachabilityTargetValidation = { + isDirectory: false, + isInsideCwd: false, + isValid: targets.length === 1, + targetExists: false, + } + + if (!result.isValid || !targets[0]) { + return result + } + + // Resolve cwd to absolute path to handle relative cwd values. + const absoluteCwd = path.resolve(cwd) + + // Resolve target path to absolute for validation. + const targetPath = path.isAbsolute(targets[0]) + ? targets[0] + : path.resolve(absoluteCwd, targets[0]) + + // Check if target is inside cwd. + const relativePath = path.relative(absoluteCwd, targetPath) + result.isInsideCwd = + !relativePath.startsWith('..') && !path.isAbsolute(relativePath) + + result.targetExists = existsSync(targetPath) + if (result.targetExists) { + const targetStat = await fs.stat(targetPath) + result.isDirectory = targetStat.isDirectory() + } + + return result +} diff --git a/src/utils/glob.mts b/src/utils/glob.mts index 6d456296a..0b8023f36 100644 --- a/src/utils/glob.mts +++ b/src/utils/glob.mts @@ -5,7 +5,7 @@ import ignore from 'ignore' import micromatch from 'micromatch' import { parse as yamlParse } from 'yaml' -import { safeReadFile } from '@socketsecurity/registry/lib/fs' +import { isDirSync, safeReadFile } from '@socketsecurity/registry/lib/fs' import { defaultIgnore } from '@socketsecurity/registry/lib/globs' import { readPackageJson } from '@socketsecurity/registry/lib/packages' import { transform } from '@socketsecurity/registry/lib/streams' @@ -289,7 +289,19 @@ export function isReportSupportedFile( export function pathsToGlobPatterns( paths: string[] | readonly string[], + cwd?: string | undefined, ): string[] { // TODO: Does not support `~/` paths. - return paths.map(p => (p === '.' || p === './' ? '**/*' : p)) + return paths.map(p => { + // Convert current directory references to glob patterns. + if (p === '.' || p === './') { + return '**/*' + } + const absolutePath = path.isAbsolute(p) ? p : path.resolve(cwd ?? process.cwd(), p) + // If the path is a directory, scan it recursively for all files. + if (isDirSync(absolutePath)) { + return `${p}/**/*` + } + return p + }) } diff --git a/src/utils/path-resolve.mts b/src/utils/path-resolve.mts index f6f744c07..f4877543c 100644 --- a/src/utils/path-resolve.mts +++ b/src/utils/path-resolve.mts @@ -114,7 +114,7 @@ export async function getPackageFilesForScan( ...options, } as PackageFilesForScanOptions - const filepaths = await globWithGitIgnore(pathsToGlobPatterns(inputPaths), { + const filepaths = await globWithGitIgnore(pathsToGlobPatterns(inputPaths, options?.cwd), { cwd, socketConfig, }) diff --git a/src/utils/path-resolve.test.mts b/src/utils/path-resolve.test.mts index 437a81b4c..001902251 100644 --- a/src/utils/path-resolve.test.mts +++ b/src/utils/path-resolve.test.mts @@ -152,6 +152,27 @@ describe('Path Resolve', () => { ]) }) + it('should handle a directory path input', async () => { + const subDirPath = normalizePath(path.join(mockFixturePath, 'subdir')) + mockTestFs({ + [`${mockFixturePath}/package.json`]: '{}', + [`${subDirPath}/package.json`]: '{}', + [`${subDirPath}/nested/package.json`]: '{}', + }) + + const actual = await sortedGetPackageFilesFullScans( + [subDirPath], + globPatterns, + { + cwd: mockFixturePath, + }, + ) + expect(actual.map(normalizePath)).toEqual([ + `${subDirPath}/nested/package.json`, + `${subDirPath}/package.json`, + ]) + }) + it('should respect ignores from socket config', async () => { mockTestFs({ [`${mockFixturePath}/bar/package-lock.json`]: '{}',