Skip to content

Commit b513478

Browse files
committed
fix(reach): tighten --exclude-paths validation and accept config-file lists
Address review findings on the unified --exclude-paths flag: - Reject '**/' (and any trailing-slash degenerate form): validation now strips the trailing slash before the degenerate-set check, matching Node's stripTrailingSlash. Previously '**/' slipped through and compiled to '.*', silently excluding everything. - Accept a list value from a --config file (not just a comma-separated string): a new normalize_exclude_paths() handles both, so config-file-supplied patterns flow through the same validation instead of crashing on str.split. Adds tests for '**/' rejection and config-file list/string/validation paths.
1 parent 064bfc3 commit b513478

5 files changed

Lines changed: 55 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.4.2"
9+
version = "2.4.3"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.4.2'
2+
__version__ = '2.4.3'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/config.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,34 @@ 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+
5876
def validate_exclude_paths(patterns: List[str]) -> None:
5977
"""Validate --exclude-paths patterns (mirrors Node's assertValidExcludePaths).
6078
6179
Patterns are scan-root-relative globs. Reject the cases coana's --exclude-dirs / fast-glob
6280
cannot honor: negation, absolute paths, ``..`` traversal, and degenerate match-everything.
6381
Exits with code 1 on the first invalid pattern.
6482
"""
65-
degenerate = {"", ".", "./", "./**", "/", "**", "/**"}
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 = {"", ".", "**", "./**", "/**"}
6686
for p in patterns:
6787
norm = (p or "").strip().replace("\\", "/")
6888
if norm.startswith("!"):
@@ -74,7 +94,7 @@ def validate_exclude_paths(patterns: List[str]) -> None:
7494
if norm == ".." or norm.startswith("../") or "/../" in norm or norm.endswith("/.."):
7595
logging.error(f"--exclude-paths: '..' path traversal is not allowed: {p!r}")
7696
exit(1)
77-
if norm in degenerate:
97+
if norm.rstrip("/") in degenerate:
7898
logging.error(f"--exclude-paths: pattern would exclude everything: {p!r}")
7999
exit(1)
80100

@@ -289,7 +309,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
289309
'reach_lazy_mode': args.reach_lazy_mode,
290310
'reach_ecosystems': args.reach_ecosystems.split(',') if args.reach_ecosystems else None,
291311
'reach_exclude_paths': args.reach_exclude_paths.split(',') if args.reach_exclude_paths else None,
292-
'exclude_paths': [p.strip() for p in args.exclude_paths.split(',') if p.strip()] if args.exclude_paths else None,
312+
'exclude_paths': normalize_exclude_paths(args.exclude_paths),
293313
'reach_skip_cache': args.reach_skip_cache,
294314
'reach_min_severity': args.reach_min_severity,
295315
'reach_output_file': args.reach_output_file,

tests/unit/test_exclude_paths.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_reach_exclude_paths_still_works_and_warns(caplog):
8787

8888
@pytest.mark.parametrize(
8989
"bad",
90-
["!foo", "/abs/path", "..", "../escape", "a/../b", ".", "**", "/**", "./**"],
90+
["!foo", "/abs/path", "..", "../escape", "a/../b", ".", "**", "**/", "/**", "./", "./**"],
9191
)
9292
def test_exclude_paths_validation_rejects(bad):
9393
with pytest.raises(SystemExit) as exc:
@@ -101,6 +101,34 @@ def test_exclude_paths_validation_rejects_within_csv():
101101
assert exc.value.code == 1
102102

103103

104+
def _write_config(tmp_path, value):
105+
import json
106+
path = tmp_path / "socketcli.json"
107+
path.write_text(json.dumps({"socketcli": {"exclude_paths": value}}), encoding="utf-8")
108+
return str(path)
109+
110+
111+
def test_exclude_paths_from_config_file_list(tmp_path):
112+
"""A JSON list in --config flows through normalization (not just CSV strings)."""
113+
cfg = _write_config(tmp_path, ["tests/**", "packages/legacy"])
114+
config = CliConfig.from_args(BASE_ARGS + ["--config", cfg])
115+
assert config.exclude_paths == ["tests/**", "packages/legacy"]
116+
117+
118+
def test_exclude_paths_from_config_file_string(tmp_path):
119+
cfg = _write_config(tmp_path, "tests/**, packages/legacy")
120+
config = CliConfig.from_args(BASE_ARGS + ["--config", cfg])
121+
assert config.exclude_paths == ["tests/**", "packages/legacy"]
122+
123+
124+
def test_exclude_paths_from_config_file_is_validated(tmp_path):
125+
"""Config-file patterns are validated too (not bypassed)."""
126+
cfg = _write_config(tmp_path, ["../escape"])
127+
with pytest.raises(SystemExit) as exc:
128+
CliConfig.from_args(BASE_ARGS + ["--config", cfg])
129+
assert exc.value.code == 1
130+
131+
104132
def test_exclude_paths_valid_globs_accepted():
105133
config = CliConfig.from_args(BASE_ARGS + ["--exclude-paths", "tests/**,**/*.spec.ts,packages/legacy"])
106134
assert config.exclude_paths == ["tests/**", "**/*.spec.ts", "packages/legacy"]

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)