use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::*; use dioxus_free_icons::Icon; use crate::app::Route; use crate::components::page_header::PageHeader; use crate::components::severity_badge::SeverityBadge; use crate::infrastructure::dast::fetch_dast_findings; #[component] pub fn DastFindingsPage() -> Element { let findings = use_resource(|| async { fetch_dast_findings().await.ok() }); let mut filter_severity = use_signal(|| "all".to_string()); let mut filter_vuln_type = use_signal(|| "all".to_string()); let mut filter_exploitable = use_signal(|| "all".to_string()); let mut search_text = use_signal(String::new); rsx! { div { class: "back-nav", button { class: "btn btn-ghost btn-back", onclick: move |_| { navigator().go_back(); }, Icon { icon: BsArrowLeft, width: 16, height: 16 } "Back" } } PageHeader { title: "DAST Findings", description: "Vulnerabilities discovered through dynamic application security testing", } // Filter bar div { style: "display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; align-items: center;", // Search div { style: "flex: 1; min-width: 180px;", input { class: "chat-input", style: "width: 100%; padding: 6px 10px; font-size: 0.85rem;", placeholder: "Search title or endpoint...", value: "{search_text}", oninput: move |e| search_text.set(e.value()), } } // Severity select { style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;", value: "{filter_severity}", onchange: move |e| filter_severity.set(e.value()), option { value: "all", "All Severities" } option { value: "critical", "Critical" } option { value: "high", "High" } option { value: "medium", "Medium" } option { value: "low", "Low" } option { value: "info", "Info" } } // Vuln type select { style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;", value: "{filter_vuln_type}", onchange: move |e| filter_vuln_type.set(e.value()), option { value: "all", "All Types" } option { value: "sql_injection", "SQL Injection" } option { value: "xss", "XSS" } option { value: "auth_bypass", "Auth Bypass" } option { value: "ssrf", "SSRF" } option { value: "api_misconfiguration", "API Misconfiguration" } option { value: "open_redirect", "Open Redirect" } option { value: "idor", "IDOR" } option { value: "information_disclosure", "Information Disclosure" } option { value: "security_misconfiguration", "Security Misconfiguration" } option { value: "broken_auth", "Broken Auth" } option { value: "dns_misconfiguration", "DNS Misconfiguration" } option { value: "email_security", "Email Security" } option { value: "tls_misconfiguration", "TLS Misconfiguration" } option { value: "cookie_security", "Cookie Security" } option { value: "csp_issue", "CSP Issue" } option { value: "cors_misconfiguration", "CORS Misconfiguration" } option { value: "rate_limit_absent", "Rate Limit Absent" } option { value: "console_log_leakage", "Console Log Leakage" } option { value: "security_header_missing", "Security Header Missing" } option { value: "known_cve_exploit", "Known CVE Exploit" } option { value: "other", "Other" } } // Exploitable select { style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;", value: "{filter_exploitable}", onchange: move |e| filter_exploitable.set(e.value()), option { value: "all", "All" } option { value: "yes", "Exploitable" } option { value: "no", "Unconfirmed" } } } div { class: "card", match &*findings.read() { Some(Some(data)) => { let sev_filter = filter_severity.read().clone(); let vt_filter = filter_vuln_type.read().clone(); let exp_filter = filter_exploitable.read().clone(); let search = search_text.read().to_lowercase(); let filtered: Vec<_> = data.data.iter().filter(|f| { let severity = f.get("severity").and_then(|v| v.as_str()).unwrap_or("info"); let vuln_type = f.get("vuln_type").and_then(|v| v.as_str()).unwrap_or(""); let exploitable = f.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false); let title = f.get("title").and_then(|v| v.as_str()).unwrap_or("").to_lowercase(); let endpoint = f.get("endpoint").and_then(|v| v.as_str()).unwrap_or("").to_lowercase(); (sev_filter == "all" || severity == sev_filter) && (vt_filter == "all" || vuln_type == vt_filter) && match exp_filter.as_str() { "yes" => exploitable, "no" => !exploitable, _ => true, } && (search.is_empty() || title.contains(&search) || endpoint.contains(&search)) }).collect(); if filtered.is_empty() { if data.data.is_empty() { rsx! { p { style: "padding: 16px;", "No DAST findings yet. Run a scan to discover vulnerabilities." } } } else { rsx! { p { style: "padding: 16px; color: var(--text-secondary);", "No findings match the current filters." } } } } else { rsx! { div { style: "padding: 8px 16px; font-size: 0.8rem; color: var(--text-secondary);", "Showing {filtered.len()} of {data.data.len()} findings" } table { class: "table", thead { tr { th { "Severity" } th { "Type" } th { "Title" } th { "Endpoint" } th { "Method" } th { "Exploitable" } } } tbody { for finding in filtered { { let id = finding.get("_id").and_then(|v| v.get("$oid")).and_then(|v| v.as_str()).unwrap_or("").to_string(); let severity = finding.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string(); rsx! { tr { td { SeverityBadge { severity: severity } } td { span { class: "badge", "{finding.get(\"vuln_type\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } } td { Link { to: Route::DastFindingDetailPage { id }, "{finding.get(\"title\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } } td { code { class: "text-sm", "{finding.get(\"endpoint\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } } td { "{finding.get(\"method\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } td { if finding.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false) { span { class: "badge badge-danger", "Confirmed" } } else { span { class: "badge", "Unconfirmed" } } } } } } } } } } } }, Some(None) => rsx! { p { "Failed to load findings." } }, None => rsx! { p { "Loading..." } }, } } } }