mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-08-19 01:37:14 +02:00
feat: Add Python fuzzing vertical with Atheris integration
This commit implements a complete Python fuzzing workflow using Atheris: ## Python Worker (workers/python/) - Dockerfile with Python 3.11, Atheris, and build tools - Generic worker.py for dynamic workflow discovery - requirements.txt with temporalio, boto3, atheris dependencies - Added to docker-compose.temporal.yaml with dedicated cache volume ## AtherisFuzzer Module (backend/toolbox/modules/fuzzer/) - Reusable module extending BaseModule - Auto-discovers fuzz targets (fuzz_*.py, *_fuzz.py, fuzz_target.py) - Recursive search to find targets in nested directories - Dynamically loads TestOneInput() function - Configurable max_iterations and timeout - Real-time stats callback support for live monitoring - Returns findings as ModuleFinding objects ## Atheris Fuzzing Workflow (backend/toolbox/workflows/atheris_fuzzing/) - Temporal workflow for orchestrating fuzzing - Downloads user code from MinIO - Executes AtherisFuzzer module - Uploads results to MinIO - Cleans up cache after execution - metadata.yaml with vertical: python for routing ## Test Project (test_projects/python_fuzz_waterfall/) - Demonstrates stateful waterfall vulnerability - main.py with check_secret() that leaks progress - fuzz_target.py with Atheris TestOneInput() harness - Complete README with usage instructions ## Backend Fixes - Fixed parameter merging in REST API endpoints (workflows.py) - Changed workflow parameter passing from positional args to kwargs (manager.py) - Default parameters now properly merged with user parameters ## Testing ✅ Worker discovered AtherisFuzzingWorkflow ✅ Workflow executed end-to-end successfully ✅ Fuzz target auto-discovered in nested directories ✅ Atheris ran 100,000 iterations ✅ Results uploaded and cache cleaned
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
Atheris Fuzzing Workflow
|
||||
|
||||
Fuzzes user-provided Python code using Atheris.
|
||||
"""
|
||||
|
||||
from .workflow import AtherisFuzzingWorkflow
|
||||
|
||||
__all__ = ["AtherisFuzzingWorkflow"]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Atheris Fuzzing Workflow Activities
|
||||
|
||||
Activities specific to the Atheris fuzzing workflow.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from temporalio import activity
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Add toolbox to path for module imports
|
||||
sys.path.insert(0, '/app/toolbox')
|
||||
|
||||
|
||||
@activity.defn(name="fuzz_with_atheris")
|
||||
async def fuzz_activity(workspace_path: str, config: dict) -> dict:
|
||||
"""
|
||||
Fuzzing activity using the AtherisFuzzer module on user code.
|
||||
|
||||
This activity:
|
||||
1. Imports the reusable AtherisFuzzer module
|
||||
2. Sets up real-time stats callback
|
||||
3. Executes fuzzing on user's TestOneInput() function
|
||||
4. Returns findings as ModuleResult
|
||||
|
||||
Args:
|
||||
workspace_path: Path to the workspace directory (user's uploaded code)
|
||||
config: Fuzzer configuration (target_file, max_iterations, timeout_seconds)
|
||||
|
||||
Returns:
|
||||
Fuzzer results dictionary (findings, summary, metadata)
|
||||
"""
|
||||
logger.info(f"Activity: fuzz_with_atheris (workspace={workspace_path})")
|
||||
|
||||
try:
|
||||
# Import reusable AtherisFuzzer module
|
||||
from modules.fuzzer import AtherisFuzzer
|
||||
|
||||
workspace = Path(workspace_path)
|
||||
if not workspace.exists():
|
||||
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||
|
||||
# Get activity info for real-time stats
|
||||
info = activity.info()
|
||||
run_id = info.workflow_id
|
||||
|
||||
# Define stats callback for real-time monitoring
|
||||
async def stats_callback(stats_data: Dict[str, Any]):
|
||||
"""Callback for live fuzzing statistics"""
|
||||
try:
|
||||
logger.info("LIVE_STATS", extra={
|
||||
"stats_type": "fuzzing_live_update",
|
||||
"workflow_type": "atheris_fuzzing",
|
||||
"run_id": run_id,
|
||||
"executions": stats_data.get("total_execs", 0),
|
||||
"executions_per_sec": stats_data.get("execs_per_sec", 0.0),
|
||||
"crashes": stats_data.get("crashes", 0),
|
||||
"corpus_size": stats_data.get("corpus_size", 0),
|
||||
"coverage": stats_data.get("coverage", 0.0),
|
||||
"elapsed_time": stats_data.get("elapsed_time", 0),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error in stats callback: {e}")
|
||||
|
||||
# Add stats callback to config
|
||||
config["stats_callback"] = stats_callback
|
||||
|
||||
# Execute the fuzzer module
|
||||
fuzzer = AtherisFuzzer()
|
||||
result = await fuzzer.execute(config, workspace)
|
||||
|
||||
logger.info(
|
||||
f"✓ Fuzzing completed: "
|
||||
f"{result.summary.get('total_executions', 0)} executions, "
|
||||
f"{result.summary.get('crashes_found', 0)} crashes"
|
||||
)
|
||||
|
||||
return result.dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fuzzing failed: {e}", exc_info=True)
|
||||
raise
|
||||
@@ -0,0 +1,76 @@
|
||||
name: atheris_fuzzing
|
||||
version: "1.0.0"
|
||||
vertical: python
|
||||
description: "Fuzz Python code using Atheris with real-time monitoring. Automatically discovers and fuzzes TestOneInput() functions in user code."
|
||||
author: "FuzzForge Team"
|
||||
category: "fuzzing"
|
||||
tags:
|
||||
- "fuzzing"
|
||||
- "atheris"
|
||||
- "python"
|
||||
- "coverage"
|
||||
- "security"
|
||||
|
||||
supported_volume_modes:
|
||||
- "ro"
|
||||
|
||||
default_volume_mode: "ro"
|
||||
default_target_path: "/workspace"
|
||||
|
||||
requirements:
|
||||
tools:
|
||||
- "atheris_fuzzer"
|
||||
resources:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
timeout: 3600
|
||||
|
||||
has_docker: false
|
||||
|
||||
default_parameters:
|
||||
target_file: null
|
||||
max_iterations: 100000
|
||||
timeout_seconds: 300
|
||||
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
target_file:
|
||||
type: string
|
||||
description: "Python file with TestOneInput() function (auto-discovered if not specified)"
|
||||
max_iterations:
|
||||
type: integer
|
||||
default: 100000
|
||||
description: "Maximum fuzzing iterations"
|
||||
timeout_seconds:
|
||||
type: integer
|
||||
default: 300
|
||||
description: "Fuzzing timeout in seconds (5 minutes)"
|
||||
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
findings:
|
||||
type: array
|
||||
description: "Crashes and vulnerabilities found during fuzzing"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
severity:
|
||||
type: string
|
||||
category:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
summary:
|
||||
type: object
|
||||
description: "Fuzzing execution summary"
|
||||
properties:
|
||||
total_executions:
|
||||
type: integer
|
||||
crashes_found:
|
||||
type: integer
|
||||
execution_time:
|
||||
type: number
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Atheris Fuzzing Workflow - Temporal Version
|
||||
|
||||
Fuzzes user-provided Python code using Atheris with real-time monitoring.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from temporalio import workflow
|
||||
from temporalio.common import RetryPolicy
|
||||
|
||||
# Import for type hints (will be executed by worker)
|
||||
with workflow.unsafe.imports_passed_through():
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@workflow.defn
|
||||
class AtherisFuzzingWorkflow:
|
||||
"""
|
||||
Fuzz Python code using Atheris.
|
||||
|
||||
User workflow:
|
||||
1. User runs: ff workflow run atheris_fuzzing .
|
||||
2. CLI uploads project to MinIO
|
||||
3. Worker downloads project
|
||||
4. Worker fuzzes TestOneInput() function
|
||||
5. Crashes reported as findings
|
||||
"""
|
||||
|
||||
@workflow.run
|
||||
async def run(
|
||||
self,
|
||||
target_id: str, # MinIO UUID of uploaded user code
|
||||
target_file: Optional[str] = None, # Optional: specific file to fuzz
|
||||
max_iterations: int = 100000,
|
||||
timeout_seconds: int = 300
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Main workflow execution.
|
||||
|
||||
Args:
|
||||
target_id: UUID of the uploaded target in MinIO
|
||||
target_file: Optional specific Python file with TestOneInput() (auto-discovered if None)
|
||||
max_iterations: Maximum fuzzing iterations
|
||||
timeout_seconds: Fuzzing timeout in seconds
|
||||
|
||||
Returns:
|
||||
Dictionary containing findings and summary
|
||||
"""
|
||||
workflow_id = workflow.info().workflow_id
|
||||
|
||||
workflow.logger.info(
|
||||
f"Starting AtherisFuzzingWorkflow "
|
||||
f"(workflow_id={workflow_id}, target_id={target_id}, "
|
||||
f"target_file={target_file or 'auto-discover'}, max_iterations={max_iterations}, "
|
||||
f"timeout_seconds={timeout_seconds})"
|
||||
)
|
||||
|
||||
results = {
|
||||
"workflow_id": workflow_id,
|
||||
"target_id": target_id,
|
||||
"status": "running",
|
||||
"steps": []
|
||||
}
|
||||
|
||||
try:
|
||||
# Step 1: Download user's project from MinIO
|
||||
workflow.logger.info("Step 1: Downloading user code from MinIO")
|
||||
target_path = await workflow.execute_activity(
|
||||
"get_target",
|
||||
target_id,
|
||||
start_to_close_timeout=timedelta(minutes=5),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=1),
|
||||
maximum_interval=timedelta(seconds=30),
|
||||
maximum_attempts=3
|
||||
)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "download_target",
|
||||
"status": "success",
|
||||
"target_path": target_path
|
||||
})
|
||||
workflow.logger.info(f"✓ User code downloaded to: {target_path}")
|
||||
|
||||
# Step 2: Run Atheris fuzzing
|
||||
workflow.logger.info("Step 2: Running Atheris fuzzing")
|
||||
|
||||
# Use defaults if parameters are None
|
||||
max_iterations = max_iterations if max_iterations is not None else 100000
|
||||
timeout_seconds = timeout_seconds if timeout_seconds is not None else 300
|
||||
|
||||
fuzz_config = {
|
||||
"target_file": target_file,
|
||||
"max_iterations": max_iterations,
|
||||
"timeout_seconds": timeout_seconds
|
||||
}
|
||||
|
||||
fuzz_results = await workflow.execute_activity(
|
||||
"fuzz_with_atheris",
|
||||
args=[target_path, fuzz_config],
|
||||
start_to_close_timeout=timedelta(seconds=timeout_seconds + 60),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=2),
|
||||
maximum_interval=timedelta(seconds=60),
|
||||
maximum_attempts=1 # Fuzzing shouldn't retry
|
||||
)
|
||||
)
|
||||
|
||||
results["steps"].append({
|
||||
"step": "fuzzing",
|
||||
"status": "success",
|
||||
"executions": fuzz_results.get("summary", {}).get("total_executions", 0),
|
||||
"crashes": fuzz_results.get("summary", {}).get("crashes_found", 0)
|
||||
})
|
||||
workflow.logger.info(
|
||||
f"✓ Fuzzing completed: "
|
||||
f"{fuzz_results.get('summary', {}).get('total_executions', 0)} executions, "
|
||||
f"{fuzz_results.get('summary', {}).get('crashes_found', 0)} crashes"
|
||||
)
|
||||
|
||||
# Step 3: Upload results to MinIO
|
||||
workflow.logger.info("Step 3: Uploading results")
|
||||
try:
|
||||
results_url = await workflow.execute_activity(
|
||||
"upload_results",
|
||||
args=[workflow_id, fuzz_results, "json"],
|
||||
start_to_close_timeout=timedelta(minutes=2)
|
||||
)
|
||||
results["results_url"] = results_url
|
||||
workflow.logger.info(f"✓ Results uploaded to: {results_url}")
|
||||
except Exception as e:
|
||||
workflow.logger.warning(f"Failed to upload results: {e}")
|
||||
results["results_url"] = None
|
||||
|
||||
# Step 4: Cleanup cache
|
||||
workflow.logger.info("Step 4: Cleaning up cache")
|
||||
try:
|
||||
await workflow.execute_activity(
|
||||
"cleanup_cache",
|
||||
target_path,
|
||||
start_to_close_timeout=timedelta(minutes=1)
|
||||
)
|
||||
workflow.logger.info("✓ Cache cleaned up")
|
||||
except Exception as e:
|
||||
workflow.logger.warning(f"Cache cleanup failed: {e}")
|
||||
|
||||
# Mark workflow as successful
|
||||
results["status"] = "success"
|
||||
results["findings"] = fuzz_results.get("findings", [])
|
||||
results["summary"] = fuzz_results.get("summary", {})
|
||||
workflow.logger.info(
|
||||
f"✓ Workflow completed successfully: {workflow_id} "
|
||||
f"({results['summary'].get('crashes_found', 0)} crashes found)"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
workflow.logger.error(f"Workflow failed: {e}")
|
||||
results["status"] = "error"
|
||||
results["error"] = str(e)
|
||||
results["steps"].append({
|
||||
"step": "error",
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
})
|
||||
raise
|
||||
Reference in New Issue
Block a user