- True SSE streaming via broadcast channels (DashMap per session) - Session pause/resume with watch channels + dashboard buttons - AES-256-GCM credential encryption at rest (PENTEST_ENCRYPTION_KEY) - Concurrency limiter (Semaphore, max 5 sessions, 429 on overflow) - Browser tool: headless Chrome CDP automation (navigate, click, fill, screenshot, evaluate) - Report code-level correlation: SAST findings, code graph, SBOM linked per DAST finding - Split html.rs (1919 LOC) into html/ module directory (8 files) - Wizard: target/repo dropdowns from existing data, SSH key display, close button on all steps - Auth: auto-register with optional registration URL (Playwright discovery), plus-addressing email, IMAP overrides - Attack chain: tool input/output in detail panel, running node pulse animation - Architecture docs with Mermaid diagrams + 8 screenshots Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
310 lines
17 KiB
Rust
310 lines
17 KiB
Rust
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::pentest_wizard::PentestWizard;
|
|
use crate::infrastructure::pentest::{
|
|
fetch_pentest_sessions, fetch_pentest_stats, pause_pentest_session, resume_pentest_session,
|
|
stop_pentest_session,
|
|
};
|
|
|
|
#[component]
|
|
pub fn PentestDashboardPage() -> Element {
|
|
let mut sessions = use_resource(|| async { fetch_pentest_sessions().await.ok() });
|
|
let stats = use_resource(|| async { fetch_pentest_stats().await.ok() });
|
|
|
|
let mut show_wizard = use_signal(|| false);
|
|
|
|
// Extract stats values
|
|
let running_sessions = {
|
|
let s = stats.read();
|
|
match &*s {
|
|
Some(Some(data)) => data
|
|
.data
|
|
.get("running_sessions")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
_ => 0,
|
|
}
|
|
};
|
|
let total_vulns = {
|
|
let s = stats.read();
|
|
match &*s {
|
|
Some(Some(data)) => data
|
|
.data
|
|
.get("total_vulnerabilities")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
_ => 0,
|
|
}
|
|
};
|
|
let tool_invocations = {
|
|
let s = stats.read();
|
|
match &*s {
|
|
Some(Some(data)) => data
|
|
.data
|
|
.get("total_tool_invocations")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0),
|
|
_ => 0,
|
|
}
|
|
};
|
|
let success_rate = {
|
|
let s = stats.read();
|
|
match &*s {
|
|
Some(Some(data)) => data
|
|
.data
|
|
.get("tool_success_rate")
|
|
.and_then(|v| v.as_f64())
|
|
.unwrap_or(0.0),
|
|
_ => 0.0,
|
|
}
|
|
};
|
|
|
|
// Severity counts from stats (nested under severity_distribution)
|
|
let sev_dist = {
|
|
let s = stats.read();
|
|
match &*s {
|
|
Some(Some(data)) => data
|
|
.data
|
|
.get("severity_distribution")
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null),
|
|
_ => serde_json::Value::Null,
|
|
}
|
|
};
|
|
let severity_critical = sev_dist
|
|
.get("critical")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0);
|
|
let severity_high = sev_dist.get("high").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let severity_medium = sev_dist.get("medium").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let severity_low = sev_dist.get("low").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
|
|
rsx! {
|
|
PageHeader {
|
|
title: "Pentest Dashboard",
|
|
description: "AI-powered penetration testing sessions — autonomous security assessment",
|
|
}
|
|
|
|
// Stat cards
|
|
div { class: "stat-cards", style: "margin-bottom: 24px;",
|
|
div { class: "stat-card-item",
|
|
div { class: "stat-card-value", "{running_sessions}" }
|
|
div { class: "stat-card-label",
|
|
Icon { icon: BsPlayCircle, width: 14, height: 14 }
|
|
" Running Sessions"
|
|
}
|
|
}
|
|
div { class: "stat-card-item",
|
|
div { class: "stat-card-value", "{total_vulns}" }
|
|
div { class: "stat-card-label",
|
|
Icon { icon: BsShieldExclamation, width: 14, height: 14 }
|
|
" Total Vulnerabilities"
|
|
}
|
|
}
|
|
div { class: "stat-card-item",
|
|
div { class: "stat-card-value", "{tool_invocations}" }
|
|
div { class: "stat-card-label",
|
|
Icon { icon: BsWrench, width: 14, height: 14 }
|
|
" Tool Invocations"
|
|
}
|
|
}
|
|
div { class: "stat-card-item",
|
|
div { class: "stat-card-value", "{success_rate:.0}%" }
|
|
div { class: "stat-card-label",
|
|
Icon { icon: BsCheckCircle, width: 14, height: 14 }
|
|
" Success Rate"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Severity distribution
|
|
div { class: "card", style: "margin-bottom: 24px; padding: 16px;",
|
|
div { style: "display: flex; align-items: center; gap: 16px; flex-wrap: wrap;",
|
|
span { style: "font-weight: 600; color: var(--text-secondary); font-size: 0.85rem;", "Severity Distribution" }
|
|
span {
|
|
class: "badge",
|
|
style: "background: #dc2626; color: #fff;",
|
|
"Critical: {severity_critical}"
|
|
}
|
|
span {
|
|
class: "badge",
|
|
style: "background: #ea580c; color: #fff;",
|
|
"High: {severity_high}"
|
|
}
|
|
span {
|
|
class: "badge",
|
|
style: "background: #d97706; color: #fff;",
|
|
"Medium: {severity_medium}"
|
|
}
|
|
span {
|
|
class: "badge",
|
|
style: "background: #2563eb; color: #fff;",
|
|
"Low: {severity_low}"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actions row
|
|
div { style: "display: flex; gap: 12px; margin-bottom: 24px;",
|
|
button {
|
|
class: "btn btn-primary",
|
|
onclick: move |_| show_wizard.set(true),
|
|
Icon { icon: BsPlusCircle, width: 14, height: 14 }
|
|
" New Pentest"
|
|
}
|
|
}
|
|
|
|
// Sessions list
|
|
div { class: "card",
|
|
div { class: "card-header", "Recent Pentest Sessions" }
|
|
match &*sessions.read() {
|
|
Some(Some(data)) => {
|
|
let sess_list = &data.data;
|
|
if sess_list.is_empty() {
|
|
rsx! {
|
|
div { style: "padding: 32px; text-align: center; color: var(--text-secondary);",
|
|
p { "No pentest sessions yet. Start one to begin autonomous security testing." }
|
|
}
|
|
}
|
|
} else {
|
|
rsx! {
|
|
div { style: "display: grid; gap: 12px; padding: 16px;",
|
|
for session in sess_list {
|
|
{
|
|
let id = session.get("_id")
|
|
.and_then(|v| v.get("$oid"))
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("-").to_string();
|
|
let target_name = session.get("target_name").and_then(|v| v.as_str()).unwrap_or("Unknown Target").to_string();
|
|
let status = session.get("status").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
|
|
let strategy = session.get("strategy").and_then(|v| v.as_str()).unwrap_or("-").to_string();
|
|
let findings_count = session.get("findings_count").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let tool_count = session.get("tool_invocations").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let created_at = session.get("created_at").and_then(|v| v.as_str()).unwrap_or("-").to_string();
|
|
let status_style = match status.as_str() {
|
|
"running" => "background: #16a34a; color: #fff;",
|
|
"completed" => "background: #2563eb; color: #fff;",
|
|
"failed" => "background: #dc2626; color: #fff;",
|
|
"paused" => "background: #d97706; color: #fff;",
|
|
_ => "background: var(--bg-tertiary); color: var(--text-secondary);",
|
|
};
|
|
{
|
|
let is_session_running = status == "running";
|
|
let is_session_paused = status == "paused";
|
|
let stop_id = id.clone();
|
|
let pause_id = id.clone();
|
|
let resume_id = id.clone();
|
|
rsx! {
|
|
div { class: "card", style: "padding: 16px; transition: border-color 0.15s;",
|
|
Link {
|
|
to: Route::PentestSessionPage { session_id: id.clone() },
|
|
style: "text-decoration: none; cursor: pointer; display: block;",
|
|
div { style: "display: flex; justify-content: space-between; align-items: flex-start;",
|
|
div {
|
|
div { style: "font-weight: 600; font-size: 1rem; margin-bottom: 4px; color: var(--text-primary);",
|
|
"{target_name}"
|
|
}
|
|
div { style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;",
|
|
span {
|
|
class: "badge",
|
|
style: "{status_style}",
|
|
"{status}"
|
|
}
|
|
span {
|
|
class: "badge",
|
|
style: "background: var(--bg-tertiary); color: var(--text-secondary);",
|
|
"{strategy}"
|
|
}
|
|
}
|
|
}
|
|
div { style: "text-align: right; font-size: 0.85rem; color: var(--text-secondary);",
|
|
div { style: "margin-bottom: 4px;",
|
|
Icon { icon: BsShieldExclamation, width: 12, height: 12 }
|
|
" {findings_count} findings"
|
|
}
|
|
div { style: "margin-bottom: 4px;",
|
|
Icon { icon: BsWrench, width: 12, height: 12 }
|
|
" {tool_count} tools"
|
|
}
|
|
div { "{created_at}" }
|
|
}
|
|
}
|
|
}
|
|
if is_session_running || is_session_paused {
|
|
div { style: "margin-top: 8px; display: flex; justify-content: flex-end; gap: 6px;",
|
|
if is_session_running {
|
|
button {
|
|
class: "btn btn-ghost",
|
|
style: "font-size: 0.8rem; padding: 4px 12px; color: #d97706; border-color: #d97706;",
|
|
onclick: move |e| {
|
|
e.stop_propagation();
|
|
e.prevent_default();
|
|
let sid = pause_id.clone();
|
|
spawn(async move {
|
|
let _ = pause_pentest_session(sid).await;
|
|
sessions.restart();
|
|
});
|
|
},
|
|
Icon { icon: BsPauseCircle, width: 12, height: 12 }
|
|
" Pause"
|
|
}
|
|
}
|
|
if is_session_paused {
|
|
button {
|
|
class: "btn btn-ghost",
|
|
style: "font-size: 0.8rem; padding: 4px 12px; color: #16a34a; border-color: #16a34a;",
|
|
onclick: move |e| {
|
|
e.stop_propagation();
|
|
e.prevent_default();
|
|
let sid = resume_id.clone();
|
|
spawn(async move {
|
|
let _ = resume_pentest_session(sid).await;
|
|
sessions.restart();
|
|
});
|
|
},
|
|
Icon { icon: BsPlayCircle, width: 12, height: 12 }
|
|
" Resume"
|
|
}
|
|
}
|
|
button {
|
|
class: "btn btn-ghost",
|
|
style: "font-size: 0.8rem; padding: 4px 12px; color: #dc2626; border-color: #dc2626;",
|
|
onclick: move |e| {
|
|
e.stop_propagation();
|
|
e.prevent_default();
|
|
let sid = stop_id.clone();
|
|
spawn(async move {
|
|
let _ = stop_pentest_session(sid).await;
|
|
sessions.restart();
|
|
});
|
|
},
|
|
Icon { icon: BsStopCircle, width: 12, height: 12 }
|
|
" Stop"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Some(None) => rsx! { p { style: "padding: 16px;", "Failed to load sessions." } },
|
|
None => rsx! { p { style: "padding: 16px;", "Loading..." } },
|
|
}
|
|
}
|
|
|
|
// Pentest Wizard
|
|
if *show_wizard.read() {
|
|
PentestWizard { show: show_wizard }
|
|
}
|
|
}
|
|
}
|