mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-07 14:57:47 +02: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.
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
from . import worker
|
||||||
|
|
||||||
|
__all__ = ["worker"]
|
||||||
|
|||||||
@@ -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,
|
config as config_cmd,
|
||||||
ai,
|
ai,
|
||||||
ingest,
|
ingest,
|
||||||
|
worker,
|
||||||
)
|
)
|
||||||
from .fuzzy import enhanced_command_not_found_handler
|
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(monitor.app, name="monitor", help="📊 Real-time monitoring")
|
||||||
app.add_typer(ai.app, name="ai", help="🤖 AI integration features")
|
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(ingest.app, name="ingest", help="🧠 Ingest knowledge into AI")
|
||||||
|
app.add_typer(worker.app, name="worker", help="🔧 Manage Temporal workers")
|
||||||
|
|
||||||
# Help and utility commands
|
# Help and utility commands
|
||||||
@app.command()
|
@app.command()
|
||||||
@@ -409,7 +411,7 @@ def main():
|
|||||||
'init', 'status', 'config', 'clean',
|
'init', 'status', 'config', 'clean',
|
||||||
'workflows', 'workflow',
|
'workflows', 'workflow',
|
||||||
'findings', 'finding',
|
'findings', 'finding',
|
||||||
'monitor', 'ai', 'ingest',
|
'monitor', 'ai', 'ingest', 'worker',
|
||||||
'version'
|
'version'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from typing import Optional, Dict, Any
|
|||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.status import Status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -163,11 +164,25 @@ class WorkerManager:
|
|||||||
Platform string: "linux/amd64" or "linux/arm64"
|
Platform string: "linux/amd64" or "linux/arm64"
|
||||||
"""
|
"""
|
||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
if machine in ["x86_64", "amd64"]:
|
system = platform.system().lower()
|
||||||
return "linux/amd64"
|
|
||||||
elif machine in ["arm64", "aarch64"]:
|
logger.debug(f"Platform detection: machine={machine}, system={system}")
|
||||||
return "linux/arm64"
|
|
||||||
return "unknown"
|
# 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:
|
def _read_worker_metadata(self, vertical: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -213,28 +228,39 @@ class WorkerManager:
|
|||||||
|
|
||||||
platforms = metadata.get("platforms", {})
|
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
|
# Try detected platform first
|
||||||
if detected_platform in platforms:
|
if detected_platform in platforms:
|
||||||
dockerfile = platforms[detected_platform].get("dockerfile", "Dockerfile")
|
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
|
return dockerfile
|
||||||
|
|
||||||
# Fallback to default platform
|
# Fallback to default platform
|
||||||
default_platform = metadata.get("default_platform", "linux/amd64")
|
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:
|
if default_platform in platforms:
|
||||||
dockerfile = platforms[default_platform].get("dockerfile", "Dockerfile.amd64")
|
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
|
return dockerfile
|
||||||
|
|
||||||
# Last resort
|
# Last resort: just use Dockerfile
|
||||||
|
logger.warning(f"No suitable Dockerfile found for {vertical}, using 'Dockerfile'")
|
||||||
return "Dockerfile"
|
return "Dockerfile"
|
||||||
|
|
||||||
def _run_docker_compose(self, *args: str, env: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess:
|
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:
|
||||||
*args: Arguments to pass to docker-compose
|
*args: Arguments to pass to docker compose
|
||||||
env: Optional environment variables to set
|
env: Optional environment variables to set
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -243,7 +269,7 @@ class WorkerManager:
|
|||||||
Raises:
|
Raises:
|
||||||
subprocess.CalledProcessError: If command fails
|
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)}")
|
logger.debug(f"Running: {' '.join(cmd)}")
|
||||||
|
|
||||||
# Merge with current environment
|
# Merge with current environment
|
||||||
@@ -342,9 +368,67 @@ class WorkerManager:
|
|||||||
console.print(f"❌ Unexpected error: {e}", style="red")
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
return False
|
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:
|
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.
|
Wait for a worker to be healthy and ready to process tasks.
|
||||||
|
Shows live progress updates during startup.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_name: Name of the Docker Compose service
|
service_name: Name of the Docker Compose service
|
||||||
@@ -352,56 +436,74 @@ class WorkerManager:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if worker is ready, False if timeout reached
|
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
|
timeout = timeout or self.startup_timeout
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
container_name = self._service_to_container_name(service_name)
|
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)
|
time.sleep(self.health_check_interval)
|
||||||
continue
|
|
||||||
|
|
||||||
# Check container health status
|
# Timeout reached
|
||||||
try:
|
elapsed = int(time.time() - start_time)
|
||||||
result = subprocess.run(
|
logger.warning(f"Worker {service_name} did not become ready within {elapsed}s")
|
||||||
["docker", "inspect", "-f", "{{.State.Health.Status}}", container_name],
|
console.print(f"⚠️ Worker startup timeout after {elapsed}s", style="yellow")
|
||||||
capture_output=True,
|
console.print(f" Last state: {container_state}, health: {health_status}", style="dim")
|
||||||
text=True,
|
return False
|
||||||
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
|
|
||||||
|
|
||||||
def stop_worker(self, service_name: str) -> bool:
|
def stop_worker(self, service_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -432,6 +534,75 @@ class WorkerManager:
|
|||||||
console.print(f"❌ Unexpected error: {e}", style="red")
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
return False
|
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(
|
def ensure_worker_running(
|
||||||
self,
|
self,
|
||||||
worker_info: Dict[str, Any],
|
worker_info: Dict[str, Any],
|
||||||
|
|||||||
@@ -110,7 +110,22 @@ fuzzforge workflow run secret_detection ./codebase
|
|||||||
|
|
||||||
### Manual Worker Management
|
### 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
|
```bash
|
||||||
# Start a single worker
|
# Start a single worker
|
||||||
@@ -123,6 +138,33 @@ docker compose --profile workers up -d
|
|||||||
docker stop fuzzforge-worker-ossfuzz
|
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
|
### Resource Comparison
|
||||||
|
|
||||||
| Command | Workers Started | RAM Usage |
|
| Command | Workers Started | RAM Usage |
|
||||||
|
|||||||
@@ -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