mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-05-16 21:33:27 +02:00
feat: Add native findings format and fix critical ID bug
Priority 1 implementation: - Created native FuzzForge findings format schema with full support for: - 5-level severity (critical/high/medium/low/info) - Confidence levels - CWE and OWASP categorization - found_by attribution (module, tool, type) - LLM context tracking (model, prompt, temperature) - Updated ModuleFinding model with new fields: - Added rule_id for pattern identification - Added found_by for detection attribution - Added llm_context for LLM-detected findings - Added confidence, cwe, owasp, references - Added column_start/end for precise location - Updated create_finding() helper with new required fields - Enhanced _generate_summary() with confidence and source tracking - Fixed critical ID bug in CLI: - Changed 'ff finding show' to use --id (unique) instead of --rule - Added new show_findings_by_rule() function to show ALL findings matching a rule - Updated display_finding_detail() to support both native and SARIF formats - Now properly handles multiple findings with same rule_id Breaking changes: - create_finding() now requires rule_id and found_by parameters - show_finding() now uses --id instead of --rule flag
This commit is contained in:
@@ -152,12 +152,13 @@ def get_findings(
|
||||
|
||||
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")
|
||||
finding_id: str = typer.Option(..., "--id", "-i", help="Unique 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.
|
||||
Use the unique finding ID (shown in the findings table) to view details.
|
||||
"""
|
||||
try:
|
||||
require_project()
|
||||
@@ -173,91 +174,239 @@ def show_finding(
|
||||
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
|
||||
findings_dict = findings.sarif # Will become native format
|
||||
else:
|
||||
sarif_data = findings_data.sarif_data
|
||||
findings_dict = findings_data.sarif_data # Will become findings_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
|
||||
# Find the specific finding by unique ID
|
||||
# For now, support both SARIF (old) and native format (new)
|
||||
matching_finding = None
|
||||
for result in results:
|
||||
if result.get("ruleId") == rule_id:
|
||||
matching_finding = result
|
||||
break
|
||||
|
||||
# Try native format first
|
||||
if "findings" in findings_dict:
|
||||
for finding in findings_dict.get("findings", []):
|
||||
if finding.get("id") == finding_id or finding.get("id", "").startswith(finding_id):
|
||||
matching_finding = finding
|
||||
break
|
||||
# Fallback to SARIF format (for backward compatibility during transition)
|
||||
elif "runs" in findings_dict:
|
||||
runs = findings_dict.get("runs", [])
|
||||
if runs:
|
||||
run_data = runs[0]
|
||||
results = run_data.get("results", [])
|
||||
for result in results:
|
||||
# Check if finding ID is in properties
|
||||
props = result.get("properties", {})
|
||||
fid = props.get("findingId", "")
|
||||
if fid == finding_id or fid.startswith(finding_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"❌ No finding found with ID: {finding_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)
|
||||
display_finding_detail(matching_finding, 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)
|
||||
def show_findings_by_rule(
|
||||
run_id: str = typer.Argument(..., help="Run ID to get findings from"),
|
||||
rule_id: str = typer.Option(..., "--rule", "-r", help="Rule ID to filter findings")
|
||||
):
|
||||
"""
|
||||
🔍 Show all findings matching a specific rule
|
||||
|
||||
# Get location
|
||||
locations = finding.get("locations", [])
|
||||
location_str = "Unknown location"
|
||||
code_snippet = None
|
||||
This command shows ALL findings that match the given rule ID.
|
||||
Useful when you have multiple instances of the same vulnerability type.
|
||||
"""
|
||||
try:
|
||||
require_project()
|
||||
validate_run_id(run_id)
|
||||
|
||||
if locations:
|
||||
physical_location = locations[0].get("physicalLocation", {})
|
||||
artifact_location = physical_location.get("artifactLocation", {})
|
||||
region = physical_location.get("region", {})
|
||||
# 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)
|
||||
|
||||
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']}"
|
||||
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)
|
||||
findings_dict = findings.sarif
|
||||
else:
|
||||
findings_dict = findings_data.sarif_data
|
||||
|
||||
# Get code snippet if available
|
||||
if region.get("snippet", {}).get("text"):
|
||||
code_snippet = region["snippet"]["text"].strip()
|
||||
# Find all findings matching the rule
|
||||
matching_findings = []
|
||||
|
||||
# Try native format first
|
||||
if "findings" in findings_dict:
|
||||
for finding in findings_dict.get("findings", []):
|
||||
if finding.get("rule_id") == rule_id:
|
||||
matching_findings.append(finding)
|
||||
# Fallback to SARIF format
|
||||
elif "runs" in findings_dict:
|
||||
runs = findings_dict.get("runs", [])
|
||||
if runs:
|
||||
run_data = runs[0]
|
||||
results = run_data.get("results", [])
|
||||
for result in results:
|
||||
if result.get("ruleId") == rule_id:
|
||||
matching_findings.append(result)
|
||||
|
||||
if not matching_findings:
|
||||
console.print(f"❌ No findings 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)
|
||||
|
||||
console.print(f"\n🔍 Found {len(matching_findings)} finding(s) matching rule: [bold cyan]{rule_id}[/bold cyan]\n")
|
||||
|
||||
# Display each finding
|
||||
for i, finding in enumerate(matching_findings, 1):
|
||||
console.print(f"[bold]Finding {i} of {len(matching_findings)}[/bold]")
|
||||
display_finding_detail(finding, run_id)
|
||||
if i < len(matching_findings):
|
||||
console.print("\n" + "─" * 80 + "\n")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ Failed to get findings: {e}", style="red")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def display_finding_detail(finding: Dict[str, Any], run_id: str):
|
||||
"""Display detailed information about a single finding (supports both native and SARIF format)"""
|
||||
|
||||
# Detect format and extract fields
|
||||
is_native = "rule_id" in finding # Native format has rule_id, SARIF has ruleId
|
||||
|
||||
if is_native:
|
||||
# Native FuzzForge format
|
||||
finding_id = finding.get("id", "unknown")
|
||||
rule_id = finding.get("rule_id", "unknown")
|
||||
title = finding.get("title", "No title")
|
||||
description = finding.get("description", "No description")
|
||||
severity = finding.get("severity", "info")
|
||||
confidence = finding.get("confidence", "medium")
|
||||
category = finding.get("category", "unknown")
|
||||
cwe = finding.get("cwe")
|
||||
owasp = finding.get("owasp")
|
||||
recommendation = finding.get("recommendation")
|
||||
|
||||
# Found by information
|
||||
found_by = finding.get("found_by", {})
|
||||
module = found_by.get("module", "unknown")
|
||||
tool_name = found_by.get("tool_name", "Unknown")
|
||||
tool_version = found_by.get("tool_version", "unknown")
|
||||
detection_type = found_by.get("type", "unknown")
|
||||
|
||||
# LLM context if available
|
||||
llm_context = finding.get("llm_context")
|
||||
|
||||
# Location
|
||||
location = finding.get("location", {})
|
||||
file_path = location.get("file", "")
|
||||
line_start = location.get("line_start")
|
||||
column_start = location.get("column_start")
|
||||
code_snippet = location.get("snippet")
|
||||
|
||||
location_str = file_path if file_path else "Unknown location"
|
||||
if line_start:
|
||||
location_str += f":{line_start}"
|
||||
if column_start:
|
||||
location_str += f":{column_start}"
|
||||
|
||||
else:
|
||||
# SARIF format (backward compatibility)
|
||||
props = finding.get("properties", {})
|
||||
finding_id = props.get("findingId", "unknown")
|
||||
rule_id = finding.get("ruleId", "unknown")
|
||||
title = props.get("title", "No title")
|
||||
severity = finding.get("level", "note")
|
||||
confidence = "medium" # Not available in SARIF
|
||||
category = "unknown"
|
||||
cwe = None
|
||||
owasp = None
|
||||
|
||||
message = finding.get("message", {})
|
||||
description = message.get("text", "No description")
|
||||
recommendation = None
|
||||
|
||||
module = "unknown"
|
||||
tool_name = "Unknown"
|
||||
tool_version = "unknown"
|
||||
detection_type = "tool"
|
||||
llm_context = None
|
||||
|
||||
# Location from SARIF
|
||||
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']}"
|
||||
|
||||
if region.get("snippet", {}).get("text"):
|
||||
code_snippet = region["snippet"]["text"].strip()
|
||||
|
||||
# Get severity style
|
||||
severity_color = {
|
||||
"critical": "red",
|
||||
"high": "red",
|
||||
"medium": "yellow",
|
||||
"low": "blue",
|
||||
"info": "cyan",
|
||||
# SARIF levels
|
||||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"note": "blue",
|
||||
"info": "cyan"
|
||||
}.get(level.lower(), "white")
|
||||
"note": "blue"
|
||||
}.get(severity.lower(), "white")
|
||||
|
||||
# Build detailed content
|
||||
content_lines = []
|
||||
content_lines.append(f"[bold]Finding ID:[/bold] {finding_id}")
|
||||
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]Title:[/bold] {title}")
|
||||
content_lines.append(f"[bold]Severity:[/bold] [{severity_color}]{severity.upper()}[/{severity_color}] (Confidence: {confidence})")
|
||||
|
||||
if cwe:
|
||||
content_lines.append(f"[bold]CWE:[/bold] {cwe}")
|
||||
if owasp:
|
||||
content_lines.append(f"[bold]OWASP:[/bold] {owasp}")
|
||||
|
||||
content_lines.append(f"[bold]Category:[/bold] {category}")
|
||||
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]Found by:[/bold] {tool_name} v{tool_version} ({module}) [{detection_type}]")
|
||||
|
||||
if llm_context:
|
||||
model = llm_context.get("model", "unknown")
|
||||
content_lines.append(f"[bold]LLM Model:[/bold] {model}")
|
||||
|
||||
content_lines.append(f"[bold]Run ID:[/bold] {run_id}")
|
||||
content_lines.append("")
|
||||
content_lines.append("[bold]Summary:[/bold]")
|
||||
content_lines.append(message_text)
|
||||
content_lines.append("")
|
||||
content_lines.append("[bold]Description:[/bold]")
|
||||
content_lines.append(message_markdown)
|
||||
content_lines.append(description)
|
||||
|
||||
if recommendation:
|
||||
content_lines.append("")
|
||||
content_lines.append("[bold]Recommendation:[/bold]")
|
||||
content_lines.append(recommendation)
|
||||
|
||||
if code_snippet:
|
||||
content_lines.append("")
|
||||
@@ -276,7 +425,7 @@ def display_finding_detail(finding: Dict[str, Any], tool: Dict[str, Any], run_id
|
||||
padding=(1, 2)
|
||||
))
|
||||
console.print()
|
||||
console.print(f"💡 Export this run: [bold cyan]ff findings export {run_id} --format sarif[/bold cyan]")
|
||||
console.print(f"💡 Export this run: [bold cyan]ff findings export {run_id} --format native[/bold cyan]")
|
||||
|
||||
|
||||
def display_findings_table(sarif_data: Dict[str, Any]):
|
||||
|
||||
Reference in New Issue
Block a user