| |
| """ |
| Fuzzing Campaign Orchestration Script |
| |
| This script orchestrates fuzzing campaigns for multiple API endpoints, |
| including campaign lifecycle management, crash deduplication, and automated |
| bug filing via FuzzingOrchestrator. |
| |
| Usage: |
| python run_fuzzing_campaigns.py --duration 3600 |
| python run_fuzzing_campaigns.py --duration 60 --campaign "POST /api/auth/login" |
| |
| Environment Variables: |
| GITHUB_TOKEN: GitHub Personal Access Token (automatic in CI/CD) |
| GITHUB_REPOSITORY: Repository in format "owner/repo" |
| FUZZ_CAMPAIGN_DURATION: Campaign duration in seconds (default: 3600) |
| """ |
|
|
| import argparse |
| import json |
| import os |
| import sys |
| import time |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Dict, List, Any |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) |
|
|
| from tests.fuzzing.campaigns.fuzzing_orchestrator import FuzzingOrchestrator |
| from tests.fuzzing.campaigns.crash_deduplicator import CrashDeduplicator |
|
|
|
|
| def run_campaign( |
| target_endpoint, |
| test_file, |
| duration_seconds=3600, |
| github_token=None, |
| github_repository=None |
| ): |
| """ |
| Run a single fuzzing campaign with lifecycle management. |
| |
| Args: |
| target_endpoint: API endpoint to fuzz (e.g., "POST /api/auth/login") |
| test_file: Path to test file (e.g., "tests/fuzzing/test_auth_api_fuzzing.py::test_login_endpoint_fuzzing") |
| duration_seconds: Campaign duration in seconds (default: 3600) |
| github_token: GitHub token for bug filing |
| github_repository: GitHub repository in format "owner/repo" |
| |
| Returns: |
| Dict with campaign_id, status, executions, crashes, bugs_filed |
| """ |
| print(f"\n{'='*60}") |
| print(f"Starting Campaign: {target_endpoint}") |
| print(f"{'='*60}") |
| print(f"Test file: {test_file}") |
| print(f"Duration: {duration_seconds}s") |
| print(f"Started at: {datetime.utcnow().isoformat()}Z") |
|
|
| |
| orchestrator = FuzzingOrchestrator(github_token, github_repository) |
|
|
| |
| start_result = orchestrator.start_campaign(target_endpoint, test_file, duration_seconds) |
| campaign_id = start_result["campaign_id"] |
|
|
| if start_result["status"] != "running": |
| print(f"✗ Failed to start campaign: {start_result.get('error', 'Unknown error')}") |
| return { |
| "target_endpoint": target_endpoint, |
| "status": "failed", |
| "error": start_result.get("error", "Unknown error") |
| } |
|
|
| print(f"✓ Campaign started: {campaign_id}") |
| print(f"PID: {start_result['pid']}") |
|
|
| |
| elapsed = 0 |
| monitor_interval = 60 |
| while elapsed < duration_seconds: |
| time.sleep(min(monitor_interval, duration_seconds - elapsed)) |
| elapsed += min(monitor_interval, duration_seconds - elapsed) |
|
|
| stats = orchestrator.monitor_campaign(campaign_id) |
| print(f" [{elapsed}s] Executions: {stats['executions']}, Crashes: {stats['crashes']}, Status: {stats['status']}") |
|
|
| |
| if stats["status"] == "failed": |
| print(f"✗ Campaign failed: {stats.get('error', 'Unknown error')}") |
| orchestrator.stop_campaign(campaign_id) |
| return { |
| "target_endpoint": target_endpoint, |
| "campaign_id": campaign_id, |
| "status": "failed", |
| "error": stats.get("error", "Unknown error") |
| } |
|
|
| |
| print(f"\nStopping campaign...") |
| stop_result = orchestrator.stop_campaign(campaign_id) |
|
|
| if stop_result["status"] != "stopped": |
| print(f"✗ Failed to stop campaign: {stop_result.get('error', 'Unknown error')}") |
| return { |
| "target_endpoint": target_endpoint, |
| "campaign_id": campaign_id, |
| "status": "error", |
| "error": stop_result.get("error", "Unknown error") |
| } |
|
|
| print(f"✓ Campaign stopped") |
|
|
| |
| stats = orchestrator.monitor_campaign(campaign_id) |
| print(f"\nCampaign Statistics:") |
| print(f" Executions: {stats['executions']}") |
| print(f" Crashes: {stats['crashes']}") |
|
|
| |
| campaign_crash_dir = Path(start_result["crash_dir"]) |
| if campaign_crash_dir.exists() and stats["crashes"] > 0: |
| print(f"\nDeduplicating crashes...") |
| deduplicator = CrashDeduplicator() |
| crashes_by_signature = deduplicator.deduplicate_crashes(campaign_crash_dir) |
| unique_crashes = len(crashes_by_signature) |
| print(f" Total crashes: {stats['crashes']}") |
| print(f" Unique crashes: {unique_crashes}") |
|
|
| |
| if unique_crashes > 0: |
| print(f"\nFiling bugs for {unique_crashes} unique crashes...") |
| filed_bugs = orchestrator.file_bugs_for_crashes(target_endpoint, crashes_by_signature) |
| bugs_filed = len([b for b in filed_bugs if b.get("status") == "created"]) |
|
|
| print(f" Bugs filed: {bugs_filed}") |
|
|
| |
| for bug_result in filed_bugs: |
| if bug_result.get("status") == "created": |
| print(f" ✓ {bug_result.get('issue_url', 'N/A')}") |
| elif bug_result.get("status") == "duplicate": |
| print(f" ⊘ Duplicate: {bug_result.get('issue_url', 'N/A')}") |
| elif bug_result.get("status") == "error": |
| print(f" ✗ Error: {bug_result.get('error', 'Unknown error')}") |
|
|
| return { |
| "target_endpoint": target_endpoint, |
| "campaign_id": campaign_id, |
| "status": "completed", |
| "executions": stats["executions"], |
| "crashes": stats["crashes"], |
| "unique_crashes": unique_crashes, |
| "bugs_filed": bugs_filed, |
| "bug_results": filed_bugs |
| } |
| else: |
| return { |
| "target_endpoint": target_endpoint, |
| "campaign_id": campaign_id, |
| "status": "completed", |
| "executions": stats["executions"], |
| "crashes": stats["crashes"], |
| "unique_crashes": 0, |
| "bugs_filed": 0 |
| } |
| else: |
| return { |
| "target_endpoint": target_endpoint, |
| "campaign_id": campaign_id, |
| "status": "completed", |
| "executions": stats["executions"], |
| "crashes": 0, |
| "unique_crashes": 0, |
| "bugs_filed": 0 |
| } |
|
|
|
|
| def run_all_campaigns(duration_seconds=3600, github_token=None, github_repository=None): |
| """ |
| Run all fuzzing campaigns. |
| |
| Args: |
| duration_seconds: Campaign duration in seconds (default: 3600) |
| github_token: GitHub token for bug filing |
| github_repository: GitHub repository in format "owner/repo" |
| |
| Returns: |
| Dict with overall summary (total_executions, total_crashes, bugs_filed, campaign_results) |
| """ |
| |
| campaign_configs = [ |
| { |
| "target": "POST /api/auth/login", |
| "test_file": "tests/fuzzing/test_auth_api_fuzzing.py::test_login_endpoint_fuzzing" |
| }, |
| { |
| "target": "POST /api/agents/{id}/run", |
| "test_file": "tests/fuzzing/test_agent_api_fuzzing.py::test_agent_run_fuzzing" |
| }, |
| { |
| "target": "POST /api/canvas/present", |
| "test_file": "tests/fuzzing/test_canvas_presentation_fuzzing.py::test_canvas_present_fuzzing" |
| }, |
| { |
| "target": "POST /api/workflows", |
| "test_file": "tests/fuzzing/test_workflow_api_fuzzing.py::test_workflow_create_fuzzing" |
| }, |
| { |
| "target": "POST /api/skills/import", |
| "test_file": "tests/fuzzing/test_skill_installation_fuzzing.py::test_skill_import_fuzzing" |
| } |
| ] |
|
|
| print(f"\n{'='*60}") |
| print(f"Running All Fuzzing Campaigns") |
| print(f"{'='*60}") |
| print(f"Total campaigns: {len(campaign_configs)}") |
| print(f"Duration per campaign: {duration_seconds}s") |
| print(f"Estimated total time: {len(campaign_configs) * duration_seconds}s (~{len(campaign_configs) * duration_seconds // 60} min)") |
| print(f"Started at: {datetime.utcnow().isoformat()}Z") |
|
|
| |
| campaign_results = [] |
| total_executions = 0 |
| total_crashes = 0 |
| total_bugs_filed = 0 |
|
|
| |
| for i, config in enumerate(campaign_configs, 1): |
| print(f"\n\nCampaign {i}/{len(campaign_configs)}") |
|
|
| try: |
| result = run_campaign( |
| config["target"], |
| config["test_file"], |
| duration_seconds, |
| github_token, |
| github_repository |
| ) |
| campaign_results.append(result) |
|
|
| |
| if result["status"] == "completed": |
| total_executions += result.get("executions", 0) |
| total_crashes += result.get("crashes", 0) |
| total_bugs_filed += result.get("bugs_filed", 0) |
| else: |
| print(f"✗ Campaign failed: {result.get('error', 'Unknown error')}") |
|
|
| except Exception as e: |
| print(f"✗ Campaign error: {e}") |
| campaign_results.append({ |
| "target_endpoint": config["target"], |
| "status": "error", |
| "error": str(e) |
| }) |
|
|
| |
| print(f"\n\n{'='*60}") |
| print(f"Overall Summary") |
| print(f"{'='*60}") |
| print(f"Total campaigns: {len(campaign_configs)}") |
| print(f"Successful campaigns: {len([r for r in campaign_results if r['status'] == 'completed'])}") |
| print(f"Failed campaigns: {len([r for r in campaign_results if r['status'] != 'completed'])}") |
| print(f"Total executions: {total_executions}") |
| print(f"Total crashes: {total_crashes}") |
| print(f"Bugs filed: {total_bugs_filed}") |
| print(f"Completed at: {datetime.utcnow().isoformat()}Z") |
|
|
| return { |
| "total_campaigns": len(campaign_configs), |
| "successful_campaigns": len([r for r in campaign_results if r["status"] == "completed"]), |
| "failed_campaigns": len([r for r in campaign_results if r["status"] != "completed"]), |
| "total_executions": total_executions, |
| "total_crashes": total_crashes, |
| "bugs_filed": total_bugs_filed, |
| "campaign_results": campaign_results |
| } |
|
|
|
|
| def main(): |
| """Main entry point.""" |
| parser = argparse.ArgumentParser( |
| description="Orchestrate fuzzing campaigns for API endpoints", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Run all campaigns for 1 hour (3600 seconds) |
| python run_fuzzing_campaigns.py --duration 3600 |
| |
| # Run single campaign for 60 seconds |
| python run_fuzzing_campaigns.py --duration 60 --campaign "POST /api/auth/login" |
| |
| # Run with environment variable override |
| FUZZ_CAMPAIGN_DURATION=7200 python run_fuzzing_campaigns.py |
| """ |
| ) |
|
|
| parser.add_argument( |
| "--duration", |
| type=int, |
| default=None, |
| help="Campaign duration in seconds (default: FUZZ_CAMPAIGN_DURATION env var or 3600)" |
| ) |
|
|
| parser.add_argument( |
| "--campaign", |
| type=str, |
| default=None, |
| help="Run specific campaign by target endpoint (e.g., 'POST /api/auth/login')" |
| ) |
|
|
| args = parser.parse_args() |
|
|
| |
| github_token = os.getenv("GITHUB_TOKEN") |
| github_repository = os.getenv("GITHUB_REPOSITORY") |
|
|
| if not github_token: |
| print("Error: GITHUB_TOKEN environment variable not set") |
| sys.exit(2) |
|
|
| if not github_repository: |
| print("Error: GITHUB_REPOSITORY environment variable not set") |
| sys.exit(2) |
|
|
| |
| duration = args.duration or int(os.getenv("FUZZ_CAMPAIGN_DURATION", "3600")) |
|
|
| print(f"Fuzzing Campaign Configuration:") |
| print(f" GitHub Repository: {github_repository}") |
| print(f" Duration: {duration}s") |
| print(f" Campaign: {args.campaign or 'All campaigns'}") |
|
|
| |
| try: |
| if args.campaign: |
| |
| result = run_campaign(args.campaign, None, duration, github_token, github_repository) |
|
|
| |
| print(json.dumps(result, indent=2)) |
|
|
| |
| if result["status"] == "completed": |
| if result.get("crashes", 0) > 0: |
| sys.exit(1) |
| else: |
| sys.exit(0) |
| else: |
| sys.exit(2) |
|
|
| else: |
| |
| summary = run_all_campaigns(duration, github_token, github_repository) |
|
|
| |
| print(json.dumps(summary, indent=2)) |
|
|
| |
| if summary.get("total_crashes", 0) > 0: |
| sys.exit(1) |
| elif summary.get("failed_campaigns", 0) > 0: |
| sys.exit(2) |
| else: |
| sys.exit(0) |
|
|
| except KeyboardInterrupt: |
| print("\n\nInterrupted by user") |
| sys.exit(2) |
| except Exception as e: |
| print(f"\n\nError: {e}") |
| sys.exit(2) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|