Add DAST scanning and code knowledge graph features across the stack: - compliance-dast and compliance-graph workspace crates - Agent API handlers and routes for DAST targets/scans and graph builds - Core models and traits for DAST and graph domains - Dashboard pages for DAST targets/findings/overview and graph explorer/impact - Toast notification system with auto-dismiss for async action feedback - Button click animations and disabled states for better UX Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
114 lines
5.7 KiB
Rust
114 lines
5.7 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use crate::components::page_header::PageHeader;
|
|
use crate::components::severity_badge::SeverityBadge;
|
|
use crate::infrastructure::dast::fetch_dast_finding_detail;
|
|
|
|
#[component]
|
|
pub fn DastFindingDetailPage(id: String) -> Element {
|
|
let finding = use_resource(move || {
|
|
let fid = id.clone();
|
|
async move { fetch_dast_finding_detail(fid).await.ok() }
|
|
});
|
|
|
|
rsx! {
|
|
PageHeader {
|
|
title: "DAST Finding Detail",
|
|
description: "Full evidence and details for a dynamic security finding",
|
|
}
|
|
|
|
div { class: "card",
|
|
match &*finding.read() {
|
|
Some(Some(resp)) => {
|
|
let f = resp.data.clone();
|
|
let severity = f.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
|
|
rsx! {
|
|
div { class: "flex items-center gap-4 mb-4",
|
|
SeverityBadge { severity: severity }
|
|
h2 { "{f.get(\"title\").and_then(|v| v.as_str()).unwrap_or(\"Unknown Finding\")}" }
|
|
}
|
|
|
|
div { class: "grid grid-cols-2 gap-4 mb-4",
|
|
div {
|
|
strong { "Vulnerability Type: " }
|
|
span { class: "badge", "{f.get(\"vuln_type\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
|
}
|
|
div {
|
|
strong { "CWE: " }
|
|
span { "{f.get(\"cwe\").and_then(|v| v.as_str()).unwrap_or(\"N/A\")}" }
|
|
}
|
|
div {
|
|
strong { "Endpoint: " }
|
|
code { "{f.get(\"endpoint\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
|
}
|
|
div {
|
|
strong { "Method: " }
|
|
span { "{f.get(\"method\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
|
}
|
|
div {
|
|
strong { "Parameter: " }
|
|
code { "{f.get(\"parameter\").and_then(|v| v.as_str()).unwrap_or(\"N/A\")}" }
|
|
}
|
|
div {
|
|
strong { "Exploitable: " }
|
|
if f.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false) {
|
|
span { class: "badge badge-danger", "Confirmed" }
|
|
} else {
|
|
span { class: "badge", "Unconfirmed" }
|
|
}
|
|
}
|
|
}
|
|
|
|
h3 { "Description" }
|
|
p { "{f.get(\"description\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
|
|
|
if let Some(remediation) = f.get("remediation").and_then(|v| v.as_str()) {
|
|
h3 { class: "mt-4", "Remediation" }
|
|
p { "{remediation}" }
|
|
}
|
|
|
|
h3 { class: "mt-4", "Evidence" }
|
|
if let Some(evidence_list) = f.get("evidence").and_then(|v| v.as_array()) {
|
|
for (i, evidence) in evidence_list.iter().enumerate() {
|
|
div { class: "card mb-3",
|
|
h4 { "Evidence #{i + 1}" }
|
|
div { class: "grid grid-cols-2 gap-2",
|
|
div {
|
|
strong { "Request: " }
|
|
code { "{evidence.get(\"request_method\").and_then(|v| v.as_str()).unwrap_or(\"-\")} {evidence.get(\"request_url\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
|
}
|
|
div {
|
|
strong { "Response Status: " }
|
|
span { "{evidence.get(\"response_status\").and_then(|v| v.as_u64()).unwrap_or(0)}" }
|
|
}
|
|
}
|
|
if let Some(payload) = evidence.get("payload").and_then(|v| v.as_str()) {
|
|
div { class: "mt-2",
|
|
strong { "Payload: " }
|
|
code { class: "block bg-gray-900 text-green-400 p-2 rounded mt-1",
|
|
"{payload}"
|
|
}
|
|
}
|
|
}
|
|
if let Some(snippet) = evidence.get("response_snippet").and_then(|v| v.as_str()) {
|
|
div { class: "mt-2",
|
|
strong { "Response Snippet: " }
|
|
pre { class: "block bg-gray-900 text-gray-300 p-2 rounded mt-1 overflow-x-auto text-sm",
|
|
"{snippet}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
p { "No evidence collected." }
|
|
}
|
|
}
|
|
},
|
|
Some(None) => rsx! { p { "Finding not found." } },
|
|
None => rsx! { p { "Loading..." } },
|
|
}
|
|
}
|
|
}
|
|
}
|