mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-02-12 19:12:49 +00:00
feat(cli): add worker management commands with improved progress feedback
Add comprehensive CLI commands for managing Temporal workers: - ff worker list - List workers with status and uptime - ff worker start <name> - 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
This commit is contained in:
@@ -12,3 +12,6 @@ Command modules for FuzzForge CLI.
|
||||
#
|
||||
# Additional attribution and requirements are provided in the NOTICE file.
|
||||
|
||||
from . import worker
|
||||
|
||||
__all__ = ["worker"]
|
||||
|
||||
225
cli/src/fuzzforge_cli/commands/worker.py
Normal file
225
cli/src/fuzzforge_cli/commands/worker.py
Normal file
@@ -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-<name>[/cyan]")
|
||||
console.print(" Or run a workflow, which auto-starts workers: [cyan]ff workflow run <workflow> <target>[/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()
|
||||
@@ -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'
|
||||
]
|
||||
|
||||
|
||||
@@ -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 == "<no value>" 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 == "<no value>" 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],
|
||||
|
||||
@@ -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 |
|
||||
|
||||
616
docs/docs/reference/cli-reference.md
Normal file
616
docs/docs/reference/cli-reference.md
Normal file
@@ -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 <key> # Get specific value
|
||||
ff config <key> <value> # 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 <workflow>` — Show detailed workflow information
|
||||
- `params <workflow>` — 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 <COMMAND>
|
||||
```
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
#### `ff workflow run`
|
||||
|
||||
Execute a security testing workflow.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
ff workflow run <workflow> <target> [params...] [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<workflow>` — Workflow name
|
||||
- `<target>` — 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 <run_id>
|
||||
```
|
||||
|
||||
**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 <run_id> [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 <run_id> --rule <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 <COMMAND>
|
||||
```
|
||||
|
||||
**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 <name> [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<name>` — 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 <run_id>` — Live monitoring for a specific execution
|
||||
- `stats <run_id>` — 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 <run_id>` — Analyze findings with AI
|
||||
- `explain <finding_id>` — Get AI explanation of a finding
|
||||
- `remediate <finding_id>` — 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 <path>` — Ingest a file
|
||||
- `directory <path>` — Ingest directory contents
|
||||
- `workflow <workflow_name>` — 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
|
||||
```
|
||||
Reference in New Issue
Block a user