mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-12 07:06:34 +02:00
Merge pull request #20 from FuzzingLabs/dev
Release: v0.7.1 - Worker fixes, monitor consolidation, and findings improvements
This commit is contained in:
@@ -55,9 +55,12 @@ async def get_run_status(
|
|||||||
is_failed = workflow_status == "FAILED"
|
is_failed = workflow_status == "FAILED"
|
||||||
is_running = workflow_status == "RUNNING"
|
is_running = workflow_status == "RUNNING"
|
||||||
|
|
||||||
|
# Extract workflow name from run_id (format: workflow_name-unique_id)
|
||||||
|
workflow_name = run_id.rsplit('-', 1)[0] if '-' in run_id else "unknown"
|
||||||
|
|
||||||
return WorkflowStatus(
|
return WorkflowStatus(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
workflow="unknown", # Temporal doesn't track workflow name in status
|
workflow=workflow_name,
|
||||||
status=workflow_status,
|
status=workflow_status,
|
||||||
is_completed=is_completed,
|
is_completed=is_completed,
|
||||||
is_failed=is_failed,
|
is_failed=is_failed,
|
||||||
@@ -123,6 +126,9 @@ async def get_run_findings(
|
|||||||
else:
|
else:
|
||||||
sarif = {}
|
sarif = {}
|
||||||
|
|
||||||
|
# Extract workflow name from run_id (format: workflow_name-unique_id)
|
||||||
|
workflow_name = run_id.rsplit('-', 1)[0] if '-' in run_id else "unknown"
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
metadata = {
|
metadata = {
|
||||||
"completion_time": status.get("close_time"),
|
"completion_time": status.get("close_time"),
|
||||||
@@ -130,7 +136,7 @@ async def get_run_findings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return WorkflowFindings(
|
return WorkflowFindings(
|
||||||
workflow="unknown",
|
workflow=workflow_name,
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
sarif=sarif,
|
sarif=sarif,
|
||||||
metadata=metadata
|
metadata=metadata
|
||||||
|
|||||||
@@ -566,7 +566,6 @@ async def get_workflow_worker_info(
|
|||||||
return {
|
return {
|
||||||
"workflow": workflow_name,
|
"workflow": workflow_name,
|
||||||
"vertical": vertical,
|
"vertical": vertical,
|
||||||
"worker_container": f"fuzzforge-worker-{vertical}",
|
|
||||||
"worker_service": f"worker-{vertical}",
|
"worker_service": f"worker-{vertical}",
|
||||||
"task_queue": f"{vertical}-queue",
|
"task_queue": f"{vertical}-queue",
|
||||||
"required": True
|
"required": True
|
||||||
|
|||||||
@@ -140,11 +140,145 @@ def get_findings(
|
|||||||
else: # table format
|
else: # table format
|
||||||
display_findings_table(findings.sarif)
|
display_findings_table(findings.sarif)
|
||||||
|
|
||||||
|
# Suggest export command and show command
|
||||||
|
console.print(f"\n💡 View full details of a finding: [bold cyan]ff finding show {run_id} --rule <rule-id>[/bold cyan]")
|
||||||
|
console.print(f"💡 Export these findings: [bold cyan]ff findings export {run_id} --format sarif[/bold cyan]")
|
||||||
|
console.print(" Supported formats: [cyan]sarif[/cyan] (standard), [cyan]json[/cyan], [cyan]csv[/cyan], [cyan]html[/cyan]")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"❌ Failed to get findings: {e}", style="red")
|
console.print(f"❌ Failed to get findings: {e}", style="red")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def show_finding(
|
||||||
|
run_id: str = typer.Argument(..., help="Run ID to get finding from"),
|
||||||
|
rule_id: str = typer.Option(..., "--rule", "-r", help="Rule ID of the specific finding to show")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
🔍 Show detailed information about a specific finding
|
||||||
|
|
||||||
|
This function is registered as a command in main.py under the finding (singular) command group.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
require_project()
|
||||||
|
validate_run_id(run_id)
|
||||||
|
|
||||||
|
# Try to get from database first, fallback to API
|
||||||
|
db = get_project_db()
|
||||||
|
findings_data = None
|
||||||
|
if db:
|
||||||
|
findings_data = db.get_findings(run_id)
|
||||||
|
|
||||||
|
if not findings_data:
|
||||||
|
with get_client() as client:
|
||||||
|
console.print(f"🔍 Fetching findings for run: {run_id}")
|
||||||
|
findings = client.get_run_findings(run_id)
|
||||||
|
sarif_data = findings.sarif
|
||||||
|
else:
|
||||||
|
sarif_data = findings_data.sarif_data
|
||||||
|
|
||||||
|
# Find the specific finding by rule_id
|
||||||
|
runs = sarif_data.get("runs", [])
|
||||||
|
if not runs:
|
||||||
|
console.print("❌ No findings data available", style="red")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
run_data = runs[0]
|
||||||
|
results = run_data.get("results", [])
|
||||||
|
tool = run_data.get("tool", {}).get("driver", {})
|
||||||
|
|
||||||
|
# Search for matching finding
|
||||||
|
matching_finding = None
|
||||||
|
for result in results:
|
||||||
|
if result.get("ruleId") == rule_id:
|
||||||
|
matching_finding = result
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matching_finding:
|
||||||
|
console.print(f"❌ No finding found with rule ID: {rule_id}", style="red")
|
||||||
|
console.print(f"💡 Use [bold cyan]ff findings get {run_id}[/bold cyan] to see all findings", style="dim")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Display detailed finding
|
||||||
|
display_finding_detail(matching_finding, tool, run_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ Failed to get finding: {e}", style="red")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def display_finding_detail(finding: Dict[str, Any], tool: Dict[str, Any], run_id: str):
|
||||||
|
"""Display detailed information about a single finding"""
|
||||||
|
rule_id = finding.get("ruleId", "unknown")
|
||||||
|
level = finding.get("level", "note")
|
||||||
|
message = finding.get("message", {})
|
||||||
|
message_text = message.get("text", "No summary available")
|
||||||
|
message_markdown = message.get("markdown", message_text)
|
||||||
|
|
||||||
|
# Get location
|
||||||
|
locations = finding.get("locations", [])
|
||||||
|
location_str = "Unknown location"
|
||||||
|
code_snippet = None
|
||||||
|
|
||||||
|
if locations:
|
||||||
|
physical_location = locations[0].get("physicalLocation", {})
|
||||||
|
artifact_location = physical_location.get("artifactLocation", {})
|
||||||
|
region = physical_location.get("region", {})
|
||||||
|
|
||||||
|
file_path = artifact_location.get("uri", "")
|
||||||
|
if file_path:
|
||||||
|
location_str = file_path
|
||||||
|
if region.get("startLine"):
|
||||||
|
location_str += f":{region['startLine']}"
|
||||||
|
if region.get("startColumn"):
|
||||||
|
location_str += f":{region['startColumn']}"
|
||||||
|
|
||||||
|
# Get code snippet if available
|
||||||
|
if region.get("snippet", {}).get("text"):
|
||||||
|
code_snippet = region["snippet"]["text"].strip()
|
||||||
|
|
||||||
|
# Get severity style
|
||||||
|
severity_color = {
|
||||||
|
"error": "red",
|
||||||
|
"warning": "yellow",
|
||||||
|
"note": "blue",
|
||||||
|
"info": "cyan"
|
||||||
|
}.get(level.lower(), "white")
|
||||||
|
|
||||||
|
# Build detailed content
|
||||||
|
content_lines = []
|
||||||
|
content_lines.append(f"[bold]Rule ID:[/bold] {rule_id}")
|
||||||
|
content_lines.append(f"[bold]Severity:[/bold] [{severity_color}]{level.upper()}[/{severity_color}]")
|
||||||
|
content_lines.append(f"[bold]Location:[/bold] {location_str}")
|
||||||
|
content_lines.append(f"[bold]Tool:[/bold] {tool.get('name', 'Unknown')} v{tool.get('version', 'unknown')}")
|
||||||
|
content_lines.append(f"[bold]Run ID:[/bold] {run_id}")
|
||||||
|
content_lines.append("")
|
||||||
|
content_lines.append(f"[bold]Summary:[/bold]")
|
||||||
|
content_lines.append(message_text)
|
||||||
|
content_lines.append("")
|
||||||
|
content_lines.append(f"[bold]Description:[/bold]")
|
||||||
|
content_lines.append(message_markdown)
|
||||||
|
|
||||||
|
if code_snippet:
|
||||||
|
content_lines.append("")
|
||||||
|
content_lines.append(f"[bold]Code Snippet:[/bold]")
|
||||||
|
content_lines.append(f"[dim]{code_snippet}[/dim]")
|
||||||
|
|
||||||
|
content = "\n".join(content_lines)
|
||||||
|
|
||||||
|
# Display in panel
|
||||||
|
console.print()
|
||||||
|
console.print(Panel(
|
||||||
|
content,
|
||||||
|
title=f"🔍 Finding Detail",
|
||||||
|
border_style=severity_color,
|
||||||
|
box=box.ROUNDED,
|
||||||
|
padding=(1, 2)
|
||||||
|
))
|
||||||
|
console.print()
|
||||||
|
console.print(f"💡 Export this run: [bold cyan]ff findings export {run_id} --format sarif[/bold cyan]")
|
||||||
|
|
||||||
|
|
||||||
def display_findings_table(sarif_data: Dict[str, Any]):
|
def display_findings_table(sarif_data: Dict[str, Any]):
|
||||||
"""Display SARIF findings in a rich table format"""
|
"""Display SARIF findings in a rich table format"""
|
||||||
runs = sarif_data.get("runs", [])
|
runs = sarif_data.get("runs", [])
|
||||||
@@ -195,8 +329,8 @@ def display_findings_table(sarif_data: Dict[str, Any]):
|
|||||||
# Detailed results - Rich Text-based table with proper emoji alignment
|
# Detailed results - Rich Text-based table with proper emoji alignment
|
||||||
results_table = Table(box=box.ROUNDED)
|
results_table = Table(box=box.ROUNDED)
|
||||||
results_table.add_column("Severity", width=12, justify="left", no_wrap=True)
|
results_table.add_column("Severity", width=12, justify="left", no_wrap=True)
|
||||||
results_table.add_column("Rule", width=25, justify="left", style="bold cyan", no_wrap=True)
|
results_table.add_column("Rule", justify="left", style="bold cyan", no_wrap=True)
|
||||||
results_table.add_column("Message", width=55, justify="left", no_wrap=True)
|
results_table.add_column("Message", width=45, justify="left", no_wrap=True)
|
||||||
results_table.add_column("Location", width=20, justify="left", style="dim", no_wrap=True)
|
results_table.add_column("Location", width=20, justify="left", style="dim", no_wrap=True)
|
||||||
|
|
||||||
for result in results[:50]: # Limit to first 50 results
|
for result in results[:50]: # Limit to first 50 results
|
||||||
@@ -224,18 +358,16 @@ def display_findings_table(sarif_data: Dict[str, Any]):
|
|||||||
severity_text = Text(level.upper(), style=severity_style(level))
|
severity_text = Text(level.upper(), style=severity_style(level))
|
||||||
severity_text.truncate(12, overflow="ellipsis")
|
severity_text.truncate(12, overflow="ellipsis")
|
||||||
|
|
||||||
rule_text = Text(rule_id)
|
# Show full rule ID without truncation
|
||||||
rule_text.truncate(25, overflow="ellipsis")
|
|
||||||
|
|
||||||
message_text = Text(message)
|
message_text = Text(message)
|
||||||
message_text.truncate(55, overflow="ellipsis")
|
message_text.truncate(45, overflow="ellipsis")
|
||||||
|
|
||||||
location_text = Text(location_str)
|
location_text = Text(location_str)
|
||||||
location_text.truncate(20, overflow="ellipsis")
|
location_text.truncate(20, overflow="ellipsis")
|
||||||
|
|
||||||
results_table.add_row(
|
results_table.add_row(
|
||||||
severity_text,
|
severity_text,
|
||||||
rule_text,
|
rule_id, # Pass string directly to show full UUID
|
||||||
message_text,
|
message_text,
|
||||||
location_text
|
location_text
|
||||||
)
|
)
|
||||||
@@ -307,16 +439,20 @@ def findings_history(
|
|||||||
def export_findings(
|
def export_findings(
|
||||||
run_id: str = typer.Argument(..., help="Run ID to export findings for"),
|
run_id: str = typer.Argument(..., help="Run ID to export findings for"),
|
||||||
format: str = typer.Option(
|
format: str = typer.Option(
|
||||||
"json", "--format", "-f",
|
"sarif", "--format", "-f",
|
||||||
help="Export format: json, csv, html, sarif"
|
help="Export format: sarif (standard), json, csv, html"
|
||||||
),
|
),
|
||||||
output: Optional[str] = typer.Option(
|
output: Optional[str] = typer.Option(
|
||||||
None, "--output", "-o",
|
None, "--output", "-o",
|
||||||
help="Output file path (defaults to findings-<run-id>.<format>)"
|
help="Output file path (defaults to findings-<run-id>-<timestamp>.<format>)"
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
📤 Export security findings in various formats
|
📤 Export security findings in various formats
|
||||||
|
|
||||||
|
SARIF is the standard format for security findings and is recommended
|
||||||
|
for interoperability with other security tools. Filenames are automatically
|
||||||
|
made unique with timestamps to prevent overwriting previous exports.
|
||||||
"""
|
"""
|
||||||
db = get_project_db()
|
db = get_project_db()
|
||||||
if not db:
|
if not db:
|
||||||
@@ -334,9 +470,10 @@ def export_findings(
|
|||||||
else:
|
else:
|
||||||
sarif_data = findings_data.sarif_data
|
sarif_data = findings_data.sarif_data
|
||||||
|
|
||||||
# Generate output filename
|
# Generate output filename with timestamp for uniqueness
|
||||||
if not output:
|
if not output:
|
||||||
output = f"findings-{run_id[:8]}.{format}"
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
output = f"findings-{run_id[:8]}-{timestamp}.{format}"
|
||||||
|
|
||||||
output_path = Path(output)
|
output_path = Path(output)
|
||||||
|
|
||||||
|
|||||||
@@ -59,66 +59,6 @@ def format_number(num: int) -> str:
|
|||||||
return str(num)
|
return str(num)
|
||||||
|
|
||||||
|
|
||||||
@app.command("stats")
|
|
||||||
def fuzzing_stats(
|
|
||||||
run_id: str = typer.Argument(..., help="Run ID to get statistics for"),
|
|
||||||
refresh: int = typer.Option(
|
|
||||||
5, "--refresh", "-r",
|
|
||||||
help="Refresh interval in seconds"
|
|
||||||
),
|
|
||||||
once: bool = typer.Option(
|
|
||||||
False, "--once",
|
|
||||||
help="Show stats once and exit"
|
|
||||||
)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
📊 Show current fuzzing statistics for a run
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with get_client() as client:
|
|
||||||
if once:
|
|
||||||
# Show stats once
|
|
||||||
stats = client.get_fuzzing_stats(run_id)
|
|
||||||
display_stats_table(stats)
|
|
||||||
else:
|
|
||||||
# Live updating stats
|
|
||||||
console.print(f"📊 [bold]Live Fuzzing Statistics[/bold] (Run: {run_id[:12]}...)")
|
|
||||||
console.print(f"Refreshing every {refresh}s. Press Ctrl+C to stop.\n")
|
|
||||||
|
|
||||||
with Live(auto_refresh=False, console=console) as live:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
# Check workflow status
|
|
||||||
run_status = client.get_run_status(run_id)
|
|
||||||
stats = client.get_fuzzing_stats(run_id)
|
|
||||||
table = create_stats_table(stats)
|
|
||||||
live.update(table, refresh=True)
|
|
||||||
|
|
||||||
# Exit if workflow completed or failed
|
|
||||||
if getattr(run_status, 'is_completed', False) or getattr(run_status, 'is_failed', False):
|
|
||||||
final_status = getattr(run_status, 'status', 'Unknown')
|
|
||||||
if getattr(run_status, 'is_completed', False):
|
|
||||||
console.print("\n✅ [bold green]Workflow completed[/bold green]", style="green")
|
|
||||||
else:
|
|
||||||
console.print(f"\n⚠️ [bold yellow]Workflow ended[/bold yellow] | Status: {final_status}", style="yellow")
|
|
||||||
break
|
|
||||||
|
|
||||||
time.sleep(refresh)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
console.print("\n📊 Monitoring stopped", style="yellow")
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
console.print(f"❌ Failed to get fuzzing stats: {e}", style="red")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def display_stats_table(stats):
|
|
||||||
"""Display stats in a simple table"""
|
|
||||||
table = create_stats_table(stats)
|
|
||||||
console.print(table)
|
|
||||||
|
|
||||||
|
|
||||||
def create_stats_table(stats) -> Panel:
|
def create_stats_table(stats) -> Panel:
|
||||||
"""Create a rich table for fuzzing statistics"""
|
"""Create a rich table for fuzzing statistics"""
|
||||||
# Create main stats table
|
# Create main stats table
|
||||||
@@ -266,8 +206,8 @@ def crash_reports(
|
|||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _live_monitor(run_id: str, refresh: int):
|
def _live_monitor(run_id: str, refresh: int, once: bool = False, style: str = "inline"):
|
||||||
"""Helper for live monitoring with inline real-time display"""
|
"""Helper for live monitoring with inline real-time display or table display"""
|
||||||
with get_client() as client:
|
with get_client() as client:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
@@ -319,7 +259,6 @@ def _live_monitor(run_id: str, refresh: int):
|
|||||||
self.elapsed_time = 0
|
self.elapsed_time = 0
|
||||||
self.last_crash_time = None
|
self.last_crash_time = None
|
||||||
|
|
||||||
with Live(auto_refresh=False, console=console) as live:
|
|
||||||
# Initial fetch
|
# Initial fetch
|
||||||
try:
|
try:
|
||||||
run_status = client.get_run_status(run_id)
|
run_status = client.get_run_status(run_id)
|
||||||
@@ -328,6 +267,20 @@ def _live_monitor(run_id: str, refresh: int):
|
|||||||
stats = FallbackStats(run_id)
|
stats = FallbackStats(run_id)
|
||||||
run_status = type("RS", (), {"status":"Unknown","is_completed":False,"is_failed":False})()
|
run_status = type("RS", (), {"status":"Unknown","is_completed":False,"is_failed":False})()
|
||||||
|
|
||||||
|
# Handle --once mode: show stats once and exit
|
||||||
|
if once:
|
||||||
|
if style == "table":
|
||||||
|
console.print(create_stats_table(stats))
|
||||||
|
else:
|
||||||
|
console.print(render_inline_stats(run_status, stats))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Live monitoring mode
|
||||||
|
with Live(auto_refresh=False, console=console) as live:
|
||||||
|
# Render based on style
|
||||||
|
if style == "table":
|
||||||
|
live.update(create_stats_table(stats), refresh=True)
|
||||||
|
else:
|
||||||
live.update(render_inline_stats(run_status, stats), refresh=True)
|
live.update(render_inline_stats(run_status, stats), refresh=True)
|
||||||
|
|
||||||
# Polling loop
|
# Polling loop
|
||||||
@@ -354,7 +307,10 @@ def _live_monitor(run_id: str, refresh: int):
|
|||||||
except Exception:
|
except Exception:
|
||||||
stats = FallbackStats(run_id)
|
stats = FallbackStats(run_id)
|
||||||
|
|
||||||
# Update display
|
# Update display based on style
|
||||||
|
if style == "table":
|
||||||
|
live.update(create_stats_table(stats), refresh=True)
|
||||||
|
else:
|
||||||
live.update(render_inline_stats(run_status, stats), refresh=True)
|
live.update(render_inline_stats(run_status, stats), refresh=True)
|
||||||
|
|
||||||
# Check if completed
|
# Check if completed
|
||||||
@@ -386,17 +342,36 @@ def live_monitor(
|
|||||||
refresh: int = typer.Option(
|
refresh: int = typer.Option(
|
||||||
2, "--refresh", "-r",
|
2, "--refresh", "-r",
|
||||||
help="Refresh interval in seconds"
|
help="Refresh interval in seconds"
|
||||||
|
),
|
||||||
|
once: bool = typer.Option(
|
||||||
|
False, "--once",
|
||||||
|
help="Show stats once and exit"
|
||||||
|
),
|
||||||
|
style: str = typer.Option(
|
||||||
|
"inline", "--style",
|
||||||
|
help="Display style: 'inline' (default) or 'table'"
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
📺 Real-time inline monitoring with live statistics updates
|
📺 Real-time monitoring with live statistics updates
|
||||||
|
|
||||||
|
Display styles:
|
||||||
|
- inline: Visual inline display with emojis (default)
|
||||||
|
- table: Clean table-based display
|
||||||
|
|
||||||
|
Use --once to show stats once without continuous monitoring (useful for scripts)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_live_monitor(run_id, refresh)
|
# Validate style
|
||||||
|
if style not in ["inline", "table"]:
|
||||||
|
console.print(f"❌ Invalid style: {style}. Must be 'inline' or 'table'", style="red")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_live_monitor(run_id, refresh, once, style)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
console.print("\n\n📊 Monitoring stopped by user.", style="yellow")
|
console.print("\n\n📊 Monitoring stopped by user.", style="yellow")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"\n❌ Failed to start live monitoring: {e}", style="red")
|
console.print(f"\n❌ Failed to start monitoring: {e}", style="red")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -423,6 +398,5 @@ def monitor_callback(ctx: typer.Context):
|
|||||||
console = Console()
|
console = Console()
|
||||||
console.print("📊 [bold cyan]Monitor Command[/bold cyan]")
|
console.print("📊 [bold cyan]Monitor Command[/bold cyan]")
|
||||||
console.print("\nAvailable subcommands:")
|
console.print("\nAvailable subcommands:")
|
||||||
console.print(" • [cyan]ff monitor stats <run-id>[/cyan] - Show execution statistics")
|
console.print(" • [cyan]ff monitor live <run-id>[/cyan] - Monitor with live updates (supports --once, --style)")
|
||||||
console.print(" • [cyan]ff monitor crashes <run-id>[/cyan] - Show crash reports")
|
console.print(" • [cyan]ff monitor crashes <run-id>[/cyan] - Show crash reports")
|
||||||
console.print(" • [cyan]ff monitor live <run-id>[/cyan] - Real-time inline monitoring")
|
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ def execute_workflow(
|
|||||||
should_auto_start = auto_start if auto_start is not None else config.workers.auto_start_workers
|
should_auto_start = auto_start if auto_start is not None else config.workers.auto_start_workers
|
||||||
should_auto_stop = auto_stop if auto_stop is not None else config.workers.auto_stop_workers
|
should_auto_stop = auto_stop if auto_stop is not None else config.workers.auto_stop_workers
|
||||||
|
|
||||||
worker_container = None # Track for cleanup
|
worker_service = None # Track for cleanup
|
||||||
worker_mgr = None
|
worker_mgr = None
|
||||||
wait_completed = False # Track if wait completed successfully
|
wait_completed = False # Track if wait completed successfully
|
||||||
|
|
||||||
@@ -384,7 +384,6 @@ def execute_workflow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Ensure worker is running
|
# Ensure worker is running
|
||||||
worker_container = worker_info["worker_container"]
|
|
||||||
worker_service = worker_info.get("worker_service", f"worker-{worker_info['vertical']}")
|
worker_service = worker_info.get("worker_service", f"worker-{worker_info['vertical']}")
|
||||||
if not worker_mgr.ensure_worker_running(worker_info, auto_start=should_auto_start):
|
if not worker_mgr.ensure_worker_running(worker_info, auto_start=should_auto_start):
|
||||||
console.print(
|
console.print(
|
||||||
@@ -434,7 +433,7 @@ def execute_workflow(
|
|||||||
# Don't fail the whole operation if database save fails
|
# Don't fail the whole operation if database save fails
|
||||||
console.print(f"⚠️ Failed to save execution to database: {e}", style="yellow")
|
console.print(f"⚠️ Failed to save execution to database: {e}", style="yellow")
|
||||||
|
|
||||||
console.print(f"\n💡 Monitor progress: [bold cyan]fuzzforge monitor stats {response.run_id}[/bold cyan]")
|
console.print(f"\n💡 Monitor progress: [bold cyan]fuzzforge monitor live {response.run_id}[/bold cyan]")
|
||||||
console.print(f"💡 Check status: [bold cyan]fuzzforge workflow status {response.run_id}[/bold cyan]")
|
console.print(f"💡 Check status: [bold cyan]fuzzforge workflow status {response.run_id}[/bold cyan]")
|
||||||
|
|
||||||
# Suggest --live for fuzzing workflows
|
# Suggest --live for fuzzing workflows
|
||||||
@@ -461,7 +460,7 @@ def execute_workflow(
|
|||||||
console.print("\n⏹️ Live monitoring stopped (execution continues in background)", style="yellow")
|
console.print("\n⏹️ Live monitoring stopped (execution continues in background)", style="yellow")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"⚠️ Failed to start live monitoring: {e}", style="yellow")
|
console.print(f"⚠️ Failed to start live monitoring: {e}", style="yellow")
|
||||||
console.print(f"💡 You can still monitor manually: [bold cyan]fuzzforge monitor {response.run_id}[/bold cyan]")
|
console.print(f"💡 You can still monitor manually: [bold cyan]fuzzforge monitor live {response.run_id}[/bold cyan]")
|
||||||
|
|
||||||
# Wait for completion if requested
|
# Wait for completion if requested
|
||||||
elif wait:
|
elif wait:
|
||||||
@@ -527,11 +526,11 @@ def execute_workflow(
|
|||||||
handle_error(e, "executing workflow")
|
handle_error(e, "executing workflow")
|
||||||
finally:
|
finally:
|
||||||
# Stop worker if auto-stop is enabled and wait completed
|
# Stop worker if auto-stop is enabled and wait completed
|
||||||
if should_auto_stop and worker_container and worker_mgr and wait_completed:
|
if should_auto_stop and worker_service and worker_mgr and wait_completed:
|
||||||
try:
|
try:
|
||||||
console.print("\n🛑 Stopping worker (auto-stop enabled)...")
|
console.print("\n🛑 Stopping worker (auto-stop enabled)...")
|
||||||
if worker_mgr.stop_worker(worker_container):
|
if worker_mgr.stop_worker(worker_service):
|
||||||
console.print(f"✅ Worker stopped: {worker_container}")
|
console.print(f"✅ Worker stopped: {worker_service}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(
|
console.print(
|
||||||
f"⚠️ Failed to stop worker: {e}",
|
f"⚠️ Failed to stop worker: {e}",
|
||||||
@@ -608,7 +607,7 @@ def workflow_status(
|
|||||||
|
|
||||||
# Show next steps
|
# Show next steps
|
||||||
if status.is_running:
|
if status.is_running:
|
||||||
console.print(f"\n💡 Monitor live: [bold cyan]fuzzforge monitor {execution_id}[/bold cyan]")
|
console.print(f"\n💡 Monitor live: [bold cyan]fuzzforge monitor live {execution_id}[/bold cyan]")
|
||||||
elif status.is_completed:
|
elif status.is_completed:
|
||||||
console.print(f"💡 View findings: [bold cyan]fuzzforge finding {execution_id}[/bold cyan]")
|
console.print(f"💡 View findings: [bold cyan]fuzzforge finding {execution_id}[/bold cyan]")
|
||||||
elif status.is_failed:
|
elif status.is_failed:
|
||||||
@@ -770,7 +769,7 @@ def retry_workflow(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"⚠️ Failed to save execution to database: {e}", style="yellow")
|
console.print(f"⚠️ Failed to save execution to database: {e}", style="yellow")
|
||||||
|
|
||||||
console.print(f"\n💡 Monitor progress: [bold cyan]fuzzforge monitor stats {response.run_id}[/bold cyan]")
|
console.print(f"\n💡 Monitor progress: [bold cyan]fuzzforge monitor live {response.run_id}[/bold cyan]")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
handle_error(e, "retrying workflow")
|
handle_error(e, "retrying workflow")
|
||||||
|
|||||||
@@ -260,57 +260,17 @@ def workflow_main():
|
|||||||
|
|
||||||
# === Finding commands (singular) ===
|
# === Finding commands (singular) ===
|
||||||
|
|
||||||
@finding_app.command("export")
|
@finding_app.command("show")
|
||||||
def export_finding(
|
def show_finding_detail(
|
||||||
execution_id: Optional[str] = typer.Argument(None, help="Execution ID (defaults to latest)"),
|
run_id: str = typer.Argument(..., help="Run ID to get finding from"),
|
||||||
format: str = typer.Option(
|
rule_id: str = typer.Option(..., "--rule", "-r", help="Rule ID of the specific finding to show")
|
||||||
"sarif", "--format", "-f",
|
|
||||||
help="Export format: sarif, json, csv"
|
|
||||||
),
|
|
||||||
output: Optional[str] = typer.Option(
|
|
||||||
None, "--output", "-o",
|
|
||||||
help="Output file (defaults to stdout)"
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
📤 Export findings to file
|
🔍 Show detailed information about a specific finding
|
||||||
"""
|
"""
|
||||||
from .commands.findings import export_findings
|
from .commands.findings import show_finding
|
||||||
from .database import get_project_db
|
show_finding(run_id=run_id, rule_id=rule_id)
|
||||||
from .exceptions import require_project
|
|
||||||
|
|
||||||
try:
|
|
||||||
require_project()
|
|
||||||
|
|
||||||
# If no ID provided, get the latest
|
|
||||||
if not execution_id:
|
|
||||||
db = get_project_db()
|
|
||||||
if db:
|
|
||||||
recent_runs = db.list_runs(limit=1)
|
|
||||||
if recent_runs:
|
|
||||||
execution_id = recent_runs[0].run_id
|
|
||||||
console.print(f"🔍 Using most recent execution: {execution_id}")
|
|
||||||
else:
|
|
||||||
console.print("⚠️ No findings found in project database", style="yellow")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
console.print("❌ No project database found", style="red")
|
|
||||||
return
|
|
||||||
|
|
||||||
export_findings(run_id=execution_id, format=format, output=output)
|
|
||||||
except Exception as e:
|
|
||||||
console.print(f"❌ Failed to export findings: {e}", style="red")
|
|
||||||
|
|
||||||
|
|
||||||
@finding_app.command("analyze")
|
|
||||||
def analyze_finding(
|
|
||||||
finding_id: Optional[str] = typer.Argument(None, help="Finding ID to analyze")
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
🤖 AI analysis of a finding
|
|
||||||
"""
|
|
||||||
from .commands.ai import analyze_finding as ai_analyze
|
|
||||||
ai_analyze(finding_id)
|
|
||||||
|
|
||||||
@finding_app.callback(invoke_without_command=True)
|
@finding_app.callback(invoke_without_command=True)
|
||||||
def finding_main(
|
def finding_main(
|
||||||
@@ -322,7 +282,7 @@ def finding_main(
|
|||||||
Examples:
|
Examples:
|
||||||
fuzzforge finding # Show latest finding
|
fuzzforge finding # Show latest finding
|
||||||
fuzzforge finding <id> # Show specific finding
|
fuzzforge finding <id> # Show specific finding
|
||||||
fuzzforge finding export # Export latest findings
|
fuzzforge finding show <run-id> --rule <id> # Show specific finding detail
|
||||||
"""
|
"""
|
||||||
# Check if a subcommand is being invoked
|
# Check if a subcommand is being invoked
|
||||||
if ctx.invoked_subcommand is not None:
|
if ctx.invoked_subcommand is not None:
|
||||||
@@ -418,7 +378,7 @@ def main():
|
|||||||
|
|
||||||
# Handle finding command with pattern recognition
|
# Handle finding command with pattern recognition
|
||||||
if len(args) >= 2 and args[0] == 'finding':
|
if len(args) >= 2 and args[0] == 'finding':
|
||||||
finding_subcommands = ['export', 'analyze']
|
finding_subcommands = ['show']
|
||||||
# Skip custom dispatching if help flags are present
|
# Skip custom dispatching if help flags are present
|
||||||
if not any(arg in ['--help', '-h', '--version', '-v'] for arg in args):
|
if not any(arg in ['--help', '-h', '--version', '-v'] for arg in args):
|
||||||
if args[1] not in finding_subcommands:
|
if args[1] not in finding_subcommands:
|
||||||
|
|||||||
@@ -95,17 +95,30 @@ class WorkerManager:
|
|||||||
check=True
|
check=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_worker_running(self, container_name: str) -> bool:
|
def _service_to_container_name(self, service_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Check if a worker container is running.
|
Convert service name to container name based on docker-compose naming convention.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
container_name: Name of the Docker container (e.g., "fuzzforge-worker-ossfuzz")
|
service_name: Docker Compose service name (e.g., "worker-python")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Container name (e.g., "fuzzforge-worker-python")
|
||||||
|
"""
|
||||||
|
return f"fuzzforge-{service_name}"
|
||||||
|
|
||||||
|
def is_worker_running(self, service_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a worker service is running.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service_name: Name of the Docker Compose service (e.g., "worker-ossfuzz")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if container is running, False otherwise
|
True if container is running, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
container_name = self._service_to_container_name(service_name)
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
|
["docker", "inspect", "-f", "{{.State.Running}}", container_name],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -120,46 +133,42 @@ class WorkerManager:
|
|||||||
logger.debug(f"Failed to check worker status: {e}")
|
logger.debug(f"Failed to check worker status: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def start_worker(self, container_name: str) -> bool:
|
def start_worker(self, service_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Start a worker container using docker.
|
Start a worker service using docker-compose.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
container_name: Name of the Docker container to start
|
service_name: Name of the Docker Compose service to start (e.g., "worker-python")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if started successfully, False otherwise
|
True if started successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
console.print(f"🚀 Starting worker: {container_name}")
|
console.print(f"🚀 Starting worker: {service_name}")
|
||||||
|
|
||||||
# Use docker start directly (works with container name)
|
# Use docker-compose up to create and start the service
|
||||||
subprocess.run(
|
result = self._run_docker_compose("up", "-d", service_name)
|
||||||
["docker", "start", container_name],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Worker {container_name} started")
|
logger.info(f"Worker {service_name} started")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"Failed to start worker {container_name}: {e.stderr}")
|
logger.error(f"Failed to start worker {service_name}: {e.stderr}")
|
||||||
console.print(f"❌ Failed to start worker: {e.stderr}", style="red")
|
console.print(f"❌ Failed to start worker: {e.stderr}", style="red")
|
||||||
|
console.print(f"💡 Start the worker manually: docker compose up -d {service_name}", style="yellow")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error starting worker {container_name}: {e}")
|
logger.error(f"Unexpected error starting worker {service_name}: {e}")
|
||||||
console.print(f"❌ Unexpected error: {e}", style="red")
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def wait_for_worker_ready(self, container_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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
container_name: Name of the Docker container
|
service_name: Name of the Docker Compose service
|
||||||
timeout: Maximum seconds to wait (uses instance default if not specified)
|
timeout: Maximum seconds to wait (uses instance default if not specified)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -170,13 +179,14 @@ class WorkerManager:
|
|||||||
"""
|
"""
|
||||||
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)
|
||||||
|
|
||||||
console.print("⏳ Waiting for worker to be ready...")
|
console.print("⏳ Waiting for worker to be ready...")
|
||||||
|
|
||||||
while time.time() - start_time < timeout:
|
while time.time() - start_time < timeout:
|
||||||
# Check if container is running
|
# Check if container is running
|
||||||
if not self.is_worker_running(container_name):
|
if not self.is_worker_running(service_name):
|
||||||
logger.debug(f"Worker {container_name} not running yet")
|
logger.debug(f"Worker {service_name} not running yet")
|
||||||
time.sleep(self.health_check_interval)
|
time.sleep(self.health_check_interval)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -193,16 +203,16 @@ class WorkerManager:
|
|||||||
|
|
||||||
# If no health check is defined, assume healthy after running
|
# If no health check is defined, assume healthy after running
|
||||||
if health_status == "<no value>" or health_status == "":
|
if health_status == "<no value>" or health_status == "":
|
||||||
logger.info(f"Worker {container_name} is running (no health check)")
|
logger.info(f"Worker {service_name} is running (no health check)")
|
||||||
console.print(f"✅ Worker ready: {container_name}")
|
console.print(f"✅ Worker ready: {service_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if health_status == "healthy":
|
if health_status == "healthy":
|
||||||
logger.info(f"Worker {container_name} is healthy")
|
logger.info(f"Worker {service_name} is healthy")
|
||||||
console.print(f"✅ Worker ready: {container_name}")
|
console.print(f"✅ Worker ready: {service_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug(f"Worker {container_name} health: {health_status}")
|
logger.debug(f"Worker {service_name} health: {health_status}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Failed to check health: {e}")
|
logger.debug(f"Failed to check health: {e}")
|
||||||
@@ -210,41 +220,36 @@ class WorkerManager:
|
|||||||
time.sleep(self.health_check_interval)
|
time.sleep(self.health_check_interval)
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
logger.warning(f"Worker {container_name} did not become ready within {elapsed:.1f}s")
|
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")
|
console.print(f"⚠️ Worker startup timeout after {elapsed:.1f}s", style="yellow")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop_worker(self, container_name: str) -> bool:
|
def stop_worker(self, service_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Stop a worker container using docker.
|
Stop a worker service using docker-compose.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
container_name: Name of the Docker container to stop
|
service_name: Name of the Docker Compose service to stop
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if stopped successfully, False otherwise
|
True if stopped successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
console.print(f"🛑 Stopping worker: {container_name}")
|
console.print(f"🛑 Stopping worker: {service_name}")
|
||||||
|
|
||||||
# Use docker stop directly (works with container name)
|
# Use docker-compose down to stop and remove the service
|
||||||
subprocess.run(
|
result = self._run_docker_compose("stop", service_name)
|
||||||
["docker", "stop", container_name],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Worker {container_name} stopped")
|
logger.info(f"Worker {service_name} stopped")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error(f"Failed to stop worker {container_name}: {e.stderr}")
|
logger.error(f"Failed to stop worker {service_name}: {e.stderr}")
|
||||||
console.print(f"❌ Failed to stop worker: {e.stderr}", style="red")
|
console.print(f"❌ Failed to stop worker: {e.stderr}", style="red")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error stopping worker {container_name}: {e}")
|
logger.error(f"Unexpected error stopping worker {service_name}: {e}")
|
||||||
console.print(f"❌ Unexpected error: {e}", style="red")
|
console.print(f"❌ Unexpected error: {e}", style="red")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -257,17 +262,18 @@ class WorkerManager:
|
|||||||
Ensure a worker is running, starting it if necessary.
|
Ensure a worker is running, starting it if necessary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
worker_info: Worker information dict from API (contains worker_container, etc.)
|
worker_info: Worker information dict from API (contains worker_service, etc.)
|
||||||
auto_start: Whether to automatically start the worker if not running
|
auto_start: Whether to automatically start the worker if not running
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if worker is running, False otherwise
|
True if worker is running, False otherwise
|
||||||
"""
|
"""
|
||||||
container_name = worker_info["worker_container"]
|
# Get worker_service (docker-compose service name)
|
||||||
|
service_name = worker_info.get("worker_service", f"worker-{worker_info['vertical']}")
|
||||||
vertical = worker_info["vertical"]
|
vertical = worker_info["vertical"]
|
||||||
|
|
||||||
# Check if already running
|
# Check if already running
|
||||||
if self.is_worker_running(container_name):
|
if self.is_worker_running(service_name):
|
||||||
console.print(f"✓ Worker already running: {vertical}")
|
console.print(f"✓ Worker already running: {vertical}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -279,8 +285,8 @@ class WorkerManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Start the worker
|
# Start the worker
|
||||||
if not self.start_worker(container_name):
|
if not self.start_worker(service_name):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Wait for it to be ready
|
# Wait for it to be ready
|
||||||
return self.wait_for_worker_ready(container_name)
|
return self.wait_for_worker_ready(service_name)
|
||||||
|
|||||||
Generated
+3
-3
@@ -1257,7 +1257,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzzforge-ai"
|
name = "fuzzforge-ai"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = { editable = "../ai" }
|
source = { editable = "../ai" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "a2a-sdk" },
|
{ name = "a2a-sdk" },
|
||||||
@@ -1303,7 +1303,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzzforge-cli"
|
name = "fuzzforge-cli"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fuzzforge-ai" },
|
{ name = "fuzzforge-ai" },
|
||||||
@@ -1347,7 +1347,7 @@ provides-extras = ["dev"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzzforge-sdk"
|
name = "fuzzforge-sdk"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = { editable = "../sdk" }
|
source = { editable = "../sdk" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
# Development: docker-compose -f docker-compose.temporal.yaml up
|
# Development: docker-compose -f docker-compose.temporal.yaml up
|
||||||
# Production: docker-compose -f docker-compose.temporal.yaml -f docker-compose.temporal.prod.yaml up
|
# Production: docker-compose -f docker-compose.temporal.yaml -f docker-compose.temporal.prod.yaml up
|
||||||
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Temporal Server - Workflow Orchestration
|
# Temporal Server - Workflow Orchestration
|
||||||
|
|||||||
Reference in New Issue
Block a user