"""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"