| """ |
| Fuzzing test configuration with Atheris setup. |
| |
| This module provides pytest fixtures for coverage-guided fuzzing using |
| Atheris to discover crashes, security vulnerabilities, and edge cases. |
| |
| Fixtures: |
| - atheris_fuzz_target: Base fixture for Atheris fuzz targets |
| - fuzz_input_data: Provider for random fuzz input |
| - fuzz_timeout: Timeout fixture (default 300s for fuzzing) |
| - Import existing fixtures from e2e_ui for auth and database isolation |
| """ |
|
|
| import os |
| import sys |
| import tempfile |
| from typing import Generator, Callable, Any |
|
|
| import pytest |
| from sqlalchemy import create_engine |
| from sqlalchemy.orm import Session, sessionmaker |
|
|
| |
| backend_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| if backend_dir not in sys.path: |
| sys.path.insert(0, backend_dir) |
|
|
| |
| |
| |
| |
| from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user, test_user |
| from tests.e2e_ui.fixtures.database_fixtures import db_session |
|
|
| |
| __all__ = [ |
| 'authenticated_user', |
| 'test_user', |
| 'db_session', |
| 'atheris_fuzz_target', |
| 'fuzz_input_data', |
| 'fuzz_timeout', |
| ] |
|
|
|
|
| |
| |
| |
|
|
| |
| try: |
| import atheris |
| ATHERIS_AVAILABLE = True |
| except ImportError: |
| ATHERIS_AVAILABLE = False |
| print("Warning: Atheris not installed. Fuzzing tests will be skipped.") |
| print("Install with: pip install atheris") |
|
|
|
|
| @pytest.fixture(scope="session") |
| def atheris_available() -> bool: |
| """ |
| Check if Atheris is available for fuzzing. |
| |
| Returns: |
| bool: True if Atheris is installed, False otherwise |
| """ |
| return ATHERIS_AVAILABLE |
|
|
|
|
| @pytest.fixture(scope="session") |
| def fuzz_timeout() -> int: |
| """ |
| Default timeout for fuzzing tests in seconds. |
| |
| Fuzzing runs can take a long time to discover interesting inputs. |
| Default: 300 seconds (5 minutes) |
| |
| Returns: |
| int: Timeout in seconds |
| """ |
| return int(os.getenv("FUZZ_TIMEOUT", "300")) |
|
|
|
|
| @pytest.fixture(scope="function") |
| def fuzz_input_data() -> Callable[[], bytes]: |
| """ |
| Provider for random fuzz input data. |
| |
| This fixture generates random byte sequences for fuzzing. |
| Atheris will mutate this input to discover crashes. |
| |
| Returns: |
| Callable: Function that generates random bytes |
| """ |
| def _generate_random_bytes(max_length: int = 1024) -> bytes: |
| """Generate random bytes for fuzzing. |
| |
| Args: |
| max_length: Maximum length of random bytes (default: 1024) |
| |
| Returns: |
| bytes: Random byte sequence |
| """ |
| import os |
| length = os.urandom(1)[0] % max_length |
| return os.urandom(length) |
|
|
| return _generate_random_bytes |
|
|
|
|
| @pytest.fixture(scope="function") |
| def atheris_fuzz_target(): |
| """ |
| Base fixture for Atheris fuzz targets. |
| |
| This fixture provides a context manager for running Atheris fuzzing. |
| If Atheris is not installed, the test will be skipped. |
| |
| Usage: |
| def test_parse_json_fuzz(atheris_fuzz_target): |
| with atheris_fuzz_target() as (data, fdp): |
| # Mutate input with fdp |
| json_str = fdp.ConsumeRandomLengthString() |
| # Test code that should not crash |
| parse_json(json_str) |
| |
| Yields: |
| tuple: (data bytes, FileDictProto object) if Atheris available |
| |
| Raises: |
| pytest.skip.Exception: If Atheris is not installed |
| """ |
| if not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed - fuzzing test skipped") |
|
|
| import atheris |
|
|
| class FuzzTargetContext: |
| """Context manager for Atheris fuzz target.""" |
|
|
| def __enter__(self): |
| |
| atheris.Setup(sys.argv, []) |
|
|
| |
| from atheris import fp |
| return b"", fp |
|
|
| def __exit__(self, exc_type, exc_val, exc_tb): |
| if exc_type is not None: |
| |
| return False |
| return True |
|
|
| return FuzzTargetContext() |
|
|
|
|
| @pytest.fixture(scope="function") |
| def fuzz_crash_dir() -> str: |
| """ |
| Create temporary directory for crash artifacts. |
| |
| When Atheris discovers a crash, it saves the crashing input |
| to a directory for later analysis. |
| |
| Returns: |
| str: Path to crash artifacts directory |
| """ |
| crash_dir = tempfile.mkdtemp(prefix="fuzz_crashes_") |
| return crash_dir |
|
|
|
|
| @pytest.fixture(scope="function", autouse=True) |
| def cleanup_fuzz_artifacts(request): |
| """ |
| Clean up fuzzing artifacts after each test. |
| |
| This autouse fixture ensures that temporary files created during |
| fuzzing are cleaned up after the test completes. |
| |
| Args: |
| request: Pytest request object |
| """ |
| yield |
|
|
| |
| if hasattr(request, "funcargs"): |
| crash_dir = request.funcargs.get("fuzz_crash_dir") |
| if crash_dir and os.path.exists(crash_dir): |
| import shutil |
| try: |
| shutil.rmtree(crash_dir) |
| except Exception: |
| pass |
|
|
|
|
| |
| |
| |
|
|
| def pytest_configure(config): |
| """ |
| Pytest configuration hook for fuzzing tests. |
| |
| Register custom markers for fuzzing test categorization. |
| |
| Args: |
| config: Pytest config object |
| """ |
| config.addinivalue_line( |
| "markers", |
| "fuzzing: Mark test as fuzzing test (requires Atheris)" |
| ) |
| config.addinivalue_line( |
| "markers", |
| "crash: Mark test as expected to discover crash" |
| ) |
| config.addinivalue_line( |
| "markers", |
| "hang: Mark test as expected to discover hang (timeout)" |
| ) |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def skip_fuzzing_without_atheris(request): |
| """ |
| Automatically skip fuzzing tests if Atheris is not installed. |
| |
| This autouse fixture checks for the 'fuzzing' marker and skips |
| the test if Atheris is not available. |
| |
| Args: |
| request: Pytest request object |
| """ |
| if request.node.get_closest_marker('fuzzing') and not ATHERIS_AVAILABLE: |
| pytest.skip("Atheris not installed - fuzzing test skipped") |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture(scope="session") |
| def fuzz_campaign_duration() -> int: |
| """ |
| Duration for fuzzing campaign in seconds. |
| |
| Longer campaigns discover more bugs but take more time. |
| Default: 60 seconds for quick fuzzing runs |
| |
| Returns: |
| int: Campaign duration in seconds |
| """ |
| return int(os.getenv("FUZZ_CAMPAIGN_DURATION", "60")) |
|
|
|
|
| @pytest.fixture(scope="function") |
| def fuzz_stats(): |
| """ |
| Track fuzzing statistics during test run. |
| |
| This fixture provides a dictionary to store fuzzing metrics |
| like executions, crashes, coverage. |
| |
| Returns: |
| dict: Statistics dictionary |
| """ |
| stats = { |
| "executions": 0, |
| "crashes": 0, |
| "hangs": 0, |
| "coverage": 0.0, |
| "start_time": None, |
| "end_time": None, |
| } |
| return stats |
|
|