feat: pentest feature improvements — streaming, pause/resume, encryption, browser tool, reports, docs

- 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>
This commit is contained in:
Sharang Parnerkar
2026-03-17 00:07:50 +01:00
parent 11e1c5f438
commit a912ec9ad9
45 changed files with 5927 additions and 2133 deletions

View File

@@ -4,59 +4,18 @@ use dioxus_free_icons::Icon;
use crate::app::Route;
use crate::components::page_header::PageHeader;
use crate::infrastructure::dast::fetch_dast_targets;
use crate::components::pentest_wizard::PentestWizard;
use crate::infrastructure::pentest::{
create_pentest_session, fetch_pentest_sessions, fetch_pentest_stats, stop_pentest_session,
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 targets = use_resource(|| async { fetch_dast_targets().await.ok() });
let mut show_modal = use_signal(|| false);
let mut new_target_id = use_signal(String::new);
let mut new_strategy = use_signal(|| "comprehensive".to_string());
let mut new_message = use_signal(String::new);
let mut creating = use_signal(|| false);
let on_create = move |_| {
let tid = new_target_id.read().clone();
let strat = new_strategy.read().clone();
let msg = new_message.read().clone();
if tid.is_empty() || msg.is_empty() {
return;
}
creating.set(true);
spawn(async move {
match create_pentest_session(tid, strat, msg).await {
Ok(resp) => {
let session_id = resp
.data
.get("_id")
.and_then(|v| v.get("$oid"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
creating.set(false);
show_modal.set(false);
new_target_id.set(String::new());
new_message.set(String::new());
if !session_id.is_empty() {
navigator().push(Route::PentestSessionPage {
session_id: session_id.clone(),
});
} else {
sessions.restart();
}
}
Err(_) => {
creating.set(false);
}
}
});
};
let mut show_wizard = use_signal(|| false);
// Extract stats values
let running_sessions = {
@@ -193,7 +152,7 @@ pub fn PentestDashboardPage() -> Element {
div { style: "display: flex; gap: 12px; margin-bottom: 24px;",
button {
class: "btn btn-primary",
onclick: move |_| show_modal.set(true),
onclick: move |_| show_wizard.set(true),
Icon { icon: BsPlusCircle, width: 14, height: 14 }
" New Pentest"
}
@@ -235,7 +194,10 @@ pub fn PentestDashboardPage() -> Element {
};
{
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 {
@@ -272,8 +234,42 @@ pub fn PentestDashboardPage() -> Element {
}
}
}
if is_session_running {
div { style: "margin-top: 8px; display: flex; justify-content: flex-end;",
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;",
@@ -305,97 +301,9 @@ pub fn PentestDashboardPage() -> Element {
}
}
// New Pentest Modal
if *show_modal.read() {
div {
style: "position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000;",
onclick: move |_| show_modal.set(false),
div {
style: "background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; padding: 24px; width: 480px; max-width: 90vw;",
onclick: move |e| e.stop_propagation(),
h3 { style: "margin: 0 0 16px 0;", "New Pentest Session" }
// Target selection
div { style: "margin-bottom: 12px;",
label { style: "display: block; font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 4px;",
"Target"
}
select {
class: "chat-input",
style: "width: 100%; padding: 8px; resize: none; height: auto;",
value: "{new_target_id}",
onchange: move |e| new_target_id.set(e.value()),
option { value: "", "Select a target..." }
match &*targets.read() {
Some(Some(data)) => {
rsx! {
for target in &data.data {
{
let tid = target.get("_id")
.and_then(|v| v.get("$oid"))
.and_then(|v| v.as_str())
.unwrap_or("").to_string();
let tname = target.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string();
let turl = target.get("base_url").and_then(|v| v.as_str()).unwrap_or("").to_string();
rsx! {
option { value: "{tid}", "{tname} ({turl})" }
}
}
}
}
},
_ => rsx! {},
}
}
}
// Strategy selection
div { style: "margin-bottom: 12px;",
label { style: "display: block; font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 4px;",
"Strategy"
}
select {
class: "chat-input",
style: "width: 100%; padding: 8px; resize: none; height: auto;",
value: "{new_strategy}",
onchange: move |e| new_strategy.set(e.value()),
option { value: "comprehensive", "Comprehensive" }
option { value: "quick", "Quick Scan" }
option { value: "owasp_top_10", "OWASP Top 10" }
option { value: "api_focused", "API Focused" }
option { value: "authentication", "Authentication" }
}
}
// Initial message
div { style: "margin-bottom: 16px;",
label { style: "display: block; font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 4px;",
"Initial Instructions"
}
textarea {
class: "chat-input",
style: "width: 100%; min-height: 80px;",
placeholder: "Describe the scope and goals of this pentest...",
value: "{new_message}",
oninput: move |e| new_message.set(e.value()),
}
}
div { style: "display: flex; justify-content: flex-end; gap: 8px;",
button {
class: "btn btn-ghost",
onclick: move |_| show_modal.set(false),
"Cancel"
}
button {
class: "btn btn-primary",
disabled: *creating.read() || new_target_id.read().is_empty() || new_message.read().is_empty(),
onclick: on_create,
if *creating.read() { "Creating..." } else { "Start Pentest" }
}
}
}
}
// Pentest Wizard
if *show_wizard.read() {
PentestWizard { show: show_wizard }
}
}
}