""" Agent API fuzzing harness for discovering crashes in agent execution endpoints. This module uses Atheris to fuzz agent execution endpoints: - POST /api/agents/{id}/run - Agent execution with parameters - GET /api/agents/{id}/status - Agent status queries - DELETE /api/agents/{id} - Agent deletion - GET /api/agents - Agent listing with query parameters Target: Agent run parsing/validation code crashes Coverage: SQL injection, XSS, null bytes, huge inputs, malformed data """ import os import sys import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session # Add backend to path for imports 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 core.database import get_db from main_api_app import app # Import existing fixtures (FUZZ-02: reuse to avoid duplication) from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user from tests.e2e_ui.fixtures.database_fixtures import db_session # Try to import Atheris (graceful degradation) try: import atheris from atheris import fp ATHERIS_AVAILABLE = True except ImportError: ATHERIS_AVAILABLE = False # ============================================================================ # AGENT RUN FUZZING (POST /api/agents/{id}/run) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_agent_run_fuzzing(db_session: Session, authenticated_user): """ Fuzz agent execution endpoint (POST /api/agents/{id}/run). Target crashes in: - Agent ID parsing/validation - Parameter JSON parsing - Command injection via parameters - SQL injection via agent_id Edge cases: - None, empty string, invalid UUID for agent_id - SQL injection: "; DROP TABLE agents; -- - XSS: - Huge agent_id (1000+ chars) - Null bytes in agent_id - Malformed JSON in parameters Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database to use isolated test session app.dependency_overrides[get_db] = lambda: db_session # Create TestClient with isolated database client = TestClient(app) # Authorization headers headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz agent run endpoint with mutated input. Args: data: Random bytes from Atheris fuzzer Raises: Exception: Crash discovered (Atheris catches this) """ try: fdp = fp.FuzzedDataProvider(data) # Fuzz agent_id with edge cases agent_id_option = fdp.ConsumeIntInRange(0, 5) if agent_id_option == 0: agent_id = None elif agent_id_option == 1: agent_id = "" elif agent_id_option == 2: # SQL injection agent_id = "'; DROP TABLE agents; --" elif agent_id_option == 3: # XSS attempt agent_id = "" elif agent_id_option == 4: # Huge string (potential buffer overflow) agent_id = "A" * 1000 else: # Random string up to 50 chars agent_id = fdp.ConsumeRandomLengthString(50) # Fuzz parameters dict (0-10 keys, random values) num_params = fdp.ConsumeIntInRange(0, 10) parameters = {} for i in range(num_params): key = fdp.ConsumeRandomLengthString(20) value_option = fdp.ConsumeIntInRange(0, 4) if value_option == 0: value = None elif value_option == 1: value = fdp.ConsumeRandomLengthString(100) elif value_option == 2: value = fdp.ConsumeIntInRange(-1000000, 1000000) else: # Nested dict for deep structure testing value = { "nested": fdp.ConsumeRandomLengthString(50), "value": fdp.ConsumeIntInRange(0, 1000) } parameters[key] = value # Make request with fuzzed data # Handle None agent_id gracefully if agent_id is None: # FastAPI will return 422 for missing path parameter response = client.post( "/api/agents/None/run", json={"parameters": parameters}, headers=headers ) else: response = client.post( f"/api/agents/{agent_id}/run", json={"parameters": parameters}, headers=headers ) # Assert acceptable status codes (no crashes = 500 errors) # 200: Success, 400: Bad request, 401: Unauthorized, 404: Not found, 422: Validation error assert response.status_code in [200, 400, 401, 404, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError, IndexError, AttributeError) as e: # Expected: parsing errors from malformed input are OK # These are edge cases, not crashes pass except Exception as e: # Unexpected: crash discovered # Re-raise for Atheris to catch and save crash input raise Exception(f"Crash in agent run fuzzing: {e}") # Run Atheris fuzzing # Use environment variable for iterations (default: 10000 for production) iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # AGENT STATUS FUZZING (GET /api/agents/{id}/status) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_agent_status_fuzzing(db_session: Session, authenticated_user): """ Fuzz agent status endpoint (GET /api/agents/{id}/status). Target crashes in: - Agent ID parsing/validation - Status query logic - SQL injection via agent_id Edge cases: - None, empty string, invalid UUID for agent_id - SQL injection payloads - Unicode strings, null bytes - Huge agent_id length Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz agent status endpoint.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz agent_id agent_id_option = fdp.ConsumeIntInRange(0, 4) if agent_id_option == 0: agent_id = "" elif agent_id_option == 1: agent_id = "'; DROP TABLE agents; --" elif agent_id_option == 2: agent_id = "\x00\x00\x00" # Null bytes elif agent_id_option == 3: agent_id = "A" * 10000 # Huge input else: agent_id = fdp.ConsumeRandomLengthString(100) # Make request response = client.get( f"/api/agents/{agent_id}/status", headers=headers ) # Assert acceptable status codes assert response.status_code in [200, 400, 404, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): # Expected: parsing errors pass except Exception as e: # Crash discovered raise Exception(f"Crash in agent status fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # AGENT DELETE FUZZING (DELETE /api/agents/{id}) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_agent_delete_fuzzing(db_session: Session, authenticated_user): """ Fuzz agent deletion endpoint (DELETE /api/agents/{id}). Target crashes in: - Agent ID parsing/validation - Deletion logic - Cascade delete operations - Running task checks Edge cases: - SQL injection via agent_id - Invalid UUIDs - Agents with running tasks (should return 409, not crash) - Non-existent agents (404, not crash) Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz agent delete endpoint.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz agent_id agent_id_option = fdp.ConsumeIntInRange(0, 3) if agent_id_option == 0: agent_id = "'; DELETE FROM agents; --" elif agent_id_option == 1: agent_id = "../../../../etc/passwd" # Path traversal attempt else: agent_id = fdp.ConsumeRandomLengthString(50) # Make request response = client.delete( f"/api/agents/{agent_id}", headers=headers ) # Assert acceptable status codes # 200: Success, 400: Bad request, 404: Not found, 409: Conflict (running tasks), 422: Validation assert response.status_code in [200, 400, 404, 409, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): pass except Exception as e: raise Exception(f"Crash in agent delete fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # AGENT LIST FUZZING (GET /api/agents with query parameters) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_agent_list_fuzzing(db_session: Session, authenticated_user): """ Fuzz agent listing endpoint (GET /api/agents) with query parameters. Target crashes in: - Query parameter parsing - Category filtering logic - Pagination (limit/offset) - SQL injection via query params Edge cases: - Negative limit/offset values - Huge limit values (potential DoS) - SQL injection in category parameter - Invalid filter combinations Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz agent list endpoint with query parameters.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz category parameter category_option = fdp.ConsumeIntInRange(0, 3) if category_option == 0: category = None elif category_option == 1: category = "'; DROP TABLE agents; --" else: category = fdp.ConsumeRandomLengthString(30) # Fuzz limit parameter limit_option = fdp.ConsumeIntInRange(0, 3) if limit_option == 0: limit = None elif limit_option == 1: limit = -100 # Negative value elif limit_option == 2: limit = 999999999 # Huge value (DoS attempt) else: limit = fdp.ConsumeIntInRange(1, 100) # Fuzz offset parameter offset_option = fdp.ConsumeIntInRange(0, 2) if offset_option == 0: offset = None elif offset_option == 1: offset = -50 # Negative offset else: offset = fdp.ConsumeIntInRange(0, 1000) # Build query parameters params = {} if category is not None: params["category"] = category if limit is not None: params["limit"] = limit if offset is not None: params["offset"] = offset # Make request response = client.get( "/api/agents/", params=params, headers=headers ) # Assert acceptable status codes assert response.status_code in [200, 400, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): pass except Exception as e: raise Exception(f"Crash in agent list fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations)