This document describes the implementation plan for adding code coverage report generation and collection functionality to PandaFuzz. The design allows users to enable coverage collection via a checkbox when creating fuzzing jobs.
-
LibFuzzer Coverage:
- Target binaries must be compiled with Clang and the following flags:
-fprofile-instr-generate- Enables profile instrumentation-fcoverage-mapping- Generates coverage mapping information
- LibFuzzer itself no longer uses
-print_coverageor-dump_coverageflags (deprecated) - Coverage is collected using LLVM's profiling infrastructure
- Target binaries must be compiled with Clang and the following flags:
-
AFL++ Coverage:
- For LLVM-based coverage: compile with
-fprofile-instr-generate -fcoverage-mapping - AFL++ doesn't have built-in coverage report generation
- Coverage is collected post-fuzzing using:
afl-cov-fastfor efficient coverage analysisllvm-covandllvm-profdatafor LLVM-instrumented binaries
- The
afl-cov-fasttool must be installed separately
- For LLVM-based coverage: compile with
-
Required Tools:
llvm-profdata- For merging raw profile datallvm-cov- For generating coverage reportsafl-cov-fast.py(optional) - For AFL++ coverage analysisgcovr(optional) - For alternative coverage report generation
-
During Fuzzing (LibFuzzer):
- Set
LLVM_PROFILE_FILEenvironment variable - Profile data is written continuously during fuzzing
- Set
-
Post-Fuzzing (AFL++):
- Run all test cases through the instrumented binary
- Collect profile data from each execution
- Merge and generate reports
Code coverage provides critical insights into fuzzing effectiveness by showing which parts of the target code have been exercised. This feature will:
- Allow users to enable coverage collection when creating jobs
- Generate coverage reports during fuzzing execution
- Collect and store coverage data in the master node
- Provide API endpoints to retrieve coverage reports
Update the OpenAPI spec (pkg/master/api_v3/openapi.yaml):
JobConfig:
type: object
properties:
# Existing properties...
enable_coverage:
type: boolean
description: Enable code coverage collection during fuzzing
default: false
coverage_format:
type: string
enum: ["json", "html", "lcov", "cobertura"]
description: Format for coverage reports
default: "json"Update pkg/domain/job/types/job.go:
type Job struct {
// Existing fields...
// Coverage fields
EnableCoverage bool `json:"enable_coverage"`
CoverageFormat string `json:"coverage_format,omitempty"`
CoverageReportID string `json:"coverage_report_id,omitempty"`
CoverageStats *CoverageStats `json:"coverage_stats,omitempty"`
}
type CoverageStats struct {
LineCoverage float64 `json:"line_coverage"`
FunctionCoverage float64 `json:"function_coverage"`
BranchCoverage float64 `json:"branch_coverage,omitempty"`
CollectedAt time.Time `json:"collected_at"`
ReportPath string `json:"report_path"`
}In pkg/domain/fuzzer/types/config.go:
type FuzzerConfig struct {
// Existing fields...
// Coverage configuration
EnableCoverage bool `json:"enable_coverage,omitempty"`
CoverageFormat string `json:"coverage_format,omitempty"`
CoverageDir string `json:"coverage_dir,omitempty"`
}
// Add to LibFuzzerOptions
type LibFuzzerOptions struct {
// Existing fields...
// Coverage options (updated for modern LibFuzzer)
UseCounters int `json:"use_counters,omitempty"` // Enable counter tracking
PrintCoveragePCs int `json:"print_pcs,omitempty"` // Print newly covered PCs
// Note: print_coverage and dump_coverage are deprecated in modern LibFuzzer
// Coverage is now collected via Clang Coverage instrumentation
}
// Add to AFLPlusPlusOptions
type AFLPlusPlusOptions struct {
// Existing fields...
// Coverage options
UseAFLCov bool `json:"use_afl_cov,omitempty"` // Use afl-cov-fast for coverage
LLVMMode bool `json:"llvm_mode,omitempty"` // Whether target is LLVM-instrumented
SourceDir string `json:"source_dir,omitempty"` // Source code directory for afl-cov
LLVMCovBinary string `json:"llvm_cov_binary,omitempty"` // Path to llvm-cov binary
LLVMProfData string `json:"llvm_profdata_binary,omitempty"` // Path to llvm-profdata binary
}
// Add to HonggfuzzOptions
type HonggfuzzOptions struct {
// Existing fields...
// Coverage options
Sancov bool `json:"sancov,omitempty"`
SancovDir string `json:"sancov_dir,omitempty"`
CoverageReport bool `json:"coverage_report,omitempty"`
}Update pkg/domain/fuzzer/engines/libfuzzer/engine.go:
func (e *Engine) buildCommandArgs() []string {
args := []string{}
// Existing args...
if e.config.EnableCoverage {
if e.config.LibFuzzerOptions != nil {
opts := e.config.LibFuzzerOptions
// Enable counter tracking for better coverage information
args = append(args, "-use_counters=1")
// Enable PC printing for debugging (optional)
if opts.PrintCoveragePCs == 1 {
args = append(args, "-print_pcs=1")
}
// Note: Modern LibFuzzer uses Clang Coverage for detailed reports
// The deprecated -print_coverage and -dump_coverage flags are no longer used
}
}
return args
}
// Configure environment for Clang coverage collection
func (e *Engine) configureCoverageEnvironment() error {
if !e.config.EnableCoverage {
return nil
}
// Set LLVM profile file for raw coverage data
profilePath := filepath.Join(e.outputDir, "coverage", "%p-%m.profraw")
os.Setenv("LLVM_PROFILE_FILE", profilePath)
// Ensure coverage directory exists
coverageDir := filepath.Join(e.outputDir, "coverage")
return os.MkdirAll(coverageDir, 0755)
}
// Add coverage collection method using LLVM tools
func (e *Engine) collectCoverageData(ctx context.Context) (*types.CoverageData, error) {
if !e.config.EnableCoverage {
return nil, nil
}
coverageDir := filepath.Join(e.outputDir, "coverage")
// First, merge all profraw files into a single profdata file
profDataFile := filepath.Join(coverageDir, "merged.profdata")
profrawPattern := filepath.Join(coverageDir, "*.profraw")
// Find all profraw files
profrawFiles, err := filepath.Glob(profrawPattern)
if err != nil || len(profrawFiles) == 0 {
return nil, fmt.Errorf("no profraw files found")
}
// Merge profraw files using llvm-profdata
mergeArgs := append([]string{"merge", "-sparse", "-o", profDataFile}, profrawFiles...)
mergeCmd := exec.CommandContext(ctx, "llvm-profdata", mergeArgs...)
if err := mergeCmd.Run(); err != nil {
return nil, fmt.Errorf("failed to merge profile data: %w", err)
}
// Generate coverage report in requested format
data := &types.CoverageData{
Format: e.config.CoverageFormat,
CollectedAt: time.Now(),
}
// Use llvm-cov to generate coverage report
reportFile := filepath.Join(coverageDir, fmt.Sprintf("coverage.%s", e.config.CoverageFormat))
covArgs := []string{"export", e.target, "-instr-profile=" + profDataFile}
switch e.config.CoverageFormat {
case "json":
covArgs = append(covArgs, "-format=json")
case "lcov":
covArgs = append(covArgs, "-format=lcov")
case "html":
// For HTML, we use show instead of export
covArgs = []string{"show", e.target, "-instr-profile=" + profDataFile,
"-format=html", "-output-dir=" + coverageDir}
default:
return nil, fmt.Errorf("unsupported coverage format: %s", e.config.CoverageFormat)
}
if e.config.CoverageFormat != "html" {
covArgs = append(covArgs, "-o", reportFile)
}
covCmd := exec.CommandContext(ctx, "llvm-cov", covArgs...)
if err := covCmd.Run(); err != nil {
return nil, fmt.Errorf("failed to generate coverage report: %w", err)
}
// Read and parse the coverage data
if e.config.CoverageFormat != "html" {
content, err := os.ReadFile(reportFile)
if err != nil {
return nil, fmt.Errorf("failed to read coverage report: %w", err)
}
data.Data = content
}
return data, nil
}Update pkg/domain/fuzzer/engines/aflplusplus/engine.go:
func (e *Engine) buildCommandArgs() []string {
args := []string{}
// Existing args...
// Note: AFL++ doesn't have built-in coverage report generation
// Coverage is collected post-fuzzing using external tools like afl-cov
return args
}
// Configure environment for coverage collection
func (e *Engine) configureCoverageEnvironment() error {
if !e.config.EnableCoverage {
return nil
}
// For LLVM-based coverage, set profile environment variable
if e.isLLVMMode() {
profilePath := filepath.Join(e.outputDir, "coverage", "%p-%m.profraw")
os.Setenv("LLVM_PROFILE_FILE", profilePath)
// Ensure coverage directory exists
coverageDir := filepath.Join(e.outputDir, "coverage")
return os.MkdirAll(coverageDir, 0755)
}
return nil
}
// Post-processing for coverage using afl-cov or llvm-cov
func (e *Engine) collectCoverageData(ctx context.Context) (*types.CoverageData, error) {
if !e.config.EnableCoverage {
return nil, nil
}
opts := e.config.AFLPlusPlusOptions
// Check if we should use afl-cov-fast for coverage analysis
if e.shouldUseAFLCov() {
return e.collectAFLCovData(ctx)
}
// For LLVM-instrumented binaries, use llvm-cov
if e.isLLVMMode() {
return e.collectLLVMCoverageData(ctx)
}
return nil, fmt.Errorf("coverage collection not supported for this AFL++ configuration")
}
// Collect coverage using afl-cov-fast
func (e *Engine) collectAFLCovData(ctx context.Context) (*types.CoverageData, error) {
coverageCmd := exec.CommandContext(ctx, "afl-cov-fast.py",
"-m", "llvm",
"--code-dir", e.sourceDir,
"--afl-fuzzing-dir", e.outputDir,
"--coverage-cmd", fmt.Sprintf("%s @@", e.target),
"--binary-path", e.target,
"-j", "8")
output, err := coverageCmd.Output()
if err != nil {
return nil, fmt.Errorf("afl-cov-fast failed: %w", err)
}
data := &types.CoverageData{
Format: "afl-cov",
CollectedAt: time.Now(),
Data: output,
}
return data, nil
}
// Collect coverage using LLVM tools for LLVM-instrumented binaries
func (e *Engine) collectLLVMCoverageData(ctx context.Context) (*types.CoverageData, error) {
opts := e.config.AFLPlusPlusOptions
if opts.LLVMCovBinary == "" {
opts.LLVMCovBinary = "llvm-cov"
}
if opts.LLVMProfData == "" {
opts.LLVMProfData = "llvm-profdata"
}
coverageDir := filepath.Join(e.outputDir, "coverage")
// First, run the target binary on all test cases to generate profraw files
testCasesDir := filepath.Join(e.outputDir, "default", "queue")
testCases, err := filepath.Glob(filepath.Join(testCasesDir, "id:*"))
if err != nil {
return nil, fmt.Errorf("failed to find test cases: %w", err)
}
// Process test cases to generate coverage
for _, testCase := range testCases {
cmd := exec.CommandContext(ctx, e.target, testCase)
cmd.Env = append(os.Environ(),
fmt.Sprintf("LLVM_PROFILE_FILE=%s/testcase-%s.profraw", coverageDir, filepath.Base(testCase)))
cmd.Run() // Ignore errors from crashes
}
// Merge all profraw files
profDataFile := filepath.Join(coverageDir, "merged.profdata")
profrawPattern := filepath.Join(coverageDir, "*.profraw")
profrawFiles, err := filepath.Glob(profrawPattern)
if err != nil || len(profrawFiles) == 0 {
return nil, fmt.Errorf("no profraw files found")
}
mergeArgs := append([]string{"merge", "-sparse", "-o", profDataFile}, profrawFiles...)
mergeCmd := exec.CommandContext(ctx, opts.LLVMProfData, mergeArgs...)
if err := mergeCmd.Run(); err != nil {
return nil, fmt.Errorf("failed to merge profile data: %w", err)
}
// Generate coverage report in requested format
reportFile := filepath.Join(coverageDir, fmt.Sprintf("coverage.%s", e.config.CoverageFormat))
covArgs := []string{"export", e.target, "-instr-profile=" + profDataFile}
switch e.config.CoverageFormat {
case "json":
covArgs = append(covArgs, "-format=json")
case "lcov":
covArgs = append(covArgs, "-format=lcov")
case "html":
covArgs = []string{"show", e.target, "-instr-profile=" + profDataFile,
"-format=html", "-output-dir=" + coverageDir}
default:
return nil, fmt.Errorf("unsupported coverage format: %s", e.config.CoverageFormat)
}
if e.config.CoverageFormat != "html" {
covArgs = append(covArgs, "-o", reportFile)
}
covCmd := exec.CommandContext(ctx, opts.LLVMCovBinary, covArgs...)
if err := covCmd.Run(); err != nil {
return nil, fmt.Errorf("failed to generate coverage report: %w", err)
}
// Read and return the coverage data
data := &types.CoverageData{
Format: e.config.CoverageFormat,
CollectedAt: time.Now(),
}
if e.config.CoverageFormat != "html" {
content, err := os.ReadFile(reportFile)
if err != nil {
return nil, fmt.Errorf("failed to read coverage report: %w", err)
}
data.Data = content
}
return data, nil
}Update pkg/domain/bot/executor/fuzzer_executor.go:
type FuzzingResult struct {
// Existing fields...
// Coverage data
CoverageCollected bool `json:"coverage_collected"`
CoverageStats *CoverageStats `json:"coverage_stats,omitempty"`
CoverageReportID string `json:"coverage_report_id,omitempty"`
}
func (fe *FuzzerExecutor) runFuzzingJob(ctx context.Context, execCtx *ExecutionContext) *ExecutionResult {
job := execCtx.Job
// Configure fuzzer with coverage if enabled
fuzzerConfig := fe.buildFuzzerConfig(job)
if job.EnableCoverage {
fuzzerConfig.EnableCoverage = true
fuzzerConfig.CoverageFormat = job.CoverageFormat
fuzzerConfig.CoverageDir = filepath.Join(fe.config.WorkDir, job.ID, "coverage")
}
// Create and run fuzzer
fuzzer, err := fe.fuzzerFactory.CreateFuzzer(job.FuzzerType, job.TargetBinary, job.TargetArgs)
if err != nil {
return &ExecutionResult{Error: err}
}
if err := fuzzer.Configure(fuzzerConfig); err != nil {
return &ExecutionResult{Error: err}
}
// Store active fuzzer
fe.mu.Lock()
fe.activeFuzzers[job.ID] = fuzzer
fe.mu.Unlock()
defer func() {
fe.mu.Lock()
delete(fe.activeFuzzers, job.ID)
fe.mu.Unlock()
}()
// Start fuzzing
if err := fuzzer.Start(ctx); err != nil {
return &ExecutionResult{Error: err}
}
// Monitor fuzzing progress
result := fe.monitorFuzzing(ctx, fuzzer, job)
// Collect coverage if enabled
if job.EnableCoverage {
if err := fe.collectAndUploadCoverage(ctx, fuzzer, job, result); err != nil {
fe.log.WithError(err).Error("Failed to collect coverage")
}
}
return result
}
func (fe *FuzzerExecutor) collectAndUploadCoverage(ctx context.Context, fuzzer types.Fuzzer, job *jobtypes.Job, result *ExecutionResult) error {
// Ensure fuzzer has stopped before collecting coverage
fuzzer.Stop()
// Get coverage data from fuzzer using type assertion based on fuzzer type
var coverageData *types.CoverageData
var err error
switch job.FuzzerType {
case "libfuzzer":
if libfuzzerEngine, ok := fuzzer.(*libfuzzer.Engine); ok {
coverageData, err = libfuzzerEngine.collectCoverageData(ctx)
} else {
return fmt.Errorf("invalid fuzzer type assertion for libfuzzer")
}
case "afl++":
if aflEngine, ok := fuzzer.(*aflplusplus.Engine); ok {
coverageData, err = aflEngine.collectCoverageData(ctx)
} else {
return fmt.Errorf("invalid fuzzer type assertion for afl++")
}
case "honggfuzz":
if honggfuzzEngine, ok := fuzzer.(*honggfuzz.Engine); ok {
coverageData, err = honggfuzzEngine.collectCoverageData(ctx)
} else {
return fmt.Errorf("invalid fuzzer type assertion for honggfuzz")
}
default:
return fmt.Errorf("unsupported fuzzer type for coverage: %s", job.FuzzerType)
}
if err != nil {
return fmt.Errorf("failed to collect coverage: %w", err)
}
if coverageData == nil {
return nil
}
// Generate unique coverage report ID
reportID := fmt.Sprintf("coverage_%s_%d", job.ID, time.Now().Unix())
// Upload to storage
storagePath := fmt.Sprintf("coverage/%s/%s", job.ID, reportID)
if err := fe.uploadCoverageToStorage(ctx, storagePath, coverageData); err != nil {
return fmt.Errorf("failed to upload coverage: %w", err)
}
// Parse coverage stats from the data
stats, err := fe.parseCoverageStats(coverageData)
if err != nil {
fe.log.WithError(err).Warn("Failed to parse coverage stats")
}
// Update result
result.CoverageCollected = true
result.CoverageReportID = reportID
result.CoverageStats = &CoverageStats{
LineCoverage: stats.LineCoverage,
FunctionCoverage: stats.FunctionCoverage,
BranchCoverage: stats.BranchCoverage,
CollectedAt: coverageData.CollectedAt,
ReportPath: storagePath,
}
return nil
}Create new coverage storage interface in pkg/domain/coverage/repository/interface.go:
package repository
import (
"context"
"io"
"time"
)
type CoverageRepository interface {
// Store coverage report
Store(ctx context.Context, jobID, reportID string, data io.Reader) error
// Retrieve coverage report
Get(ctx context.Context, jobID, reportID string) (io.ReadCloser, error)
// List coverage reports for a job
List(ctx context.Context, jobID string) ([]*CoverageReport, error)
// Delete coverage report
Delete(ctx context.Context, jobID, reportID string) error
// Get metadata
GetMetadata(ctx context.Context, jobID, reportID string) (*CoverageMetadata, error)
}
type CoverageReport struct {
ID string `json:"id"`
JobID string `json:"job_id"`
Format string `json:"format"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
StoragePath string `json:"storage_path"`
}
type CoverageMetadata struct {
LineCoverage float64 `json:"line_coverage"`
FunctionCoverage float64 `json:"function_coverage"`
BranchCoverage float64 `json:"branch_coverage,omitempty"`
TotalLines int `json:"total_lines"`
CoveredLines int `json:"covered_lines"`
TotalFunctions int `json:"total_functions"`
CoveredFunctions int `json:"covered_functions"`
CollectedAt time.Time `json:"collected_at"`
}Add coverage endpoints to pkg/master/api_v3/openapi.yaml:
/jobs/{jobId}/coverage:
get:
tags: [jobs]
summary: List coverage reports for a job
operationId: listJobCoverage
parameters:
- $ref: '#/components/parameters/jobIdParam'
responses:
'200':
description: List of coverage reports
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CoverageReport'
'404':
$ref: '#/components/responses/NotFound'
/jobs/{jobId}/coverage/{reportId}:
get:
tags: [jobs]
summary: Download coverage report
operationId: getJobCoverageReport
parameters:
- $ref: '#/components/parameters/jobIdParam'
- name: reportId
in: path
required: true
schema:
type: string
responses:
'200':
description: Coverage report data
content:
application/json:
schema:
type: object
text/html:
schema:
type: string
text/plain:
schema:
type: string
'404':
$ref: '#/components/responses/NotFound'
/jobs/{jobId}/coverage/{reportId}/metadata:
get:
tags: [jobs]
summary: Get coverage report metadata
operationId: getJobCoverageMetadata
parameters:
- $ref: '#/components/parameters/jobIdParam'
- name: reportId
in: path
required: true
schema:
type: string
responses:
'200':
description: Coverage metadata
content:
application/json:
schema:
$ref: '#/components/schemas/CoverageMetadata'
'404':
$ref: '#/components/responses/NotFound'Update the job creation form to include coverage options:
// web/src/components/JobCreationForm.tsx
interface JobFormData {
// Existing fields...
enableCoverage: boolean;
coverageFormat: 'json' | 'html' | 'lcov' | 'cobertura';
}
const JobCreationForm: React.FC = () => {
const [formData, setFormData] = useState<JobFormData>({
// Existing defaults...
enableCoverage: false,
coverageFormat: 'json',
});
return (
<Form onSubmit={handleSubmit}>
{/* Existing form fields... */}
<Form.Group>
<Form.Check
type="checkbox"
id="enable-coverage"
label="Enable code coverage collection"
checked={formData.enableCoverage}
onChange={(e) => setFormData({
...formData,
enableCoverage: e.target.checked
})}
/>
<Form.Text className="text-muted">
Collect code coverage data during fuzzing execution
</Form.Text>
</Form.Group>
{formData.enableCoverage && (
<Form.Group>
<Form.Label>Coverage Report Format</Form.Label>
<Form.Select
value={formData.coverageFormat}
onChange={(e) => setFormData({
...formData,
coverageFormat: e.target.value as any
})}
>
<option value="json">JSON</option>
<option value="html">HTML</option>
<option value="lcov">LCOV</option>
<option value="cobertura">Cobertura XML</option>
</Form.Select>
</Form.Group>
)}
</Form>
);
};Add coverage report viewing:
// web/src/components/JobCoverageView.tsx
const JobCoverageView: React.FC<{ jobId: string }> = ({ jobId }) => {
const [reports, setReports] = useState<CoverageReport[]>([]);
const [selectedReport, setSelectedReport] = useState<string | null>(null);
const [metadata, setMetadata] = useState<CoverageMetadata | null>(null);
useEffect(() => {
fetchCoverageReports(jobId).then(setReports);
}, [jobId]);
const handleViewReport = async (reportId: string) => {
const meta = await fetchCoverageMetadata(jobId, reportId);
setMetadata(meta);
setSelectedReport(reportId);
};
return (
<div>
<h3>Coverage Reports</h3>
{metadata && (
<Card className="mb-3">
<Card.Body>
<h5>Coverage Summary</h5>
<ProgressBar>
<ProgressBar
variant="success"
now={metadata.lineCoverage}
label={`Lines: ${metadata.lineCoverage.toFixed(1)}%`}
/>
</ProgressBar>
<div className="mt-2">
<Badge bg="info">
{metadata.coveredLines}/{metadata.totalLines} lines
</Badge>
<Badge bg="info" className="ms-2">
{metadata.coveredFunctions}/{metadata.totalFunctions} functions
</Badge>
</div>
</Card.Body>
</Card>
)}
<Table striped bordered hover>
<thead>
<tr>
<th>Report ID</th>
<th>Format</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{reports.map(report => (
<tr key={report.id}>
<td>{report.id}</td>
<td>{report.format}</td>
<td>{new Date(report.createdAt).toLocaleString()}</td>
<td>
<Button
size="sm"
onClick={() => handleViewReport(report.id)}
>
View
</Button>
<Button
size="sm"
variant="secondary"
className="ms-2"
onClick={() => downloadReport(jobId, report.id)}
>
Download
</Button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
);
};Add coverage tracking tables:
-- migrations/XXX_add_coverage_support.sql
-- Coverage reports table
CREATE TABLE coverage_reports (
id VARCHAR(255) PRIMARY KEY,
job_id VARCHAR(255) NOT NULL,
format VARCHAR(50) NOT NULL,
storage_path TEXT NOT NULL,
size BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE,
INDEX idx_coverage_job_id (job_id),
INDEX idx_coverage_created_at (created_at)
);
-- Coverage metadata table
CREATE TABLE coverage_metadata (
report_id VARCHAR(255) PRIMARY KEY,
line_coverage FLOAT,
function_coverage FLOAT,
branch_coverage FLOAT,
total_lines INT,
covered_lines INT,
total_functions INT,
covered_functions INT,
collected_at TIMESTAMP,
FOREIGN KEY (report_id) REFERENCES coverage_reports(id) ON DELETE CASCADE
);
-- Update jobs table
ALTER TABLE jobs ADD COLUMN enable_coverage BOOLEAN DEFAULT FALSE;
ALTER TABLE jobs ADD COLUMN coverage_format VARCHAR(50);
ALTER TABLE jobs ADD COLUMN coverage_report_id VARCHAR(255);-
API Updates
- Update OpenAPI spec with coverage fields
- Add coverage endpoints
- Update job creation endpoint to accept coverage options
-
Domain Model Updates
- Add coverage fields to Job struct
- Update FuzzerConfig with coverage options
- Add coverage-specific options to each fuzzer type
-
Fuzzer Engine Updates
- Implement coverage collection in LibFuzzer engine
- Implement coverage collection in AFL++ engine
- Implement coverage collection in Honggfuzz engine
- Add coverage data parsing methods
-
Bot Executor Updates
- Add coverage configuration to fuzzer setup
- Implement coverage collection after fuzzing
- Upload coverage reports to storage
-
Storage Layer
- Create CoverageRepository interface
- Implement file system storage backend
- Implement S3 storage backend
- Add coverage metadata storage
-
Database Updates
- Create migration for coverage tables
- Update job repository to handle coverage fields
- Implement coverage repository
-
API Implementation
- Implement coverage listing endpoint
- Implement coverage download endpoint
- Implement coverage metadata endpoint
-
Web UI Updates
- Add coverage checkbox to job creation form
- Add coverage format selection
- Create coverage report viewing component
- Add coverage summary visualization
-
Testing
- Unit tests for coverage collection
- Integration tests for coverage flow
- E2E tests for coverage UI
-
Documentation
- Update API documentation
- Add coverage usage guide
- Document coverage formats
The JSON format follows the LLVM coverage JSON schema, providing detailed line-by-line and function coverage information.
LCOV format is widely supported by coverage visualization tools and CI systems.
HTML reports provide a visual representation of code coverage with syntax highlighting.
Cobertura format is commonly used in Java ecosystems and supported by many CI tools.
- Input Validation: Validate coverage format selection to prevent injection
- File Access: Restrict coverage report access to authorized users
- Storage Limits: Implement size limits for coverage reports
- Cleanup: Implement retention policies for old coverage reports
- Async Processing: Process coverage reports asynchronously to avoid blocking fuzzing
- Compression: Compress large coverage reports before storage
- Caching: Cache coverage metadata for quick access
- Batch Operations: Support batch coverage report retrieval
- Differential Coverage: Show coverage changes between fuzzing runs
- Coverage Trends: Track coverage progress over time
- Coverage Targets: Set coverage goals and alerts
- Coverage Merging: Combine coverage from multiple fuzzing jobs
- Real-time Updates: Stream coverage updates during fuzzing