Skip to content

Commit d1060bc

Browse files
cursoragentsimonhj
andcommitted
feat(scan): add tier1 exclude paths flag
Co-authored-by: Simon <simonhj@users.noreply.github.com>
1 parent e2198d1 commit d1060bc

14 files changed

Lines changed: 638 additions & 21 deletions

src/commands/ci/handle-ci.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export async function handleCi(autoManifest: boolean): Promise<void> {
5151
pendingHead: true,
5252
pullRequest: 0,
5353
reach: {
54+
excludePaths: [],
5455
reachAnalysisMemoryLimit: 0,
5556
reachAnalysisTimeout: 0,
5657
reachConcurrency: 1,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path'
33
import { joinAnd } from '@socketsecurity/registry/lib/arrays'
44
import { logger } from '@socketsecurity/registry/lib/logger'
55

6+
import { assertNoNegationPatterns } from './exclude-paths.mts'
67
import { handleCreateNewScan } from './handle-create-new-scan.mts'
78
import { outputCreateNewScan } from './output-create-new-scan.mts'
89
import { reachabilityFlags } from './reachability-flags.mts'
@@ -279,6 +280,7 @@ async function run(
279280
setAsAlertsPage: boolean
280281
tmp: boolean
281282
// Reachability flags.
283+
excludePaths: string[] | undefined
282284
reach: boolean
283285
reachAnalysisMemoryLimit: number
284286
reachAnalysisTimeout: number
@@ -463,9 +465,14 @@ async function run(
463465
logger.error('')
464466
}
465467

468+
const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
469+
assertNoNegationPatterns(excludePaths)
470+
466471
const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])
467472

468473
// Validation helpers for better readability.
474+
const hasExcludePaths = excludePaths.length > 0
475+
469476
const hasReachEcosystems = reachEcosystems.length > 0
470477

471478
const hasReachExcludePaths = reachExcludePaths.length > 0
@@ -488,6 +495,7 @@ async function run(
488495
reachVersion !== reachabilityFlags['reachVersion']?.default
489496

490497
const isUsingAnyReachabilityFlags =
498+
hasExcludePaths ||
491499
hasReachEcosystems ||
492500
hasReachExcludePaths ||
493501
isUsingNonDefaultAnalytics ||
@@ -608,6 +616,7 @@ async function run(
608616
pendingHead: Boolean(pendingHead),
609617
pullRequest: Number(pullRequest),
610618
reach: {
619+
excludePaths,
611620
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
612621
reachAnalysisTimeout: Number(reachAnalysisTimeout),
613622
reachConcurrency: Number(reachConcurrency),

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('socket scan create', async () => {
5555
--workspace The workspace in the Socket Organization that the repository is in to associate with the full scan.
5656
5757
Reachability Options (when --reach is used)
58+
--exclude-paths List of glob patterns to exclude from the entire Tier 1 scan, including SCA/SBOM manifest discovery. Patterns are matched relative to the project root. Bare directory names are auto-extended to recursive globs (e.g. `tests` becomes `tests/**`). Trailing slashes are stripped. Negation patterns (`!path`) are not supported. Accepts a comma-separated value or multiple flags.
5859
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
5960
--reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly.
6061
--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. NPM reachability analysis does not support concurrent execution, so the concurrency level is ignored for NPM.
@@ -185,6 +186,38 @@ describe('socket scan create', async () => {
185186
},
186187
)
187188

189+
cmdit(
190+
[
191+
'scan',
192+
'create',
193+
FLAG_ORG,
194+
'fakeOrg',
195+
'target',
196+
FLAG_DRY_RUN,
197+
'--repo',
198+
'xyz',
199+
'--branch',
200+
'abc',
201+
'--exclude-paths',
202+
'tests',
203+
FLAG_CONFIG,
204+
'{"apiToken":"fakeToken"}',
205+
],
206+
'should fail when --exclude-paths is used without --reach',
207+
async cmd => {
208+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
209+
const output = stdout + stderr
210+
expect(output).toContain(
211+
'Reachability analysis flags require --reach to be enabled',
212+
)
213+
expect(output).toContain('add --reach flag to use --reach-* options')
214+
expect(
215+
code,
216+
'should exit with non-zero code when validation fails',
217+
).not.toBe(0)
218+
},
219+
)
220+
188221
cmdit(
189222
[
190223
'scan',
@@ -437,6 +470,32 @@ describe('socket scan create', async () => {
437470
},
438471
)
439472

473+
cmdit(
474+
[
475+
'scan',
476+
'create',
477+
FLAG_ORG,
478+
'fakeOrg',
479+
'test/fixtures/commands/scan/simple-npm',
480+
FLAG_DRY_RUN,
481+
'--repo',
482+
'xyz',
483+
'--branch',
484+
'abc',
485+
'--reach',
486+
'--exclude-paths',
487+
'tests',
488+
FLAG_CONFIG,
489+
'{"apiToken":"fakeToken"}',
490+
],
491+
'should succeed when --exclude-paths is used with --reach',
492+
async cmd => {
493+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
494+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
495+
expect(code, 'should exit with code 0 when all flags are valid').toBe(0)
496+
},
497+
)
498+
440499
cmdit(
441500
[
442501
'scan',

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path'
33
import { joinAnd } from '@socketsecurity/registry/lib/arrays'
44
import { logger } from '@socketsecurity/registry/lib/logger'
55

6+
import { assertNoNegationPatterns } from './exclude-paths.mts'
67
import { handleScanReach } from './handle-scan-reach.mts'
78
import { reachabilityFlags } from './reachability-flags.mts'
89
import { suggestTarget } from './suggest_target.mts'
@@ -167,8 +168,10 @@ async function run(
167168
const dryRun = !!cli.flags['dryRun']
168169

169170
// Process comma-separated values for isMultiple flags.
171+
const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
170172
const reachEcosystemsRaw = cmdFlagValueToArray(cli.flags['reachEcosystems'])
171173
const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])
174+
assertNoNegationPatterns(excludePaths)
172175

173176
// Validate ecosystem values.
174177
const reachEcosystems: PURL_Type[] = []
@@ -272,6 +275,7 @@ async function run(
272275
outputKind,
273276
outputPath: outputPath || '',
274277
reachabilityOptions: {
278+
excludePaths,
275279
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
276280
reachAnalysisTimeout: Number(reachAnalysisTimeout),
277281
reachConcurrency: Number(reachConcurrency),

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('socket scan reach', async () => {
3737
--output Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.
3838
3939
Reachability Options
40+
--exclude-paths List of glob patterns to exclude from the entire Tier 1 scan, including SCA/SBOM manifest discovery. Patterns are matched relative to the project root. Bare directory names are auto-extended to recursive globs (e.g. \`tests\` becomes \`tests/**\`). Trailing slashes are stripped. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
4041
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
4142
--reach-analysis-timeout Set timeout for the reachability analysis. Split analysis runs may cause the total scan time to exceed this timeout significantly.
4243
--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. NPM reachability analysis does not support concurrent execution, so the concurrency level is ignored for NPM.
@@ -295,6 +296,50 @@ describe('socket scan reach', async () => {
295296
'scan',
296297
'reach',
297298
FLAG_DRY_RUN,
299+
'--exclude-paths',
300+
'node_modules,dist',
301+
'--org',
302+
'fakeOrg',
303+
FLAG_CONFIG,
304+
'{"apiToken":"fakeToken"}',
305+
],
306+
'should accept --exclude-paths with comma-separated values',
307+
async cmd => {
308+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
309+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
310+
expect(code, 'should exit with code 0').toBe(0)
311+
},
312+
)
313+
314+
cmdit(
315+
[
316+
'scan',
317+
'reach',
318+
FLAG_DRY_RUN,
319+
'--exclude-paths',
320+
'node_modules',
321+
'--exclude-paths',
322+
'dist',
323+
'--org',
324+
'fakeOrg',
325+
FLAG_CONFIG,
326+
'{"apiToken":"fakeToken"}',
327+
],
328+
'should accept multiple --exclude-paths flags',
329+
async cmd => {
330+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
331+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
332+
expect(code, 'should exit with code 0').toBe(0)
333+
},
334+
)
335+
336+
cmdit(
337+
[
338+
'scan',
339+
'reach',
340+
FLAG_DRY_RUN,
341+
'--exclude-paths',
342+
'build',
298343
'--reach-exclude-paths',
299344
'node_modules,dist',
300345
'--org',
@@ -310,6 +355,29 @@ describe('socket scan reach', async () => {
310355
},
311356
)
312357

358+
cmdit(
359+
[
360+
'scan',
361+
'reach',
362+
FLAG_DRY_RUN,
363+
'--exclude-paths',
364+
'!tests/keep',
365+
'--org',
366+
'fakeOrg',
367+
FLAG_CONFIG,
368+
'{"apiToken":"fakeToken"}',
369+
],
370+
'should reject --exclude-paths negation patterns',
371+
async cmd => {
372+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
373+
const output = stdout + stderr
374+
expect(output).toContain(
375+
"--exclude-paths does not support negation patterns. Got: '!tests/keep'.",
376+
)
377+
expect(code, 'should exit with non-zero code').not.toBe(0)
378+
},
379+
)
380+
313381
cmdit(
314382
[
315383
'scan',

src/commands/scan/create-scan-from-github.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ async function scanOneRepo(
250250
pendingHead: true,
251251
pullRequest: 0,
252252
reach: {
253+
excludePaths: [],
253254
reachAnalysisMemoryLimit: 0,
254255
reachAnalysisTimeout: 0,
255256
reachConcurrency: 1,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { InputError } from '../../utils/errors.mts'
2+
3+
export function excludePathToProjectIgnorePath(path: string): string {
4+
const stripped = stripTrailingSlash(path)
5+
return stripped.endsWith('/**') ? stripped : `${stripped}/**`
6+
}
7+
8+
export function assertNoNegationPatterns(paths: readonly string[]): void {
9+
for (const path of paths) {
10+
if (path.startsWith('!')) {
11+
throw new InputError(
12+
`--exclude-paths does not support negation patterns. Got: '${path}'.`,
13+
)
14+
}
15+
}
16+
}
17+
18+
export function normalizeExcludePath(path: string): string {
19+
const stripped = stripTrailingSlash(path)
20+
return stripped.endsWith('/*') ? stripped : `${stripped}/**`
21+
}
22+
23+
function stripTrailingSlash(path: string): string {
24+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
assertNoNegationPatterns,
5+
excludePathToProjectIgnorePath,
6+
normalizeExcludePath,
7+
} from './exclude-paths.mts'
8+
import { InputError } from '../../utils/errors.mts'
9+
10+
describe('exclude-paths', () => {
11+
describe('assertNoNegationPatterns', () => {
12+
it('allows positive patterns', () => {
13+
expect(() =>
14+
assertNoNegationPatterns(['tests', 'packages/*']),
15+
).not.toThrow()
16+
})
17+
18+
it('rejects negation patterns', () => {
19+
expect(() => assertNoNegationPatterns(['!tests/keep'])).toThrow(
20+
InputError,
21+
)
22+
expect(() => assertNoNegationPatterns(['!tests/keep'])).toThrow(
23+
"--exclude-paths does not support negation patterns. Got: '!tests/keep'.",
24+
)
25+
})
26+
})
27+
28+
describe('excludePathToProjectIgnorePath', () => {
29+
it.each([
30+
['packages/*', 'packages/*/**'],
31+
['tests', 'tests/**'],
32+
['tests/', 'tests/**'],
33+
['tests/**', 'tests/**'],
34+
])('converts %s to %s', (input, expected) => {
35+
expect(excludePathToProjectIgnorePath(input)).toBe(expected)
36+
})
37+
})
38+
39+
describe('normalizeExcludePath', () => {
40+
it.each([
41+
['tests', 'tests/**'],
42+
['tests/', 'tests/**'],
43+
['tests/*', 'tests/*'],
44+
['tests/**', 'tests/**/**'],
45+
])('normalizes %s to %s', (input, expected) => {
46+
expect(normalizeExcludePath(input)).toBe(expected)
47+
})
48+
})
49+
})

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug'
66
import { logger } from '@socketsecurity/registry/lib/logger'
77
import { pluralize } from '@socketsecurity/registry/lib/words'
88

9+
import {
10+
excludePathToProjectIgnorePath,
11+
normalizeExcludePath,
12+
} from './exclude-paths.mts'
913
import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts'
1014
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
1115
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
@@ -172,8 +176,27 @@ export async function handleCreateNewScan({
172176
? socketYmlResult.data?.parsed
173177
: undefined
174178

179+
const excludePaths = reach.runReachabilityAnalysis ? reach.excludePaths : []
180+
const scaExcludeGlobs = excludePaths.map(excludePathToProjectIgnorePath)
181+
const coanaExcludeGlobs = excludePaths.map(normalizeExcludePath)
182+
const effectiveSocketConfig = scaExcludeGlobs.length
183+
? {
184+
...socketConfig,
185+
projectIgnorePaths: [
186+
...(socketConfig?.projectIgnorePaths ?? []),
187+
...scaExcludeGlobs,
188+
],
189+
}
190+
: socketConfig
191+
const mergedReachabilityOptions = excludePaths.length
192+
? {
193+
...reach,
194+
reachExcludePaths: [...reach.reachExcludePaths, ...coanaExcludeGlobs],
195+
}
196+
: reach
197+
175198
const packagePaths = await getPackageFilesForScan(targets, supportedFiles, {
176-
config: socketConfig,
199+
config: effectiveSocketConfig,
177200
cwd,
178201
})
179202

@@ -213,7 +236,7 @@ export async function handleCreateNewScan({
213236
logger.error('')
214237
logger.info('Starting reachability analysis...')
215238
debugFn('notice', 'Reachability analysis enabled')
216-
debugDir('inspect', { reachabilityOptions: reach })
239+
debugDir('inspect', { reachabilityOptions: mergedReachabilityOptions })
217240

218241
spinner.start()
219242

@@ -222,7 +245,7 @@ export async function handleCreateNewScan({
222245
cwd,
223246
orgSlug,
224247
packagePaths,
225-
reachabilityOptions: reach,
248+
reachabilityOptions: mergedReachabilityOptions,
226249
repoName,
227250
spinner,
228251
target: targets[0]!,

0 commit comments

Comments
 (0)