Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cli/data/socket-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ FLAGS=(
[repos update]="--default-branch --homepage --interactive --org --repo-description --repo-name --visibility"
[repos view]="--interactive --org --repo-name"
[scan]=""
[scan create]="--auto-manifest --branch --commit-hash --commit-message --committers --cwd --default-branch --interactive --json --markdown --org --pull-request --reach --reach-analysis-memory-limit --reach-analysis-timeout --reach-disable-analytics --reach-ecosystems --reach-exclude-paths --read-only --repo --report --set-as-alerts-page --tmp"
[scan create]="--auto-manifest --branch --commit-hash --commit-message --committers --cwd --default-branch --exclude-paths --interactive --json --markdown --org --pull-request --reach --reach-analysis-memory-limit --reach-analysis-timeout --reach-disable-analytics --reach-ecosystems --reach-exclude-paths --read-only --repo --report --set-as-alerts-page --tmp"
[scan del]="--interactive --org"
[scan diff]="--depth --file --interactive --org"
[scan list]="--branch --direction --from-time --interactive --json --markdown --org --page --per-page --repo --sort --until-time"
[scan metadata]="--interactive --org"
[scan reach]="--reach-analysis-memory-limit --reach-analysis-timeout --reach-disable-analytics --reach-ecosystems --reach-exclude-paths"
[scan reach]="--exclude-paths --reach-analysis-memory-limit --reach-analysis-timeout --reach-disable-analytics --reach-ecosystems --reach-exclude-paths"
[scan report]="--fold --interactive --license --org --report-level --short"
[scan view]="--interactive --org --stream"
[threat-feed]="--direction --eco --filter --interactive --json --markdown --org --page --per-page"
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/ci/handle-ci.mts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function handleCi(autoManifest: boolean): Promise<void> {
pendingHead: true,
pullRequest: 0,
reach: {
excludePaths: [],
reachAnalysisMemoryLimit: 0,
reachAnalysisTimeout: 0,
reachConcurrency: 1,
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger'

const logger = getDefaultLogger()

import { assertNoNegationPatterns } from './exclude-paths.mts'
import { handleCreateNewScan } from './handle-create-new-scan.mts'
import { outputCreateNewScan } from './output-create-new-scan.mts'
import { reachabilityFlags } from './reachability-flags.mts'
import { excludePathsFlag, 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'
Expand Down Expand Up @@ -307,6 +308,7 @@ async function run(
hidden,
flags: {
...generalFlags,
...excludePathsFlag,
...reachabilityFlags,
},
help: command => `
Expand All @@ -317,7 +319,7 @@ async function run(
${getFlagApiRequirementsOutput(`${parentName}:${CMD_NAME}`)}

Options
${getFlagListOutput(generalFlags)}
${getFlagListOutput({ ...generalFlags, ...excludePathsFlag })}

Reachability Options (when --reach is used)
${getFlagListOutput(reachabilityFlags)}
Expand Down Expand Up @@ -587,6 +589,9 @@ async function run(
logger.error('')
}

const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
assertNoNegationPatterns(excludePaths)

const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])

// Validation helpers for better readability.
Expand Down Expand Up @@ -785,6 +790,7 @@ async function run(
pendingHead: Boolean(pendingHead),
pullRequest: validatedPullRequest,
reach: {
excludePaths,
runReachabilityAnalysis: Boolean(reach),
reachAnalysisMemoryLimit: validatedReachAnalysisMemoryLimit,
reachAnalysisTimeout: validatedReachAnalysisTimeout,
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import path from 'node:path'

import { joinAnd } from '@socketsecurity/lib/arrays'

import { assertNoNegationPatterns } from './exclude-paths.mts'
import { handleScanReach } from './handle-scan-reach.mts'
import { reachabilityFlags } from './reachability-flags.mts'
import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts'
import { suggestTarget } from './suggest_target.mts'
import { validateReachabilityTarget } from './validate-reachability-target.mts'
import { outputDryRunExecute } from '../../utils/dry-run/output.mts'
Expand Down Expand Up @@ -98,6 +99,7 @@ async function run(
hidden,
flags: {
...generalFlags,
...excludePathsFlag,
...reachabilityFlags,
},
help: command =>
Expand All @@ -112,7 +114,7 @@ async function run(
${getFlagListOutput(generalFlags)}

Reachability Options
${getFlagListOutput(reachabilityFlags)}
${getFlagListOutput({ ...excludePathsFlag, ...reachabilityFlags })}

Runs the Socket reachability analysis without creating a scan in Socket.
The output is written to .socket.facts.json in the current working directory
Expand Down Expand Up @@ -164,8 +166,10 @@ async function run(
const dryRun = !!cli.flags['dryRun']

// Process comma-separated values for isMultiple flags.
const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
const reachEcosystemsRaw = cmdFlagValueToArray(cli.flags['reachEcosystems'])
const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])
assertNoNegationPatterns(excludePaths)

// Validate ecosystem values.
const reachEcosystems: PURL_Type[] = []
Expand Down Expand Up @@ -313,6 +317,7 @@ async function run(
outputPath: outputPath || '',
targets,
reachabilityOptions: {
excludePaths,
reachAnalysisMemoryLimit: validatedReachAnalysisMemoryLimit,
reachAnalysisTimeout: validatedReachAnalysisTimeout,
reachConcurrency: validatedReachConcurrency,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/scan/create-scan-from-github.mts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ async function scanOneRepo(
pendingHead: true,
pullRequest: 0,
reach: {
excludePaths: [],
runReachabilityAnalysis: false,
reachAnalysisMemoryLimit: 0,
reachAnalysisTimeout: 0,
Expand Down
194 changes: 194 additions & 0 deletions packages/cli/src/commands/scan/exclude-paths.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import path from 'node:path'

import { InputError } from '../../utils/error/errors.mts'

import type { ReachabilityOptions } from './perform-reachability-analysis.mts'
import type { SocketYml } from '@socketsecurity/config'

type ApplyFullExcludePathsOptions = {
cwd: string
reachabilityOptions: ReachabilityOptions
socketConfig: SocketYml | undefined
target: string
}

type ApplyFullExcludePathsResult = {
effectiveSocketConfig: SocketYml | undefined
mergedReachabilityOptions: ReachabilityOptions
}

/**
* Converts a user-facing full-scan exclude path into the socket.yml
* projectIgnorePaths shape used by SCA manifest discovery.
*/
export function excludePathToProjectIgnorePath(path: string): string {
const stripped = stripTrailingSlash(path)
return stripped.endsWith('/**') ? stripped : `${stripped}/**`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows paths miss SCA exclusion

Medium Severity

excludePathToProjectIgnorePath stores --exclude-paths values in projectIgnorePaths without converting backslashes. Windows-style paths can still reach Coana after later normalization, but SCA manifest discovery receives unmatched patterns and includes manifests from excluded directories.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cfb459. Configure here.

}

/**
* Rejects gitignore-style negation patterns for --exclude-paths because the
* flag is a positive full-exclusion list, not a complete ignore language.
*/
export function assertNoNegationPatterns(paths: readonly string[]): void {
for (const path of paths) {
if (path.startsWith('!')) {
throw new InputError(
`--exclude-paths does not support negation patterns. Got: '${path}'.`,
)
}
}
}

/**
* Normalizes a reachability exclude path to a recursive directory glob without
* changing explicit one-level or recursive glob suffixes.
*/
export function normalizeExcludePath(path: string): string {
const stripped = stripTrailingSlash(path)
return stripped.endsWith('/*') || stripped.endsWith('/**')
? stripped
: `${stripped}/**`
}

/**
* Applies --exclude-paths consistently to SCA manifest discovery and Coana.
* SCA exclusion always applies when paths are provided. The reachability
* options are merged unconditionally; callers decide whether to actually run
* reachability and consume them.
*/
export function applyFullExcludePaths({
cwd,
reachabilityOptions,
socketConfig,
target,
}: ApplyFullExcludePathsOptions): ApplyFullExcludePathsResult {
const { excludePaths } = reachabilityOptions
const scaExcludeGlobs = excludePaths.map(excludePathToProjectIgnorePath)
const coanaExcludeGlobs = projectIgnorePathsToReachExcludePaths(
scaExcludeGlobs,
{
cwd,
target,
},
)
const socketConfigReachExcludeGlobs = excludePaths.length
? projectIgnorePathsToReachExcludePaths(socketConfig?.projectIgnorePaths, {
cwd,
target,
})
: []
const effectiveSocketConfig = scaExcludeGlobs.length
? {
...socketConfig,
version: socketConfig?.version ?? 2,
issueRules: socketConfig?.issueRules ?? {},
githubApp: socketConfig?.githubApp ?? {},
projectIgnorePaths: [
...(socketConfig?.projectIgnorePaths ?? []),
...scaExcludeGlobs,
],
}
: socketConfig
const mergedReachabilityOptions = excludePaths.length
? {
...reachabilityOptions,
reachExcludePaths: [
...socketConfigReachExcludeGlobs,
...reachabilityOptions.reachExcludePaths,
...coanaExcludeGlobs,
],
}
: reachabilityOptions

return { effectiveSocketConfig, mergedReachabilityOptions }
}

/**
* Translates project-root projectIgnorePaths into Coana --exclude-dirs values,
* which are interpreted relative to the current reachability analysis target.
*/
export function projectIgnorePathsToReachExcludePaths(
paths: readonly string[] | undefined,
options: { cwd: string; target: string },
): string[] {
// GitHub App-style projectIgnorePaths support negation. Coana's
// --exclude-dirs does not, so keep the existing Coana behavior and let it
// infer config ignores itself when any negation is present.
if (!Array.isArray(paths) || paths.some(path => path.includes('!'))) {
return []
}

// projectIgnorePaths are rooted at the project cwd. Coana receives excludes
// relative to its analysis target, so nested target scans need translation.
const targetPath = path.isAbsolute(options.target)
? path.relative(options.cwd, options.target)
: options.target
const targetPattern = toPosixPath(stripTrailingSlash(targetPath))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nested relative targets drop excludes

Medium Severity

The projectIgnorePathsToReachExcludePaths function preserves leading ./ in relative targets, while projectIgnorePaths are normalized without it. This path normalization mismatch prevents --exclude-paths from correctly applying to nested targets, causing Coana to analyze directories that should be excluded.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cfb459. Configure here.

return paths.flatMap(path =>
projectIgnorePathToReachExcludePaths(path, targetPattern),
)
}

function projectIgnorePathToReachExcludePaths(
path: string,
targetPattern: string,
): string[] {
const reachPath = pathRelativeToTarget(path, targetPattern)
if (!reachPath) {
return []
}
return expandReachExcludePath(reachPath)
}

function expandReachExcludePath(path: string): string[] {
if (path === '**') {
return ['**']
}
const firstSlash = path.indexOf('/')
const prefix = firstSlash === -1 || firstSlash === path.length - 1 ? '**/' : ''
const normalized = stripTrailingSlash(
path.startsWith('/') ? path.slice(1) : path,
)
const pattern = `${prefix}${normalized}`
return pattern.endsWith('/*') || pattern.endsWith('/**')
? [pattern]
: [pattern, `${pattern}/**`]
}

function pathRelativeToTarget(path: string, target: string): string | undefined {
const normalized = normalizeProjectIgnorePath(path)
if (target === '.' || target === '') {
return normalized
}

// Ignore paths outside the analysis target. They still affect SCA manifest
// discovery through projectIgnorePaths, but Coana cannot exclude directories
// outside the target it is analyzing.
if (normalized === target) {
return '**'
}
const targetPrefix = `${target}/`
if (normalized.startsWith(targetPrefix)) {
return normalized.slice(targetPrefix.length)
}
const recursiveTargetPrefix = `${targetPrefix}**/`
if (normalized.startsWith(recursiveTargetPrefix)) {
return normalized.slice(targetPrefix.length)
}
return undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard excludes miss nested targets

Medium Severity

pathRelativeToTarget treats glob patterns as literal prefixes when translating to Coana paths. Project-root patterns containing wildcards before the target, such as workspace globs, are dropped for nested reachability targets even when they match directories inside that target.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3cfb459. Configure here.

}

function normalizeProjectIgnorePath(path: string): string {
return stripTrailingSlash(
toPosixPath(path.startsWith('/') ? path.slice(1) : path),
)
}

function toPosixPath(path: string): string {
return path.replaceAll('\\', '/')
}

function stripTrailingSlash(path: string): string {
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
}
15 changes: 12 additions & 3 deletions packages/cli/src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { pluralize } from '@socketsecurity/lib/words'

const logger = getDefaultLogger()

import { applyFullExcludePaths } from './exclude-paths.mts'
import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts'
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
Expand Down Expand Up @@ -157,8 +158,16 @@ export async function handleCreateNewScan({
? socketYmlResult.data?.parsed
: undefined

const { effectiveSocketConfig, mergedReachabilityOptions } =
applyFullExcludePaths({
cwd,
reachabilityOptions: reach,
socketConfig,
target: targets[0]!,
})

const packagePaths = await getPackageFilesForScan(targets, supportedFiles, {
config: socketConfig,
config: effectiveSocketConfig,
cwd,
})

Expand Down Expand Up @@ -209,7 +218,7 @@ export async function handleCreateNewScan({
logger.error('')
logger.info('Starting reachability analysis...')
debug('notice', 'Reachability analysis enabled')
debugDir('inspect', { reachabilityOptions: reach })
debugDir('inspect', { reachabilityOptions: mergedReachabilityOptions })

spinner.start()

Expand All @@ -218,7 +227,7 @@ export async function handleCreateNewScan({
cwd,
orgSlug,
packagePaths,
reachabilityOptions: reach,
reachabilityOptions: mergedReachabilityOptions,
repoName,
spinner,
target: firstTarget,
Expand Down
13 changes: 11 additions & 2 deletions packages/cli/src/commands/scan/handle-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { pluralize } from '@socketsecurity/lib/words'

const logger = getDefaultLogger()

import { applyFullExcludePaths } from './exclude-paths.mts'
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
import { outputScanReach } from './output-scan-reach.mts'
import { performReachabilityAnalysis } from './perform-reachability-analysis.mts'
Expand Down Expand Up @@ -57,8 +58,16 @@ export async function handleScanReach({
? socketYmlResult.data?.parsed
: undefined

const { effectiveSocketConfig, mergedReachabilityOptions } =
applyFullExcludePaths({
cwd,
reachabilityOptions,
socketConfig,
target: targets[0]!,
})

const packagePaths = await getPackageFilesForScan(targets, supportedFiles, {
config: socketConfig,
config: effectiveSocketConfig,
cwd,
})

Expand Down Expand Up @@ -88,7 +97,7 @@ export async function handleScanReach({
orgSlug,
outputPath,
packagePaths,
reachabilityOptions,
reachabilityOptions: mergedReachabilityOptions,
spinner,
target: targets[0]!,
uploadManifests: true,
Expand Down
Loading
Loading