fix(scripts): prevent command injection in check-quality-gates.py #10635

Merged
HAL9000 merged 2 commits from fix/v370/quality-gates-command-injection into master 2026-06-10 11:30:55 +00:00
3 changed files with 339 additions and 8 deletions
@@ -0,0 +1,84 @@
Feature: Quality gates script prevents command injection attacks
Outdated
Review

⚠️ BLOCKING (UNRESOLVED from review #6826): Missing mandatory @tdd_issue_7286 regression tag.

Per the TDD bug fix workflow in CONTRIBUTING.md, this is a Type/Bug fix and at least one BDD scenario must be tagged @tdd_issue @tdd_issue_7286 to serve as the regression test. The feature file has no tags on any scenario.

Add @tdd_issue @tdd_issue_7286 above the most representative injection-prevention scenario, for example:

  @tdd_issue @tdd_issue_7286
  Scenario: Paths with semicolon are rejected
    ...
⚠️ BLOCKING (UNRESOLVED from review #6826): Missing mandatory `@tdd_issue_7286` regression tag. Per the TDD bug fix workflow in CONTRIBUTING.md, this is a `Type/Bug` fix and at least one BDD scenario must be tagged `@tdd_issue @tdd_issue_7286` to serve as the regression test. The feature file has no tags on any scenario. Add `@tdd_issue @tdd_issue_7286` above the most representative injection-prevention scenario, for example: ```gherkin @tdd_issue @tdd_issue_7286 Scenario: Paths with semicolon are rejected ... ```
As a security-conscious developer
I want the quality gates script to validate all path arguments
So that command injection attacks cannot be executed through path manipulation
Background:
Given the quality gates script is available
And the project root is set correctly
Scenario: Valid paths are accepted
When I validate the path "src/cleveragents"
Then the path validation should succeed
And the validated path should be absolute
Scenario: Empty paths are rejected
When I validate the path ""
Then the path validation should fail
And the path validation error should contain "Path cannot be empty"
Scenario: Nonexistent paths are rejected
When I validate the path "src/cleveragents/not_a_real_path_7286"
Then the path validation should fail
And the path validation error should contain "Path does not exist"
Scenario: Paths with path traversal are rejected
When I validate the path "src/../../../etc/passwd"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
@tdd_issue @tdd_issue_7286
Outdated
Review

BLOCKER — Missing @tdd_issue_7286 regression tag.

Per the mandatory bug fix workflow in CONTRIBUTING.md, this is a Type/Bug fix. At least one BDD scenario must be tagged @tdd_issue @tdd_issue_7286 to serve as a regression test proving the bug existed before the fix was applied.

Fix: Add @tdd_issue @tdd_issue_7286 above at least one injection-prevention scenario (e.g., the semicolon, pipe, or path-traversal rejection scenarios):

  @tdd_issue @tdd_issue_7286
  Scenario: Paths with semicolon are rejected
    When I validate the path "src/cleveragents;rm -rf /"
    Then the validation should fail
    And the path validation error should contain "dangerous pattern"

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — Missing `@tdd_issue_7286` regression tag.** Per the mandatory bug fix workflow in CONTRIBUTING.md, this is a `Type/Bug` fix. At least one BDD scenario must be tagged `@tdd_issue @tdd_issue_7286` to serve as a regression test proving the bug existed before the fix was applied. **Fix:** Add `@tdd_issue @tdd_issue_7286` above at least one injection-prevention scenario (e.g., the semicolon, pipe, or path-traversal rejection scenarios): ```gherkin @tdd_issue @tdd_issue_7286 Scenario: Paths with semicolon are rejected When I validate the path "src/cleveragents;rm -rf /" Then the validation should fail And the path validation error should contain "dangerous pattern" ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
Scenario: Paths with semicolon are rejected
When I validate the path "src/cleveragents;rm -rf /"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with pipe are rejected
When I validate the path "src/cleveragents|cat /etc/passwd"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with ampersand are rejected
When I validate the path "src/cleveragents&whoami"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with dollar sign are rejected
When I validate the path "src/cleveragents$(whoami)"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with backticks are rejected
When I validate the path "src/cleveragents`whoami`"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with parentheses are rejected
When I validate the path "src/cleveragents(whoami)"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with angle brackets are rejected
When I validate the path "src/cleveragents<file"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Paths with newlines are rejected
When I validate the path "src/cleveragents\nmalicious"
Then the path validation should fail
And the path validation error should contain "dangerous pattern"
Scenario: Security check uses validated paths
When I run the security check
Then the subprocess should not be called with shell=True
And all path arguments should be validated
Scenario: Dead code check uses validated paths
When I run the dead code check
Then the subprocess should not be called with shell=True
And all path arguments should be validated
Scenario: Complexity check uses validated paths
When I run the complexity check
Then the subprocess should not be called with shell=True
And all path arguments should be validated
@@ -0,0 +1,156 @@
"""Step definitions for quality gates command injection tests."""
import importlib.util
import os
import types
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
Outdated
Review

BLOCKER — Module-level execution uses a relative path; this is why unit_tests CI is failing.

_qg = _load_quality_gates_module() runs at Behave collection time. Inside _load_quality_gates_module(), "scripts/check-quality-gates.py" is resolved relative to the current working directory at collection time. When run from a directory other than the project root, this causes a FileNotFoundError that fails every scenario in this file.

Additionally, spec.loader can be None per typeshed — calling spec.loader.exec_module(mod) without a guard will raise AttributeError if loader is absent.

Fix:

_SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "check-quality-gates.py"

def _load_quality_gates_module() -> types.ModuleType:
    spec = importlib.util.spec_from_file_location("check_quality_gates", _SCRIPT_PATH)
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot load module from {_SCRIPT_PATH}")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — Module-level execution uses a relative path; this is why unit_tests CI is failing.** `_qg = _load_quality_gates_module()` runs at Behave collection time. Inside `_load_quality_gates_module()`, `"scripts/check-quality-gates.py"` is resolved relative to the current working directory at collection time. When run from a directory other than the project root, this causes a `FileNotFoundError` that fails every scenario in this file. Additionally, `spec.loader` can be `None` per typeshed — calling `spec.loader.exec_module(mod)` without a guard will raise `AttributeError` if loader is absent. **Fix:** ```python _SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "check-quality-gates.py" def _load_quality_gates_module() -> types.ModuleType: spec = importlib.util.spec_from_file_location("check_quality_gates", _SCRIPT_PATH) if spec is None or spec.loader is None: raise ImportError(f"Cannot load module from {_SCRIPT_PATH}") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
_SCRIPT_PATH = (
Path(__file__).parent.parent.parent / "scripts" / "check-quality-gates.py"
Outdated
Review

⚠️ BLOCKING (UNRESOLVED from review #6826): Module load uses a relative path and runs at import time, causing unit_tests CI to fail.

Two problems in _load_quality_gates_module():

  1. "scripts/check-quality-gates.py" is a relative path — when Behave is invoked from any directory other than the project root, this raises FileNotFoundError at module load time, failing the entire step file collection.
  2. spec.loader can be None per typeshed. Calling spec.loader.exec_module(mod) without a guard will raise AttributeError.

Required fix:

import types
_SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "check-quality-gates.py"

def _load_quality_gates_module() -> types.ModuleType:
    spec = importlib.util.spec_from_file_location("check_quality_gates", _SCRIPT_PATH)
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot load module from {_SCRIPT_PATH}")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod
⚠️ BLOCKING (UNRESOLVED from review #6826): Module load uses a relative path and runs at import time, causing `unit_tests` CI to fail. Two problems in `_load_quality_gates_module()`: 1. `"scripts/check-quality-gates.py"` is a relative path — when Behave is invoked from any directory other than the project root, this raises `FileNotFoundError` at module load time, failing the entire step file collection. 2. `spec.loader` can be `None` per typeshed. Calling `spec.loader.exec_module(mod)` without a guard will raise `AttributeError`. Required fix: ```python import types _SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "check-quality-gates.py" def _load_quality_gates_module() -> types.ModuleType: spec = importlib.util.spec_from_file_location("check_quality_gates", _SCRIPT_PATH) if spec is None or spec.loader is None: raise ImportError(f"Cannot load module from {_SCRIPT_PATH}") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod ```
)
def _load_quality_gates_module() -> types.ModuleType:
"""Load the check-quality-gates.py script as a module via importlib."""
spec = importlib.util.spec_from_file_location("check_quality_gates", _SCRIPT_PATH)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {_SCRIPT_PATH}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
_qg = _load_quality_gates_module()
@given("the quality gates script is available")
def step_script_available(context: Context) -> None:
"""Verify the quality gates script is available."""
script_path = Path("scripts/check-quality-gates.py")
assert script_path.exists(), f"Script {script_path} does not exist"
@given("the project root is set correctly")
def step_project_root_set(context: Context) -> None:
"""Set up the project root context."""
context.original_cwd = os.getcwd()
@when('I validate the path "{path_str}"')
def step_validate_path(context: Context, path_str: str) -> None:
"""Validate a path string."""
context.path_str = path_str
context.validation_error = None
context.validated_path = None
try:
context.validated_path = _qg.validate_path(path_str)
except ValueError as e:
context.validation_error = str(e)
@when('I validate the path ""')
def step_validate_empty_path(context: Context) -> None:
"""Validate an empty path string."""
step_validate_path(context, "")
@then("the path validation should succeed")
def step_validation_success(context: Context) -> None:
"""Assert that validation succeeded."""
assert context.validation_error is None, (
f"Expected validation to succeed but got: {context.validation_error}"
)
assert context.validated_path is not None, "Expected a validated path"
@then("the path validation should fail")
def step_validation_fail(context: Context) -> None:
"""Assert that validation failed."""
assert context.validation_error is not None, (
"Expected validation to fail but it succeeded"
)
@then("the validated path should be absolute")
def step_path_is_absolute(context: Context) -> None:
"""Assert that the returned path is absolute."""
assert context.validated_path is not None, "No validated path available"
assert context.validated_path.is_absolute(), (
f"Expected absolute path but got: {context.validated_path}"
)
@then('the path validation error should contain "{text}"')
def step_error_contains_text(context: Context, text: str) -> None:
"""Assert that the path validation error message contains specific text."""
assert context.validation_error is not None, "No error message available"
assert text.lower() in context.validation_error.lower(), (
f"Expected error to contain '{text}' but got: {context.validation_error}"
)
@when("I run the security check")
def step_run_security_check(context: Context) -> None:
"""Run the security check function."""
mock_result = MagicMock(returncode=0, stdout='{"results": []}', stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.security_result = _qg.check_security()
context.mock_run = mock_run
@when("I run the dead code check")
def step_run_dead_code_check(context: Context) -> None:
"""Run the dead code check function."""
mock_result = MagicMock(returncode=0, stdout="", stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.dead_code_result = _qg.check_dead_code()
context.mock_run = mock_run
@when("I run the complexity check")
def step_run_complexity_check(context: Context) -> None:
"""Run the complexity check function."""
mock_result = MagicMock(returncode=0, stdout="{}", stderr="")
with patch.object(_qg.subprocess, "run", return_value=mock_result) as mock_run:
context.complexity_result = _qg.check_complexity()
context.mock_run = mock_run
@then("the subprocess should not be called with shell=True")
def step_subprocess_shell_not_true(context: Context) -> None:
"""Assert subprocess.run does not enable shell execution."""
assert hasattr(context, "mock_run"), "No mock_run available"
for call in context.mock_run.call_args_list:
kwargs = (
call.kwargs
if hasattr(call, "kwargs")
Outdated
Review

BLOCKER — This step is a no-op; it does not verify paths were actually validated.

assert context.mock_run.called only checks that subprocess.run was called at all — not that path arguments went through validate_path(). An implementation that bypassed validation entirely would still pass this assertion.

Fix: Inspect call_args_list to verify that path arguments are absolute (i.e., have been processed by validate_path()):

for call in context.mock_run.call_args_list:
    cmd = call.args[0] if call.args else call.kwargs.get("args", [])
    for arg in cmd:
        if isinstance(arg, str) and arg.startswith("/"):
            assert Path(arg).is_absolute(), f"Path {arg} is not absolute"

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — This step is a no-op; it does not verify paths were actually validated.** `assert context.mock_run.called` only checks that `subprocess.run` was called at all — not that path arguments went through `validate_path()`. An implementation that bypassed validation entirely would still pass this assertion. **Fix:** Inspect `call_args_list` to verify that path arguments are absolute (i.e., have been processed by `validate_path()`): ```python for call in context.mock_run.call_args_list: cmd = call.args[0] if call.args else call.kwargs.get("args", []) for arg in cmd: if isinstance(arg, str) and arg.startswith("/"): assert Path(arg).is_absolute(), f"Path {arg} is not absolute" ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
else (call[1] if len(call) > 1 else {})
)
assert kwargs.get("shell") is not True, (
f"Expected shell not to be True but got: {kwargs.get('shell')}"
)
@then("all path arguments should be validated")
def step_all_paths_validated(context: Context) -> None:
"""Assert that path arguments were resolved before subprocess execution."""
assert hasattr(context, "mock_run"), "No mock_run available"
assert context.mock_run.called, "subprocess.run was not called"
path_like_args = []
for call in context.mock_run.call_args_list:
cmd = call.args[0] if call.args else call.kwargs.get("args", [])
for arg in cmd:
if not isinstance(arg, str):
continue
if "/" in arg or arg.endswith((".py", ".toml")):
path_like_args.append(arg)
assert path_like_args, "No path-like subprocess arguments were found"
for arg in path_like_args:
assert Path(arg).is_absolute(), f"Path argument was not validated: {arg}"
+99 -8
View File
@@ -5,7 +5,7 @@ Aggregates results from multiple quality tools and produces a summary
report. Returns non-zero exit code if any critical gate fails.
Usage:
Review

BLOCKER — shell=False is redundant and is causing the lint CI failure.

shell=False is the default when subprocess.run() is called with a list argument. Adding it explicitly is flagged by ruff as a redundant keyword, which is the direct cause of the CI / lint gate failure.

Fix: Remove shell=False, from this call (and all other subprocess.run() calls in this file). The list-form invocation already prevents shell interpretation.


Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — `shell=False` is redundant and is causing the lint CI failure.** `shell=False` is the default when `subprocess.run()` is called with a list argument. Adding it explicitly is flagged by ruff as a redundant keyword, which is the direct cause of the `CI / lint` gate failure. **Fix:** Remove `shell=False,` from this call (and all other `subprocess.run()` calls in this file). The list-form invocation already prevents shell interpretation. --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max 10]
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max E]
Gates checked:
1. Coverage >= threshold (default 85%)
@@ -17,10 +17,80 @@ Gates checked:
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
_SAFE_PATH_RE = re.compile(r"^[A-Za-z0-9_./-]+$")
Outdated
Review

Suggestion: The project_root parameter on line 25 is never used — none of the callers pass it, making the containment validation dead code. Either remove the parameter or update all callers to pass the project root for defense-in-depth.

Note: With hardcoded strings, the traversal check is also triggered by the .. pattern string check (line 42) before resolve, making the relative_to check unreachable when project_root is the only validation path.

Suggestion: The `project_root` parameter on line 25 is never used — none of the callers pass it, making the containment validation dead code. Either remove the parameter or update all callers to pass the project root for defense-in-depth. Note: With hardcoded strings, the traversal check is also triggered by the `..` pattern string check (line 42) before resolve, making the `relative_to` check unreachable when `project_root` is the only validation path.
Outdated
Review

⚠️ BLOCKING (UNRESOLVED from review #6826): project_root parameter is dead code.

The signature accepts project_root: Path | None = None but no caller passes it. The containment check via .relative_to(project_root_resolved) is therefore never executed in any production path.

Fix options:

  1. Remove the project_root parameter entirely (simplest), OR
  2. Update all callers (check_security, check_dead_code, check_complexity) to pass Path(".").resolve() so the containment check actually runs.

Until one of these is done, the containment validation is permanently dead code.

⚠️ BLOCKING (UNRESOLVED from review #6826): `project_root` parameter is dead code. The signature accepts `project_root: Path | None = None` but no caller passes it. The containment check via `.relative_to(project_root_resolved)` is therefore never executed in any production path. Fix options: 1. Remove the `project_root` parameter entirely (simplest), OR 2. Update all callers (`check_security`, `check_dead_code`, `check_complexity`) to pass `Path(".").resolve()` so the containment check actually runs. Until one of these is done, the containment validation is permanently dead code.
def validate_path(path_str: str, project_root: Path | None = None) -> Path:
"""Validate and sanitize a path argument to prevent command injection.
Checks for safe path characters, dangerous shell metacharacters, path
traversal patterns, project-root containment, and on-disk existence.
Outdated
Review

BLOCKER — validate_path() does not verify path existence.

Issue #7286 Acceptance Criterion explicitly states: "Paths must be resolved via pathlib.Path and confirmed to exist." This implementation calls .resolve() but never calls .is_file() or .is_dir(). A path like vulture_whitelist.py that does not exist on disk passes all validation checks here and is passed silently to subprocess, which then fails with a cryptic runtime error rather than a clear validation failure.

Required fix — add after resolved = path_obj.resolve():

if not resolved.is_file() and not resolved.is_dir():
    raise ValueError(f"Path does not exist: {resolved}")

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — `validate_path()` does not verify path existence.** Issue #7286 Acceptance Criterion explicitly states: *"Paths must be resolved via `pathlib.Path` and confirmed to exist."* This implementation calls `.resolve()` but never calls `.is_file()` or `.is_dir()`. A path like `vulture_whitelist.py` that does not exist on disk passes all validation checks here and is passed silently to subprocess, which then fails with a cryptic runtime error rather than a clear validation failure. **Required fix** — add after `resolved = path_obj.resolve()`: ```python if not resolved.is_file() and not resolved.is_dir(): raise ValueError(f"Path does not exist: {resolved}") ``` --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
Args:
path_str: The path string to validate
project_root: Optional project root for relative path validation
Returns:
Outdated
Review

BLOCKER — project_root parameter is dead code; containment check never runs.

No caller passes project_root. The resolved.relative_to(project_root_resolved) containment check in the block below is never executed in any production code path, making the project root containment protection entirely dead code.

Fix options (choose one):

  1. Remove the project_root parameter entirely, OR
  2. Update all three callers (check_security, check_dead_code, check_complexity) to pass Path(".").resolve() so containment actually runs.

Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker

**BLOCKER — `project_root` parameter is dead code; containment check never runs.** No caller passes `project_root`. The `resolved.relative_to(project_root_resolved)` containment check in the block below is never executed in any production code path, making the project root containment protection entirely dead code. **Fix options (choose one):** 1. Remove the `project_root` parameter entirely, OR 2. Update all three callers (`check_security`, `check_dead_code`, `check_complexity`) to pass `Path(".").resolve()` so containment actually runs. --- Automated by CleverAgents Bot Supervisor: PR Review | Agent: pr-review-worker
A validated, resolved Path object
Raises:
ValueError: If the path is invalid or contains dangerous patterns
"""
if not path_str:
raise ValueError("Path cannot be empty")
# Check for dangerous patterns that could be used for injection
dangerous_patterns = [
"..", # Path traversal
";", # Command separator
"|", # Pipe operator
Outdated
Review

⚠️ BLOCKING (UNRESOLVED from review #6826): shell=False is redundant and is causing the CI / lint gate to fail.

shell=False is the default behaviour of subprocess.run() when the command is passed as a list. Ruff flags this as a redundant keyword argument. This pattern appears in all 5 affected subprocess.run() calls in this file and is almost certainly the direct cause of the lint CI failure.

Fix: Remove shell=False, from all subprocess.run() calls. The list-form invocation already prevents shell interpretation — removing the redundant flag does not reduce security.

⚠️ BLOCKING (UNRESOLVED from review #6826): `shell=False` is redundant and is causing the `CI / lint` gate to fail. `shell=False` is the default behaviour of `subprocess.run()` when the command is passed as a list. Ruff flags this as a redundant keyword argument. This pattern appears in all 5 affected `subprocess.run()` calls in this file and is almost certainly the direct cause of the lint CI failure. Fix: Remove `shell=False,` from all `subprocess.run()` calls. The list-form invocation already prevents shell interpretation — removing the redundant flag does not reduce security.
"&", # Background/AND operator
Outdated
Review

Suggestion: shell=False is the default for subprocess.run() and therefore redundant. Adding it is likely causing the lint failure. Since the subprocess calls already use the list form (which prevents shell interpretation regardless), the security value is minimal. Remove for lint compatibility, or keep as a defensive comment if desired.

Suggestion: `shell=False` is the default for subprocess.run() and therefore redundant. Adding it is likely causing the lint failure. Since the subprocess calls already use the list form (which prevents shell interpretation regardless), the security value is minimal. Remove for lint compatibility, or keep as a defensive comment if desired.
"$", # Variable/command substitution
"`", # Backtick command substitution (deprecated)
"(", # Subshell grouping start
")", # Subshell grouping end
"<", # Input redirection
">", # Output redirection
"\n", # Newline injection
"\r", # Carriage return injection
"\\n", # Escaped newline injection in feature/CLI input
"\\r", # Escaped carriage return injection in feature/CLI input
]
for pattern in dangerous_patterns:
if pattern in path_str:
Outdated
Review

⚠️ BLOCKING: validate_path() does not verify file/directory existence. The issue specification requires "Paths must be resolved via pathlib.Path and confirmed to exist." A path like "vulture_whitelist.py" passes validation even if it does not exist, causing the subprocess to fail at runtime — masking the real error behind a false validation pass.

Add after path.resolve():
if not path.is_file() and not path.is_dir():
raise ValueError(f"Path does not exist: {path}")

⚠️ BLOCKING: validate_path() does not verify file/directory existence. The issue specification requires "Paths must be resolved via pathlib.Path and confirmed to exist." A path like "vulture_whitelist.py" passes validation even if it does not exist, causing the subprocess to fail at runtime — masking the real error behind a false validation pass. Add after `path.resolve()`: if not path.is_file() and not path.is_dir(): raise ValueError(f"Path does not exist: {path}")
Outdated
Review

⚠️ BLOCKING (UNRESOLVED from review #6826): validate_path() does not verify path existence.

Issue #7286 Acceptance Criterion: "Paths must be resolved via pathlib.Path and confirmed to exist." The .resolve() call is present but .is_file() / .is_dir() is never called. A path like vulture_whitelist.py that does not exist on disk passes all validation and is silently forwarded to the subprocess, which then fails with a cryptic runtime error rather than a clear ValueError.

Required fix — add after resolved = path_obj.resolve():

if not resolved.is_file() and not resolved.is_dir():
    raise ValueError(f"Path does not exist: {resolved}")
⚠️ BLOCKING (UNRESOLVED from review #6826): `validate_path()` does not verify path existence. Issue #7286 Acceptance Criterion: "Paths must be resolved via `pathlib.Path` and confirmed to exist." The `.resolve()` call is present but `.is_file()` / `.is_dir()` is never called. A path like `vulture_whitelist.py` that does not exist on disk passes all validation and is silently forwarded to the subprocess, which then fails with a cryptic runtime error rather than a clear `ValueError`. Required fix — add after `resolved = path_obj.resolve()`: ```python if not resolved.is_file() and not resolved.is_dir(): raise ValueError(f"Path does not exist: {resolved}") ```
raise ValueError(f"Path contains dangerous pattern: {pattern}")
if _SAFE_PATH_RE.fullmatch(path_str) is None:
raise ValueError("Path contains characters outside the safe allowlist")
# Convert to Path object
path_obj = Path(path_str)
# Resolve to absolute path to prevent traversal
try:
resolved = path_obj.resolve()
except (OSError, RuntimeError) as e:
raise ValueError(f"Cannot resolve path: {e}") from e
if not resolved.is_file() and not resolved.is_dir():
raise ValueError(f"Path does not exist: {resolved}")
# If project_root is provided, ensure path is within it
if project_root is not None:
project_root_resolved = project_root.resolve()
try:
resolved.relative_to(project_root_resolved)
except ValueError as e:
msg = f"Path {resolved} is outside project root {project_root_resolved}"
raise ValueError(msg) from e
return resolved
def check_coverage(min_coverage: float) -> tuple[bool, str]:
"""Check test coverage meets minimum threshold."""
@@ -66,13 +136,21 @@ def check_typecheck() -> tuple[bool, str]:
def check_security() -> tuple[bool, str]:
"""Check bandit finds no high-severity issues."""
# Validate the path argument to prevent command injection
try:
project_root = Path(".").resolve()
config_path = validate_path("pyproject.toml", project_root)
src_path = validate_path("src/cleveragents", project_root)
except ValueError as e:
return False, f"Security: invalid path - {e}"
result = subprocess.run(
[
"bandit",
"-c",
"pyproject.toml",
str(config_path),
"-r",
"src/cleveragents",
str(src_path),
"--severity-level",
"high",
"--format",
@@ -96,15 +174,21 @@ def check_security() -> tuple[bool, str]:
def check_dead_code() -> tuple[bool, str]:
"""Check vulture finds no dead code."""
# Validate path arguments to prevent command injection
try:
project_root = Path(".").resolve()
src_path = validate_path("src/cleveragents", project_root)
whitelist_path = validate_path("vulture_whitelist.py", project_root)
except ValueError as e:
return False, f"Dead code: invalid path - {e}"
result = subprocess.run(
[
"vulture",
"src/cleveragents",
"vulture_whitelist.py",
str(src_path),
str(whitelist_path),
"--min-confidence",
"80",
"--exclude",
"src/cleveragents/discovery",
],
capture_output=True,
text=True,
@@ -118,11 +202,18 @@ def check_dead_code() -> tuple[bool, str]:
def check_complexity(max_grade: str = "E") -> tuple[bool, str]:
"""Check no functions exceed maximum complexity grade."""
# Validate path argument to prevent command injection
try:
project_root = Path(".").resolve()
src_path = validate_path("src/cleveragents", project_root)
except ValueError as e:
return False, f"Complexity: invalid path - {e}"
result = subprocess.run(
[
"radon",
"cc",
"src/cleveragents",
str(src_path),
"--min",
max_grade,
"--show-complexity",