-
Notifications
You must be signed in to change notification settings - Fork 41
feat(scan): add --exclude-paths flag for full Tier 1 exclusion (port of #1298) #1306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}/**` | ||
| } | ||
|
|
||
| /** | ||
| * 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)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested relative targets drop excludesMedium Severity The 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wildcard excludes miss nested targetsMedium Severity
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 | ||
| } | ||


There was a problem hiding this comment.
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
excludePathToProjectIgnorePathstores--exclude-pathsvalues inprojectIgnorePathswithout 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.Reviewed by Cursor Bugbot for commit 3cfb459. Configure here.