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
on:
workflow_dispatch:
push:
branches:
- master
workflow_dispatch:
paths:
- "docs/**"
jobs:
build:

View File

@@ -1,9 +1,14 @@
name: Docusaurus test deployment
on:
workflow_dispatch:
push:
paths:
- "docs/**"
pull_request:
branches:
- master
paths:
- "docs/**"
jobs:
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">
<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="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>
@@ -165,7 +165,7 @@ _AI agents automatically analyzing code and providing security insights_
- 🌐 [Website](https://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)
---
@@ -194,7 +194,7 @@ Planned features and improvements:
- ☁️ Multi-tenant SaaS platform with team collaboration
- 📊 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
fuzzforge workflow security_assessment /path/to/your/code
# Monitor progress in real-time
fuzzforge monitor live <execution-id>
# View findings when complete
fuzzforge finding <execution-id>
@@ -222,7 +220,6 @@ $ ff workflow security_assessment ./my-project
- `--timeout, -t` - Execution timeout in seconds
- `--interactive/--no-interactive, -i/-n` - Interactive parameter input
- `--wait, -w` - Wait for execution to complete
- `--live, -l` - Show live monitoring during execution
**Worker Lifecycle Options (v0.7.0):**
- `--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
```
### 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
#### `fuzzforge config show`
@@ -560,7 +524,6 @@ cli/
│ ├── workflows.py # Workflow management
│ ├── runs.py # Run management
│ ├── findings.py # Findings management
│ ├── monitor.py # Real-time monitoring
│ ├── config.py # Configuration commands
│ └── status.py # Status information
├── pyproject.toml # Project configuration
@@ -641,7 +604,6 @@ fuzzforge --help
# Command-specific help
ff workflows --help
ff workflow run --help
ff monitor live --help
# Show 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.
from __future__ import annotations
from pathlib import Path
import os
from pathlib import Path
from textwrap import dedent
from typing import Optional
@@ -32,17 +31,20 @@ app = typer.Typer()
@app.command()
def project(
name: Optional[str] = typer.Option(
None, "--name", "-n",
help="Project name (defaults to current directory name)"
None, "--name", "-n", help="Project name (defaults to current directory name)"
),
api_url: Optional[str] = typer.Option(
None, "--api-url", "-u",
help="FuzzForge API URL (defaults to http://localhost:8000)"
None,
"--api-url",
"-u",
help="FuzzForge API URL (defaults to http://localhost:8000)",
),
force: bool = typer.Option(
False, "--force", "-f",
help="Force initialization even if project already exists"
)
False,
"--force",
"-f",
help="Force initialization even if project already exists",
),
):
"""
📁 Initialize a new FuzzForge project in the current directory.
@@ -58,24 +60,20 @@ def project(
# Check if project already exists
if fuzzforge_dir.exists() and not force:
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")
raise typer.Exit(1)
# Get project name
if not name:
name = Prompt.ask(
"Project name",
default=current_dir.name,
console=console
)
name = Prompt.ask("Project name", default=current_dir.name, console=console)
# Get API URL
if not api_url:
api_url = Prompt.ask(
"FuzzForge API URL",
default="http://localhost:8000",
console=console
"FuzzForge API URL", default="http://localhost:8000", console=console
)
# Confirm initialization
@@ -117,15 +115,15 @@ def project(
]
if gitignore_path.exists():
with open(gitignore_path, 'r') as f:
with open(gitignore_path, "r") as f:
existing_content = f.read()
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")
console.print("📝 Updated .gitignore with FuzzForge entries")
else:
with open(gitignore_path, 'w') as f:
with open(gitignore_path, "w") as f:
f.write(f"{chr(10).join(gitignore_entries)}\n")
console.print("📝 Created .gitignore")
@@ -145,9 +143,6 @@ fuzzforge workflows
# Submit a workflow for analysis
fuzzforge workflow <workflow-name> /path/to/target
# Monitor run progress
fuzzforge monitor live <run-id>
# View findings
fuzzforge finding <run-id>
```
@@ -159,7 +154,7 @@ fuzzforge finding <run-id>
- `.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)
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.
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.panel import Panel
from rich.text import Text
@@ -34,10 +34,9 @@ class FuzzyMatcher:
"workflows": ["list", "info"],
"runs": ["submit", "status", "list", "rerun"],
"findings": ["get", "list", "export", "all"],
"monitor": ["stats", "crashes", "live"],
"config": ["set", "get", "list", "init"],
"ai": ["ask", "summarize", "explain"],
"ingest": ["project", "findings"]
"ingest": ["project", "findings"],
}
# Common workflow names
@@ -47,7 +46,7 @@ class FuzzyMatcher:
"infrastructure_scan",
"static_analysis_scan",
"penetration_testing_scan",
"secret_detection_scan"
"secret_detection_scan",
]
# Common parameter names
@@ -60,24 +59,25 @@ class FuzzyMatcher:
"param-file",
"interactive",
"wait",
"live",
"format",
"output",
"severity",
"since",
"limit",
"stats",
"export"
"export",
]
# Common values
self.common_values = {
"volume_mode": ["ro", "rw"],
"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."""
if command_group and command_group in self.commands:
# Search within a specific command group
@@ -86,9 +86,7 @@ class FuzzyMatcher:
# Search all main commands
candidates = list(self.commands.keys())
matches = difflib.get_close_matches(
user_input, candidates, n=1, cutoff=0.6
)
matches = difflib.get_close_matches(user_input, candidates, n=1, cutoff=0.6)
if matches:
match = matches[0]
@@ -114,7 +112,7 @@ class FuzzyMatcher:
def find_closest_parameter(self, user_input: str) -> Optional[Tuple[str, float]]:
"""Find the closest matching parameter name."""
# Remove leading dashes
clean_input = user_input.lstrip('-')
clean_input = user_input.lstrip("-")
matches = difflib.get_close_matches(
clean_input, self.parameter_names, n=1, cutoff=0.6
@@ -139,7 +137,9 @@ class FuzzyMatcher:
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."""
if not user_command:
return None
@@ -153,11 +153,9 @@ class FuzzyMatcher:
if closest:
match, confidence = closest
suggestions["type"] = "main_command"
suggestions["suggestions"].append({
"text": match,
"confidence": confidence,
"type": "command"
})
suggestions["suggestions"].append(
{"text": match, "confidence": confidence, "type": "command"}
)
# Check subcommand if present
elif len(user_command) > 1:
@@ -167,11 +165,13 @@ class FuzzyMatcher:
if closest:
match, confidence = closest
suggestions["type"] = "subcommand"
suggestions["suggestions"].append({
"text": f"{main_cmd} {match}",
"confidence": confidence,
"type": "subcommand"
})
suggestions["suggestions"].append(
{
"text": f"{main_cmd} {match}",
"confidence": confidence,
"type": "subcommand",
}
)
return suggestions if suggestions["suggestions"] else None
@@ -210,17 +210,19 @@ def display_command_suggestion(suggestions: Dict[str, Any]):
# Add helpful context
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":
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(
text,
title="🤔 Command Suggestion",
border_style="yellow",
expand=False
))
console.print(
Panel(text, title="🤔 Command Suggestion", border_style="yellow", expand=False)
)
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("?\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(
text,
title="🔧 Workflow Suggestion",
border_style="yellow",
expand=False
))
console.print(
Panel(text, title="🔧 Workflow Suggestion", border_style="yellow", expand=False)
)
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")
console.print(Panel(
text,
title="⚙️ Parameter Suggestion",
border_style="yellow",
expand=False
))
console.print(
Panel(text, title="⚙️ Parameter Suggestion", border_style="yellow", expand=False)
)
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
fuzzy_matcher = FuzzyMatcher()
fuzzy_matcher = FuzzyMatcher()