Merge branch 'master' into dev for v0.7.0 release

Resolved conflicts:
- Kept monitor.py (dev version - required for live monitoring)
- Kept workflow_exec.py (dev version - includes worker management, --live, --fail-on, --export-sarif)
- Kept main.py (dev version - includes new command structure)

All conflicts resolved in favor of dev branch features for 0.7.0 release.
This commit is contained in:
tduhamel42
2025-10-16 12:32:25 +02:00
7 changed files with 149 additions and 116 deletions

70
.github/workflows/ci-python.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Python CI
# This is a dumb Ci to ensure that the python client and backend builds correctly
# It could be optimized to run faster, building, testing and linting only changed code
# but for now it is good enough. It runs on every push and PR to any branch.
# It also runs on demand.
on:
workflow_dispatch:
push:
paths:
- "ai/**"
- "backend/**"
- "cli/**"
- "sdk/**"
- "src/**"
pull_request:
paths:
- "ai/**"
- "backend/**"
- "cli/**"
- "sdk/**"
- "src/**"
jobs:
ci:
name: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Setup uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Set up Python
run: uv python install
# Validate no obvious issues
# Quick hack because CLI returns non-zero exit code when no args are provided
- name: Run base command
run: |
set +e
uv run ff
if [ $? -ne 2 ]; then
echo "Expected exit code 2 from 'uv run ff', got $?"
exit 1
fi
- name: Build fuzzforge_ai package
run: uv build
- name: Build ai package
working-directory: ai
run: uv build
- name: Build cli package
working-directory: cli
run: uv build
- name: Build sdk package
working-directory: sdk
run: uv build
- name: Build backend package
working-directory: backend
run: uv build

View File

@@ -1,10 +1,13 @@
name: Deploy Docusaurus to GitHub Pages name: Deploy Docusaurus to GitHub Pages
on: on:
workflow_dispatch:
push: push:
branches: branches:
- master - master
workflow_dispatch: paths:
- "docs/**"
jobs: jobs:
build: build:

View File

@@ -1,9 +1,14 @@
name: Docusaurus test deployment name: Docusaurus test deployment
on: on:
workflow_dispatch:
push:
paths:
- "docs/**"
pull_request: pull_request:
branches: paths:
- master - "docs/**"
jobs: jobs:
test-deploy: test-deploy:

View File

@@ -6,7 +6,7 @@
<p align="center"><strong>AI-powered workflow automation and AI Agents for AppSec, Fuzzing & Offensive Security</strong></p> <p align="center"><strong>AI-powered workflow automation and AI Agents for AppSec, Fuzzing & Offensive Security</strong></p>
<p align="center"> <p align="center">
<a href="https://discord.com/invite/acqv9FVG"><img src="https://img.shields.io/discord/1420767905255133267?logo=discord&label=Discord" alt="Discord"></a> <a href="https://discord.gg/8XEX33UUwZ/"><img src="https://img.shields.io/discord/1420767905255133267?logo=discord&label=Discord" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-BSL%20%2B%20Apache-orange" alt="License: BSL + Apache"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/license-BSL%20%2B%20Apache-orange" alt="License: BSL + Apache"></a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+"/></a> <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+"/></a>
<a href="https://fuzzforge.ai"><img src="https://img.shields.io/badge/Website-fuzzforge.ai-blue" alt="Website"/></a> <a href="https://fuzzforge.ai"><img src="https://img.shields.io/badge/Website-fuzzforge.ai-blue" alt="Website"/></a>
@@ -165,7 +165,7 @@ _AI agents automatically analyzing code and providing security insights_
- 🌐 [Website](https://fuzzforge.ai) - 🌐 [Website](https://fuzzforge.ai)
- 📖 [Documentation](https://docs.fuzzforge.ai) - 📖 [Documentation](https://docs.fuzzforge.ai)
- 💬 [Community Discord](https://discord.com/invite/acqv9FVG) - 💬 [Community Discord](https://discord.gg/8XEX33UUwZ)
- 🎓 [FuzzingLabs Academy](https://academy.fuzzinglabs.com/?coupon=GITHUB_FUZZFORGE) - 🎓 [FuzzingLabs Academy](https://academy.fuzzinglabs.com/?coupon=GITHUB_FUZZFORGE)
--- ---
@@ -194,7 +194,7 @@ Planned features and improvements:
- ☁️ Multi-tenant SaaS platform with team collaboration - ☁️ Multi-tenant SaaS platform with team collaboration
- 📊 Advanced reporting & analytics - 📊 Advanced reporting & analytics
👉 Follow updates in the [GitHub issues](../../issues) and [Discord](https://discord.com/invite/acqv9FVG). 👉 Follow updates in the [GitHub issues](../../issues) and [Discord](https://discord.gg/8XEX33UUwZ)
--- ---

View File

@@ -80,8 +80,6 @@ fuzzforge workflows info security_assessment
# Submit a workflow for analysis # Submit a workflow for analysis
fuzzforge workflow security_assessment /path/to/your/code fuzzforge workflow security_assessment /path/to/your/code
# Monitor progress in real-time
fuzzforge monitor live <execution-id>
# View findings when complete # View findings when complete
fuzzforge finding <execution-id> fuzzforge finding <execution-id>
@@ -222,7 +220,6 @@ $ ff workflow security_assessment ./my-project
- `--timeout, -t` - Execution timeout in seconds - `--timeout, -t` - Execution timeout in seconds
- `--interactive/--no-interactive, -i/-n` - Interactive parameter input - `--interactive/--no-interactive, -i/-n` - Interactive parameter input
- `--wait, -w` - Wait for execution to complete - `--wait, -w` - Wait for execution to complete
- `--live, -l` - Show live monitoring during execution
**Worker Lifecycle Options (v0.7.0):** **Worker Lifecycle Options (v0.7.0):**
- `--auto-start/--no-auto-start` - Auto-start required worker (default: from config) - `--auto-start/--no-auto-start` - Auto-start required worker (default: from config)
@@ -320,39 +317,6 @@ fuzzforge finding export abc123def456 --format csv --output report.csv
fuzzforge finding export --format html --output report.html fuzzforge finding export --format html --output report.html
``` ```
### Real-time Monitoring
#### `fuzzforge monitor stats <execution-id>`
Show current fuzzing statistics.
```bash
# Show stats once
fuzzforge monitor stats abc123def456 --once
# Live updating stats (default)
fuzzforge monitor stats abc123def456 --refresh 5
```
#### `fuzzforge monitor crashes <run-id>`
Display crash reports for a fuzzing run.
```bash
fuzzforge monitor crashes abc123def456 --limit 50
```
#### `fuzzforge monitor live <run-id>`
Real-time monitoring dashboard with live updates.
```bash
fuzzforge monitor live abc123def456 --refresh 3
```
Features:
- Live updating statistics
- Progress indicators and bars
- Run status monitoring
- Automatic completion detection
### Configuration Management ### Configuration Management
#### `fuzzforge config show` #### `fuzzforge config show`
@@ -560,7 +524,6 @@ cli/
│ ├── workflows.py # Workflow management │ ├── workflows.py # Workflow management
│ ├── runs.py # Run management │ ├── runs.py # Run management
│ ├── findings.py # Findings management │ ├── findings.py # Findings management
│ ├── monitor.py # Real-time monitoring
│ ├── config.py # Configuration commands │ ├── config.py # Configuration commands
│ └── status.py # Status information │ └── status.py # Status information
├── pyproject.toml # Project configuration ├── pyproject.toml # Project configuration
@@ -641,7 +604,6 @@ fuzzforge --help
# Command-specific help # Command-specific help
ff workflows --help ff workflows --help
ff workflow run --help ff workflow run --help
ff monitor live --help
# Show version # Show version
fuzzforge --version fuzzforge --version
@@ -683,4 +645,4 @@ Contributions are welcome! Please see the main FuzzForge repository for contribu
--- ---
**FuzzForge CLI** - Making security testing workflows accessible and efficient from the command line. **FuzzForge CLI** - Making security testing workflows accessible and efficient from the command line.

View File

@@ -10,11 +10,10 @@
# #
# Additional attribution and requirements are provided in the NOTICE file. # Additional attribution and requirements are provided in the NOTICE file.
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import os import os
from pathlib import Path
from textwrap import dedent from textwrap import dedent
from typing import Optional from typing import Optional
@@ -32,17 +31,20 @@ app = typer.Typer()
@app.command() @app.command()
def project( def project(
name: Optional[str] = typer.Option( name: Optional[str] = typer.Option(
None, "--name", "-n", None, "--name", "-n", help="Project name (defaults to current directory name)"
help="Project name (defaults to current directory name)"
), ),
api_url: Optional[str] = typer.Option( api_url: Optional[str] = typer.Option(
None, "--api-url", "-u", None,
help="FuzzForge API URL (defaults to http://localhost:8000)" "--api-url",
"-u",
help="FuzzForge API URL (defaults to http://localhost:8000)",
), ),
force: bool = typer.Option( force: bool = typer.Option(
False, "--force", "-f", False,
help="Force initialization even if project already exists" "--force",
) "-f",
help="Force initialization even if project already exists",
),
): ):
""" """
📁 Initialize a new FuzzForge project in the current directory. 📁 Initialize a new FuzzForge project in the current directory.
@@ -58,24 +60,20 @@ def project(
# Check if project already exists # Check if project already exists
if fuzzforge_dir.exists() and not force: if fuzzforge_dir.exists() and not force:
if fuzzforge_dir.is_dir() and any(fuzzforge_dir.iterdir()): if fuzzforge_dir.is_dir() and any(fuzzforge_dir.iterdir()):
console.print("❌ FuzzForge project already exists in this directory", style="red") console.print(
"❌ FuzzForge project already exists in this directory", style="red"
)
console.print("Use --force to reinitialize", style="dim") console.print("Use --force to reinitialize", style="dim")
raise typer.Exit(1) raise typer.Exit(1)
# Get project name # Get project name
if not name: if not name:
name = Prompt.ask( name = Prompt.ask("Project name", default=current_dir.name, console=console)
"Project name",
default=current_dir.name,
console=console
)
# Get API URL # Get API URL
if not api_url: if not api_url:
api_url = Prompt.ask( api_url = Prompt.ask(
"FuzzForge API URL", "FuzzForge API URL", default="http://localhost:8000", console=console
default="http://localhost:8000",
console=console
) )
# Confirm initialization # Confirm initialization
@@ -117,15 +115,15 @@ def project(
] ]
if gitignore_path.exists(): if gitignore_path.exists():
with open(gitignore_path, 'r') as f: with open(gitignore_path, "r") as f:
existing_content = f.read() existing_content = f.read()
if "# FuzzForge CLI" not in existing_content: if "# FuzzForge CLI" not in existing_content:
with open(gitignore_path, 'a') as f: with open(gitignore_path, "a") as f:
f.write(f"\n{chr(10).join(gitignore_entries)}\n") f.write(f"\n{chr(10).join(gitignore_entries)}\n")
console.print("📝 Updated .gitignore with FuzzForge entries") console.print("📝 Updated .gitignore with FuzzForge entries")
else: else:
with open(gitignore_path, 'w') as f: with open(gitignore_path, "w") as f:
f.write(f"{chr(10).join(gitignore_entries)}\n") f.write(f"{chr(10).join(gitignore_entries)}\n")
console.print("📝 Created .gitignore") console.print("📝 Created .gitignore")
@@ -145,9 +143,6 @@ fuzzforge workflows
# Submit a workflow for analysis # Submit a workflow for analysis
fuzzforge workflow <workflow-name> /path/to/target fuzzforge workflow <workflow-name> /path/to/target
# Monitor run progress
fuzzforge monitor live <run-id>
# View findings # View findings
fuzzforge finding <run-id> fuzzforge finding <run-id>
``` ```
@@ -159,7 +154,7 @@ fuzzforge finding <run-id>
- `.fuzzforge/findings.db` - Local database for runs and findings - `.fuzzforge/findings.db` - Local database for runs and findings
""" """
with open(readme_path, 'w') as f: with open(readme_path, "w") as f:
f.write(readme_content) f.write(readme_content)
console.print("📚 Created README.md") console.print("📚 Created README.md")

View File

@@ -14,9 +14,9 @@ Provides "Did you mean...?" functionality and intelligent command/parameter sugg
# #
# Additional attribution and requirements are provided in the NOTICE file. # Additional attribution and requirements are provided in the NOTICE file.
import difflib import difflib
from typing import List, Optional, Dict, Any, Tuple from typing import Any, Dict, List, Optional, Tuple
from rich.console import Console from rich.console import Console
from rich.panel import Panel from rich.panel import Panel
from rich.text import Text from rich.text import Text
@@ -34,10 +34,9 @@ class FuzzyMatcher:
"workflows": ["list", "info"], "workflows": ["list", "info"],
"runs": ["submit", "status", "list", "rerun"], "runs": ["submit", "status", "list", "rerun"],
"findings": ["get", "list", "export", "all"], "findings": ["get", "list", "export", "all"],
"monitor": ["stats", "crashes", "live"],
"config": ["set", "get", "list", "init"], "config": ["set", "get", "list", "init"],
"ai": ["ask", "summarize", "explain"], "ai": ["ask", "summarize", "explain"],
"ingest": ["project", "findings"] "ingest": ["project", "findings"],
} }
# Common workflow names # Common workflow names
@@ -47,7 +46,7 @@ class FuzzyMatcher:
"infrastructure_scan", "infrastructure_scan",
"static_analysis_scan", "static_analysis_scan",
"penetration_testing_scan", "penetration_testing_scan",
"secret_detection_scan" "secret_detection_scan",
] ]
# Common parameter names # Common parameter names
@@ -60,24 +59,25 @@ class FuzzyMatcher:
"param-file", "param-file",
"interactive", "interactive",
"wait", "wait",
"live",
"format", "format",
"output", "output",
"severity", "severity",
"since", "since",
"limit", "limit",
"stats", "stats",
"export" "export",
] ]
# Common values # Common values
self.common_values = { self.common_values = {
"volume_mode": ["ro", "rw"], "volume_mode": ["ro", "rw"],
"format": ["json", "csv", "html", "sarif"], "format": ["json", "csv", "html", "sarif"],
"severity": ["critical", "high", "medium", "low", "info"] "severity": ["critical", "high", "medium", "low", "info"],
} }
def find_closest_command(self, user_input: str, command_group: Optional[str] = None) -> Optional[Tuple[str, float]]: def find_closest_command(
self, user_input: str, command_group: Optional[str] = None
) -> Optional[Tuple[str, float]]:
"""Find the closest matching command.""" """Find the closest matching command."""
if command_group and command_group in self.commands: if command_group and command_group in self.commands:
# Search within a specific command group # Search within a specific command group
@@ -86,9 +86,7 @@ class FuzzyMatcher:
# Search all main commands # Search all main commands
candidates = list(self.commands.keys()) candidates = list(self.commands.keys())
matches = difflib.get_close_matches( matches = difflib.get_close_matches(user_input, candidates, n=1, cutoff=0.6)
user_input, candidates, n=1, cutoff=0.6
)
if matches: if matches:
match = matches[0] match = matches[0]
@@ -114,7 +112,7 @@ class FuzzyMatcher:
def find_closest_parameter(self, user_input: str) -> Optional[Tuple[str, float]]: def find_closest_parameter(self, user_input: str) -> Optional[Tuple[str, float]]:
"""Find the closest matching parameter name.""" """Find the closest matching parameter name."""
# Remove leading dashes # Remove leading dashes
clean_input = user_input.lstrip('-') clean_input = user_input.lstrip("-")
matches = difflib.get_close_matches( matches = difflib.get_close_matches(
clean_input, self.parameter_names, n=1, cutoff=0.6 clean_input, self.parameter_names, n=1, cutoff=0.6
@@ -139,7 +137,9 @@ class FuzzyMatcher:
return [] return []
def get_command_suggestions(self, user_command: List[str]) -> Optional[Dict[str, Any]]: def get_command_suggestions(
self, user_command: List[str]
) -> Optional[Dict[str, Any]]:
"""Get suggestions for a user command that may have typos.""" """Get suggestions for a user command that may have typos."""
if not user_command: if not user_command:
return None return None
@@ -153,11 +153,9 @@ class FuzzyMatcher:
if closest: if closest:
match, confidence = closest match, confidence = closest
suggestions["type"] = "main_command" suggestions["type"] = "main_command"
suggestions["suggestions"].append({ suggestions["suggestions"].append(
"text": match, {"text": match, "confidence": confidence, "type": "command"}
"confidence": confidence, )
"type": "command"
})
# Check subcommand if present # Check subcommand if present
elif len(user_command) > 1: elif len(user_command) > 1:
@@ -167,11 +165,13 @@ class FuzzyMatcher:
if closest: if closest:
match, confidence = closest match, confidence = closest
suggestions["type"] = "subcommand" suggestions["type"] = "subcommand"
suggestions["suggestions"].append({ suggestions["suggestions"].append(
"text": f"{main_cmd} {match}", {
"confidence": confidence, "text": f"{main_cmd} {match}",
"type": "subcommand" "confidence": confidence,
}) "type": "subcommand",
}
)
return suggestions if suggestions["suggestions"] else None return suggestions if suggestions["suggestions"] else None
@@ -210,17 +210,19 @@ def display_command_suggestion(suggestions: Dict[str, Any]):
# Add helpful context # Add helpful context
if suggestion_type == "main_command": if suggestion_type == "main_command":
text.append("\n💡 Use 'fuzzforge --help' to see all available commands", style="dim") text.append(
"\n💡 Use 'fuzzforge --help' to see all available commands", style="dim"
)
elif suggestion_type == "subcommand": elif suggestion_type == "subcommand":
main_cmd = suggestions["original"][0] main_cmd = suggestions["original"][0]
text.append(f"\n💡 Use 'fuzzforge {main_cmd} --help' to see available subcommands", style="dim") text.append(
f"\n💡 Use 'fuzzforge {main_cmd} --help' to see available subcommands",
style="dim",
)
console.print(Panel( console.print(
text, Panel(text, title="🤔 Command Suggestion", border_style="yellow", expand=False)
title="🤔 Command Suggestion", )
border_style="yellow",
expand=False
))
def display_workflow_suggestion(original: str, suggestion: str): def display_workflow_suggestion(original: str, suggestion: str):
@@ -234,14 +236,13 @@ def display_workflow_suggestion(original: str, suggestion: str):
text.append(f"'{suggestion}'", style="bold green") text.append(f"'{suggestion}'", style="bold green")
text.append("?\n\n") text.append("?\n\n")
text.append("💡 Use 'fuzzforge workflows' to see all available workflows", style="dim") text.append(
"💡 Use 'fuzzforge workflows' to see all available workflows", style="dim"
)
console.print(Panel( console.print(
text, Panel(text, title="🔧 Workflow Suggestion", border_style="yellow", expand=False)
title="🔧 Workflow Suggestion", )
border_style="yellow",
expand=False
))
def display_parameter_suggestion(original: str, suggestion: str): def display_parameter_suggestion(original: str, suggestion: str):
@@ -257,12 +258,9 @@ def display_parameter_suggestion(original: str, suggestion: str):
text.append("💡 Use '--help' to see all available parameters", style="dim") text.append("💡 Use '--help' to see all available parameters", style="dim")
console.print(Panel( console.print(
text, Panel(text, title="⚙️ Parameter Suggestion", border_style="yellow", expand=False)
title="⚙️ Parameter Suggestion", )
border_style="yellow",
expand=False
))
def enhanced_command_not_found_handler(command_parts: List[str]): def enhanced_command_not_found_handler(command_parts: List[str]):
@@ -306,4 +304,4 @@ def enhanced_parameter_not_found_handler(parameter_name: str):
# Global fuzzy matcher instance # Global fuzzy matcher instance
fuzzy_matcher = FuzzyMatcher() fuzzy_matcher = FuzzyMatcher()