From 38b2c8ea6ebfedd406b2e76f2088fde392e79b11 Mon Sep 17 00:00:00 2001 From: tduhamel42 Date: Wed, 29 Oct 2025 13:24:04 +0100 Subject: [PATCH] feat(cli): add worker management commands with improved progress feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive CLI commands for managing Temporal workers: - ff worker list - List workers with status and uptime - ff worker start - Start specific worker with optional rebuild - ff worker stop - Safely stop all workers without affecting core services Improvements: - Live progress display during worker startup with Rich Status spinner - Real-time elapsed time counter and container state updates - Health check status tracking (starting โ†’ unhealthy โ†’ healthy) - Helpful contextual hints at 10s, 30s, 60s intervals - Better timeout messages showing last known state Worker management enhancements: - Use 'docker compose' (space) instead of 'docker-compose' (hyphen) - Stop workers individually with 'docker stop' to avoid stopping core services - Platform detection and Dockerfile selection (ARM64/AMD64) Documentation: - Updated docker-setup.md with CLI commands as primary method - Created comprehensive cli-reference.md with all commands and examples - Added worker management best practices --- cli/src/fuzzforge_cli/commands/__init__.py | 3 + cli/src/fuzzforge_cli/commands/worker.py | 225 ++++++++ cli/src/fuzzforge_cli/main.py | 4 +- cli/src/fuzzforge_cli/worker_manager.py | 277 +++++++-- docs/docs/how-to/docker-setup.md | 44 +- docs/docs/reference/cli-reference.md | 616 +++++++++++++++++++++ 6 files changed, 1114 insertions(+), 55 deletions(-) create mode 100644 cli/src/fuzzforge_cli/commands/worker.py create mode 100644 docs/docs/reference/cli-reference.md diff --git a/cli/src/fuzzforge_cli/commands/__init__.py b/cli/src/fuzzforge_cli/commands/__init__.py index 7e53182..afcf0d9 100644 --- a/cli/src/fuzzforge_cli/commands/__init__.py +++ b/cli/src/fuzzforge_cli/commands/__init__.py @@ -12,3 +12,6 @@ Command modules for FuzzForge CLI. # # Additional attribution and requirements are provided in the NOTICE file. +from . import worker + +__all__ = ["worker"] diff --git a/cli/src/fuzzforge_cli/commands/worker.py b/cli/src/fuzzforge_cli/commands/worker.py new file mode 100644 index 0000000..06b8b03 --- /dev/null +++ b/cli/src/fuzzforge_cli/commands/worker.py @@ -0,0 +1,225 @@ +""" +Worker management commands for FuzzForge CLI. + +Provides commands to start, stop, and list Temporal workers. +""" +# Copyright (c) 2025 FuzzingLabs +# +# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file +# at the root of this repository for details. +# +# After the Change Date (four years from publication), this version of the +# Licensed Work will be made available under the Apache License, Version 2.0. +# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 +# +# Additional attribution and requirements are provided in the NOTICE file. + +import subprocess +import sys +import typer +from pathlib import Path +from rich.console import Console +from rich.table import Table +from typing import Optional + +from ..worker_manager import WorkerManager + +console = Console() +app = typer.Typer( + name="worker", + help="๐Ÿ”ง Manage Temporal workers", + no_args_is_help=True, +) + + +@app.command("stop") +def stop_workers( + all: bool = typer.Option( + False, "--all", + help="Stop all workers (default behavior, flag for clarity)" + ) +): + """ + ๐Ÿ›‘ Stop all running FuzzForge workers. + + This command stops all worker containers using the proper Docker Compose + profile flag to ensure workers are actually stopped (since they're in profiles). + + Examples: + $ ff worker stop + $ ff worker stop --all + """ + try: + worker_mgr = WorkerManager() + success = worker_mgr.stop_all_workers() + + if success: + sys.exit(0) + else: + console.print("โš ๏ธ Some workers may not have stopped properly", style="yellow") + sys.exit(1) + + except Exception as e: + console.print(f"โŒ Error: {e}", style="red") + sys.exit(1) + + +@app.command("list") +def list_workers( + all: bool = typer.Option( + False, "--all", "-a", + help="Show all workers (including stopped)" + ) +): + """ + ๐Ÿ“‹ List FuzzForge workers and their status. + + By default, shows only running workers. Use --all to see all workers. + + Examples: + $ ff worker list + $ ff worker list --all + """ + try: + # Get list of running workers + result = subprocess.run( + ["docker", "ps", "--filter", "name=fuzzforge-worker-", + "--format", "{{.Names}}\t{{.Status}}\t{{.RunningFor}}"], + capture_output=True, + text=True, + check=False + ) + + running_workers = [] + if result.stdout.strip(): + for line in result.stdout.strip().splitlines(): + parts = line.split('\t') + if len(parts) >= 3: + running_workers.append({ + "name": parts[0].replace("fuzzforge-worker-", ""), + "status": "Running", + "uptime": parts[2] + }) + + # If --all, also get stopped workers + stopped_workers = [] + if all: + result_all = subprocess.run( + ["docker", "ps", "-a", "--filter", "name=fuzzforge-worker-", + "--format", "{{.Names}}\t{{.Status}}"], + capture_output=True, + text=True, + check=False + ) + + all_worker_names = set() + for line in result_all.stdout.strip().splitlines(): + parts = line.split('\t') + if len(parts) >= 2: + worker_name = parts[0].replace("fuzzforge-worker-", "") + all_worker_names.add(worker_name) + # If not running, it's stopped + if not any(w["name"] == worker_name for w in running_workers): + stopped_workers.append({ + "name": worker_name, + "status": "Stopped", + "uptime": "-" + }) + + # Display results + if not running_workers and not stopped_workers: + console.print("โ„น๏ธ No workers found", style="cyan") + console.print("\n๐Ÿ’ก Start a worker with: [cyan]docker compose up -d worker-[/cyan]") + console.print(" Or run a workflow, which auto-starts workers: [cyan]ff workflow run [/cyan]") + return + + # Create table + table = Table(title="FuzzForge Workers", show_header=True, header_style="bold cyan") + table.add_column("Worker", style="cyan", no_wrap=True) + table.add_column("Status", style="green") + table.add_column("Uptime", style="dim") + + # Add running workers + for worker in running_workers: + table.add_row( + worker["name"], + f"[green]โ—[/green] {worker['status']}", + worker["uptime"] + ) + + # Add stopped workers if --all + for worker in stopped_workers: + table.add_row( + worker["name"], + f"[red]โ—[/red] {worker['status']}", + worker["uptime"] + ) + + console.print(table) + + # Summary + if running_workers: + console.print(f"\nโœ… {len(running_workers)} worker(s) running") + if stopped_workers: + console.print(f"โน๏ธ {len(stopped_workers)} worker(s) stopped") + + except Exception as e: + console.print(f"โŒ Error listing workers: {e}", style="red") + sys.exit(1) + + +@app.command("start") +def start_worker( + name: str = typer.Argument( + ..., + help="Worker name (e.g., 'python', 'android', 'secrets')" + ), + build: bool = typer.Option( + False, "--build", + help="Rebuild worker image before starting" + ) +): + """ + ๐Ÿš€ Start a specific worker. + + The worker name should be the vertical name (e.g., 'python', 'android', 'rust'). + + Examples: + $ ff worker start python + $ ff worker start android --build + """ + try: + service_name = f"worker-{name}" + + console.print(f"๐Ÿš€ Starting worker: [cyan]{service_name}[/cyan]") + + # Build docker compose command + cmd = ["docker", "compose", "up", "-d"] + if build: + cmd.append("--build") + cmd.append(service_name) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False + ) + + if result.returncode == 0: + console.print(f"โœ… Worker [cyan]{service_name}[/cyan] started successfully") + else: + console.print(f"โŒ Failed to start worker: {result.stderr}", style="red") + console.print( + f"\n๐Ÿ’ก Try manually: [yellow]docker compose up -d {service_name}[/yellow]", + style="dim" + ) + sys.exit(1) + + except Exception as e: + console.print(f"โŒ Error: {e}", style="red") + sys.exit(1) + + +if __name__ == "__main__": + app() diff --git a/cli/src/fuzzforge_cli/main.py b/cli/src/fuzzforge_cli/main.py index 0d3f940..66b7c25 100644 --- a/cli/src/fuzzforge_cli/main.py +++ b/cli/src/fuzzforge_cli/main.py @@ -29,6 +29,7 @@ from .commands import ( config as config_cmd, ai, ingest, + worker, ) from .fuzzy import enhanced_command_not_found_handler @@ -334,6 +335,7 @@ app.add_typer(finding_app, name="finding", help="๐Ÿ” View and analyze findings" app.add_typer(monitor.app, name="monitor", help="๐Ÿ“Š Real-time monitoring") app.add_typer(ai.app, name="ai", help="๐Ÿค– AI integration features") app.add_typer(ingest.app, name="ingest", help="๐Ÿง  Ingest knowledge into AI") +app.add_typer(worker.app, name="worker", help="๐Ÿ”ง Manage Temporal workers") # Help and utility commands @app.command() @@ -409,7 +411,7 @@ def main(): 'init', 'status', 'config', 'clean', 'workflows', 'workflow', 'findings', 'finding', - 'monitor', 'ai', 'ingest', + 'monitor', 'ai', 'ingest', 'worker', 'version' ] diff --git a/cli/src/fuzzforge_cli/worker_manager.py b/cli/src/fuzzforge_cli/worker_manager.py index 0493f7a..a9b3eaf 100644 --- a/cli/src/fuzzforge_cli/worker_manager.py +++ b/cli/src/fuzzforge_cli/worker_manager.py @@ -25,6 +25,7 @@ from typing import Optional, Dict, Any import requests import yaml from rich.console import Console +from rich.status import Status logger = logging.getLogger(__name__) console = Console() @@ -163,11 +164,25 @@ class WorkerManager: Platform string: "linux/amd64" or "linux/arm64" """ machine = platform.machine().lower() - if machine in ["x86_64", "amd64"]: - return "linux/amd64" - elif machine in ["arm64", "aarch64"]: - return "linux/arm64" - return "unknown" + system = platform.system().lower() + + logger.debug(f"Platform detection: machine={machine}, system={system}") + + # Normalize machine architecture + if machine in ["x86_64", "amd64", "x64"]: + detected = "linux/amd64" + elif machine in ["arm64", "aarch64", "armv8", "arm64v8"]: + detected = "linux/arm64" + else: + # Fallback to amd64 for unknown architectures + logger.warning( + f"Unknown architecture '{machine}' detected, falling back to linux/amd64. " + f"Please report this issue if you're experiencing problems." + ) + detected = "linux/amd64" + + logger.info(f"Detected platform: {detected}") + return detected def _read_worker_metadata(self, vertical: str) -> dict: """ @@ -213,28 +228,39 @@ class WorkerManager: platforms = metadata.get("platforms", {}) + if not platforms: + # Metadata exists but no platform definitions + logger.debug(f"No platform definitions in metadata for {vertical}, using Dockerfile") + return "Dockerfile" + # Try detected platform first if detected_platform in platforms: dockerfile = platforms[detected_platform].get("dockerfile", "Dockerfile") - logger.debug(f"Selected {dockerfile} for {vertical} on {detected_platform}") + logger.info(f"โœ“ Selected {dockerfile} for {vertical} on {detected_platform}") return dockerfile # Fallback to default platform default_platform = metadata.get("default_platform", "linux/amd64") + logger.warning( + f"Platform {detected_platform} not found in metadata for {vertical}, " + f"falling back to default: {default_platform}" + ) + if default_platform in platforms: dockerfile = platforms[default_platform].get("dockerfile", "Dockerfile.amd64") - logger.debug(f"Using default platform {default_platform}: {dockerfile}") + logger.info(f"Using default platform {default_platform}: {dockerfile}") return dockerfile - # Last resort + # Last resort: just use Dockerfile + logger.warning(f"No suitable Dockerfile found for {vertical}, using 'Dockerfile'") return "Dockerfile" def _run_docker_compose(self, *args: str, env: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess: """ - Run docker-compose command with optional environment variables. + Run docker compose command with optional environment variables. Args: - *args: Arguments to pass to docker-compose + *args: Arguments to pass to docker compose env: Optional environment variables to set Returns: @@ -243,7 +269,7 @@ class WorkerManager: Raises: subprocess.CalledProcessError: If command fails """ - cmd = ["docker-compose", "-f", str(self.compose_file)] + list(args) + cmd = ["docker", "compose", "-f", str(self.compose_file)] + list(args) logger.debug(f"Running: {' '.join(cmd)}") # Merge with current environment @@ -342,9 +368,67 @@ class WorkerManager: console.print(f"โŒ Unexpected error: {e}", style="red") return False + def _get_container_state(self, service_name: str) -> str: + """ + Get the current state of a container (running, created, restarting, etc.). + + Args: + service_name: Name of the Docker Compose service + + Returns: + Container state string (running, created, restarting, exited, etc.) or "unknown" + """ + try: + container_name = self._service_to_container_name(service_name) + result = subprocess.run( + ["docker", "inspect", "-f", "{{.State.Status}}", container_name], + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0: + return result.stdout.strip() + return "unknown" + except Exception as e: + logger.debug(f"Failed to get container state: {e}") + return "unknown" + + def _get_health_status(self, container_name: str) -> str: + """ + Get container health status. + + Args: + container_name: Docker container name + + Returns: + Health status: "healthy", "unhealthy", "starting", "none", or "unknown" + """ + try: + result = subprocess.run( + ["docker", "inspect", "-f", "{{.State.Health.Status}}", container_name], + capture_output=True, + text=True, + check=False + ) + + if result.returncode != 0: + return "unknown" + + health_status = result.stdout.strip() + + if health_status == "" or health_status == "": + return "none" # No health check defined + + return health_status # healthy, unhealthy, starting + + except Exception as e: + logger.debug(f"Failed to check health: {e}") + return "unknown" + def wait_for_worker_ready(self, service_name: str, timeout: Optional[int] = None) -> bool: """ Wait for a worker to be healthy and ready to process tasks. + Shows live progress updates during startup. Args: service_name: Name of the Docker Compose service @@ -352,56 +436,74 @@ class WorkerManager: Returns: True if worker is ready, False if timeout reached - - Raises: - TimeoutError: If worker doesn't become ready within timeout """ timeout = timeout or self.startup_timeout start_time = time.time() container_name = self._service_to_container_name(service_name) + last_status_msg = "" - console.print("โณ Waiting for worker to be ready...") + with Status("[bold cyan]Starting worker...", console=console, spinner="dots") as status: + while time.time() - start_time < timeout: + elapsed = int(time.time() - start_time) + + # Get container state + container_state = self._get_container_state(service_name) + + # Get health status + health_status = self._get_health_status(container_name) + + # Build status message based on current state + if container_state == "created": + status_msg = f"[cyan]Worker starting... ({elapsed}s)[/cyan]" + elif container_state == "restarting": + status_msg = f"[yellow]Worker restarting... ({elapsed}s)[/yellow]" + elif container_state == "running": + if health_status == "starting": + status_msg = f"[cyan]Worker running, health check starting... ({elapsed}s)[/cyan]" + elif health_status == "unhealthy": + status_msg = f"[yellow]Worker running, health check: unhealthy ({elapsed}s)[/yellow]" + elif health_status == "healthy": + status_msg = f"[green]Worker healthy! ({elapsed}s)[/green]" + status.update(status_msg) + console.print(f"โœ… Worker ready: {service_name} (took {elapsed}s)") + logger.info(f"Worker {service_name} is healthy (took {elapsed}s)") + return True + elif health_status == "none": + # No health check defined, assume ready + status_msg = f"[green]Worker running (no health check) ({elapsed}s)[/green]" + status.update(status_msg) + console.print(f"โœ… Worker ready: {service_name} (took {elapsed}s)") + logger.info(f"Worker {service_name} is running, no health check (took {elapsed}s)") + return True + else: + status_msg = f"[cyan]Worker running ({elapsed}s)[/cyan]" + elif not container_state or container_state == "exited": + status_msg = f"[yellow]Waiting for container to start... ({elapsed}s)[/yellow]" + else: + status_msg = f"[cyan]Worker state: {container_state} ({elapsed}s)[/cyan]" + + # Show helpful hints at certain intervals + if elapsed == 10: + status_msg += " [dim](pulling image if not cached)[/dim]" + elif elapsed == 30: + status_msg += " [dim](large images can take time)[/dim]" + elif elapsed == 60: + status_msg += " [dim](still working...)[/dim]" + + # Update status if changed + if status_msg != last_status_msg: + status.update(status_msg) + last_status_msg = status_msg + logger.debug(f"Worker {service_name} - state: {container_state}, health: {health_status}") - while time.time() - start_time < timeout: - # Check if container is running - if not self.is_worker_running(service_name): - logger.debug(f"Worker {service_name} not running yet") time.sleep(self.health_check_interval) - continue - # Check container health status - try: - result = subprocess.run( - ["docker", "inspect", "-f", "{{.State.Health.Status}}", container_name], - capture_output=True, - text=True, - check=False - ) - - health_status = result.stdout.strip() - - # If no health check is defined, assume healthy after running - if health_status == "" or health_status == "": - logger.info(f"Worker {service_name} is running (no health check)") - console.print(f"โœ… Worker ready: {service_name}") - return True - - if health_status == "healthy": - logger.info(f"Worker {service_name} is healthy") - console.print(f"โœ… Worker ready: {service_name}") - return True - - logger.debug(f"Worker {service_name} health: {health_status}") - - except Exception as e: - logger.debug(f"Failed to check health: {e}") - - time.sleep(self.health_check_interval) - - elapsed = time.time() - start_time - logger.warning(f"Worker {service_name} did not become ready within {elapsed:.1f}s") - console.print(f"โš ๏ธ Worker startup timeout after {elapsed:.1f}s", style="yellow") - return False + # Timeout reached + elapsed = int(time.time() - start_time) + logger.warning(f"Worker {service_name} did not become ready within {elapsed}s") + console.print(f"โš ๏ธ Worker startup timeout after {elapsed}s", style="yellow") + console.print(f" Last state: {container_state}, health: {health_status}", style="dim") + return False def stop_worker(self, service_name: str) -> bool: """ @@ -432,6 +534,75 @@ class WorkerManager: console.print(f"โŒ Unexpected error: {e}", style="red") return False + def stop_all_workers(self) -> bool: + """ + Stop all running FuzzForge worker containers. + + This uses `docker stop` to stop worker containers individually, + avoiding the Docker Compose profile issue and preventing accidental + shutdown of core services. + + Returns: + True if all workers stopped successfully, False otherwise + """ + try: + console.print("๐Ÿ›‘ Stopping all FuzzForge workers...") + + # Get list of all running worker containers + result = subprocess.run( + ["docker", "ps", "--filter", "name=fuzzforge-worker-", "--format", "{{.Names}}"], + capture_output=True, + text=True, + check=False + ) + + running_workers = [name.strip() for name in result.stdout.splitlines() if name.strip()] + + if not running_workers: + console.print("โœ“ No workers running") + return True + + console.print(f"Found {len(running_workers)} running worker(s):") + for worker in running_workers: + console.print(f" - {worker}") + + # Stop each worker container individually using docker stop + # This is safer than docker compose down and won't affect core services + failed_workers = [] + for worker in running_workers: + try: + logger.info(f"Stopping {worker}...") + result = subprocess.run( + ["docker", "stop", worker], + capture_output=True, + text=True, + check=True, + timeout=30 + ) + console.print(f" โœ“ Stopped {worker}") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to stop {worker}: {e.stderr}") + failed_workers.append(worker) + console.print(f" โœ— Failed to stop {worker}", style="red") + except subprocess.TimeoutExpired: + logger.error(f"Timeout stopping {worker}") + failed_workers.append(worker) + console.print(f" โœ— Timeout stopping {worker}", style="red") + + if failed_workers: + console.print(f"\nโš ๏ธ {len(failed_workers)} worker(s) failed to stop", style="yellow") + console.print("๐Ÿ’ก Try manually: docker stop " + " ".join(failed_workers), style="dim") + return False + + console.print("\nโœ… All workers stopped") + logger.info("All workers stopped successfully") + return True + + except Exception as e: + logger.error(f"Unexpected error stopping workers: {e}") + console.print(f"โŒ Unexpected error: {e}", style="red") + return False + def ensure_worker_running( self, worker_info: Dict[str, Any], diff --git a/docs/docs/how-to/docker-setup.md b/docs/docs/how-to/docker-setup.md index 471f945..5496e62 100644 --- a/docs/docs/how-to/docker-setup.md +++ b/docs/docs/how-to/docker-setup.md @@ -110,7 +110,22 @@ fuzzforge workflow run secret_detection ./codebase ### Manual Worker Management -Start specific workers when needed: +FuzzForge CLI provides convenient commands for managing workers: + +```bash +# List all workers and their status +ff worker list +ff worker list --all # Include stopped workers + +# Start a specific worker +ff worker start python +ff worker start android --build # Rebuild before starting + +# Stop all workers +ff worker stop +``` + +You can also use Docker commands directly: ```bash # Start a single worker @@ -123,6 +138,33 @@ docker compose --profile workers up -d docker stop fuzzforge-worker-ossfuzz ``` +### Stopping Workers Properly + +The easiest way to stop workers is using the CLI: + +```bash +# Stop all running workers (recommended) +ff worker stop +``` + +This command safely stops all worker containers without affecting core services. + +Alternatively, you can use Docker commands: + +```bash +# Stop individual worker +docker stop fuzzforge-worker-python + +# Stop all workers using docker compose +# Note: This requires the --profile flag because workers are in profiles +docker compose down --profile workers +``` + +**Important:** Workers use Docker Compose profiles to prevent auto-starting. When using Docker commands directly: +- `docker compose down` (without `--profile workers`) does NOT stop workers +- Workers remain running unless explicitly stopped with the profile flag or `docker stop` +- Use `ff worker stop` for the safest option that won't affect core services + ### Resource Comparison | Command | Workers Started | RAM Usage | diff --git a/docs/docs/reference/cli-reference.md b/docs/docs/reference/cli-reference.md new file mode 100644 index 0000000..801398c --- /dev/null +++ b/docs/docs/reference/cli-reference.md @@ -0,0 +1,616 @@ +# FuzzForge CLI Reference + +Complete reference for the FuzzForge CLI (`ff` command). Use this as your quick lookup for all commands, options, and examples. + +--- + +## Global Options + +| Option | Description | +|--------|-------------| +| `--help`, `-h` | Show help message | +| `--version`, `-v` | Show version information | + +--- + +## Core Commands + +### `ff init` + +Initialize a new FuzzForge project in the current directory. + +**Usage:** +```bash +ff init [OPTIONS] +``` + +**Options:** +- `--name`, `-n` โ€” Project name (defaults to current directory name) +- `--api-url`, `-u` โ€” FuzzForge API URL (defaults to http://localhost:8000) +- `--force`, `-f` โ€” Force initialization even if project already exists + +**Examples:** +```bash +ff init # Initialize with defaults +ff init --name my-project # Set custom project name +ff init --api-url http://prod:8000 # Use custom API URL +``` + +--- + +### `ff status` + +Show project and latest execution status. + +**Usage:** +```bash +ff status +``` + +**Example Output:** +``` +๐Ÿ“Š Project Status + Project: my-security-project + API URL: http://localhost:8000 + +Latest Execution: + Run ID: security_scan-a1b2c3 + Workflow: security_assessment + Status: COMPLETED + Started: 2 hours ago +``` + +--- + +### `ff config` + +Manage project configuration. + +**Usage:** +```bash +ff config # Show all config +ff config # Get specific value +ff config # Set value +``` + +**Examples:** +```bash +ff config # Display all settings +ff config api_url # Get API URL +ff config api_url http://prod:8000 # Set API URL +``` + +--- + +### `ff clean` + +Clean old execution data and findings. + +**Usage:** +```bash +ff clean [OPTIONS] +``` + +**Options:** +- `--days`, `-d` โ€” Remove data older than this many days (default: 90) +- `--dry-run` โ€” Show what would be deleted without deleting + +**Examples:** +```bash +ff clean # Clean data older than 90 days +ff clean --days 30 # Clean data older than 30 days +ff clean --dry-run # Preview what would be deleted +``` + +--- + +## Workflow Commands + +### `ff workflows` + +Browse and list available workflows. + +**Usage:** +```bash +ff workflows [COMMAND] +``` + +**Subcommands:** +- `list` โ€” List all available workflows +- `info ` โ€” Show detailed workflow information +- `params ` โ€” Show workflow parameters + +**Examples:** +```bash +ff workflows list # List all workflows +ff workflows info python_sast # Show workflow details +ff workflows params python_sast # Show parameters +``` + +--- + +### `ff workflow` + +Execute and manage individual workflows. + +**Usage:** +```bash +ff workflow +``` + +**Subcommands:** + +#### `ff workflow run` + +Execute a security testing workflow. + +**Usage:** +```bash +ff workflow run [params...] [OPTIONS] +``` + +**Arguments:** +- `` โ€” Workflow name +- `` โ€” Target path to analyze +- `[params...]` โ€” Parameters as `key=value` pairs + +**Options:** +- `--param-file`, `-f` โ€” JSON file containing workflow parameters +- `--timeout`, `-t` โ€” Execution timeout in seconds +- `--interactive` / `--no-interactive`, `-i` / `-n` โ€” Interactive parameter input (default: interactive) +- `--wait`, `-w` โ€” Wait for execution to complete +- `--live`, `-l` โ€” Start live monitoring after execution +- `--auto-start` / `--no-auto-start` โ€” Automatically start required worker +- `--auto-stop` / `--no-auto-stop` โ€” Automatically stop worker after completion +- `--fail-on` โ€” Fail build if findings match SARIF level (error, warning, note, info, all, none) +- `--export-sarif` โ€” Export SARIF results to file after completion + +**Examples:** +```bash +# Basic workflow execution +ff workflow run python_sast ./project + +# With parameters +ff workflow run python_sast ./project check_secrets=true + +# CI/CD integration - fail on errors +ff workflow run python_sast ./project --wait --no-interactive \ + --fail-on error --export-sarif results.sarif + +# With parameter file +ff workflow run python_sast ./project --param-file config.json + +# Live monitoring for fuzzing +ff workflow run atheris_fuzzing ./project --live +``` + +#### `ff workflow status` + +Check status of latest or specific workflow execution. + +**Usage:** +```bash +ff workflow status [run_id] +``` + +**Examples:** +```bash +ff workflow status # Show latest execution status +ff workflow status python_sast-abc123 # Show specific execution +``` + +#### `ff workflow history` + +Show execution history. + +**Usage:** +```bash +ff workflow history [OPTIONS] +``` + +**Options:** +- `--limit`, `-l` โ€” Number of executions to show (default: 10) + +**Example:** +```bash +ff workflow history --limit 20 +``` + +#### `ff workflow retry` + +Retry a failed workflow execution. + +**Usage:** +```bash +ff workflow retry +``` + +**Example:** +```bash +ff workflow retry python_sast-abc123 +``` + +--- + +## Finding Commands + +### `ff findings` + +Browse all findings across executions. + +**Usage:** +```bash +ff findings [COMMAND] +``` + +**Subcommands:** + +#### `ff findings list` + +List findings from a specific run. + +**Usage:** +```bash +ff findings list [run_id] [OPTIONS] +``` + +**Options:** +- `--format` โ€” Output format: table, json, sarif (default: table) +- `--save` โ€” Save findings to file + +**Examples:** +```bash +ff findings list # Show latest findings +ff findings list python_sast-abc123 # Show specific run +ff findings list --format json # JSON output +ff findings list --format sarif --save # Export SARIF +``` + +#### `ff findings export` + +Export findings to various formats. + +**Usage:** +```bash +ff findings export [OPTIONS] +``` + +**Options:** +- `--format` โ€” Output format: json, sarif, csv +- `--output`, `-o` โ€” Output file path + +**Example:** +```bash +ff findings export python_sast-abc123 --format sarif --output results.sarif +``` + +#### `ff findings history` + +Show finding history across multiple runs. + +**Usage:** +```bash +ff findings history [OPTIONS] +``` + +**Options:** +- `--limit`, `-l` โ€” Number of runs to include (default: 10) + +--- + +### `ff finding` + +View and analyze individual findings. + +**Usage:** +```bash +ff finding [id] # Show latest or specific finding +ff finding show --rule # Show specific finding detail +``` + +**Examples:** +```bash +ff finding # Show latest finding +ff finding python_sast-abc123 # Show specific run findings +ff finding show python_sast-abc123 --rule f2cf5e3e # Show specific finding +``` + +--- + +## Worker Management Commands + +### `ff worker` + +Manage Temporal workers for workflow execution. + +**Usage:** +```bash +ff worker +``` + +**Subcommands:** + +#### `ff worker list` + +List FuzzForge workers and their status. + +**Usage:** +```bash +ff worker list [OPTIONS] +``` + +**Options:** +- `--all`, `-a` โ€” Show all workers (including stopped) + +**Examples:** +```bash +ff worker list # Show running workers +ff worker list --all # Show all workers +``` + +**Example Output:** +``` +FuzzForge Workers +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Worker โ”ƒ Status โ”ƒ Uptime โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ android โ”‚ โ— Running โ”‚ 5 minutes ago โ”‚ +โ”‚ python โ”‚ โ— Running โ”‚ 10 minutes ago โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โœ… 2 worker(s) running +``` + +#### `ff worker start` + +Start a specific worker. + +**Usage:** +```bash +ff worker start [OPTIONS] +``` + +**Arguments:** +- `` โ€” Worker name (e.g., python, android, rust, secrets) + +**Options:** +- `--build` โ€” Rebuild worker image before starting + +**Examples:** +```bash +ff worker start python # Start Python worker +ff worker start android --build # Rebuild and start Android worker +``` + +**Available Workers:** +- `python` โ€” Python security analysis and fuzzing +- `android` โ€” Android APK analysis +- `rust` โ€” Rust fuzzing and analysis +- `secrets` โ€” Secret detection workflows +- `ossfuzz` โ€” OSS-Fuzz integration + +#### `ff worker stop` + +Stop all running FuzzForge workers. + +**Usage:** +```bash +ff worker stop [OPTIONS] +``` + +**Options:** +- `--all` โ€” Stop all workers (default behavior, flag for clarity) + +**Example:** +```bash +ff worker stop +``` + +**Note:** This command stops only worker containers, leaving core services (backend, temporal, minio) running. + +--- + +## Monitoring Commands + +### `ff monitor` + +Real-time monitoring for running workflows. + +**Usage:** +```bash +ff monitor [COMMAND] +``` + +**Subcommands:** +- `live ` โ€” Live monitoring for a specific execution +- `stats ` โ€” Show statistics for fuzzing workflows + +**Examples:** +```bash +ff monitor live atheris-abc123 # Monitor fuzzing campaign +ff monitor stats atheris-abc123 # Show fuzzing statistics +``` + +--- + +## AI Integration Commands + +### `ff ai` + +AI-powered analysis and assistance. + +**Usage:** +```bash +ff ai [COMMAND] +``` + +**Subcommands:** +- `analyze ` โ€” Analyze findings with AI +- `explain ` โ€” Get AI explanation of a finding +- `remediate ` โ€” Get remediation suggestions + +**Examples:** +```bash +ff ai analyze python_sast-abc123 # Analyze all findings +ff ai explain python_sast-abc123:finding1 # Explain specific finding +ff ai remediate python_sast-abc123:finding1 # Get fix suggestions +``` + +--- + +## Knowledge Ingestion Commands + +### `ff ingest` + +Ingest knowledge into the AI knowledge base. + +**Usage:** +```bash +ff ingest [COMMAND] +``` + +**Subcommands:** +- `file ` โ€” Ingest a file +- `directory ` โ€” Ingest directory contents +- `workflow ` โ€” Ingest workflow documentation + +**Examples:** +```bash +ff ingest file ./docs/security.md # Ingest single file +ff ingest directory ./docs # Ingest directory +ff ingest workflow python_sast # Ingest workflow docs +``` + +--- + +## Common Workflow Examples + +### CI/CD Integration + +```bash +# Run security scan in CI, fail on errors +ff workflow run python_sast . \ + --wait \ + --no-interactive \ + --fail-on error \ + --export-sarif results.sarif +``` + +### Local Development + +```bash +# Quick security check +ff workflow run python_sast ./my-code + +# Check specific file types +ff workflow run python_sast . file_extensions='[".py",".js"]' + +# Interactive parameter configuration +ff workflow run python_sast . --interactive +``` + +### Fuzzing Workflows + +```bash +# Start fuzzing with live monitoring +ff workflow run atheris_fuzzing ./project --live + +# Long-running fuzzing campaign +ff workflow run ossfuzz_campaign ./project \ + --auto-start \ + duration=3600 \ + --live +``` + +### Worker Management + +```bash +# Check which workers are running +ff worker list + +# Start needed worker manually +ff worker start python --build + +# Stop all workers when done +ff worker stop +``` + +--- + +## Configuration Files + +### Project Config (`.fuzzforge/config.json`) + +```json +{ + "project_name": "my-security-project", + "api_url": "http://localhost:8000", + "default_workflow": "python_sast", + "auto_start_workers": true, + "auto_stop_workers": false +} +``` + +### Parameter File Example + +```json +{ + "check_secrets": true, + "file_extensions": [".py", ".js", ".go"], + "severity_threshold": "medium", + "exclude_patterns": ["**/test/**", "**/vendor/**"] +} +``` + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | General error | +| 2 | Findings matched `--fail-on` criteria | +| 3 | Worker startup failed | +| 4 | Workflow execution failed | + +--- + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `FUZZFORGE_API_URL` | Backend API URL | http://localhost:8000 | +| `FUZZFORGE_ROOT` | FuzzForge installation directory | Auto-detected | +| `FUZZFORGE_DEBUG` | Enable debug logging | false | + +--- + +## Tips and Best Practices + +1. **Use `--no-interactive` in CI/CD** โ€” Prevents prompts that would hang automated pipelines +2. **Use `--fail-on` for quality gates** โ€” Fail builds based on finding severity +3. **Export SARIF for tool integration** โ€” Most security tools support SARIF format +4. **Let workflows auto-start workers** โ€” More efficient than manually managing workers +5. **Use `--wait` with `--export-sarif`** โ€” Ensures results are available before export +6. **Check `ff worker list` regularly** โ€” Helps manage system resources +7. **Use parameter files for complex configs** โ€” Easier to version control and reuse + +--- + +## Related Documentation + +- [Docker Setup](../how-to/docker-setup.md) โ€” Worker management and Docker configuration +- [Getting Started](../tutorial/getting-started.md) โ€” Complete setup guide +- [Workflow Guide](../how-to/workflows.md) โ€” Detailed workflow documentation +- [CI/CD Integration](../how-to/ci-cd.md) โ€” CI/CD setup examples + +--- + +**Need Help?** + +```bash +ff --help # General help +ff workflow run --help # Command-specific help +ff worker --help # Worker management help +```