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

- Discord + Discord License: BSL + Apache Python 3.11+ Website @@ -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()