diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml
new file mode 100644
index 0000000..35138f1
--- /dev/null
+++ b/.github/workflows/ci-python.yml
@@ -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
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index e2cf828..b5f866c 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -1,10 +1,13 @@
name: Deploy Docusaurus to GitHub Pages
on:
+ workflow_dispatch:
+
push:
branches:
- master
- workflow_dispatch:
+ paths:
+ - "docs/**"
jobs:
build:
diff --git a/.github/workflows/docs-test-deploy.yml b/.github/workflows/docs-test-deploy.yml
index 9c7d2d9..c42d773 100644
--- a/.github/workflows/docs-test-deploy.yml
+++ b/.github/workflows/docs-test-deploy.yml
@@ -1,9 +1,14 @@
name: Docusaurus test deployment
on:
+ workflow_dispatch:
+
+ push:
+ paths:
+ - "docs/**"
pull_request:
- branches:
- - master
+ paths:
+ - "docs/**"
jobs:
test-deploy:
diff --git a/README.md b/README.md
index 394d89a..ac97245 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
AI-powered workflow automation and AI Agents for AppSec, Fuzzing & Offensive Security
-
+
@@ -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)
---
diff --git a/cli/README.md b/cli/README.md
index 53060b9..47d0836 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -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
# View findings when complete
fuzzforge finding
@@ -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 `
-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 `
-Display crash reports for a fuzzing run.
-
-```bash
-fuzzforge monitor crashes abc123def456 --limit 50
-```
-
-#### `fuzzforge monitor live `
-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.
\ No newline at end of file
+**FuzzForge CLI** - Making security testing workflows accessible and efficient from the command line.
diff --git a/cli/src/fuzzforge_cli/commands/init.py b/cli/src/fuzzforge_cli/commands/init.py
index 17167c2..9a9d30a 100644
--- a/cli/src/fuzzforge_cli/commands/init.py
+++ b/cli/src/fuzzforge_cli/commands/init.py
@@ -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 /path/to/target
-# Monitor run progress
-fuzzforge monitor live
-
# View findings
fuzzforge finding
```
@@ -159,7 +154,7 @@ fuzzforge finding
- `.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")
diff --git a/cli/src/fuzzforge_cli/fuzzy.py b/cli/src/fuzzforge_cli/fuzzy.py
index 731e9df..48f16a5 100644
--- a/cli/src/fuzzforge_cli/fuzzy.py
+++ b/cli/src/fuzzforge_cli/fuzzy.py
@@ -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()
\ No newline at end of file
+fuzzy_matcher = FuzzyMatcher()