Spaces:
Sleeping
Sleeping
Download tests/fuzzing/test_workflow_api_fuzzing.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/fuzzing/test_workflow_api_fuzzing.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/fuzzing/test_workflow_api_fuzzing.py
-
curl -L -o test_workflow_api_fuzzing.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/fuzzing/test_workflow_api_fuzzing.py
17.8 kB
| """ | |
| Workflow API fuzzing harness for FastAPI endpoints. | |
| This module uses Atheris to fuzz workflow create, update, trigger, and schedule | |
| endpoints to discover crashes, security vulnerabilities, and edge cases. | |
| Coverage: | |
| - POST /api/workflows - Create workflow | |
| - PUT /api/workflows/{id} - Update workflow | |
| - POST /api/workflows/{id}/trigger - Trigger workflow execution | |
| - POST /api/workflows/{id}/schedule - Schedule workflow | |
| """ | |
| import os | |
| import sys | |
| from typing import Dict, Any | |
| import os | |
| import sys | |
| # Add backend to path | |
| 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) | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from sqlalchemy.orm import Session | |
| # Import fixtures | |
| from tests.fuzzing.conftest import ATHERIS_AVAILABLE | |
| from tests.e2e_ui.fixtures.database_fixtures import db_session | |
| from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user, test_user | |
| from main_api_app import app | |
| from core.database import get_db | |
| # Try to import Atheris | |
| try: | |
| import atheris | |
| from atheris import fp | |
| ATHERIS_AVAILABLE = True | |
| except ImportError: | |
| ATHERIS_AVAILABLE = False | |
| # ============================================================================ | |
| # TEST WORKFLOW CREATE FUZZING | |
| # ============================================================================ | |
| def test_workflow_create_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz workflow creation endpoint (POST /api/workflows). | |
| PROPERTY: Workflow create endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random workflow definitions | |
| INVARIANT: Response status code always in [200, 400, 401, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - SQL injection in workflow_name | |
| - Empty/None values for required fields | |
| - Malformed workflow_definition (nested, cyclical, huge) | |
| - Invalid trigger configurations | |
| Args: | |
| db_session: Database session with transaction rollback | |
| authenticated_user: (user, token) tuple for JWT auth | |
| """ | |
| if not ATHERIS_AVAILABLE: | |
| pytest.skip("Atheris not installed") | |
| user, token = authenticated_user | |
| # Override database dependency | |
| app.dependency_overrides[get_db] = lambda: db_session | |
| # Create TestClient | |
| client = TestClient(app) | |
| # Set auth headers | |
| headers = {"Authorization": f"Bearer {token}"} | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz workflow create endpoint with random input.""" | |
| try: | |
| # Use FuzzedDataProvider for structured input | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz workflow_name (0-100 chars, None, empty, SQL injection) | |
| workflow_name = fdp.ConsumeRandomLengthString(100) | |
| # Fuzz workflow_description (0-1000 chars, None, empty) | |
| workflow_description = fdp.ConsumeRandomLengthString(1000) | |
| # Fuzz workflow_definition dict (0-20 keys, nested structures) | |
| num_keys = fdp.ConsumeIntInRange(0, 20) | |
| workflow_definition = {} | |
| for i in range(num_keys): | |
| key = fdp.ConsumeRandomLengthString(50) | |
| value_type = fdp.ConsumeIntInRange(0, 3) | |
| if value_type == 0: | |
| value = fdp.ConsumeRandomLengthString(100) | |
| elif value_type == 1: | |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) | |
| elif value_type == 2: | |
| # Nested dict | |
| nested_keys = fdp.ConsumeIntInRange(0, 5) | |
| value = {fdp.ConsumeRandomLengthString(20): fdp.ConsumeRandomLengthString(50) for _ in range(nested_keys)} | |
| else: | |
| value = None | |
| workflow_definition[key] = value | |
| # Fuzz triggers list (0-10 items) | |
| num_triggers = fdp.ConsumeIntInRange(0, 10) | |
| triggers = [] | |
| for i in range(num_triggers): | |
| trigger = { | |
| "type": fdp.ConsumeRandomLengthString(20), | |
| "config": { | |
| "cron": fdp.ConsumeRandomLengthString(50), | |
| "enabled": fdp.ConsumeBool() | |
| } | |
| } | |
| triggers.append(trigger) | |
| # Prepare request payload | |
| payload = { | |
| "name": workflow_name if workflow_name else None, | |
| "description": workflow_description if workflow_description else None, | |
| "definition": workflow_definition, | |
| "triggers": triggers | |
| } | |
| # Call POST /api/workflows | |
| response = client.post("/api/workflows", json=payload, headers=headers) | |
| # Assert status in [200, 400, 401, 422] (no 500 errors) | |
| assert response.status_code in [200, 400, 401, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| # Only re-raise if it's a crash (not validation error) | |
| if "validation" not in str(e).lower() and "422" not in str(e): | |
| raise | |
| # Run fuzzing with Atheris | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |
| # ============================================================================ | |
| # TEST WORKFLOW UPDATE FUZZING | |
| # ============================================================================ | |
| def test_workflow_update_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz workflow update endpoint (PUT /api/workflows/{id}). | |
| PROPERTY: Workflow update endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random workflow IDs and update data | |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Invalid workflow_id formats (None, empty, huge strings) | |
| - Partial updates with null values | |
| - Malformed workflow_definition | |
| - SQL injection in update fields | |
| Args: | |
| db_session: Database session with transaction rollback | |
| authenticated_user: (user, token) tuple for JWT auth | |
| """ | |
| if not ATHERIS_AVAILABLE: | |
| pytest.skip("Atheris not installed") | |
| user, token = authenticated_user | |
| app.dependency_overrides[get_db] = lambda: db_session | |
| client = TestClient(app) | |
| headers = {"Authorization": f"Bearer {token}"} | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz workflow update endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz workflow_id (50 chars, None, empty) | |
| workflow_id = fdp.ConsumeRandomLengthString(50) | |
| # Fuzz update_data dict (partial updates, null values, huge strings) | |
| num_keys = fdp.ConsumeIntInRange(0, 10) | |
| update_data = {} | |
| for i in range(num_keys): | |
| key = fdp.ConsumeRandomLengthString(50) | |
| value_type = fdp.ConsumeIntInRange(0, 4) | |
| if value_type == 0: | |
| value = fdp.ConsumeRandomLengthString(1000) | |
| elif value_type == 1: | |
| value = None | |
| elif value_type == 2: | |
| value = fdp.ConsumeBool() | |
| elif value_type == 3: | |
| # Nested dict | |
| nested_keys = fdp.ConsumeIntInRange(0, 5) | |
| value = {fdp.ConsumeRandomLengthString(20): fdp.ConsumeRandomLengthString(100) for _ in range(nested_keys)} | |
| else: | |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) | |
| update_data[key] = value | |
| # Call PUT /api/workflows/{id} | |
| response = client.put(f"/api/workflows/{workflow_id}", json=update_data, headers=headers) | |
| # Assert status in [200, 400, 404, 422] | |
| assert response.status_code in [200, 400, 404, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| if "validation" not in str(e).lower() and "422" not in str(e): | |
| raise | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |
| # ============================================================================ | |
| # TEST WORKFLOW TRIGGER FUZZING | |
| # ============================================================================ | |
| def test_workflow_trigger_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz workflow trigger endpoint (POST /api/workflows/{id}/trigger). | |
| PROPERTY: Workflow trigger endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random workflow IDs and input data | |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Invalid workflow_id formats | |
| - Huge input_data payloads (DoS protection) | |
| - Nested structures with cyclical references | |
| - SQL injection in input fields | |
| Args: | |
| db_session: Database session with transaction rollback | |
| authenticated_user: (user, token) tuple for JWT auth | |
| """ | |
| if not ATHERIS_AVAILABLE: | |
| pytest.skip("Atheris not installed") | |
| user, token = authenticated_user | |
| app.dependency_overrides[get_db] = lambda: db_session | |
| client = TestClient(app) | |
| headers = {"Authorization": f"Bearer {token}"} | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz workflow trigger endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz workflow_id (50 chars, None, empty) | |
| workflow_id = fdp.ConsumeRandomLengthString(50) | |
| # Fuzz input_data dict (0-20 keys, nested values, huge payloads) | |
| num_keys = fdp.ConsumeIntInRange(0, 20) | |
| input_data = {} | |
| for i in range(num_keys): | |
| key = fdp.ConsumeRandomLengthString(50) | |
| value_type = fdp.ConsumeIntInRange(0, 3) | |
| if value_type == 0: | |
| # Huge string (DoS test) | |
| value = fdp.ConsumeRandomLengthString(10000) | |
| elif value_type == 1: | |
| # Nested dict (cyclical ref test) | |
| nested_keys = fdp.ConsumeIntInRange(0, 10) | |
| value = {fdp.ConsumeRandomLengthString(30): fdp.ConsumeRandomLengthString(100) for _ in range(nested_keys)} | |
| elif value_type == 2: | |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) | |
| else: | |
| value = None | |
| input_data[key] = value | |
| payload = {"input_data": input_data} | |
| # Call POST /api/workflows/{id}/trigger | |
| response = client.post(f"/api/workflows/{workflow_id}/trigger", json=payload, headers=headers) | |
| # Assert status in [200, 400, 404, 422] | |
| assert response.status_code in [200, 400, 404, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| if "validation" not in str(e).lower() and "422" not in str(e): | |
| raise | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |
| # ============================================================================ | |
| # TEST WORKFLOW SCHEDULE FUZZING | |
| # ============================================================================ | |
| def test_workflow_schedule_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz workflow schedule endpoint (POST /api/workflows/{id}/schedule). | |
| PROPERTY: Workflow schedule endpoint should not crash on malformed cron expressions | |
| STRATEGY: Use FuzzedDataProvider to generate random schedule configurations | |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Invalid cron expressions (syntax errors, out-of-range values) | |
| - Special characters in cron fields | |
| - Past dates in schedule config | |
| - Huge intervals (DoS protection) | |
| Args: | |
| db_session: Database session with transaction rollback | |
| authenticated_user: (user, token) tuple for JWT auth | |
| """ | |
| if not ATHERIS_AVAILABLE: | |
| pytest.skip("Atheris not installed") | |
| user, token = authenticated_user | |
| app.dependency_overrides[get_db] = lambda: db_session | |
| client = TestClient(app) | |
| headers = {"Authorization": f"Bearer {token}"} | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz workflow schedule endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz workflow_id (50 chars, None, empty) | |
| workflow_id = fdp.ConsumeRandomLengthString(50) | |
| # Fuzz schedule_config dict (cron expressions, intervals, invalid formats) | |
| schedule_type = fdp.ConsumeRandomLengthString(20) | |
| schedule_config = { | |
| "type": schedule_type, | |
| "cron": fdp.ConsumeRandomLengthString(100), # Invalid cron syntax | |
| "interval_seconds": fdp.ConsumeIntInRange(-1000000, 1000000), # Invalid interval | |
| "start_date": fdp.ConsumeRandomLengthString(50), # Invalid date format | |
| "enabled": fdp.ConsumeBool() | |
| } | |
| # Call POST /api/workflows/{id}/schedule | |
| response = client.post(f"/api/workflows/{workflow_id}/schedule", json=schedule_config, headers=headers) | |
| # Assert status in [200, 400, 404, 422] | |
| assert response.status_code in [200, 400, 404, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| if "validation" not in str(e).lower() and "422" not in str(e): | |
| raise | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |
| # ============================================================================ | |
| # TEST WORKFLOW DAG VALIDATION FUZZING | |
| # ============================================================================ | |
| def test_workflow_dag_validation_fuzz(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz workflow DAG validation (cyclical dependencies, missing nodes). | |
| PROPERTY: Workflow DAG validation should not crash on malformed graphs | |
| STRATEGY: Use FuzzedDataProvider to generate random node/edge configurations | |
| INVARIANT: Response status code always in [200, 400, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Cyclical node dependencies (a -> b -> a) | |
| - Missing node references (edge points to non-existent node) | |
| - Empty or None node lists | |
| - Self-referencing nodes | |
| Args: | |
| db_session: Database session with transaction rollback | |
| authenticated_user: (user, token) tuple for JWT auth | |
| """ | |
| if not ATHERIS_AVAILABLE: | |
| pytest.skip("Atheris not installed") | |
| user, token = authenticated_user | |
| app.dependency_overrides[get_db] = lambda: db_session | |
| client = TestClient(app) | |
| headers = {"Authorization": f"Bearer {token}"} | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz workflow DAG validation with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz nodes list (0-20 nodes) | |
| num_nodes = fdp.ConsumeIntInRange(0, 20) | |
| nodes = [] | |
| node_ids = [] | |
| for i in range(num_nodes): | |
| node_id = fdp.ConsumeRandomLengthString(30) | |
| node_ids.append(node_id) | |
| nodes.append({ | |
| "id": node_id, | |
| "type": fdp.ConsumeRandomLengthString(20), | |
| "config": fdp.ConsumeRandomLengthString(100) | |
| }) | |
| # Fuzz edges list (0-30 edges, potentially cyclical) | |
| num_edges = fdp.ConsumeIntInRange(0, 30) | |
| edges = [] | |
| for i in range(num_edges): | |
| # Use random node IDs or generate invalid ones | |
| from_idx = fdp.ConsumeIntInRange(0, len(node_ids) + 5) | |
| to_idx = fdp.ConsumeIntInRange(0, len(node_ids) + 5) | |
| from_node = node_ids[from_idx] if from_idx < len(node_ids) else fdp.ConsumeRandomLengthString(30) | |
| to_node = node_ids[to_idx] if to_idx < len(node_ids) else fdp.ConsumeRandomLengthString(30) | |
| edges.append({ | |
| "from": from_node, | |
| "to": to_node, | |
| "condition": fdp.ConsumeRandomLengthString(50) | |
| }) | |
| # Create workflow with DAG definition | |
| payload = { | |
| "name": fdp.ConsumeRandomLengthString(50), | |
| "definition": { | |
| "nodes": nodes, | |
| "edges": edges | |
| } | |
| } | |
| # Call POST /api/workflows (will trigger DAG validation) | |
| response = client.post("/api/workflows", json=payload, headers=headers) | |
| # Assert status in [200, 400, 422] | |
| assert response.status_code in [200, 400, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| if "validation" not in str(e).lower() and "422" not in str(e): | |
| raise | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |