7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
398 lines
14 KiB
Python
398 lines
14 KiB
Python
"""
|
|
Tests for Bearer P0 security alert fixes (PR #1934).
|
|
|
|
Verifies:
|
|
- Shell injection fix: run_command(shell=True) removed from pre_prompt.py
|
|
- GPU detection uses list-based subprocess.run (no shell=True)
|
|
- open_file_location uses PathValidator for path validation
|
|
"""
|
|
|
|
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def _load_pre_prompt_module():
|
|
"""Load the pre_prompt module from the cookiecutter-docker hooks directory."""
|
|
module_path = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "cookiecutter-docker"
|
|
/ "hooks"
|
|
/ "pre_prompt.py"
|
|
)
|
|
spec = importlib.util.spec_from_file_location(
|
|
"pre_prompt", str(module_path)
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
# Mock cookiecutter.prompt since it may not be installed in test env
|
|
sys.modules.setdefault("cookiecutter", MagicMock())
|
|
sys.modules.setdefault("cookiecutter.prompt", MagicMock())
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class TestRunCommandRemoved:
|
|
"""Verify that the insecure run_command function has been removed."""
|
|
|
|
def test_run_command_not_defined_in_module(self):
|
|
"""run_command(shell=True) must not exist in pre_prompt.py."""
|
|
module = _load_pre_prompt_module()
|
|
import inspect
|
|
|
|
module_functions = {
|
|
name
|
|
for name, obj in inspect.getmembers(module, inspect.isfunction)
|
|
if obj.__module__ == module.__name__
|
|
}
|
|
assert "run_command" not in module_functions, (
|
|
"run_command should be removed from pre_prompt.py "
|
|
"to fix shell injection"
|
|
)
|
|
|
|
def test_no_shell_true_in_source(self):
|
|
"""The pre_prompt.py source must not contain shell=True."""
|
|
module_path = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "cookiecutter-docker"
|
|
/ "hooks"
|
|
/ "pre_prompt.py"
|
|
)
|
|
source = module_path.read_text()
|
|
assert "shell=True" not in source, (
|
|
"pre_prompt.py must not use shell=True in any subprocess call"
|
|
)
|
|
|
|
|
|
class TestCheckGpuLinux:
|
|
"""Verify check_gpu_linux uses secure subprocess invocation."""
|
|
|
|
def test_calls_subprocess_run_with_list_args(self):
|
|
"""check_gpu_linux must call subprocess.run with a list, not a string."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=(
|
|
"00:02.0 VGA compatible controller: NVIDIA Corporation"
|
|
),
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_linux(context)
|
|
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args
|
|
cmd_arg = (
|
|
call_args[0][0] if call_args[0] else call_args[1].get("args")
|
|
)
|
|
assert isinstance(cmd_arg, list), (
|
|
f"subprocess.run must be called with a list, "
|
|
f"got {type(cmd_arg)}"
|
|
)
|
|
assert cmd_arg == ["lspci"]
|
|
|
|
def test_does_not_use_shell_true(self):
|
|
"""check_gpu_linux must not pass shell=True."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(stdout="", returncode=0)
|
|
module.check_gpu_linux(context)
|
|
|
|
call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {}
|
|
assert call_kwargs.get("shell") is not True, (
|
|
"check_gpu_linux must not use shell=True"
|
|
)
|
|
|
|
def test_detects_nvidia_gpu(self):
|
|
"""check_gpu_linux correctly detects NVIDIA GPU from lspci output."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
nvidia_output = (
|
|
"00:02.0 VGA compatible controller: "
|
|
"NVIDIA Corporation GeForce RTX 3080"
|
|
)
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=nvidia_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_linux(context)
|
|
|
|
assert context["_nvidia_gpu"] is True
|
|
assert context["_amd_gpu"] is False
|
|
assert context["enable_gpu"] is True
|
|
|
|
def test_detects_amd_gpu(self):
|
|
"""check_gpu_linux correctly detects AMD GPU from lspci output."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
amd_output = "06:00.0 VGA compatible controller: AMD/ATI Radeon RX 7900"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=amd_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_linux(context)
|
|
|
|
assert context["_amd_gpu"] is True
|
|
assert context["_nvidia_gpu"] is False
|
|
assert context["enable_gpu"] is True
|
|
|
|
def test_no_gpu_detected(self):
|
|
"""check_gpu_linux handles no GPU detection."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
intel_output = (
|
|
"00:02.0 VGA compatible controller: Intel Corporation UHD Graphics"
|
|
)
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=intel_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_linux(context)
|
|
|
|
assert context["_nvidia_gpu"] is False
|
|
assert context["_amd_gpu"] is False
|
|
assert context["enable_gpu"] is False
|
|
|
|
def test_filters_vga_lines_in_python(self):
|
|
"""check_gpu_linux filters VGA lines in Python, not via shell pipe."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
lines = [
|
|
"00:00.0 Host bridge: Intel Corporation",
|
|
"00:02.0 VGA compatible controller: NVIDIA Corporation",
|
|
"00:1f.0 ISA bridge: Intel Corporation",
|
|
]
|
|
lspci_output = chr(10).join(lines)
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=lspci_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_linux(context)
|
|
|
|
# subprocess.run called once with ["lspci"], not piped to grep
|
|
mock_run.assert_called_once()
|
|
assert context["_nvidia_gpu"] is True
|
|
|
|
|
|
class TestCheckGpuWindows:
|
|
"""Verify check_gpu_windows uses secure subprocess invocation."""
|
|
|
|
def test_calls_subprocess_run_with_list_args(self):
|
|
"""check_gpu_windows must use subprocess.run with a list."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=wmic_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_windows(context)
|
|
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args
|
|
cmd_arg = (
|
|
call_args[0][0] if call_args[0] else call_args[1].get("args")
|
|
)
|
|
assert isinstance(cmd_arg, list), (
|
|
f"subprocess.run must be called with a list, "
|
|
f"got {type(cmd_arg)}"
|
|
)
|
|
assert cmd_arg == [
|
|
"wmic",
|
|
"path",
|
|
"win32_VideoController",
|
|
"get",
|
|
"name",
|
|
]
|
|
|
|
def test_does_not_use_shell_true(self):
|
|
"""check_gpu_windows must not pass shell=True."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(stdout="Name", returncode=0)
|
|
module.check_gpu_windows(context)
|
|
|
|
call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {}
|
|
assert call_kwargs.get("shell") is not True, (
|
|
"check_gpu_windows must not use shell=True"
|
|
)
|
|
|
|
def test_detects_nvidia_gpu(self):
|
|
"""check_gpu_windows correctly detects NVIDIA GPU."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=wmic_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_windows(context)
|
|
|
|
assert context["_nvidia_gpu"] is True
|
|
assert context["_amd_gpu"] is False
|
|
assert context["enable_gpu"] is True
|
|
|
|
def test_detects_amd_gpu(self):
|
|
"""check_gpu_windows correctly detects AMD GPU."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
wmic_output = "Name" + chr(10) + "AMD Radeon RX 7900 XTX"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=wmic_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_windows(context)
|
|
|
|
assert context["_amd_gpu"] is True
|
|
assert context["_nvidia_gpu"] is False
|
|
assert context["enable_gpu"] is True
|
|
|
|
def test_detects_radeon_gpu(self):
|
|
"""check_gpu_windows detects Radeon-branded AMD GPUs."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
wmic_output = "Name" + chr(10) + "Radeon RX 580"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=wmic_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_windows(context)
|
|
|
|
assert context["_amd_gpu"] is True
|
|
assert context["_nvidia_gpu"] is False
|
|
assert context["enable_gpu"] is True
|
|
|
|
def test_no_gpu_detected(self):
|
|
"""check_gpu_windows handles no GPU detection."""
|
|
module = _load_pre_prompt_module()
|
|
context = {}
|
|
|
|
wmic_output = "Name" + chr(10) + "Microsoft Basic Display Adapter"
|
|
with patch.object(subprocess, "run") as mock_run:
|
|
mock_run.return_value = MagicMock(
|
|
stdout=wmic_output,
|
|
returncode=0,
|
|
)
|
|
module.check_gpu_windows(context)
|
|
|
|
assert context["_nvidia_gpu"] is False
|
|
assert context["_amd_gpu"] is False
|
|
assert context["enable_gpu"] is False
|
|
|
|
|
|
class TestOpenFileLocationPathValidation:
|
|
"""Verify open_file_location uses PathValidator for defense-in-depth."""
|
|
|
|
@patch("local_deep_research.research_library.utils.PathValidator")
|
|
@patch("local_deep_research.research_library.utils.subprocess")
|
|
def test_calls_path_validator_before_opening(
|
|
self, mock_subprocess, mock_validator_cls
|
|
):
|
|
"""open_file_location must validate path via PathValidator."""
|
|
from local_deep_research.research_library.utils import (
|
|
open_file_location,
|
|
)
|
|
|
|
mock_validated_path = MagicMock()
|
|
mock_validated_path.parent = Path("/safe/directory")
|
|
mock_validator_cls.validate_local_filesystem_path.return_value = (
|
|
mock_validated_path
|
|
)
|
|
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
|
|
|
with patch(
|
|
"local_deep_research.research_library.utils.sys"
|
|
) as mock_sys:
|
|
mock_sys.platform = "linux"
|
|
result = open_file_location("/safe/directory/file.txt")
|
|
|
|
mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
|
|
"/safe/directory/file.txt"
|
|
)
|
|
assert result is True
|
|
|
|
@patch("local_deep_research.research_library.utils.PathValidator")
|
|
@patch("local_deep_research.research_library.utils.subprocess")
|
|
def test_uses_validated_path_parent_for_folder(
|
|
self, mock_subprocess, mock_validator_cls
|
|
):
|
|
"""open_file_location must use validated path parent, not raw input."""
|
|
from local_deep_research.research_library.utils import (
|
|
open_file_location,
|
|
)
|
|
|
|
mock_validated_path = MagicMock()
|
|
mock_validated_path.parent = Path("/validated/parent")
|
|
mock_validator_cls.validate_local_filesystem_path.return_value = (
|
|
mock_validated_path
|
|
)
|
|
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
|
|
|
with patch(
|
|
"local_deep_research.research_library.utils.sys"
|
|
) as mock_sys:
|
|
mock_sys.platform = "linux"
|
|
open_file_location("/some/raw/path/file.txt")
|
|
|
|
mock_subprocess.run.assert_called_once()
|
|
call_args = mock_subprocess.run.call_args[0][0]
|
|
assert call_args == ["xdg-open", str(Path("/validated/parent"))]
|
|
|
|
@patch("local_deep_research.research_library.utils.PathValidator")
|
|
def test_returns_false_on_path_validation_failure(self, mock_validator_cls):
|
|
"""open_file_location returns False if PathValidator rejects path."""
|
|
from local_deep_research.research_library.utils import (
|
|
open_file_location,
|
|
)
|
|
|
|
mock_validator_cls.validate_local_filesystem_path.side_effect = (
|
|
ValueError("Cannot access system directories")
|
|
)
|
|
|
|
result = open_file_location("/etc/shadow")
|
|
|
|
assert result is False
|
|
mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
|
|
"/etc/shadow"
|
|
)
|
|
|
|
@patch("local_deep_research.research_library.utils.PathValidator")
|
|
def test_blocks_path_traversal_via_validator(self, mock_validator_cls):
|
|
"""open_file_location blocks path traversal via PathValidator."""
|
|
from local_deep_research.research_library.utils import (
|
|
open_file_location,
|
|
)
|
|
|
|
mock_validator_cls.validate_local_filesystem_path.side_effect = (
|
|
ValueError("Path traversal patterns not allowed")
|
|
)
|
|
|
|
result = open_file_location("../../etc/passwd")
|
|
|
|
assert result is False
|
|
mock_validator_cls.validate_local_filesystem_path.assert_called_once_with(
|
|
"../../etc/passwd"
|
|
)
|