@@ -55,6 +55,50 @@ def load_cli_config_file(config_path: str) -> dict:
5555 return scoped
5656 return data
5757
58+ def normalize_exclude_paths (value ) -> Optional [List [str ]]:
59+ """Normalize a --exclude-paths value into a clean list of patterns.
60+
61+ Accepts a comma-separated string (CLI) or a list/tuple (e.g. a JSON/TOML --config file
62+ value), so config-file-supplied patterns flow through the same validation as CLI ones.
63+ """
64+ if not value :
65+ return None
66+ if isinstance (value , str ):
67+ items = value .split ("," )
68+ elif isinstance (value , (list , tuple )):
69+ items = value
70+ else :
71+ return None
72+ cleaned = [str (p ).strip () for p in items if str (p ).strip ()]
73+ return cleaned or None
74+
75+
76+ def validate_exclude_paths (patterns : List [str ]) -> None :
77+ """Validate --exclude-paths patterns (mirrors Node's assertValidExcludePaths).
78+
79+ Patterns are scan-root-relative globs. Reject the cases coana's --exclude-dirs / fast-glob
80+ cannot honor: negation, absolute paths, ``..`` traversal, and degenerate match-everything.
81+ Exits with code 1 on the first invalid pattern.
82+ """
83+ # Degenerate match-everything forms, compared against the trailing-slash-stripped pattern
84+ # (so "**/" reduces to "**" and is rejected, matching Node's stripTrailingSlash + check).
85+ degenerate = {"" , "." , "**" , "./**" , "/**" }
86+ for p in patterns :
87+ norm = (p or "" ).strip ().replace ("\\ " , "/" )
88+ if norm .startswith ("!" ):
89+ logging .error (f"--exclude-paths: negation patterns are not supported: { p !r} " )
90+ exit (1 )
91+ if norm .startswith ("/" ):
92+ logging .error (f"--exclude-paths: patterns must be scan-root relative (no leading '/'): { p !r} " )
93+ exit (1 )
94+ if norm == ".." or norm .startswith ("../" ) or "/../" in norm or norm .endswith ("/.." ):
95+ logging .error (f"--exclude-paths: '..' path traversal is not allowed: { p !r} " )
96+ exit (1 )
97+ if norm .rstrip ("/" ) in degenerate :
98+ logging .error (f"--exclude-paths: pattern would exclude everything: { p !r} " )
99+ exit (1 )
100+
101+
58102@dataclass
59103class PluginConfig :
60104 enabled : bool = False
@@ -106,6 +150,7 @@ class CliConfig:
106150 include_module_folders : bool = False
107151 repo_is_public : bool = False
108152 excluded_ecosystems : list [str ] = field (default_factory = lambda : [])
153+ exclude_paths : Optional [List [str ]] = None
109154 version : str = __version__
110155 jira_plugin : PluginConfig = field (default_factory = PluginConfig )
111156 slack_plugin : PluginConfig = field (default_factory = PluginConfig )
@@ -167,6 +212,12 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
167212
168213 args = parser .parse_args (args_list )
169214
215+ if args .reach_exclude_paths :
216+ logging .warning (
217+ "--reach-exclude-paths is deprecated; use --exclude-paths instead. "
218+ "It is still honored and unioned with --exclude-paths."
219+ )
220+
170221 # Get API token from env or args (check multiple env var names)
171222 api_token = (
172223 os .getenv ("SOCKET_SECURITY_API_KEY" ) or
@@ -258,6 +309,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
258309 'reach_lazy_mode' : args .reach_lazy_mode ,
259310 'reach_ecosystems' : args .reach_ecosystems .split (',' ) if args .reach_ecosystems else None ,
260311 'reach_exclude_paths' : args .reach_exclude_paths .split (',' ) if args .reach_exclude_paths else None ,
312+ 'exclude_paths' : normalize_exclude_paths (args .exclude_paths ),
261313 'reach_skip_cache' : args .reach_skip_cache ,
262314 'reach_min_severity' : args .reach_min_severity ,
263315 'reach_output_file' : args .reach_output_file ,
@@ -361,6 +413,10 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
361413 logging .error ("--sarif-reachability potentially/reachable-or-potentially requires --sarif-scope full" )
362414 exit (1 )
363415
416+ # Validate --exclude-paths patterns up front (mirrors Node's assertValidExcludePaths).
417+ if config_args .get ("exclude_paths" ):
418+ validate_exclude_paths (config_args ["exclude_paths" ])
419+
364420 # Validate that only_facts_file requires reach
365421 if args .only_facts_file and not args .reach :
366422 logging .error ("--only-facts-file requires --reach to be specified" )
@@ -570,6 +626,15 @@ def create_argument_parser() -> argparse.ArgumentParser:
570626 help = "List of ecosystems to exclude from analysis (JSON array string)"
571627 )
572628
629+ path_group .add_argument (
630+ "--exclude-paths" ,
631+ dest = "exclude_paths" ,
632+ metavar = "<list>" ,
633+ help = "Comma-separated paths/globs to exclude from BOTH manifest discovery and "
634+ "reachability analysis (e.g. 'tests/**,packages/legacy,*.spec.ts'). "
635+ "Supersedes --reach-exclude-paths."
636+ )
637+
573638 # Branch and Scan Configuration
574639 config_group = parser .add_argument_group ('Branch and Scan Configuration' )
575640 config_group .add_argument (
@@ -920,7 +985,8 @@ def create_argument_parser() -> argparse.ArgumentParser:
920985 "--reach-exclude-paths" ,
921986 dest = "reach_exclude_paths" ,
922987 metavar = "<list>" ,
923- help = "Paths to exclude from reachability analysis (comma-separated)"
988+ help = "[DEPRECATED: use --exclude-paths] Paths to exclude from reachability analysis "
989+ "(comma-separated). Still honored and unioned with --exclude-paths."
924990 )
925991 reachability_group .add_argument (
926992 "--reach-min-severity" ,
0 commit comments