Trim and tighten code comments and docstrings across the repository. Comment-only: every changed file verified code-identical to main via AST/token comparison.
86 lines
2.5 KiB
Python
86 lines
2.5 KiB
Python
"""Shared fixtures for the security regression suite.
|
|
|
|
The scanners under audit must be offline-safe. An autouse session-scoped network
|
|
blocker refuses any non-loopback `socket.connect()` so a regression that reaches
|
|
the internet fails loudly instead of leaking the request.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
# Make `scripts/` importable so tests can grab scanner constants directly
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
|
|
_LOOPBACK_PREFIXES = ("127.", "::1", "localhost")
|
|
|
|
|
|
def _is_loopback(host: str | bytes) -> bool:
|
|
if isinstance(host, bytes):
|
|
try:
|
|
host = host.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return False
|
|
if not host:
|
|
return False
|
|
host = host.strip()
|
|
if host in {"::1", "localhost", "0.0.0.0"}:
|
|
return True
|
|
return host.startswith("127.")
|
|
|
|
|
|
class _BlockedSocket(socket.socket):
|
|
"""Socket subclass that refuses any non-loopback connect()."""
|
|
|
|
def connect(self, address): # type: ignore[override]
|
|
host = None
|
|
if isinstance(address, tuple) and address:
|
|
host = address[0]
|
|
if not _is_loopback(host or ""):
|
|
raise RuntimeError(
|
|
f"network access blocked by tests/security/conftest.py "
|
|
f"(attempted connect to {address!r}); the scanner suite "
|
|
"must run fully offline"
|
|
)
|
|
return super().connect(address)
|
|
|
|
def connect_ex(self, address): # type: ignore[override]
|
|
host = None
|
|
if isinstance(address, tuple) and address:
|
|
host = address[0]
|
|
if not _is_loopback(host or ""):
|
|
raise RuntimeError(
|
|
f"network access blocked by tests/security/conftest.py "
|
|
f"(attempted connect_ex to {address!r})"
|
|
)
|
|
return super().connect_ex(address)
|
|
|
|
|
|
@pytest.fixture(scope = "session", autouse = True)
|
|
def network_blocker():
|
|
"""Session-scoped fixture; swaps `socket.socket` for a blocker, restored at
|
|
teardown so interleaved sessions see a clean module."""
|
|
original = socket.socket
|
|
socket.socket = _BlockedSocket # type: ignore[assignment]
|
|
try:
|
|
yield
|
|
finally:
|
|
socket.socket = original # type: ignore[assignment]
|
|
|
|
|
|
@pytest.fixture(scope = "session")
|
|
def repo_root() -> Path:
|
|
return REPO_ROOT
|
|
|
|
|
|
@pytest.fixture(scope = "session")
|
|
def fixtures_dir() -> Path:
|
|
return Path(__file__).resolve().parent / "fixtures"
|