Download tests/fuzzing/test_skill_installation_fuzzing.py from techprotrade/annator-command-center: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/fuzzing/test_skill_installation_fuzzing.py
- Command line
-
hf download hf://spaces/techprotrade/annator-command-center/tests/fuzzing/test_skill_installation_fuzzing.py
-
curl -L -o test_skill_installation_fuzzing.py https://huggingface.co/spaces/techprotrade/annator-command-center/resolve/main/tests/fuzzing/test_skill_installation_fuzzing.py
19.7 kB
| """ | |
| Skill installation fuzzing harness for FastAPI endpoints. | |
| This module uses Atheris to fuzz skill import, execute, and promote endpoints | |
| to discover crashes, security vulnerabilities, and edge cases. | |
| Coverage: | |
| - POST /api/skills/import - Import community skill | |
| - POST /api/skills/execute - Execute skill | |
| - POST /api/skills/promote - Promote skill to Active status | |
| - Security-focused fuzzing: code injection, typosquatting, path traversal | |
| - YAML parsing fuzzing: malformed frontmatter, huge documents | |
| """ | |
| 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 SKILL IMPORT FUZZING | |
| # ============================================================================ | |
| def test_skill_import_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz skill import endpoint (POST /api/skills/import). | |
| PROPERTY: Skill import endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random skill content and metadata | |
| INVARIANT: Response status code always in [200, 400, 401, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Various import sources (github_url, file_upload, raw_content, invalid) | |
| - Malformed SKILL.md content (0-10000 chars) | |
| - SQL injection in metadata fields | |
| - XSS payloads in skill descriptions | |
| 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 skill import endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz source (github_url, file_upload, raw_content, invalid values) | |
| source_type = fdp.ConsumeIntInRange(0, 3) | |
| if source_type == 0: | |
| source = "github_url" | |
| elif source_type == 1: | |
| source = "file_upload" | |
| elif source_type == 2: | |
| source = "raw_content" | |
| else: | |
| source = fdp.ConsumeRandomLengthString(50) # Invalid source | |
| # Fuzz content (0-10000 chars, SKILL.md format, None, empty) | |
| content = fdp.ConsumeRandomLengthString(10000) | |
| # Fuzz metadata dict (0-10 keys, SQL injection, XSS) | |
| num_keys = fdp.ConsumeIntInRange(0, 10) | |
| metadata = {} | |
| for i in range(num_keys): | |
| key = fdp.ConsumeRandomLengthString(50) | |
| value = fdp.ConsumeRandomLengthString(100) | |
| metadata[key] = value | |
| payload = { | |
| "source": source, | |
| "content": content if content else None, | |
| "metadata": metadata if metadata else None | |
| } | |
| # Call POST /api/skills/import | |
| response = client.post("/api/skills/import", json=payload, headers=headers) | |
| # Assert status in [200, 400, 401, 422] | |
| assert response.status_code in [200, 400, 401, 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 SKILL EXECUTE FUZZING | |
| # ============================================================================ | |
| def test_skill_execute_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz skill execute endpoint (POST /api/skills/execute). | |
| PROPERTY: Skill execute endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random skill IDs and inputs | |
| INVARIANT: Response status code always in [200, 400, 404, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Invalid skill_id formats (None, empty, huge strings) | |
| - Code injection in inputs dict | |
| - Huge input values (DoS protection) | |
| - Invalid agent_id formats | |
| 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 skill execute endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz skill_id (50 chars, None, empty) | |
| skill_id = fdp.ConsumeRandomLengthString(50) | |
| # Fuzz inputs dict (0-20 keys, code injection, huge values) | |
| num_keys = fdp.ConsumeIntInRange(0, 20) | |
| inputs = {} | |
| for i in range(num_keys): | |
| key = fdp.ConsumeRandomLengthString(50) | |
| value_type = fdp.ConsumeIntInRange(0, 3) | |
| if value_type == 0: | |
| # Code injection payloads | |
| value = fdp.ConsumeRandomLengthString(1000) | |
| elif value_type == 1: | |
| # Huge value (DoS test) | |
| value = fdp.ConsumeRandomLengthString(10000) | |
| elif value_type == 2: | |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) | |
| else: | |
| value = None | |
| inputs[key] = value | |
| # Fuzz agent_id (50 chars, None, empty) | |
| agent_id = fdp.ConsumeRandomLengthString(50) | |
| payload = { | |
| "skill_id": skill_id if skill_id else None, | |
| "inputs": inputs, | |
| "agent_id": agent_id if agent_id else None | |
| } | |
| # Call POST /api/skills/execute | |
| response = client.post("/api/skills/execute", 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 SKILL PROMOTE FUZZING | |
| # ============================================================================ | |
| def test_skill_promote_fuzzing(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz skill promote endpoint (POST /api/skills/promote). | |
| PROPERTY: Skill promote endpoint should not crash on malformed input | |
| STRATEGY: Use FuzzedDataProvider to generate random skill IDs | |
| INVARIANT: Response status code always in [200, 400, 404, 409] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Invalid skill_id formats | |
| - Promotion from Untrusted -> Active status | |
| - Duplicate promotion attempts (409 conflict) | |
| 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 skill promote endpoint with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz skill_id (50 chars, None, empty) | |
| skill_id = fdp.ConsumeRandomLengthString(50) | |
| payload = { | |
| "skill_id": skill_id if skill_id else None | |
| } | |
| # Call POST /api/skills/promote | |
| response = client.post("/api/skills/promote", json=payload, headers=headers) | |
| # Assert status in [200, 400, 404, 409] | |
| assert response.status_code in [200, 400, 404, 409], \ | |
| 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 SKILL CONTENT SECURITY FUZZING | |
| # ============================================================================ | |
| def test_skill_content_security_fuzz(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz skill content with malicious payloads (security-focused fuzzing). | |
| PROPERTY: Skill import endpoint should not crash on malicious payloads | |
| STRATEGY: Test known attack patterns from Phase 237 bug discovery | |
| INVARIANT: Response status code always in [200, 400, 401, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Code injection: "__import__('os').system('rm -rf /')" | |
| - Typosquatting: "requests" vs "requets", "numpy" vs "numpyy" | |
| - Path traversal: "../../../etc/passwd" | |
| - Null bytes: "skill\x00name" | |
| - Metadata injection: author, tags with malicious content | |
| 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}"} | |
| # Malicious payloads from Phase 237 | |
| malicious_payloads = [ | |
| # Code injection | |
| "__import__('os').system('rm -rf /')", | |
| "eval('__import__(\"os\").system(\"id\")')", | |
| "exec(\"import os; os.system('pwd')\")", | |
| # Typosquatting | |
| "requets", # vs "requests" | |
| "numpyy", # vs "numpy" | |
| "panndas", # vs "pandas" | |
| "flaskk", # vs "flask" | |
| # Path traversal | |
| "../../../etc/passwd", | |
| "..\\..\\..\\windows\\system32", | |
| "/proc/self/environ", | |
| # Null bytes | |
| "skill\x00name", | |
| "author\x00injection", | |
| # SQL injection | |
| "'; DROP TABLE skills; --", | |
| "' OR '1'='1", | |
| "'; INSERT INTO skills VALUES (...); --", | |
| # XSS | |
| "<script>alert('XSS')</script>", | |
| "<img src=x onerror=alert('XSS')>", | |
| "javascript:alert('XSS')", | |
| ] | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz skill content with malicious payloads.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Select malicious payload | |
| payload_idx = fdp.ConsumeIntInRange(0, len(malicious_payloads) - 1) | |
| malicious_content = malicious_payloads[payload_idx] | |
| # Fuzz where to inject payload | |
| injection_type = fdp.ConsumeIntInRange(0, 3) | |
| if injection_type == 0: | |
| # Inject in skill name | |
| content = f"# SKILL.md\nname: {malicious_content}\ndescription: Test skill" | |
| elif injection_type == 1: | |
| # Inject in content | |
| content = f"# SKILL.md\nname: test\ncontent: {malicious_content}" | |
| elif injection_type == 2: | |
| # Inject in metadata | |
| content = "# SKILL.md\nname: test\n" | |
| metadata = {"author": malicious_content, "tags": [malicious_content]} | |
| else: | |
| # Full payload as content | |
| content = malicious_content | |
| # Prepare payload | |
| payload = { | |
| "source": "raw_content", | |
| "content": content, | |
| "metadata": metadata if injection_type == 2 else None | |
| } | |
| # Call POST /api/skills/import | |
| response = client.post("/api/skills/import", json=payload, headers=headers) | |
| # Assert no crashes (validation errors OK) | |
| assert response.status_code in [200, 400, 401, 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 SKILL YAML PARSING FUZZING | |
| # ============================================================================ | |
| def test_skill_yaml_parsing_fuzz(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz YAML frontmatter parsing in SKILL.md files. | |
| PROPERTY: YAML parser should not crash on malformed YAML | |
| STRATEGY: Use FuzzedDataProvider to generate random YAML content | |
| INVARIANT: Response status code always in [200, 400, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Malformed YAML syntax (unclosed brackets, invalid indentation) | |
| - Huge YAML documents (DoS protection) | |
| - Cyclical references in YAML | |
| - Missing required fields | |
| - Invalid data types | |
| 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 YAML parsing with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Fuzz YAML content (0-5000 chars) | |
| yaml_content = fdp.ConsumeRandomLengthString(5000) | |
| # Construct SKILL.md with YAML frontmatter | |
| skill_content = f"""--- | |
| name: {name} | |
| description: {fdp.ConsumeRandomLengthString(200)} | |
| author: {fdp.ConsumeRandomLengthString(50)} | |
| version: {fdp.ConsumeRandomLengthString(20)} | |
| --- | |
| ## Skill Content | |
| {fdp.ConsumeRandomLengthString(1000)} | |
| """ | |
| payload = { | |
| "source": "raw_content", | |
| "content": skill_content, | |
| "metadata": None | |
| } | |
| # Call POST /api/skills/import | |
| response = client.post("/api/skills/import", json=payload, headers=headers) | |
| # Assert no crashes (parsing errors OK) | |
| assert response.status_code in [200, 400, 422], \ | |
| f"Unexpected status {response.status_code}: {response.text[:200]}" | |
| except Exception as e: | |
| if "yaml" not in str(e).lower() and "validation" not in str(e).lower(): | |
| raise | |
| atheris.Setup(sys.argv, [fuzz_one_input]) | |
| atheris.Fuzz() | |
| # ============================================================================ | |
| # TEST SKILL DEPENDENCY INJECTION FUZZING | |
| # ============================================================================ | |
| def test_skill_dependency_injection_fuzz(db_session: Session, authenticated_user): | |
| """ | |
| Fuzz skill dependency injection in requirements.txt. | |
| PROPERTY: Skill installer should not crash on malicious dependencies | |
| STRATEGY: Test malicious packages in requirements.txt | |
| INVARIANT: Response status code always in [200, 400, 422] (no 500 errors) | |
| RADII: 10000 iterations provides coverage of: | |
| - Typosquatting packages (requets vs requests) | |
| - Malicious package names (rm -rf, ../etc/passwd) | |
| - Conflicting dependencies | |
| - Huge dependency lists | |
| - Invalid version specifiers | |
| 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}"} | |
| # Malicious dependency patterns | |
| malicious_deps = [ | |
| "requets", # Typosquatting | |
| "numpyy", # Typosquatting | |
| "../../../etc/passwd", # Path traversal | |
| "rm -rf", # Command injection | |
| "package==../..", # Path traversal | |
| "package @ file:///etc/passwd", # Local file | |
| "package @ git+git://github.com/attacker/repo.git#egg=package", # Git URL | |
| "-e ../../..", # Editable install with path traversal | |
| "package==999.999.999", # Invalid version | |
| ] | |
| def fuzz_one_input(data: bytes): | |
| """Fuzz dependency injection with random input.""" | |
| try: | |
| fdp = fp.FuzzedDataProvider(data) | |
| # Generate requirements.txt content | |
| num_deps = fdp.ConsumeIntInRange(0, 10) | |
| requirements = [] | |
| for i in range(num_deps): | |
| # Mix of legitimate and malicious dependencies | |
| if fdp.ConsumeBool(): | |
| # Malicious dependency | |
| dep_idx = fdp.ConsumeIntInRange(0, len(malicious_deps) - 1) | |
| dep = malicious_deps[dep_idx] | |
| else: | |
| # Random dependency | |
| dep = fdp.ConsumeRandomLengthString(100) | |
| requirements.append(dep) | |
| # Construct SKILL.md with dependencies | |
| requirements_str = "\n".join(requirements) | |
| skill_content = f"""--- | |
| name: test-skill | |
| description: Test skill with dependencies | |
| dependencies: | | |
| {requirements_str} | |
| --- | |
| ## Skill Content | |
| Test content | |
| """ | |
| payload = { | |
| "source": "raw_content", | |
| "content": skill_content, | |
| "metadata": None | |
| } | |
| # Call POST /api/skills/import | |
| response = client.post("/api/skills/import", json=payload, headers=headers) | |
| # Assert no crashes (validation errors OK) | |
| 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() | |