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
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
"""
|
|
Tests for the check-safe-requests pre-commit hook.
|
|
|
|
Ensures the hook correctly detects unsafe requests usage that bypasses SSRF protection.
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
from importlib import import_module
|
|
from pathlib import Path
|
|
|
|
|
|
# Add the pre-commit hooks directory to path
|
|
HOOKS_DIR = Path(__file__).parent.parent.parent / ".pre-commit-hooks"
|
|
sys.path.insert(0, str(HOOKS_DIR))
|
|
|
|
# Import the checker from the hook (must be after sys.path modification)
|
|
hook_module = import_module("check-safe-requests") # noqa: E402
|
|
RequestsChecker = hook_module.RequestsChecker
|
|
|
|
|
|
class TestRequestsChecker:
|
|
"""Tests for the RequestsChecker AST visitor."""
|
|
|
|
def _check_code(self, code: str, filename: str = "src/module.py") -> list:
|
|
"""Helper to check code and return errors."""
|
|
tree = ast.parse(code)
|
|
checker = RequestsChecker(filename)
|
|
checker.visit(tree)
|
|
return checker.errors
|
|
|
|
def test_detects_requests_get(self):
|
|
"""Should detect requests.get() calls."""
|
|
code = """
|
|
import requests
|
|
response = requests.get("http://example.com")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert "requests.get()" in errors[0][1]
|
|
assert "safe_get()" in errors[0][1]
|
|
|
|
def test_detects_requests_post(self):
|
|
"""Should detect requests.post() calls."""
|
|
code = """
|
|
import requests
|
|
response = requests.post("http://example.com", json={"key": "value"})
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert "requests.post()" in errors[0][1]
|
|
assert "safe_post()" in errors[0][1]
|
|
|
|
def test_detects_requests_session(self):
|
|
"""Should detect requests.Session() instantiation."""
|
|
code = """
|
|
import requests
|
|
session = requests.Session()
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert "requests.Session()" in errors[0][1]
|
|
assert "SafeSession()" in errors[0][1]
|
|
|
|
def test_detects_requests_put(self):
|
|
"""Should detect requests.put() calls."""
|
|
code = """
|
|
import requests
|
|
response = requests.put("http://example.com", data="test")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert "requests.put()" in errors[0][1]
|
|
|
|
def test_detects_requests_delete(self):
|
|
"""Should detect requests.delete() calls."""
|
|
code = """
|
|
import requests
|
|
response = requests.delete("http://example.com/resource/1")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert "requests.delete()" in errors[0][1]
|
|
|
|
def test_detects_multiple_violations(self):
|
|
"""Should detect multiple violations in one file."""
|
|
code = """
|
|
import requests
|
|
session = requests.Session()
|
|
response = requests.get("http://example.com")
|
|
data = requests.post("http://example.com/api", json={})
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 3
|
|
|
|
def test_allows_safe_requests_module(self):
|
|
"""Should allow direct requests in safe_requests.py (the wrapper itself)."""
|
|
code = """
|
|
import requests
|
|
response = requests.get("http://example.com")
|
|
"""
|
|
errors = self._check_code(
|
|
code, filename="src/security/safe_requests.py"
|
|
)
|
|
assert len(errors) == 0
|
|
|
|
def test_allows_test_files(self):
|
|
"""Should allow direct requests in test files."""
|
|
code = """
|
|
import requests
|
|
response = requests.get("http://example.com")
|
|
"""
|
|
# Test various test file patterns
|
|
assert len(self._check_code(code, filename="tests/test_api.py")) == 0
|
|
assert (
|
|
len(self._check_code(code, filename="tests/security/test_ssrf.py"))
|
|
== 0
|
|
)
|
|
assert len(self._check_code(code, filename="src/module_test.py")) == 0
|
|
|
|
def test_ignores_safe_get(self):
|
|
"""Should not flag safe_get() calls."""
|
|
code = """
|
|
from security import safe_get
|
|
response = safe_get("http://example.com")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 0
|
|
|
|
def test_ignores_safe_post(self):
|
|
"""Should not flag safe_post() calls."""
|
|
code = """
|
|
from security import safe_post
|
|
response = safe_post("http://example.com", json={})
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 0
|
|
|
|
def test_ignores_safe_session(self):
|
|
"""Should not flag SafeSession() calls."""
|
|
code = """
|
|
from security import SafeSession
|
|
session = SafeSession(allow_localhost=True)
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 0
|
|
|
|
def test_ignores_other_modules_get(self):
|
|
"""Should not flag get() calls on other modules."""
|
|
code = """
|
|
import os
|
|
value = os.environ.get("KEY")
|
|
|
|
my_dict = {"key": "value"}
|
|
result = my_dict.get("key")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 0
|
|
|
|
def test_reports_correct_line_numbers(self):
|
|
"""Should report the correct line number for violations."""
|
|
code = """
|
|
import requests
|
|
|
|
# Some comment
|
|
x = 1
|
|
|
|
response = requests.get("http://example.com")
|
|
"""
|
|
errors = self._check_code(code)
|
|
assert len(errors) == 1
|
|
assert errors[0][0] == 7 # Line 7
|
|
|
|
|
|
class TestHookIntegration:
|
|
"""Integration tests for the hook as a whole."""
|
|
|
|
def test_check_file_returns_true_for_clean_file(self, tmp_path):
|
|
"""check_file should return True for files without violations."""
|
|
clean_file = tmp_path / "clean.py"
|
|
clean_file.write_text("""
|
|
from security import safe_get
|
|
response = safe_get("http://example.com")
|
|
""")
|
|
assert hook_module.check_file(str(clean_file)) is True
|
|
|
|
def test_check_file_returns_false_for_violations(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""check_file should return False for files with violations."""
|
|
# Use a filename that doesn't match exclusion patterns
|
|
bad_file = tmp_path / "my_module.py"
|
|
bad_file.write_text("""
|
|
import requests
|
|
response = requests.get("http://example.com")
|
|
""")
|
|
|
|
# Monkeypatch the file path to avoid pytest temp dir containing "test_"
|
|
def patched_check(filename):
|
|
# Replace the actual path with a clean one for pattern matching
|
|
import ast
|
|
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
tree = ast.parse(content, filename=filename)
|
|
# Use a clean filename for pattern matching
|
|
checker = RequestsChecker("src/some_module.py")
|
|
checker.visit(tree)
|
|
return len(checker.errors) == 0
|
|
|
|
result = patched_check(str(bad_file))
|
|
assert result is False
|
|
|
|
def test_check_file_handles_non_python_files(self, tmp_path):
|
|
"""check_file should return True for non-Python files."""
|
|
txt_file = tmp_path / "readme.txt"
|
|
txt_file.write_text("This is not Python")
|
|
assert hook_module.check_file(str(txt_file)) is True
|
|
|
|
def test_check_file_handles_syntax_errors(self, tmp_path):
|
|
"""check_file should handle files with syntax errors gracefully."""
|
|
bad_syntax = tmp_path / "syntax_error.py"
|
|
bad_syntax.write_text("def broken(:\n pass")
|
|
# Should not raise, just return True (let other tools handle syntax)
|
|
assert hook_module.check_file(str(bad_syntax)) is True
|