mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-20 06:20:58 +03:00
Initial commit - 611 cybersecurity skills across all subdomains
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
---
|
||||
name: implementing-fuzz-testing-in-cicd-with-aflplusplus
|
||||
description: Integrate AFL++ coverage-guided fuzz testing into CI/CD pipelines to discover memory corruption, input handling, and logic vulnerabilities in C/C++ and compiled applications.
|
||||
domain: cybersecurity
|
||||
subdomain: devsecops
|
||||
tags: [aflplusplus, fuzz-testing, cicd, coverage-guided-fuzzing, security-testing, vulnerability-discovery, afl]
|
||||
version: "1.0"
|
||||
author: mahipal
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Implementing Fuzz Testing in CI/CD with AFL++
|
||||
|
||||
## Overview
|
||||
|
||||
AFL++ (American Fuzzy Lop Plus Plus) is a community-maintained fork of AFL that provides state-of-the-art coverage-guided fuzz testing for discovering vulnerabilities in compiled applications. AFL++ uses genetic algorithms to mutate inputs, tracking code coverage to find new execution paths that trigger crashes, hangs, and undefined behavior. In CI/CD environments, AFL++ can be integrated to continuously test parsers, protocol handlers, file format processors, and any code that handles untrusted input. AFL++ supports persistent mode for high-speed fuzzing (up to 100,000+ executions per second), custom mutators, QEMU mode for binary-only fuzzing, and CmpLog/RedQueen for automatic dictionary extraction.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Linux-based CI runners (AFL++ does not support Windows natively)
|
||||
- GCC or Clang compiler toolchain
|
||||
- AFL++ installed (`apt install aflplusplus` or built from source)
|
||||
- Target application with harness functions isolating input processing
|
||||
- Seed corpus of valid input samples
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Coverage-Guided Fuzzing
|
||||
|
||||
AFL++ instruments the target binary at compile time (or via QEMU/Frida for binary-only targets) to track which code paths each input exercises. When a mutated input triggers a new code path, it is saved to the corpus for further mutation. This feedback loop enables AFL++ to systematically explore program state space.
|
||||
|
||||
### Instrumentation Modes
|
||||
|
||||
| Mode | Use Case | Performance |
|
||||
|------|----------|-------------|
|
||||
| `afl-clang-fast` (LTO) | Source available, best performance | Highest |
|
||||
| `afl-clang-fast` | Source available, standard | High |
|
||||
| `afl-gcc-fast` | GCC-based projects | High |
|
||||
| `QEMU mode` | Binary-only, no source | Medium |
|
||||
| `Frida mode` | Binary-only, cross-platform | Medium |
|
||||
| `Unicorn mode` | Firmware, embedded | Low |
|
||||
|
||||
### Persistent Mode
|
||||
|
||||
Persistent mode avoids fork overhead by fuzzing within a loop:
|
||||
|
||||
```c
|
||||
#include <unistd.h>
|
||||
|
||||
__AFL_FUZZ_INIT();
|
||||
|
||||
int main() {
|
||||
__AFL_INIT();
|
||||
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
|
||||
|
||||
while (__AFL_LOOP(10000)) {
|
||||
int len = __AFL_FUZZ_TESTCASE_LEN;
|
||||
// Process buf[0..len-1]
|
||||
parse_input(buf, len);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1 --- Build the Fuzzing Harness
|
||||
|
||||
Create a harness that feeds AFL++ input to the target function:
|
||||
|
||||
```c
|
||||
// fuzz_harness.c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "target_parser.h"
|
||||
|
||||
__AFL_FUZZ_INIT();
|
||||
|
||||
int main() {
|
||||
__AFL_INIT();
|
||||
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
|
||||
|
||||
while (__AFL_LOOP(10000)) {
|
||||
int len = __AFL_FUZZ_TESTCASE_LEN;
|
||||
if (len < 4) continue;
|
||||
|
||||
// Reset state between iterations
|
||||
parser_context_t ctx;
|
||||
parser_init(&ctx);
|
||||
parser_process(&ctx, buf, len);
|
||||
parser_cleanup(&ctx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 --- Compile with AFL++ Instrumentation
|
||||
|
||||
```bash
|
||||
# Standard instrumentation
|
||||
export CC=afl-clang-fast
|
||||
export CXX=afl-clang-fast++
|
||||
|
||||
# Enable AddressSanitizer for better crash detection
|
||||
export AFL_USE_ASAN=1
|
||||
|
||||
# Build the target with instrumentation
|
||||
$CC -o fuzz_harness fuzz_harness.c -ltarget_parser -fsanitize=address
|
||||
|
||||
# Build a CmpLog binary for better coverage
|
||||
$CC -o fuzz_harness_cmplog fuzz_harness.c -ltarget_parser \
|
||||
-fsanitize=address -DCMPLOG
|
||||
```
|
||||
|
||||
### Step 3 --- Prepare Seed Corpus
|
||||
|
||||
```bash
|
||||
mkdir -p corpus/
|
||||
# Add valid input samples
|
||||
cp test_inputs/* corpus/
|
||||
# Minimize the corpus
|
||||
afl-cmin -i corpus/ -o corpus_min/ -- ./fuzz_harness @@
|
||||
# Further minimize individual inputs
|
||||
mkdir -p corpus_tmin/
|
||||
for f in corpus_min/*; do
|
||||
afl-tmin -i "$f" -o "corpus_tmin/$(basename $f)" -- ./fuzz_harness @@
|
||||
done
|
||||
```
|
||||
|
||||
### Step 4 --- Configure CI/CD Integration
|
||||
|
||||
**GitHub Actions:**
|
||||
|
||||
```yaml
|
||||
name: Fuzz Testing
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Nightly fuzzing
|
||||
|
||||
jobs:
|
||||
fuzz:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install AFL++
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y aflplusplus
|
||||
|
||||
- name: Restore corpus cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: corpus/
|
||||
key: fuzz-corpus-${{ github.sha }}
|
||||
restore-keys: fuzz-corpus-
|
||||
|
||||
- name: Build fuzzing harness
|
||||
run: |
|
||||
export CC=afl-clang-fast
|
||||
export AFL_USE_ASAN=1
|
||||
make fuzz_harness
|
||||
|
||||
- name: Run AFL++ fuzzing (CI mode)
|
||||
env:
|
||||
AFL_CMPLOG_ONLY_NEW: 1
|
||||
AFL_FAST_CAL: 1
|
||||
AFL_NO_STARTUP_CALIBRATION: 1
|
||||
run: |
|
||||
mkdir -p findings/
|
||||
timeout 7200 afl-fuzz \
|
||||
-S ci_fuzzer \
|
||||
-i corpus/ \
|
||||
-o findings/ \
|
||||
-t 5000 \
|
||||
-- ./fuzz_harness @@ || true
|
||||
|
||||
- name: Check for crashes
|
||||
run: |
|
||||
CRASHES=$(find findings/ -path "*/crashes/*" -not -name "README.txt" | wc -l)
|
||||
echo "Found $CRASHES unique crashes"
|
||||
if [ "$CRASHES" -gt 0 ]; then
|
||||
echo "::error::AFL++ found $CRASHES crashes"
|
||||
for crash in findings/*/crashes/*; do
|
||||
[ -f "$crash" ] && echo "Crash: $crash ($(wc -c < $crash) bytes)"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Update corpus cache
|
||||
if: always()
|
||||
run: |
|
||||
afl-cmin -i findings/ci_fuzzer/queue/ -o corpus/ -- ./fuzz_harness @@
|
||||
```
|
||||
|
||||
### Step 5 --- Parallel Fuzzing for Nightly Runs
|
||||
|
||||
```bash
|
||||
# Launch multiple secondary instances for better coverage
|
||||
for i in $(seq 1 $(nproc)); do
|
||||
afl-fuzz -S fuzzer_$i \
|
||||
-i corpus/ \
|
||||
-o findings/ \
|
||||
-- ./fuzz_harness @@ &
|
||||
done
|
||||
|
||||
# Wait for all fuzzers
|
||||
wait
|
||||
|
||||
# Merge and minimize corpus
|
||||
afl-cmin -i findings/*/queue/ -o corpus_merged/ -- ./fuzz_harness @@
|
||||
```
|
||||
|
||||
### Step 6 --- Crash Triage
|
||||
|
||||
```bash
|
||||
# Reproduce and categorize crashes
|
||||
for crash in findings/*/crashes/*; do
|
||||
echo "=== Testing: $crash ==="
|
||||
timeout 5 ./fuzz_harness_asan "$crash" 2>&1 | head -20
|
||||
echo "---"
|
||||
done
|
||||
|
||||
# Deduplicate crashes by stack trace
|
||||
afl-collect findings/ crashes_deduped/ -- ./fuzz_harness @@
|
||||
```
|
||||
|
||||
## CI/CD Best Practices for AFL++
|
||||
|
||||
| Setting | CI Short Run | Nightly Long Run |
|
||||
|---------|-------------|-----------------|
|
||||
| Duration | 30-60 min | 4-24 hours |
|
||||
| Mode | `-S` (secondary only) | `-S` (no `-M` for CI) |
|
||||
| `AFL_CMPLOG_ONLY_NEW` | 1 | 1 |
|
||||
| `AFL_FAST_CAL` | 1 | 0 |
|
||||
| `AFL_NO_STARTUP_CALIBRATION` | 1 | 0 |
|
||||
| Corpus caching | Required | Required |
|
||||
| Parallel instances | 1-2 | nproc |
|
||||
|
||||
## Monitoring Fuzzing Campaigns
|
||||
|
||||
```bash
|
||||
# View fuzzing statistics
|
||||
afl-whatsup findings/
|
||||
|
||||
# Key metrics to track:
|
||||
# - Total paths found (code coverage indicator)
|
||||
# - Unique crashes / unique hangs
|
||||
# - Stability percentage (should be >90%)
|
||||
# - Exec speed (execs/sec)
|
||||
# - Cycles done (full corpus cycles completed)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [AFL++ Documentation](https://aflplus.plus/docs/)
|
||||
- [AFL++ GitHub Repository](https://github.com/AFLplusplus/AFLplusplus)
|
||||
- [AFL++ Fuzzing in Depth Guide](https://aflplus.plus/docs/fuzzing_in_depth/)
|
||||
- [Google Testing Handbook - AFL++](https://appsec.guide/docs/fuzzing/c-cpp/aflpp/)
|
||||
- [OWASP Fuzzing Guide](https://owasp.org/www-community/Fuzzing)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Fuzz Testing Implementation Template
|
||||
|
||||
## Target Application
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Application Name | |
|
||||
| Target Function | |
|
||||
| Language | [ ] C [ ] C++ [ ] Other |
|
||||
| Input Type | [ ] File [ ] Network [ ] Stdin |
|
||||
|
||||
## Fuzzing Configuration
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Instrumentation | [ ] afl-clang-fast [ ] afl-gcc-fast [ ] QEMU |
|
||||
| Sanitizer | [ ] ASan [ ] UBSan [ ] MSan [ ] TSan |
|
||||
| Mode | [ ] Persistent [ ] Fork |
|
||||
| CmpLog | [ ] Enabled [ ] Disabled |
|
||||
| Timeout per exec | ms |
|
||||
| CI run duration | minutes |
|
||||
| Nightly duration | hours |
|
||||
|
||||
## Corpus Management
|
||||
|
||||
| Item | Location |
|
||||
|------|----------|
|
||||
| Seed corpus | |
|
||||
| Minimized corpus | |
|
||||
| CI cache key | |
|
||||
|
||||
## Crash Tracking
|
||||
|
||||
| Crash ID | CWE | Severity | Crash File | Stack Trace Summary | Fix Status |
|
||||
|----------|-----|----------|------------|---------------------|------------|
|
||||
| | | | | | |
|
||||
@@ -0,0 +1,36 @@
|
||||
# Standards Reference for Fuzz Testing
|
||||
|
||||
## NIST SP 800-53 Rev 5 Controls
|
||||
|
||||
| Control | Description | Fuzzing Alignment |
|
||||
|---------|-------------|-------------------|
|
||||
| SA-11(5) | Penetration Testing | Fuzz testing discovers vulnerabilities through automated input mutation |
|
||||
| SA-11(8) | Dynamic Code Analysis | AFL++ provides runtime analysis with instrumented binaries |
|
||||
| SI-10 | Information Input Validation | Fuzzing validates input handling robustness |
|
||||
| SI-17 | Fail-Safe Procedures | Crash detection ensures failures are handled safely |
|
||||
|
||||
## OWASP Testing Guide v4.2
|
||||
|
||||
- **WSTG-INPV-07**: Testing for Input Validation --- AFL++ systematically tests boundary conditions
|
||||
- **WSTG-ERRH-01**: Error Handling --- Crash analysis reveals improper error handling
|
||||
|
||||
## CWE Categories Commonly Found by Fuzzing
|
||||
|
||||
| CWE | Name | AFL++ Detection Method |
|
||||
|-----|------|----------------------|
|
||||
| CWE-120 | Buffer Overflow | ASan crash on out-of-bounds write |
|
||||
| CWE-125 | Out-of-Bounds Read | ASan crash on invalid read |
|
||||
| CWE-416 | Use After Free | ASan detects freed memory access |
|
||||
| CWE-476 | NULL Pointer Dereference | SIGSEGV on null deref |
|
||||
| CWE-190 | Integer Overflow | UBSan detects arithmetic overflow |
|
||||
| CWE-787 | Out-of-Bounds Write | ASan detects heap/stack buffer overflow |
|
||||
| CWE-400 | Uncontrolled Resource Consumption | Timeout detection for hangs |
|
||||
|
||||
## Fuzzing Maturity Levels
|
||||
|
||||
| Level | Description | CI Integration |
|
||||
|-------|-------------|----------------|
|
||||
| 1 Basic | Manual ad-hoc fuzzing | None |
|
||||
| 2 Structured | Harness-based with corpus management | PR-triggered short runs |
|
||||
| 3 Continuous | Nightly campaigns with crash tracking | Nightly + corpus caching |
|
||||
| 4 Optimized | Multi-tool (AFL++, libFuzzer), crash dedup, coverage tracking | Full CI/CD integration with gating |
|
||||
@@ -0,0 +1,64 @@
|
||||
# AFL++ Fuzz Testing Workflows
|
||||
|
||||
## Workflow 1: CI Pipeline Integration
|
||||
|
||||
```
|
||||
Code pushed to branch
|
||||
|
|
||||
Fuzzing harness compiled with afl-clang-fast + ASan
|
||||
|
|
||||
Corpus restored from CI cache
|
||||
|
|
||||
AFL++ runs in secondary mode for fixed duration
|
||||
|
|
||||
[No crashes] --> Corpus updated in cache, pipeline passes
|
||||
[Crashes found] --> Pipeline fails, crash artifacts uploaded
|
||||
|
|
||||
Developer triages crashes
|
||||
|
|
||||
Fix applied, re-run confirms no regression
|
||||
```
|
||||
|
||||
## Workflow 2: Nightly Fuzzing Campaign
|
||||
|
||||
```
|
||||
Scheduled nightly trigger (cron)
|
||||
|
|
||||
Build instrumented binary + CmpLog binary
|
||||
|
|
||||
Restore merged corpus from last run
|
||||
|
|
||||
Launch parallel AFL++ instances (nproc count)
|
||||
|
|
||||
Run for 4-8 hours
|
||||
|
|
||||
Collect results from all instances
|
||||
|
|
||||
afl-cmin merges and minimizes corpus
|
||||
|
|
||||
Deduplicate crashes by stack hash
|
||||
|
|
||||
New crashes create Jira/GitHub issues automatically
|
||||
|
|
||||
Updated corpus cached for next run
|
||||
```
|
||||
|
||||
## Workflow 3: Crash Triage and Fix
|
||||
|
||||
```
|
||||
Crash file identified in findings/
|
||||
|
|
||||
Reproduce crash with ASan-instrumented binary
|
||||
|
|
||||
Capture ASan stack trace and error type
|
||||
|
|
||||
Minimize crash input with afl-tmin
|
||||
|
|
||||
Identify root cause from stack trace
|
||||
|
|
||||
Develop fix and add crash input as regression test
|
||||
|
|
||||
Verify fix by re-running AFL++ with crash input
|
||||
|
|
||||
Update corpus to include edge case inputs
|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AFL++ Fuzzing Results Analyzer
|
||||
|
||||
Parses AFL++ output directories and generates reports on
|
||||
crash findings, corpus growth, and coverage statistics.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def parse_fuzzer_stats(stats_file: str) -> dict:
|
||||
stats = {}
|
||||
if not os.path.exists(stats_file):
|
||||
return stats
|
||||
with open(stats_file) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
stats[key.strip()] = value.strip()
|
||||
return stats
|
||||
|
||||
|
||||
def count_files_in_dir(directory: str) -> int:
|
||||
if not os.path.isdir(directory):
|
||||
return 0
|
||||
return len([f for f in os.listdir(directory) if f != "README.txt" and os.path.isfile(os.path.join(directory, f))])
|
||||
|
||||
|
||||
def analyze_fuzzer_instance(instance_dir: str) -> dict:
|
||||
name = os.path.basename(instance_dir)
|
||||
stats = parse_fuzzer_stats(os.path.join(instance_dir, "fuzzer_stats"))
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"start_time": stats.get("start_time", ""),
|
||||
"last_update": stats.get("last_update", ""),
|
||||
"execs_done": int(stats.get("execs_done", 0)),
|
||||
"execs_per_sec": float(stats.get("execs_per_sec", 0)),
|
||||
"corpus_count": count_files_in_dir(os.path.join(instance_dir, "queue")),
|
||||
"crashes_total": count_files_in_dir(os.path.join(instance_dir, "crashes")),
|
||||
"hangs_total": count_files_in_dir(os.path.join(instance_dir, "hangs")),
|
||||
"paths_total": int(stats.get("paths_total", 0)),
|
||||
"paths_found": int(stats.get("paths_found", 0)),
|
||||
"stability": stats.get("stability", ""),
|
||||
"cycles_done": int(stats.get("cycles_done", 0)),
|
||||
"bitmap_cvg": stats.get("bitmap_cvg", ""),
|
||||
"command_line": stats.get("command_line", ""),
|
||||
}
|
||||
|
||||
|
||||
def collect_crash_info(instance_dir: str) -> list:
|
||||
crashes_dir = os.path.join(instance_dir, "crashes")
|
||||
crashes = []
|
||||
if not os.path.isdir(crashes_dir):
|
||||
return crashes
|
||||
for fname in sorted(os.listdir(crashes_dir)):
|
||||
if fname == "README.txt":
|
||||
continue
|
||||
fpath = os.path.join(crashes_dir, fname)
|
||||
if os.path.isfile(fpath):
|
||||
crashes.append({
|
||||
"file": fname,
|
||||
"path": fpath,
|
||||
"size": os.path.getsize(fpath),
|
||||
"instance": os.path.basename(instance_dir),
|
||||
})
|
||||
return crashes
|
||||
|
||||
|
||||
def analyze_campaign(findings_dir: str) -> dict:
|
||||
report = {
|
||||
"findings_dir": findings_dir,
|
||||
"analyzed_at": datetime.utcnow().isoformat() + "Z",
|
||||
"instances": [],
|
||||
"total_execs": 0,
|
||||
"total_crashes": 0,
|
||||
"total_hangs": 0,
|
||||
"total_corpus": 0,
|
||||
"all_crashes": [],
|
||||
"avg_execs_per_sec": 0,
|
||||
}
|
||||
|
||||
instance_dirs = []
|
||||
for entry in sorted(os.listdir(findings_dir)):
|
||||
full_path = os.path.join(findings_dir, entry)
|
||||
if os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "fuzzer_stats")):
|
||||
instance_dirs.append(full_path)
|
||||
|
||||
if not instance_dirs:
|
||||
print(f"No fuzzer instances found in {findings_dir}")
|
||||
return report
|
||||
|
||||
exec_speeds = []
|
||||
for inst_dir in instance_dirs:
|
||||
inst = analyze_fuzzer_instance(inst_dir)
|
||||
report["instances"].append(inst)
|
||||
report["total_execs"] += inst["execs_done"]
|
||||
report["total_crashes"] += inst["crashes_total"]
|
||||
report["total_hangs"] += inst["hangs_total"]
|
||||
report["total_corpus"] += inst["corpus_count"]
|
||||
if inst["execs_per_sec"] > 0:
|
||||
exec_speeds.append(inst["execs_per_sec"])
|
||||
|
||||
crashes = collect_crash_info(inst_dir)
|
||||
report["all_crashes"].extend(crashes)
|
||||
|
||||
if exec_speeds:
|
||||
report["avg_execs_per_sec"] = round(sum(exec_speeds) / len(exec_speeds), 1)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def print_report(report: dict) -> None:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"AFL++ Fuzzing Campaign Report")
|
||||
print(f"{'='*60}")
|
||||
print(f"Findings directory: {report['findings_dir']}")
|
||||
print(f"Analyzed at: {report['analyzed_at']}")
|
||||
print(f"Fuzzer instances: {len(report['instances'])}")
|
||||
print(f"\nAggregate Statistics:")
|
||||
print(f" Total executions: {report['total_execs']:,}")
|
||||
print(f" Avg exec/sec: {report['avg_execs_per_sec']:,.1f}")
|
||||
print(f" Total corpus entries: {report['total_corpus']}")
|
||||
print(f" Total unique crashes: {report['total_crashes']}")
|
||||
print(f" Total hangs: {report['total_hangs']}")
|
||||
|
||||
print(f"\nInstance Details:")
|
||||
for inst in report["instances"]:
|
||||
print(f" {inst['name']:20s} | Execs: {inst['execs_done']:>12,} | "
|
||||
f"Speed: {inst['execs_per_sec']:>8.1f}/s | "
|
||||
f"Crashes: {inst['crashes_total']:3d} | "
|
||||
f"Corpus: {inst['corpus_count']:5d} | "
|
||||
f"Cycles: {inst['cycles_done']}")
|
||||
|
||||
if report["all_crashes"]:
|
||||
print(f"\nCrash Files ({len(report['all_crashes'])} total):")
|
||||
for crash in report["all_crashes"][:20]:
|
||||
print(f" [{crash['instance']}] {crash['file']} ({crash['size']} bytes)")
|
||||
if len(report["all_crashes"]) > 20:
|
||||
print(f" ... and {len(report['all_crashes']) - 20} more")
|
||||
|
||||
verdict = "PASS" if report["total_crashes"] == 0 else "FAIL"
|
||||
print(f"\nCI Verdict: {verdict}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python process.py <findings_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
findings_dir = sys.argv[1]
|
||||
if not os.path.isdir(findings_dir):
|
||||
print(f"Directory not found: {findings_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
report = analyze_campaign(findings_dir)
|
||||
print_report(report)
|
||||
|
||||
output = os.path.join(findings_dir, "campaign_report.json")
|
||||
with open(output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"\nReport saved to: {output}")
|
||||
|
||||
sys.exit(1 if report["total_crashes"] > 0 else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user