mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-08-19 02:57:18 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
OSS-Fuzz Module
|
||||
|
||||
This module integrates with Google's OSS-Fuzz for continuous fuzzing
|
||||
of open source projects.
|
||||
"""
|
||||
# Copyright (c) 2025 FuzzingLabs
|
||||
#
|
||||
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||
# at the root of this repository for details.
|
||||
#
|
||||
# After the Change Date (four years from publication), this version of the
|
||||
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Additional attribution and requirements are provided in the NOTICE file.
|
||||
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||
from . import register_module
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_module
|
||||
class OSSFuzzModule(BaseModule):
|
||||
"""OSS-Fuzz continuous fuzzing module"""
|
||||
|
||||
def get_metadata(self) -> ModuleMetadata:
|
||||
"""Get module metadata"""
|
||||
return ModuleMetadata(
|
||||
name="oss_fuzz",
|
||||
version="1.0.0",
|
||||
description="Google's continuous fuzzing for open source projects integration",
|
||||
author="FuzzForge Team",
|
||||
category="fuzzing",
|
||||
tags=["oss-fuzz", "continuous", "google", "open-source", "docker"],
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_name": {
|
||||
"type": "string",
|
||||
"description": "OSS-Fuzz project name"
|
||||
},
|
||||
"source_dir": {
|
||||
"type": "string",
|
||||
"description": "Source directory to fuzz"
|
||||
},
|
||||
"build_script": {
|
||||
"type": "string",
|
||||
"default": "build.sh",
|
||||
"description": "Build script path"
|
||||
},
|
||||
"dockerfile": {
|
||||
"type": "string",
|
||||
"default": "Dockerfile",
|
||||
"description": "Dockerfile path"
|
||||
},
|
||||
"project_yaml": {
|
||||
"type": "string",
|
||||
"default": "project.yaml",
|
||||
"description": "Project configuration file"
|
||||
},
|
||||
"sanitizer": {
|
||||
"type": "string",
|
||||
"enum": ["address", "memory", "undefined", "coverage"],
|
||||
"default": "address",
|
||||
"description": "Sanitizer to use"
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string",
|
||||
"enum": ["x86_64", "i386"],
|
||||
"default": "x86_64",
|
||||
"description": "Target architecture"
|
||||
},
|
||||
"fuzzing_engine": {
|
||||
"type": "string",
|
||||
"enum": ["libfuzzer", "afl", "honggfuzz"],
|
||||
"default": "libfuzzer",
|
||||
"description": "Fuzzing engine to use"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"default": 3600,
|
||||
"description": "Fuzzing timeout (seconds)"
|
||||
},
|
||||
"check_build": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Check if build is successful"
|
||||
},
|
||||
"reproduce_bugs": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Try to reproduce existing bugs"
|
||||
}
|
||||
}
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bug_type": {"type": "string"},
|
||||
"reproducer": {"type": "string"},
|
||||
"stack_trace": {"type": "string"},
|
||||
"sanitizer": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def validate_config(self, config: Dict[str, Any]) -> bool:
|
||||
"""Validate configuration"""
|
||||
project_name = config.get("project_name")
|
||||
if not project_name:
|
||||
raise ValueError("project_name is required")
|
||||
|
||||
source_dir = config.get("source_dir")
|
||||
if not source_dir:
|
||||
raise ValueError("source_dir is required")
|
||||
|
||||
return True
|
||||
|
||||
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||
"""Execute OSS-Fuzz integration"""
|
||||
self.start_timer()
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
self.validate_config(config)
|
||||
self.validate_workspace(workspace)
|
||||
|
||||
logger.info("Running OSS-Fuzz integration")
|
||||
|
||||
# Check Docker
|
||||
await self._check_docker()
|
||||
|
||||
# Clone/update OSS-Fuzz if needed
|
||||
oss_fuzz_dir = await self._setup_oss_fuzz(workspace)
|
||||
|
||||
# Setup project
|
||||
await self._setup_project(oss_fuzz_dir, config, workspace)
|
||||
|
||||
# Build and run
|
||||
findings = await self._run_oss_fuzz(oss_fuzz_dir, config, workspace)
|
||||
|
||||
# Create summary
|
||||
summary = self._create_summary(findings)
|
||||
|
||||
logger.info(f"OSS-Fuzz found {len(findings)} issues")
|
||||
|
||||
return self.create_result(
|
||||
findings=findings,
|
||||
status="success",
|
||||
summary=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"OSS-Fuzz module failed: {e}")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _check_docker(self):
|
||||
"""Check if Docker is available"""
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"docker", "--version",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError("Docker not available. OSS-Fuzz requires Docker.")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Docker check failed: {e}")
|
||||
|
||||
async def _setup_oss_fuzz(self, workspace: Path) -> Path:
|
||||
"""Setup OSS-Fuzz repository"""
|
||||
oss_fuzz_dir = workspace / "oss-fuzz"
|
||||
|
||||
if not oss_fuzz_dir.exists():
|
||||
logger.info("Cloning OSS-Fuzz repository")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"git", "clone", "https://github.com/google/oss-fuzz.git",
|
||||
cwd=workspace,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(f"Failed to clone OSS-Fuzz: {stderr.decode()}")
|
||||
|
||||
return oss_fuzz_dir
|
||||
|
||||
async def _setup_project(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path):
|
||||
"""Setup OSS-Fuzz project"""
|
||||
project_name = config["project_name"]
|
||||
project_dir = oss_fuzz_dir / "projects" / project_name
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy source if provided
|
||||
source_dir = workspace / config["source_dir"]
|
||||
if source_dir.exists():
|
||||
# Create symlink or copy source
|
||||
logger.info(f"Setting up source directory: {source_dir}")
|
||||
|
||||
# Setup required files if they don't exist
|
||||
await self._create_project_files(project_dir, config, workspace)
|
||||
|
||||
async def _create_project_files(self, project_dir: Path, config: Dict[str, Any], workspace: Path):
|
||||
"""Create required OSS-Fuzz project files"""
|
||||
|
||||
# Create Dockerfile if it doesn't exist
|
||||
dockerfile = project_dir / config.get("dockerfile", "Dockerfile")
|
||||
if not dockerfile.exists():
|
||||
dockerfile_content = f'''FROM gcr.io/oss-fuzz-base/base-builder
|
||||
COPY . $SRC/{config["project_name"]}
|
||||
WORKDIR $SRC/{config["project_name"]}
|
||||
COPY {config.get("build_script", "build.sh")} $SRC/
|
||||
'''
|
||||
dockerfile.write_text(dockerfile_content)
|
||||
|
||||
# Create build.sh if it doesn't exist
|
||||
build_script = project_dir / config.get("build_script", "build.sh")
|
||||
if not build_script.exists():
|
||||
build_content = f'''#!/bin/bash -eu
|
||||
# Build script for {config["project_name"]}
|
||||
# Add your build commands here
|
||||
echo "Building {config['project_name']}..."
|
||||
'''
|
||||
build_script.write_text(build_content)
|
||||
build_script.chmod(0o755)
|
||||
|
||||
# Create project.yaml if it doesn't exist
|
||||
project_yaml = project_dir / config.get("project_yaml", "project.yaml")
|
||||
if not project_yaml.exists():
|
||||
yaml_content = f'''homepage: "https://example.com"
|
||||
language: c++
|
||||
primary_contact: "security@example.com"
|
||||
auto_ccs:
|
||||
- "fuzzing@example.com"
|
||||
sanitizers:
|
||||
- {config.get("sanitizer", "address")}
|
||||
architectures:
|
||||
- {config.get("architecture", "x86_64")}
|
||||
fuzzing_engines:
|
||||
- {config.get("fuzzing_engine", "libfuzzer")}
|
||||
'''
|
||||
project_yaml.write_text(yaml_content)
|
||||
|
||||
async def _run_oss_fuzz(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
|
||||
"""Run OSS-Fuzz"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
project_name = config["project_name"]
|
||||
sanitizer = config.get("sanitizer", "address")
|
||||
architecture = config.get("architecture", "x86_64")
|
||||
|
||||
# Build project
|
||||
if config.get("check_build", True):
|
||||
await self._build_project(oss_fuzz_dir, project_name, sanitizer, architecture)
|
||||
|
||||
# Check build
|
||||
await self._check_build(oss_fuzz_dir, project_name, sanitizer, architecture)
|
||||
|
||||
# Run fuzzing (limited time for this integration)
|
||||
timeout = min(config.get("timeout", 300), 300) # Max 5 minutes for demo
|
||||
findings = await self._run_fuzzing(oss_fuzz_dir, project_name, sanitizer, timeout, workspace)
|
||||
|
||||
# Reproduce bugs if requested
|
||||
if config.get("reproduce_bugs", False):
|
||||
repro_findings = await self._reproduce_bugs(oss_fuzz_dir, project_name, workspace)
|
||||
findings.extend(repro_findings)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running OSS-Fuzz: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
async def _build_project(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str):
|
||||
"""Build OSS-Fuzz project"""
|
||||
cmd = [
|
||||
"python3", "infra/helper.py", "build_image", project_name
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
cwd=oss_fuzz_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.warning(f"Build image failed: {stderr.decode()}")
|
||||
|
||||
async def _check_build(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str):
|
||||
"""Check OSS-Fuzz build"""
|
||||
cmd = [
|
||||
"python3", "infra/helper.py", "check_build", project_name
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
cwd=oss_fuzz_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.warning(f"Build check failed: {stderr.decode()}")
|
||||
|
||||
async def _run_fuzzing(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, timeout: int, workspace: Path) -> List[ModuleFinding]:
|
||||
"""Run OSS-Fuzz fuzzing"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# This is a simplified version - real OSS-Fuzz runs for much longer
|
||||
cmd = [
|
||||
"python3", "infra/helper.py", "run_fuzzer", project_name,
|
||||
"--", f"-max_total_time={timeout}"
|
||||
]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
cwd=oss_fuzz_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(), timeout=timeout + 60
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.terminate()
|
||||
await process.wait()
|
||||
|
||||
# Parse output for crashes
|
||||
full_output = stdout.decode() + stderr.decode()
|
||||
findings = self._parse_oss_fuzz_output(full_output, workspace, sanitizer)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error in OSS-Fuzz execution: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
async def _reproduce_bugs(self, oss_fuzz_dir: Path, project_name: str, workspace: Path) -> List[ModuleFinding]:
|
||||
"""Reproduce existing bugs"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Look for existing testcases or artifacts
|
||||
testcases_dir = oss_fuzz_dir / "projects" / project_name / "testcases"
|
||||
if testcases_dir.exists():
|
||||
for testcase in testcases_dir.iterdir():
|
||||
if testcase.is_file():
|
||||
finding = self._create_testcase_finding(testcase, workspace)
|
||||
if finding:
|
||||
findings.append(finding)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reproducing bugs: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_oss_fuzz_output(self, output: str, workspace: Path, sanitizer: str) -> List[ModuleFinding]:
|
||||
"""Parse OSS-Fuzz output"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Look for common crash indicators
|
||||
lines = output.split('\n')
|
||||
crash_info = None
|
||||
|
||||
for line in lines:
|
||||
if "ERROR:" in line and any(term in line for term in ["AddressSanitizer", "MemorySanitizer", "UBSan"]):
|
||||
crash_info = {
|
||||
"type": self._extract_crash_type(line),
|
||||
"sanitizer": sanitizer,
|
||||
"line": line
|
||||
}
|
||||
elif crash_info and line.strip().startswith("#"):
|
||||
# Stack trace line
|
||||
if "stack_trace" not in crash_info:
|
||||
crash_info["stack_trace"] = []
|
||||
crash_info["stack_trace"].append(line.strip())
|
||||
|
||||
if crash_info:
|
||||
finding = self._create_oss_fuzz_finding(crash_info, workspace)
|
||||
if finding:
|
||||
findings.append(finding)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing OSS-Fuzz output: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _create_oss_fuzz_finding(self, crash_info: Dict[str, Any], workspace: Path) -> ModuleFinding:
|
||||
"""Create finding from OSS-Fuzz crash"""
|
||||
try:
|
||||
bug_type = crash_info.get("type", "unknown")
|
||||
sanitizer = crash_info.get("sanitizer", "unknown")
|
||||
stack_trace = '\n'.join(crash_info.get("stack_trace", [])[:20])
|
||||
|
||||
severity = self._get_oss_fuzz_severity(bug_type)
|
||||
|
||||
finding = self.create_finding(
|
||||
title=f"OSS-Fuzz {bug_type.title()}",
|
||||
description=f"OSS-Fuzz detected a {bug_type} using {sanitizer} sanitizer",
|
||||
severity=severity,
|
||||
category=self._get_oss_fuzz_category(bug_type),
|
||||
file_path=None,
|
||||
recommendation=self._get_oss_fuzz_recommendation(bug_type, sanitizer),
|
||||
metadata={
|
||||
"bug_type": bug_type,
|
||||
"sanitizer": sanitizer,
|
||||
"stack_trace": stack_trace,
|
||||
"fuzzer": "oss_fuzz"
|
||||
}
|
||||
)
|
||||
|
||||
return finding
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating OSS-Fuzz finding: {e}")
|
||||
return None
|
||||
|
||||
def _create_testcase_finding(self, testcase_file: Path, workspace: Path) -> ModuleFinding:
|
||||
"""Create finding from testcase file"""
|
||||
try:
|
||||
try:
|
||||
rel_path = testcase_file.relative_to(workspace)
|
||||
file_path = str(rel_path)
|
||||
except ValueError:
|
||||
file_path = str(testcase_file)
|
||||
|
||||
finding = self.create_finding(
|
||||
title="OSS-Fuzz Testcase",
|
||||
description=f"OSS-Fuzz testcase found: {testcase_file.name}",
|
||||
severity="info",
|
||||
category="testcase",
|
||||
file_path=file_path,
|
||||
recommendation="Analyze testcase to understand potential issues",
|
||||
metadata={
|
||||
"testcase_file": str(testcase_file),
|
||||
"fuzzer": "oss_fuzz"
|
||||
}
|
||||
)
|
||||
|
||||
return finding
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating testcase finding: {e}")
|
||||
return None
|
||||
|
||||
def _extract_crash_type(self, line: str) -> str:
|
||||
"""Extract crash type from error line"""
|
||||
if "heap-buffer-overflow" in line:
|
||||
return "heap_buffer_overflow"
|
||||
elif "stack-buffer-overflow" in line:
|
||||
return "stack_buffer_overflow"
|
||||
elif "use-after-free" in line:
|
||||
return "use_after_free"
|
||||
elif "double-free" in line:
|
||||
return "double_free"
|
||||
elif "memory leak" in line:
|
||||
return "memory_leak"
|
||||
else:
|
||||
return "unknown_crash"
|
||||
|
||||
def _get_oss_fuzz_severity(self, bug_type: str) -> str:
|
||||
"""Get severity for OSS-Fuzz bug type"""
|
||||
if bug_type in ["heap_buffer_overflow", "stack_buffer_overflow", "use_after_free", "double_free"]:
|
||||
return "critical"
|
||||
elif bug_type == "memory_leak":
|
||||
return "medium"
|
||||
else:
|
||||
return "high"
|
||||
|
||||
def _get_oss_fuzz_category(self, bug_type: str) -> str:
|
||||
"""Get category for OSS-Fuzz bug type"""
|
||||
if "overflow" in bug_type:
|
||||
return "buffer_overflow"
|
||||
elif "free" in bug_type:
|
||||
return "memory_corruption"
|
||||
elif "leak" in bug_type:
|
||||
return "memory_leak"
|
||||
else:
|
||||
return "memory_safety"
|
||||
|
||||
def _get_oss_fuzz_recommendation(self, bug_type: str, sanitizer: str) -> str:
|
||||
"""Get recommendation for OSS-Fuzz finding"""
|
||||
if "overflow" in bug_type:
|
||||
return "Fix buffer overflow by implementing proper bounds checking and using safe string functions."
|
||||
elif "use_after_free" in bug_type:
|
||||
return "Fix use-after-free by ensuring proper object lifetime management and setting pointers to NULL after freeing."
|
||||
elif "double_free" in bug_type:
|
||||
return "Fix double-free by ensuring each allocation has exactly one corresponding free operation."
|
||||
elif "leak" in bug_type:
|
||||
return "Fix memory leak by ensuring all allocated memory is properly freed in all code paths."
|
||||
else:
|
||||
return f"Address the {bug_type} issue detected by OSS-Fuzz with {sanitizer} sanitizer."
|
||||
|
||||
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
|
||||
"""Create analysis summary"""
|
||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||
category_counts = {}
|
||||
sanitizer_counts = {}
|
||||
|
||||
for finding in findings:
|
||||
severity_counts[finding.severity] += 1
|
||||
category_counts[finding.category] = category_counts.get(finding.category, 0) + 1
|
||||
|
||||
sanitizer = finding.metadata.get("sanitizer", "unknown")
|
||||
sanitizer_counts[sanitizer] = sanitizer_counts.get(sanitizer, 0) + 1
|
||||
|
||||
return {
|
||||
"total_findings": len(findings),
|
||||
"severity_counts": severity_counts,
|
||||
"category_counts": category_counts,
|
||||
"sanitizer_counts": sanitizer_counts
|
||||
}
|
||||
Reference in New Issue
Block a user