Skip to content

Commit fd5d126

Browse files
committed
Fix handling of target in reachability mode. Fix recursive inclusion of manifest files when target is a directory
1 parent 3321a4d commit fd5d126

11 files changed

+181
-19
lines changed

src/commands/scan/cmd-scan-create.mts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { outputCreateNewScan } from './output-create-new-scan.mts'
88
import { reachabilityFlags } from './reachability-flags.mts'
99
import { suggestOrgSlug } from './suggest-org-slug.mts'
1010
import { suggestTarget } from './suggest_target.mts'
11+
import { validateReachabilityTarget } from './validate-reachability-target.mts'
1112
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
1213
import { commonFlags, outputFlags } from '../../flags.mts'
1314
import { checkCommandInput } from '../../utils/check-input.mts'
@@ -451,6 +452,16 @@ async function run(
451452
reachSkipCache ||
452453
reachDisableAnalysisSplitting
453454

455+
// Validate target constraints when --reach is enabled.
456+
const reachTargetValidation = reach
457+
? await validateReachabilityTarget(targets, cwd)
458+
: {
459+
isDirectory: false,
460+
isInsideCwd: false,
461+
isValid: true,
462+
targetExists: false,
463+
}
464+
454465
const wasValidInput = checkCommandInput(
455466
outputKind,
456467
{
@@ -494,6 +505,33 @@ async function run(
494505
message: 'Reachability analysis flags require --reach to be enabled',
495506
fail: 'add --reach flag to use --reach-* options',
496507
},
508+
{
509+
nook: true,
510+
test: !reach || reachTargetValidation.isValid,
511+
message:
512+
'Reachability analysis requires exactly one target directory when --reach is enabled',
513+
fail: 'provide exactly one directory path',
514+
},
515+
{
516+
nook: true,
517+
test: !reach || reachTargetValidation.isDirectory,
518+
message:
519+
'Reachability analysis target must be a directory when --reach is enabled',
520+
fail: 'provide a directory path, not a file',
521+
},
522+
{
523+
nook: true,
524+
test: !reach || reachTargetValidation.targetExists,
525+
message: 'Target directory must exist when --reach is enabled',
526+
fail: 'provide an existing directory path',
527+
},
528+
{
529+
nook: true,
530+
test: !reach || reachTargetValidation.isInsideCwd,
531+
message:
532+
'Target directory must be inside the current working directory when --reach is enabled',
533+
fail: 'provide a path inside the working directory',
534+
},
497535
)
498536
if (!wasValidInput) {
499537
return

src/commands/scan/cmd-scan-create.test.mts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ describe('socket scan create', async () => {
304304
'create',
305305
FLAG_ORG,
306306
'fakeOrg',
307-
'target',
307+
'test/fixtures/commands/scan/reach',
308308
FLAG_DRY_RUN,
309309
'--repo',
310310
'xyz',
@@ -369,7 +369,7 @@ describe('socket scan create', async () => {
369369
'create',
370370
FLAG_ORG,
371371
'fakeOrg',
372-
'target',
372+
'test/fixtures/commands/scan/reach',
373373
FLAG_DRY_RUN,
374374
'--repo',
375375
'xyz',
@@ -404,7 +404,7 @@ describe('socket scan create', async () => {
404404
'create',
405405
FLAG_ORG,
406406
'fakeOrg',
407-
'target',
407+
'test/fixtures/commands/scan/reach',
408408
FLAG_DRY_RUN,
409409
'--repo',
410410
'xyz',
@@ -433,7 +433,7 @@ describe('socket scan create', async () => {
433433
'create',
434434
FLAG_ORG,
435435
'fakeOrg',
436-
'target',
436+
'test/fixtures/commands/scan/reach',
437437
FLAG_DRY_RUN,
438438
'--repo',
439439
'xyz',
@@ -526,7 +526,7 @@ describe('socket scan create', async () => {
526526
'create',
527527
FLAG_ORG,
528528
'fakeOrg',
529-
'target',
529+
'test/fixtures/commands/scan/reach',
530530
FLAG_DRY_RUN,
531531
'--repo',
532532
'xyz',
@@ -594,7 +594,7 @@ describe('socket scan create', async () => {
594594
'create',
595595
FLAG_ORG,
596596
'fakeOrg',
597-
'target',
597+
'test/fixtures/commands/scan/reach',
598598
FLAG_DRY_RUN,
599599
'--repo',
600600
'xyz',
@@ -620,7 +620,7 @@ describe('socket scan create', async () => {
620620
'create',
621621
FLAG_ORG,
622622
'fakeOrg',
623-
'target',
623+
'test/fixtures/commands/scan/reach',
624624
FLAG_DRY_RUN,
625625
'--repo',
626626
'xyz',
@@ -646,7 +646,7 @@ describe('socket scan create', async () => {
646646
'create',
647647
FLAG_ORG,
648648
'fakeOrg',
649-
'target',
649+
'test/fixtures/commands/scan/reach',
650650
FLAG_DRY_RUN,
651651
'--repo',
652652
'xyz',
@@ -676,7 +676,7 @@ describe('socket scan create', async () => {
676676
'create',
677677
FLAG_ORG,
678678
'fakeOrg',
679-
'target',
679+
'test/fixtures/commands/scan/reach',
680680
FLAG_DRY_RUN,
681681
'--repo',
682682
'xyz',
@@ -709,7 +709,7 @@ describe('socket scan create', async () => {
709709
'create',
710710
FLAG_ORG,
711711
'fakeOrg',
712-
'target',
712+
'test/fixtures/commands/scan/reach',
713713
FLAG_DRY_RUN,
714714
'--repo',
715715
'xyz',
@@ -734,7 +734,7 @@ describe('socket scan create', async () => {
734734
'create',
735735
FLAG_ORG,
736736
'fakeOrg',
737-
'target',
737+
'test/fixtures/commands/scan/reach',
738738
FLAG_DRY_RUN,
739739
'--repo',
740740
'xyz',
@@ -759,7 +759,7 @@ describe('socket scan create', async () => {
759759
'create',
760760
FLAG_ORG,
761761
'fakeOrg',
762-
'target',
762+
'test/fixtures/commands/scan/reach',
763763
FLAG_DRY_RUN,
764764
'--repo',
765765
'xyz',
@@ -789,7 +789,7 @@ describe('socket scan create', async () => {
789789
'create',
790790
FLAG_ORG,
791791
'fakeOrg',
792-
'target',
792+
'test/fixtures/commands/scan/reach',
793793
FLAG_DRY_RUN,
794794
'--repo',
795795
'xyz',

src/commands/scan/cmd-scan-reach.mts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
66
import { handleScanReach } from './handle-scan-reach.mts'
77
import { reachabilityFlags } from './reachability-flags.mts'
88
import { suggestTarget } from './suggest_target.mts'
9+
import { validateReachabilityTarget } from './validate-reachability-target.mts'
910
import constants from '../../constants.mts'
1011
import { commonFlags, outputFlags } from '../../flags.mts'
1112
import { checkCommandInput } from '../../utils/check-input.mts'
@@ -154,7 +155,7 @@ async function run(
154155
: processCwd
155156

156157
// Accept zero or more paths. Default to cwd() if none given.
157-
let targets = cli.input || [cwd]
158+
let targets = cli.input.length ? cli.input : [cwd]
158159

159160
// Use suggestTarget if no targets specified and in interactive mode
160161
if (!targets.length && !dryRun && interactive) {
@@ -167,6 +168,9 @@ async function run(
167168

168169
const outputKind = getOutputKind(json, markdown)
169170

171+
// Validate target constraints for reachability analysis.
172+
const targetValidation = await validateReachabilityTarget(targets, cwd)
173+
170174
const wasValidInput = checkCommandInput(
171175
outputKind,
172176
{
@@ -187,6 +191,30 @@ async function run(
187191
message: 'The json and markdown flags cannot be both set, pick one',
188192
fail: 'omit one',
189193
},
194+
{
195+
nook: true,
196+
test: targetValidation.isValid,
197+
message: 'Reachability analysis requires exactly one target directory',
198+
fail: 'provide exactly one directory path',
199+
},
200+
{
201+
nook: true,
202+
test: targetValidation.isDirectory,
203+
message: 'Reachability analysis target must be a directory',
204+
fail: 'provide a directory path, not a file',
205+
},
206+
{
207+
nook: true,
208+
test: targetValidation.targetExists,
209+
message: 'Target directory must exist',
210+
fail: 'provide an existing directory path',
211+
},
212+
{
213+
nook: true,
214+
test: targetValidation.isInsideCwd,
215+
message: 'Target directory must be inside the current working directory',
216+
fail: 'provide a path inside the working directory',
217+
},
190218
)
191219
if (!wasValidInput) {
192220
return

src/commands/scan/cmd-scan-reach.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ describe('socket scan reach', async () => {
778778
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
779779
const output = stdout + stderr
780780
expect(output).toMatch(
781-
/no eligible files|file.*dir.*must contain|not.*found/i,
781+
/Target directory must exist|no eligible files|file.*dir.*must contain|not.*found/i,
782782
)
783783
expect(code).toBeGreaterThan(0)
784784
},

src/commands/scan/handle-create-new-scan.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export async function handleCreateNewScan({
173173
reachabilityOptions: reach,
174174
repoName,
175175
spinner,
176+
target: targets[0]!,
176177
})
177178

178179
spinner.stop()

src/commands/scan/handle-scan-reach.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export async function handleScanReach({
7373
packagePaths,
7474
reachabilityOptions,
7575
spinner,
76+
target: targets[0]!,
7677
uploadManifests: true,
7778
})
7879

src/commands/scan/perform-reachability-analysis.mts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type ReachabilityAnalysisOptions = {
3232
reachabilityOptions: ReachabilityOptions
3333
repoName?: string | undefined
3434
spinner?: Spinner | undefined
35+
target: string
3536
uploadManifests?: boolean | undefined
3637
}
3738

@@ -51,9 +52,16 @@ export async function performReachabilityAnalysis(
5152
reachabilityOptions,
5253
repoName,
5354
spinner,
55+
target,
5456
uploadManifests = true,
5557
} = { __proto__: null, ...options } as ReachabilityAnalysisOptions
5658

59+
// Determine the analysis target - make it relative to cwd if absolute.
60+
let analysisTarget = target
61+
if (path.isAbsolute(analysisTarget)) {
62+
analysisTarget = path.relative(cwd, analysisTarget) || '.'
63+
}
64+
5765
// Check if user has enterprise plan for reachability analysis.
5866
const orgsCResult = await fetchOrganization()
5967
if (!orgsCResult.ok) {
@@ -136,7 +144,7 @@ export async function performReachabilityAnalysis(
136144
// Build Coana arguments.
137145
const coanaArgs = [
138146
'run',
139-
cwd,
147+
analysisTarget,
140148
'--output-dir',
141149
cwd,
142150
'--socket-mode',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { existsSync, promises as fs } from 'node:fs'
2+
import path from 'node:path'
3+
4+
export type ReachabilityTargetValidation = {
5+
isDirectory: boolean
6+
isInsideCwd: boolean
7+
isValid: boolean
8+
targetExists: boolean
9+
}
10+
11+
/**
12+
* Validates that a target directory meets the requirements for reachability analysis.
13+
*
14+
* @param targets - Array of target paths to validate.
15+
* @param cwd - Current working directory.
16+
* @returns Validation result object with boolean flags.
17+
*/
18+
export async function validateReachabilityTarget(
19+
targets: string[],
20+
cwd: string,
21+
): Promise<ReachabilityTargetValidation> {
22+
const result: ReachabilityTargetValidation = {
23+
isDirectory: false,
24+
isInsideCwd: false,
25+
isValid: targets.length === 1,
26+
targetExists: false,
27+
}
28+
29+
if (!result.isValid || !targets[0]) {
30+
return result
31+
}
32+
33+
// Resolve cwd to absolute path to handle relative cwd values.
34+
const absoluteCwd = path.resolve(cwd)
35+
36+
// Resolve target path to absolute for validation.
37+
const targetPath = path.isAbsolute(targets[0])
38+
? targets[0]
39+
: path.resolve(absoluteCwd, targets[0])
40+
41+
// Check if target is inside cwd.
42+
const relativePath = path.relative(absoluteCwd, targetPath)
43+
result.isInsideCwd =
44+
!relativePath.startsWith('..') && !path.isAbsolute(relativePath)
45+
46+
result.targetExists = existsSync(targetPath)
47+
if (result.targetExists) {
48+
const targetStat = await fs.stat(targetPath)
49+
result.isDirectory = targetStat.isDirectory()
50+
}
51+
52+
return result
53+
}

src/utils/glob.mts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ignore from 'ignore'
55
import micromatch from 'micromatch'
66
import { parse as yamlParse } from 'yaml'
77

8-
import { safeReadFile } from '@socketsecurity/registry/lib/fs'
8+
import { isDirSync, safeReadFile } from '@socketsecurity/registry/lib/fs'
99
import { defaultIgnore } from '@socketsecurity/registry/lib/globs'
1010
import { readPackageJson } from '@socketsecurity/registry/lib/packages'
1111
import { transform } from '@socketsecurity/registry/lib/streams'
@@ -289,7 +289,19 @@ export function isReportSupportedFile(
289289

290290
export function pathsToGlobPatterns(
291291
paths: string[] | readonly string[],
292+
cwd?: string | undefined,
292293
): string[] {
293294
// TODO: Does not support `~/` paths.
294-
return paths.map(p => (p === '.' || p === './' ? '**/*' : p))
295+
return paths.map(p => {
296+
// Convert current directory references to glob patterns.
297+
if (p === '.' || p === './') {
298+
return '**/*'
299+
}
300+
const absolutePath = path.isAbsolute(p) ? p : path.resolve(cwd ?? process.cwd(), p)
301+
// If the path is a directory, scan it recursively for all files.
302+
if (isDirSync(absolutePath)) {
303+
return `${p}/**/*`
304+
}
305+
return p
306+
})
295307
}

src/utils/path-resolve.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function getPackageFilesForScan(
114114
...options,
115115
} as PackageFilesForScanOptions
116116

117-
const filepaths = await globWithGitIgnore(pathsToGlobPatterns(inputPaths), {
117+
const filepaths = await globWithGitIgnore(pathsToGlobPatterns(inputPaths, options?.cwd), {
118118
cwd,
119119
socketConfig,
120120
})

0 commit comments

Comments
 (0)