Skip to content

Commit be55bd3

Browse files
authored
Merge pull request #40 from FuzzingLabs/fuzzforge-ai-new-version
Missing modifications for the new version
2 parents 9ea4d66 + cd5bfc2 commit be55bd3

File tree

19 files changed

+328
-217
lines changed

19 files changed

+328
-217
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,5 +287,6 @@ BSL 1.1 - See [LICENSE](LICENSE) for details.
287287
---
288288

289289
<p align="center">
290-
<strong>Built with ❤️ by <a href="https://fuzzinglabs.com">FuzzingLabs</a></strong>
290+
<strong>Maintained by <a href="https://fuzzinglabs.com">FuzzingLabs</a></strong>
291+
<br>
291292
</p>

fuzzforge-common/src/fuzzforge_common/sandboxes/engines/docker/cli.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def list_containers(self, all_containers: bool = True) -> list[dict]:
420420
def read_file_from_image(self, image: str, path: str) -> str:
421421
"""Read a file from inside an image without starting a long-running container.
422422
423-
Creates a temporary container, reads the file via cat, and removes it.
423+
Uses docker run with --entrypoint override to read the file via cat.
424424
425425
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
426426
:param path: Path to file inside image.
@@ -429,30 +429,14 @@ def read_file_from_image(self, image: str, path: str) -> str:
429429
"""
430430
logger = get_logger()
431431

432-
# Create a temporary container (don't start it)
433-
create_result = self._run(
434-
["create", "--rm", image, "cat", path],
432+
# Use docker run with --entrypoint to override any container entrypoint
433+
result = self._run(
434+
["run", "--rm", "--entrypoint", "cat", image, path],
435435
check=False,
436436
)
437437

438-
if create_result.returncode != 0:
439-
logger.debug("failed to create container for file read", image=image, path=path)
438+
if result.returncode != 0:
439+
logger.debug("failed to read file from image", image=image, path=path, stderr=result.stderr)
440440
return ""
441441

442-
container_id = create_result.stdout.strip()
443-
444-
try:
445-
# Start the container and capture output (cat will run and exit)
446-
start_result = self._run(
447-
["start", "-a", container_id],
448-
check=False,
449-
)
450-
451-
if start_result.returncode != 0:
452-
logger.debug("failed to read file from image", image=image, path=path)
453-
return ""
454-
455-
return start_result.stdout
456-
finally:
457-
# Cleanup: remove the container (may already be removed due to --rm)
458-
self._run(["rm", "-f", container_id], check=False)
442+
return result.stdout

fuzzforge-common/src/fuzzforge_common/sandboxes/engines/podman/cli.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ def list_containers(self, all_containers: bool = True) -> list[dict]:
481481
def read_file_from_image(self, image: str, path: str) -> str:
482482
"""Read a file from inside an image without starting a long-running container.
483483
484-
Creates a temporary container, reads the file via cat, and removes it.
484+
Uses podman run with --entrypoint override to read the file via cat.
485485
486486
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
487487
:param path: Path to file inside image.
@@ -490,33 +490,17 @@ def read_file_from_image(self, image: str, path: str) -> str:
490490
"""
491491
logger = get_logger()
492492

493-
# Create a temporary container (don't start it)
494-
create_result = self._run(
495-
["create", "--rm", image, "cat", path],
493+
# Use podman run with --entrypoint to override any container entrypoint
494+
result = self._run(
495+
["run", "--rm", "--entrypoint", "cat", image, path],
496496
check=False,
497497
)
498498

499-
if create_result.returncode != 0:
500-
logger.debug("failed to create container for file read", image=image, path=path)
499+
if result.returncode != 0:
500+
logger.debug("failed to read file from image", image=image, path=path, stderr=result.stderr)
501501
return ""
502502

503-
container_id = create_result.stdout.strip()
504-
505-
try:
506-
# Start the container and capture output (cat will run and exit)
507-
start_result = self._run(
508-
["start", "-a", container_id],
509-
check=False,
510-
)
511-
512-
if start_result.returncode != 0:
513-
logger.debug("failed to read file from image", image=image, path=path)
514-
return ""
515-
516-
return start_result.stdout
517-
finally:
518-
# Cleanup: remove the container (may already be removed due to --rm)
519-
self._run(["rm", "-f", container_id], check=False)
503+
return result.stdout
520504

521505
# -------------------------------------------------------------------------
522506
# Utility Methods

fuzzforge-mcp/src/fuzzforge_mcp/application.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ async def lifespan(_: FastMCP) -> AsyncGenerator[Settings]:
4646
4747
Typical workflow:
4848
1. Initialize a project with `init_project`
49-
2. Set project assets with `set_project_assets` (optional)
49+
2. Set project assets with `set_project_assets` (optional, only needed once for the source directory)
5050
3. List available modules with `list_modules`
51-
4. Execute a module with `execute_module`
52-
5. Get results with `get_execution_results`
51+
4. Execute a module with `execute_module` — use `assets_path` param to pass different inputs per module
52+
5. Read outputs from `results_path` returned by `execute_module` — check module's `output_artifacts` metadata for filenames
5353
""",
5454
lifespan=lifespan,
5555
)

fuzzforge-mcp/src/fuzzforge_mcp/tools/modules.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,14 @@ async def execute_module(
9292
This tool runs a module in a sandboxed environment.
9393
The module receives input assets and produces output results.
9494
95+
The response includes `results_path` pointing to the stored results archive.
96+
Use this path directly to read outputs — no need to call `get_execution_results`.
97+
9598
:param module_identifier: The identifier of the module to execute.
9699
:param configuration: Optional configuration dict to pass to the module.
97-
:param assets_path: Optional path to input assets. If not provided, uses project assets.
100+
:param assets_path: Optional path to input assets. Use this to pass specific
101+
inputs to a module (e.g. crash files to crash-analyzer) without changing
102+
the project's default assets. If not provided, uses project assets.
98103
:return: Execution result including status and results path.
99104
100105
"""

fuzzforge-mcp/src/fuzzforge_mcp/tools/projects.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,17 @@ async def init_project(project_path: str | None = None) -> dict[str, Any]:
5656

5757
@mcp.tool
5858
async def set_project_assets(assets_path: str) -> dict[str, Any]:
59-
"""Set the initial assets for a project.
59+
"""Set the initial assets (source code) for a project.
6060
61-
Assets are input files that will be provided to modules during execution.
62-
This could be source code, contracts, binaries, etc.
61+
This sets the DEFAULT source directory mounted into modules.
62+
Usually this is the project root containing source code (e.g. Cargo.toml, src/).
6363
64-
:param assets_path: Path to assets file (archive) or directory.
64+
IMPORTANT: This OVERWRITES the previous assets path. Only call this once
65+
during project setup. To pass different inputs to a specific module
66+
(e.g. crash files to crash-analyzer), use the `assets_path` parameter
67+
on `execute_module` instead.
68+
69+
:param assets_path: Path to the project source directory or archive.
6570
:return: Result including stored assets path.
6671
6772
"""

fuzzforge-modules/cargo-fuzzer/pyproject.toml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,11 @@ package = true
3333
# FuzzForge module metadata for AI agent discovery
3434
[tool.fuzzforge.module]
3535
identifier = "fuzzforge-cargo-fuzzer"
36-
category = "fuzzer"
37-
language = "rust"
38-
pipeline_stage = "fuzzing"
39-
pipeline_order = 3
4036
suggested_predecessors = ["fuzzforge-harness-tester"]
4137
continuous_mode = true
42-
typical_duration = "continuous"
4338

4439
use_cases = [
45-
"Run continuous coverage-guided fuzzing with libFuzzer",
40+
"Run continuous coverage-guided fuzzing on Rust targets with libFuzzer",
4641
"Execute cargo-fuzz on validated harnesses",
4742
"Produce crash artifacts for analysis",
4843
"Long-running fuzzing campaign"
@@ -55,10 +50,9 @@ common_inputs = [
5550
]
5651

5752
output_artifacts = [
53+
"fuzzing_results.json",
5854
"crashes/",
59-
"coverage-data/",
60-
"corpus/",
61-
"fuzzing-stats.json"
55+
"results.json"
6256
]
6357

64-
output_treatment = "Show fuzzing-stats.json as a live summary with total_executions, exec/sec, coverage_percent, and crashes_found. List files in crashes/ directory if any crashes found. The corpus/ and coverage-data/ directories are artifacts for downstream modules, don't display their contents."
58+
output_treatment = "Read fuzzing_results.json which contains: targets_fuzzed, total_crashes, total_executions, crashes_path, and results array with per-target crash info. Display summary of crashes found. The crashes/ directory contains crash inputs for downstream crash-analyzer."

fuzzforge-modules/cargo-fuzzer/src/module/mod.py

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -458,34 +458,56 @@ def _collect_crashes(self, target: str) -> list[CrashInfo]:
458458
459459
"""
460460
crashes: list[CrashInfo] = []
461+
seen_hashes: set[str] = set()
461462

462463
if self._fuzz_project_path is None or self._crashes_path is None:
463464
return crashes
464465

465-
# Check for crashes in the artifacts directory
466-
artifacts_dir = self._fuzz_project_path / "artifacts" / target
467-
468-
if artifacts_dir.is_dir():
469-
for crash_file in artifacts_dir.glob("crash-*"):
470-
if crash_file.is_file():
471-
# Copy crash to output
472-
output_crash = self._crashes_path / target
473-
output_crash.mkdir(parents=True, exist_ok=True)
474-
dest = output_crash / crash_file.name
475-
shutil.copy2(crash_file, dest)
476-
477-
# Read crash input
478-
crash_data = crash_file.read_bytes()
466+
# Check multiple possible crash locations:
467+
# 1. Standard artifacts directory (target-specific)
468+
# 2. Generic artifacts directory
469+
# 3. Fuzz project root (fork mode sometimes writes here)
470+
# 4. Project root (parent of fuzz directory)
471+
search_paths = [
472+
self._fuzz_project_path / "artifacts" / target,
473+
self._fuzz_project_path / "artifacts",
474+
self._fuzz_project_path,
475+
self._fuzz_project_path.parent,
476+
]
479477

480-
crash_info = CrashInfo(
481-
file_path=str(dest),
482-
input_hash=crash_file.name,
483-
input_size=len(crash_data),
484-
)
485-
crashes.append(crash_info)
478+
for search_dir in search_paths:
479+
if not search_dir.is_dir():
480+
continue
481+
482+
# Use rglob to recursively find crash files
483+
for crash_file in search_dir.rglob("crash-*"):
484+
if not crash_file.is_file():
485+
continue
486+
487+
# Skip duplicates by hash
488+
if crash_file.name in seen_hashes:
489+
continue
490+
seen_hashes.add(crash_file.name)
491+
492+
# Copy crash to output
493+
output_crash = self._crashes_path / target
494+
output_crash.mkdir(parents=True, exist_ok=True)
495+
dest = output_crash / crash_file.name
496+
shutil.copy2(crash_file, dest)
497+
498+
# Read crash input
499+
crash_data = crash_file.read_bytes()
500+
501+
crash_info = CrashInfo(
502+
file_path=str(dest),
503+
input_hash=crash_file.name,
504+
input_size=len(crash_data),
505+
)
506+
crashes.append(crash_info)
486507

487-
logger.info("found crash", target=target, file=crash_file.name)
508+
logger.info("found crash", target=target, file=crash_file.name, source=str(search_dir))
488509

510+
logger.info("crash collection complete", target=target, total_crashes=len(crashes))
489511
return crashes
490512

491513
def _write_output(self) -> None:

fuzzforge-modules/crash-analyzer/pyproject.toml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,11 @@ package = true
3434
# FuzzForge module metadata for AI agent discovery
3535
[tool.fuzzforge.module]
3636
identifier = "fuzzforge-crash-analyzer"
37-
category = "reporter"
38-
language = "rust"
39-
pipeline_stage = "crash-analysis"
40-
pipeline_order = 4
4137
suggested_predecessors = ["fuzzforge-cargo-fuzzer"]
4238
continuous_mode = false
43-
typical_duration = "1m"
4439

4540
use_cases = [
46-
"Analyze crash artifacts from fuzzing",
41+
"Analyze Rust crash artifacts from fuzzing",
4742
"Deduplicate crashes by stack trace signature",
4843
"Triage crashes by severity (critical, high, medium, low)",
4944
"Generate security vulnerability reports"
@@ -56,9 +51,8 @@ common_inputs = [
5651
]
5752

5853
output_artifacts = [
59-
"unique-crashes.json",
60-
"crash-report.md",
61-
"severity-analysis.json"
54+
"crash_analysis.json",
55+
"results.json"
6256
]
6357

64-
output_treatment = "Display crash-report.md as rendered markdown - this is the primary output. Show unique-crashes.json as a table with crash ID, severity, and affected function. Summarize severity-analysis.json showing counts by severity level (critical, high, medium, low)."
58+
output_treatment = "Read crash_analysis.json which contains: total_crashes, unique_crashes, duplicate_crashes, severity_summary (high/medium/low/unknown counts), and unique_analyses array with details per crash. Display a summary table of unique crashes by severity."

fuzzforge-modules/fuzzforge-module-template/pyproject.toml

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,25 @@ fuzzforge-modules-sdk = { workspace = true }
3131
package = true
3232

3333
# FuzzForge module metadata for AI agent discovery
34-
# See MODULE_METADATA.md for full documentation
3534
[tool.fuzzforge.module]
3635
# REQUIRED: Unique module identifier (should match Docker image name)
3736
identifier = "fuzzforge-module-template"
3837

39-
# REQUIRED: Module category - one of: analyzer, validator, fuzzer, reporter
40-
category = "analyzer"
41-
42-
# Optional: Target programming language
43-
language = "rust"
44-
45-
# Optional: Pipeline stage name
46-
pipeline_stage = "analysis"
47-
48-
# Optional: Numeric order in pipeline (for sorting)
49-
pipeline_order = 1
50-
51-
# Optional: List of module identifiers that must run before this one
52-
dependencies = []
38+
# Optional: List of module identifiers that should run before this one
39+
suggested_predecessors = []
5340

5441
# Optional: Whether this module supports continuous/background execution
5542
continuous_mode = false
5643

57-
# Optional: Expected runtime (e.g., "30s", "5m", "continuous")
58-
typical_duration = "30s"
59-
6044
# REQUIRED: Use cases help AI agents understand when to use this module
45+
# Include language/target info here (e.g., "Analyze Rust crate...")
6146
use_cases = [
6247
"FIXME: Describe what this module does",
6348
"FIXME: Describe typical usage scenario"
6449
]
6550

6651
# REQUIRED: What inputs the module expects
67-
input_requirements = [
52+
common_inputs = [
6853
"FIXME: List required input files or artifacts"
6954
]
7055

0 commit comments

Comments
 (0)