diff --git a/backend/app/api/v1/endpoints/agent_tasks.py b/backend/app/api/v1/endpoints/agent_tasks.py index 16e8d2f..dd6d113 100644 --- a/backend/app/api/v1/endpoints/agent_tasks.py +++ b/backend/app/api/v1/endpoints/agent_tasks.py @@ -185,7 +185,8 @@ class AgentFindingResponse(BaseModel): code_snippet: Optional[str] is_verified: bool - confidence: float + # 🔥 FIX: Map from ai_confidence in ORM, make Optional with default + confidence: Optional[float] = Field(default=0.5, validation_alias="ai_confidence") status: str suggestion: Optional[str] = None @@ -193,8 +194,10 @@ class AgentFindingResponse(BaseModel): created_at: datetime - class Config: - from_attributes = True + model_config = { + "from_attributes": True, + "populate_by_name": True, # Allow both 'confidence' and 'ai_confidence' + } class TaskSummaryResponse(BaseModel): @@ -2201,6 +2204,16 @@ async def generate_audit_report( ) findings = findings.scalars().all() + # 🔥 Helper function to normalize severity for comparison (case-insensitive) + def normalize_severity(sev: str) -> str: + return str(sev).lower().strip() if sev else "" + + # Log findings for debugging + logger.info(f"[Report] Task {task_id}: Found {len(findings)} findings from database") + if findings: + for i, f in enumerate(findings[:3]): # Log first 3 + logger.debug(f"[Report] Finding {i+1}: severity='{f.severity}', title='{f.title[:50] if f.title else 'N/A'}'") + if format == "json": # Enhanced JSON report with full metadata return { @@ -2218,10 +2231,10 @@ async def generate_audit_report( "total_findings": len(findings), "verified_findings": sum(1 for f in findings if f.is_verified), "severity_distribution": { - "critical": sum(1 for f in findings if f.severity == 'critical'), - "high": sum(1 for f in findings if f.severity == 'high'), - "medium": sum(1 for f in findings if f.severity == 'medium'), - "low": sum(1 for f in findings if f.severity == 'low'), + "critical": sum(1 for f in findings if normalize_severity(f.severity) == 'critical'), + "high": sum(1 for f in findings if normalize_severity(f.severity) == 'high'), + "medium": sum(1 for f in findings if normalize_severity(f.severity) == 'medium'), + "low": sum(1 for f in findings if normalize_severity(f.severity) == 'low'), }, "agent_metrics": { "total_iterations": task.total_iterations, @@ -2258,10 +2271,10 @@ async def generate_audit_report( # Calculate statistics total = len(findings) - critical = sum(1 for f in findings if f.severity == 'critical') - high = sum(1 for f in findings if f.severity == 'high') - medium = sum(1 for f in findings if f.severity == 'medium') - low = sum(1 for f in findings if f.severity == 'low') + critical = sum(1 for f in findings if normalize_severity(f.severity) == 'critical') + high = sum(1 for f in findings if normalize_severity(f.severity) == 'high') + medium = sum(1 for f in findings if normalize_severity(f.severity) == 'medium') + low = sum(1 for f in findings if normalize_severity(f.severity) == 'low') verified = sum(1 for f in findings if f.is_verified) with_poc = sum(1 for f in findings if f.has_poc) @@ -2323,13 +2336,13 @@ async def generate_audit_report( md_lines.append(f"| Severity | Count | Verified |") md_lines.append(f"|----------|-------|----------|") if critical > 0: - md_lines.append(f"| **CRITICAL** | {critical} | {sum(1 for f in findings if f.severity == 'critical' and f.is_verified)} |") + md_lines.append(f"| **CRITICAL** | {critical} | {sum(1 for f in findings if normalize_severity(f.severity) == 'critical' and f.is_verified)} |") if high > 0: - md_lines.append(f"| **HIGH** | {high} | {sum(1 for f in findings if f.severity == 'high' and f.is_verified)} |") + md_lines.append(f"| **HIGH** | {high} | {sum(1 for f in findings if normalize_severity(f.severity) == 'high' and f.is_verified)} |") if medium > 0: - md_lines.append(f"| **MEDIUM** | {medium} | {sum(1 for f in findings if f.severity == 'medium' and f.is_verified)} |") + md_lines.append(f"| **MEDIUM** | {medium} | {sum(1 for f in findings if normalize_severity(f.severity) == 'medium' and f.is_verified)} |") if low > 0: - md_lines.append(f"| **LOW** | {low} | {sum(1 for f in findings if f.severity == 'low' and f.is_verified)} |") + md_lines.append(f"| **LOW** | {low} | {sum(1 for f in findings if normalize_severity(f.severity) == 'low' and f.is_verified)} |") md_lines.append(f"| **Total** | {total} | {verified} |") md_lines.append("") @@ -2353,7 +2366,7 @@ async def generate_audit_report( else: # Group findings by severity for severity_level, severity_name in [('critical', 'Critical'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low')]: - severity_findings = [f for f in findings if f.severity == severity_level] + severity_findings = [f for f in findings if normalize_severity(f.severity) == severity_level] if not severity_findings: continue diff --git a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx index 38d98bd..ee442c6 100644 --- a/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx +++ b/frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx @@ -104,20 +104,18 @@ function formatBytes(bytes: number): string { // ============ Sub Components ============ -// Report stats summary +// Report stats summary - uses task statistics for reliability const ReportStats = memo(function ReportStats({ task, - findings, }: { task: AgentTask; - findings: AgentFinding[]; + findings: AgentFinding[]; // Keep for API compatibility, but use task stats }) { - const severityCounts = { - critical: findings.filter(f => f.severity.toLowerCase() === "critical").length, - high: findings.filter(f => f.severity.toLowerCase() === "high").length, - medium: findings.filter(f => f.severity.toLowerCase() === "medium").length, - low: findings.filter(f => f.severity.toLowerCase() === "low").length, - }; + // Use task's reliable statistics instead of computing from findings array + // This ensures consistency even when findings array is empty or not loaded + const totalFindings = task.findings_count || 0; + const criticalAndHigh = (task.critical_count || 0) + (task.high_count || 0); + const verified = task.verified_count || 0; return (
@@ -137,7 +135,7 @@ const ReportStats = memo(function ReportStats({ Findings
- {findings.length} + {totalFindings}
@@ -147,7 +145,7 @@ const ReportStats = memo(function ReportStats({ Critical
- {severityCounts.critical + severityCounts.high} + {criticalAndHigh}
@@ -157,7 +155,7 @@ const ReportStats = memo(function ReportStats({ Verified
- {findings.filter(f => f.is_verified).length} + {verified}
@@ -351,49 +349,15 @@ export const ReportExportDialog = memo(function ReportExportDialog({ setPreview(prev => ({ ...prev, loading: true, error: null })); try { - // For JSON, we can generate it client-side + // For JSON, fetch from backend API to ensure data consistency + // The backend properly queries findings from the database if (format === "json") { - const reportData = { - report_metadata: { - task_id: task.id, - project_id: task.project_id, - generated_at: new Date().toISOString(), - task_status: task.status, - duration_seconds: task.completed_at && task.started_at - ? Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 1000) - : null, - }, - summary: { - security_score: task.security_score, - total_files_analyzed: task.analyzed_files, - total_findings: findings.length, - verified_findings: findings.filter(f => f.is_verified).length, - severity_distribution: { - critical: task.critical_count, - high: task.high_count, - medium: task.medium_count, - low: task.low_count, - }, - }, - findings: findings.map(f => ({ - id: f.id, - vulnerability_type: f.vulnerability_type, - severity: f.severity, - title: f.title, - description: f.description, - file_path: f.file_path, - line_start: f.line_start, - line_end: f.line_end, - code_snippet: f.code_snippet, - is_verified: f.is_verified, - has_poc: f.has_poc, - suggestion: f.suggestion, - ai_confidence: f.ai_confidence, - })), - }; + const response = await apiClient.get(`/agent-tasks/${task.id}/report`, { + params: { format: "json" }, + }); setPreview({ - content: JSON.stringify(reportData, null, 2), + content: JSON.stringify(response.data, null, 2), format: "json", loading: false, error: null, diff --git a/frontend/src/pages/AgentAudit/components/StatsPanel.tsx b/frontend/src/pages/AgentAudit/components/StatsPanel.tsx index 771b8a5..2b109b5 100644 --- a/frontend/src/pages/AgentAudit/components/StatsPanel.tsx +++ b/frontend/src/pages/AgentAudit/components/StatsPanel.tsx @@ -8,7 +8,6 @@ import { memo } from "react"; import { Activity, FileCode, Repeat, Zap, Bug, Shield, AlertTriangle } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { calculateSeverityCounts } from "../utils"; import type { StatsPanelProps } from "../types"; // Circular progress component @@ -86,8 +85,15 @@ function MetricCard({ icon, label, value, suffix = "", colorClass = "text-slate- export const StatsPanel = memo(function StatsPanel({ task, findings }: StatsPanelProps) { if (!task) return null; - const severityCounts = calculateSeverityCounts(findings); - const totalFindings = Object.values(severityCounts).reduce((a, b) => a + b, 0); + // 🔥 Use task's reliable statistics instead of computing from findings array + // This ensures consistency even when findings array is empty or not loaded + const severityCounts = { + critical: task.critical_count || 0, + high: task.high_count || 0, + medium: task.medium_count || 0, + low: task.low_count || 0, + }; + const totalFindings = task.findings_count || 0; const progressPercent = task.progress_percentage || 0; // Determine score color @@ -212,11 +218,10 @@ export const StatsPanel = memo(function StatsPanel({ task, findings }: StatsPane color={getScoreColor(task.security_score)} />
- = 80 ? 'text-emerald-400' : + = 80 ? 'text-emerald-400' : task.security_score >= 60 ? 'text-amber-400' : - 'text-rose-400' - }`}> + 'text-rose-400' + }`}> {task.security_score.toFixed(0)}