Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe94e54155 |
@@ -0,0 +1,78 @@
|
||||
Feature: Quality gates script prevents command injection attacks
|
||||
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 validation should succeed
|
||||
And the returned path should be absolute
|
||||
|
||||
Scenario: Empty paths are rejected
|
||||
When I validate the path ""
|
||||
Then the validation should fail
|
||||
And the path validation error should contain "Path cannot be empty"
|
||||
|
||||
Scenario: Paths with path traversal are rejected
|
||||
When I validate the path "src/../../../etc/passwd"
|
||||
Then the validation should fail
|
||||
And the path validation error should contain "dangerous pattern"
|
||||
|
||||
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"
|
||||
|
||||
Scenario: Paths with pipe are rejected
|
||||
When I validate the path "src/cleveragents|cat /etc/passwd"
|
||||
Then the 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 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 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 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 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 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 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 be called with shell=False
|
||||
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 be called with shell=False
|
||||
And all path arguments should be validated
|
||||
|
||||
Scenario: Complexity check uses validated paths
|
||||
When I run the complexity check
|
||||
Then the subprocess should be called with shell=False
|
||||
And all path arguments should be validated
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Step definitions for quality gates command injection tests."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
|
||||
def _load_quality_gates_module():
|
||||
"""Load the check-quality-gates.py script as a module via importlib."""
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"check_quality_gates", "scripts/check-quality-gates.py"
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@then("the 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 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 returned 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 be called with shell=False")
|
||||
def step_subprocess_shell_false(context: Context) -> None:
|
||||
"""Assert that subprocess.run was called with shell=False."""
|
||||
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")
|
||||
else (call[1] if len(call) > 1 else {})
|
||||
)
|
||||
assert kwargs.get("shell") is False, (
|
||||
f"Expected shell=False but got: {kwargs.get('shell')}"
|
||||
)
|
||||
|
||||
|
||||
@then("all path arguments should be validated")
|
||||
def step_all_paths_validated(context: Context) -> None:
|
||||
"""Assert that all path arguments were validated."""
|
||||
assert hasattr(context, "mock_run"), "No mock_run available"
|
||||
assert context.mock_run.called, "subprocess.run was not called"
|
||||
@@ -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:
|
||||
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max 10]
|
||||
python scripts/check-quality-gates.py [--coverage-min 85] [--complexity-max F]
|
||||
|
||||
Gates checked:
|
||||
1. Coverage >= threshold (default 85%)
|
||||
@@ -22,6 +22,69 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def validate_path(path_str: str, project_root: Path | None = None) -> Path:
|
||||
"""Validate and sanitize a path argument to prevent command injection.
|
||||
|
||||
Checks for dangerous shell metacharacters and path traversal patterns.
|
||||
Rejects paths that contain characters that could be exploited for
|
||||
command injection when passed as subprocess arguments.
|
||||
|
||||
Args:
|
||||
path_str: The path string to validate
|
||||
project_root: Optional project root for relative path validation
|
||||
|
||||
Returns:
|
||||
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
|
||||
"&", # Background/AND operator
|
||||
"$", # Variable/command substitution
|
||||
"`", # Backtick command substitution (deprecated)
|
||||
"(", # Subshell grouping start
|
||||
")", # Subshell grouping end
|
||||
"<", # Input redirection
|
||||
">", # Output redirection
|
||||
"\n", # Newline injection
|
||||
"\r", # Carriage return injection
|
||||
]
|
||||
for pattern in dangerous_patterns:
|
||||
if pattern in path_str:
|
||||
raise ValueError(f"Path contains dangerous pattern: {pattern}")
|
||||
|
||||
# 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 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 "
|
||||
f"{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."""
|
||||
result = subprocess.run(
|
||||
@@ -29,6 +92,7 @@ def check_coverage(min_coverage: float) -> tuple[bool, str]:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return False, f"Coverage command failed: {result.stderr.strip()}"
|
||||
@@ -50,6 +114,7 @@ def check_typecheck() -> tuple[bool, str]:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
@@ -66,13 +131,19 @@ 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:
|
||||
src_path = validate_path("src/cleveragents")
|
||||
except ValueError as e:
|
||||
return False, f"Security: invalid path - {e}"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bandit",
|
||||
"-c",
|
||||
"pyproject.toml",
|
||||
"-r",
|
||||
"src/cleveragents",
|
||||
str(src_path),
|
||||
"--severity-level",
|
||||
"high",
|
||||
"--format",
|
||||
@@ -81,6 +152,7 @@ def check_security() -> tuple[bool, str]:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
@@ -96,19 +168,28 @@ 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:
|
||||
src_path = validate_path("src/cleveragents")
|
||||
whitelist_path = validate_path("vulture_whitelist.py")
|
||||
exclude_path = validate_path("src/cleveragents/discovery")
|
||||
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",
|
||||
str(exclude_path),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True, "Dead code: none detected"
|
||||
@@ -118,11 +199,17 @@ 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:
|
||||
src_path = validate_path("src/cleveragents")
|
||||
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",
|
||||
@@ -131,6 +218,7 @@ def check_complexity(max_grade: str = "E") -> tuple[bool, str]:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
|
||||
Reference in New Issue
Block a user