feat: AI-driven automated penetration testing system

Add a complete AI pentest system where Claude autonomously drives security
testing via tool-calling. The LLM selects from 16 tools, chains results,
and builds an attack chain DAG.

Core:
- PentestTool trait (dyn-compatible) with PentestToolContext/Result
- PentestSession, AttackChainNode, PentestMessage, PentestEvent models
- 10 new DastVulnType variants (DNS, DMARC, TLS, cookies, CSP, CORS, etc.)
- LLM client chat_with_tools() for OpenAI-compatible tool calling

Tools (16 total):
- 5 agent wrappers: SQL injection, XSS, auth bypass, SSRF, API fuzzer
- 11 new infra tools: DNS checker, DMARC checker, TLS analyzer,
  security headers, cookie analyzer, CSP analyzer, rate limit tester,
  console log detector, CORS checker, OpenAPI parser, recon
- ToolRegistry for tool lookup and LLM definition generation

Orchestrator:
- PentestOrchestrator with iterative tool-calling loop (max 50 rounds)
- Attack chain node recording per tool invocation
- SSE event broadcasting for real-time progress
- Strategy-aware system prompts (quick/comprehensive/targeted/aggressive/stealth)

API (9 endpoints):
- POST/GET /pentest/sessions, GET /pentest/sessions/:id
- POST /pentest/sessions/:id/chat, GET /pentest/sessions/:id/stream
- GET /pentest/sessions/:id/attack-chain, messages, findings
- GET /pentest/stats

Dashboard:
- Pentest dashboard with stat cards, severity distribution, session list
- Chat-based session page with split layout (chat + findings/attack chain)
- Inline tool execution indicators, auto-polling, new session modal
- Sidebar navigation item

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-11 19:23:21 +01:00
parent 3ec1456b0d
commit 6bc8ba89d1
40 changed files with 7546 additions and 90 deletions

View File

@@ -12,6 +12,8 @@ pub mod impact_analysis;
pub mod issues;
pub mod mcp_servers;
pub mod overview;
pub mod pentest_dashboard;
pub mod pentest_session;
pub mod repositories;
pub mod sbom;
pub mod settings;
@@ -30,6 +32,8 @@ pub use impact_analysis::ImpactAnalysisPage;
pub use issues::IssuesPage;
pub use mcp_servers::McpServersPage;
pub use overview::OverviewPage;
pub use pentest_dashboard::PentestDashboardPage;
pub use pentest_session::PentestSessionPage;
pub use repositories::RepositoriesPage;
pub use sbom::SbomPage;
pub use settings::SettingsPage;

View File

@@ -0,0 +1,396 @@
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::infrastructure::dast::fetch_dast_targets;
use crate::infrastructure::pentest::{
create_pentest_session, fetch_pentest_sessions, fetch_pentest_stats,
};
#[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.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);
}
}
});
};
// 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("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("success_rate")
.and_then(|v| v.as_f64())
.unwrap_or(0.0),
_ => 0.0,
}
};
// Severity counts from stats
let severity_critical = {
let s = stats.read();
match &*s {
Some(Some(data)) => data
.data
.get("severity_critical")
.and_then(|v| v.as_u64())
.unwrap_or(0),
_ => 0,
}
};
let severity_high = {
let s = stats.read();
match &*s {
Some(Some(data)) => data
.data
.get("severity_high")
.and_then(|v| v.as_u64())
.unwrap_or(0),
_ => 0,
}
};
let severity_medium = {
let s = stats.read();
match &*s {
Some(Some(data)) => data
.data
.get("severity_medium")
.and_then(|v| v.as_u64())
.unwrap_or(0),
_ => 0,
}
};
let severity_low = {
let s = stats.read();
match &*s {
Some(Some(data)) => data
.data
.get("severity_low")
.and_then(|v| v.as_u64())
.unwrap_or(0),
_ => 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_modal.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.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);",
};
rsx! {
Link {
to: Route::PentestSessionPage { session_id: id.clone() },
class: "card",
style: "padding: 16px; text-decoration: none; cursor: pointer; transition: border-color 0.15s;",
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}" }
}
}
}
}
}
}
}
}
}
},
Some(None) => rsx! { p { style: "padding: 16px;", "Failed to load sessions." } },
None => rsx! { p { style: "padding: 16px;", "Loading..." } },
}
}
// 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.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" }
}
}
}
}
}
}
}

View File

@@ -0,0 +1,445 @@
use dioxus::prelude::*;
use dioxus_free_icons::icons::bs_icons::*;
use dioxus_free_icons::Icon;
use crate::app::Route;
use crate::infrastructure::pentest::{
fetch_attack_chain, fetch_pentest_findings, fetch_pentest_messages, fetch_pentest_session,
send_pentest_message,
};
#[component]
pub fn PentestSessionPage(session_id: String) -> Element {
let sid = session_id.clone();
let sid_for_session = session_id.clone();
let sid_for_findings = session_id.clone();
let sid_for_chain = session_id.clone();
let mut session = use_resource(move || {
let id = sid_for_session.clone();
async move { fetch_pentest_session(id).await.ok() }
});
let mut messages_res = use_resource(move || {
let id = sid.clone();
async move { fetch_pentest_messages(id).await.ok() }
});
let mut findings = use_resource(move || {
let id = sid_for_findings.clone();
async move { fetch_pentest_findings(id).await.ok() }
});
let mut attack_chain = use_resource(move || {
let id = sid_for_chain.clone();
async move { fetch_attack_chain(id).await.ok() }
});
let mut input_text = use_signal(String::new);
let mut sending = use_signal(|| false);
let mut right_tab = use_signal(|| "findings".to_string());
// Auto-poll messages every 3s when session is running
let session_status = {
let s = session.read();
match &*s {
Some(Some(resp)) => resp
.data
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string(),
_ => "unknown".to_string(),
}
};
let is_running = session_status == "running";
let sid_for_poll = session_id.clone();
use_effect(move || {
if is_running {
let _sid = sid_for_poll.clone();
spawn(async move {
#[cfg(feature = "web")]
gloo_timers::future::TimeoutFuture::new(3_000).await;
#[cfg(not(feature = "web"))]
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
messages_res.restart();
findings.restart();
attack_chain.restart();
session.restart();
});
}
});
// Send message handler
let sid_for_send = session_id.clone();
let mut do_send = move || {
let text = input_text.read().trim().to_string();
if text.is_empty() || *sending.read() {
return;
}
let sid = sid_for_send.clone();
input_text.set(String::new());
sending.set(true);
spawn(async move {
let _ = send_pentest_message(sid, text).await;
sending.set(false);
messages_res.restart();
});
};
let mut do_send_click = do_send.clone();
// Session header info
let target_name = {
let s = session.read();
match &*s {
Some(Some(resp)) => resp
.data
.get("target_name")
.and_then(|v| v.as_str())
.unwrap_or("Pentest Session")
.to_string(),
_ => "Pentest Session".to_string(),
}
};
let strategy = {
let s = session.read();
match &*s {
Some(Some(resp)) => resp
.data
.get("strategy")
.and_then(|v| v.as_str())
.unwrap_or("-")
.to_string(),
_ => "-".to_string(),
}
};
let header_tool_count = {
let s = session.read();
match &*s {
Some(Some(resp)) => resp
.data
.get("tool_invocations")
.and_then(|v| v.as_u64())
.unwrap_or(0),
_ => 0,
}
};
let header_findings_count = {
let f = findings.read();
match &*f {
Some(Some(data)) => data.total.unwrap_or(0),
_ => 0,
}
};
let status_style = match session_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);",
};
rsx! {
div { class: "back-nav",
Link {
to: Route::PentestDashboardPage {},
class: "btn btn-ghost btn-back",
Icon { icon: BsArrowLeft, width: 16, height: 16 }
"Back to Pentest Dashboard"
}
}
// Session header
div { style: "display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 8px;",
div {
h2 { style: "margin: 0 0 4px 0;", "{target_name}" }
div { style: "display: flex; gap: 8px; align-items: center; flex-wrap: wrap;",
span { class: "badge", style: "{status_style}", "{session_status}" }
span { class: "badge", style: "background: var(--bg-tertiary); color: var(--text-secondary);",
"{strategy}"
}
}
}
div { style: "display: flex; gap: 16px; font-size: 0.85rem; color: var(--text-secondary);",
span {
Icon { icon: BsWrench, width: 14, height: 14 }
" {header_tool_count} tools"
}
span {
Icon { icon: BsShieldExclamation, width: 14, height: 14 }
" {header_findings_count} findings"
}
}
}
// Split layout: chat left, findings/chain right
div { style: "display: grid; grid-template-columns: 1fr 380px; gap: 16px; height: calc(100vh - 220px); min-height: 400px;",
// Left: Chat area
div { class: "card", style: "display: flex; flex-direction: column; overflow: hidden;",
div { class: "card-header", style: "flex-shrink: 0;", "Chat" }
// Messages
div {
style: "flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px;",
match &*messages_res.read() {
Some(Some(data)) => {
let msgs = &data.data;
if msgs.is_empty() {
rsx! {
div { style: "text-align: center; color: var(--text-secondary); padding: 32px;",
h3 { style: "margin-bottom: 8px;", "Start the conversation" }
p { "Send a message to guide the pentest agent." }
}
}
} else {
rsx! {
for (i, msg) in msgs.iter().enumerate() {
{
let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("assistant").to_string();
let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
let msg_type = msg.get("type").and_then(|v| v.as_str()).unwrap_or("text").to_string();
let tool_name = msg.get("tool_name").and_then(|v| v.as_str()).unwrap_or("").to_string();
let tool_status = msg.get("tool_status").and_then(|v| v.as_str()).unwrap_or("").to_string();
if msg_type == "tool_call" || msg_type == "tool_result" {
// Tool invocation indicator
let tool_icon_style = match tool_status.as_str() {
"success" => "color: #16a34a;",
"error" => "color: #dc2626;",
"running" => "color: #d97706;",
_ => "color: var(--text-secondary);",
};
rsx! {
div {
key: "{i}",
style: "display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: var(--bg-tertiary); border-radius: 6px; font-size: 0.8rem; color: var(--text-secondary);",
span { style: "{tool_icon_style}",
Icon { icon: BsWrench, width: 12, height: 12 }
}
span { style: "font-family: monospace;", "{tool_name}" }
if !tool_status.is_empty() {
span { class: "badge", style: "font-size: 0.7rem;", "{tool_status}" }
}
if !content.is_empty() {
details { style: "margin-left: auto; cursor: pointer;",
summary { style: "font-size: 0.75rem;", "details" }
pre { style: "margin-top: 4px; padding: 8px; background: var(--bg-primary); border-radius: 4px; font-size: 0.75rem; overflow-x: auto; max-height: 200px; white-space: pre-wrap;",
"{content}"
}
}
}
}
}
} else if role == "user" {
// User message - right aligned
rsx! {
div {
key: "{i}",
style: "display: flex; justify-content: flex-end;",
div {
style: "max-width: 80%; padding: 10px 14px; background: #2563eb; color: #fff; border-radius: 12px 12px 2px 12px; font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap;",
"{content}"
}
}
}
} else {
// Assistant message - left aligned
rsx! {
div {
key: "{i}",
style: "display: flex; gap: 8px; align-items: flex-start;",
div {
style: "flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center;",
Icon { icon: BsCpu, width: 14, height: 14 }
}
div {
style: "max-width: 80%; padding: 10px 14px; background: var(--bg-tertiary); border-radius: 12px 12px 12px 2px; font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap;",
"{content}"
}
}
}
}
}
}
}
}
},
Some(None) => rsx! { p { style: "padding: 16px; color: var(--text-secondary);", "Failed to load messages." } },
None => rsx! { p { style: "padding: 16px; color: var(--text-secondary);", "Loading messages..." } },
}
if *sending.read() {
div { style: "display: flex; gap: 8px; align-items: flex-start;",
div {
style: "flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center;",
Icon { icon: BsCpu, width: 14, height: 14 }
}
div {
style: "padding: 10px 14px; background: var(--bg-tertiary); border-radius: 12px 12px 12px 2px; font-size: 0.9rem; color: var(--text-secondary);",
"Thinking..."
}
}
}
}
// Input area
div { style: "flex-shrink: 0; padding: 12px; border-top: 1px solid var(--border-color); display: flex; gap: 8px;",
textarea {
class: "chat-input",
style: "flex: 1;",
placeholder: "Guide the pentest agent...",
value: "{input_text}",
oninput: move |e| input_text.set(e.value()),
onkeydown: move |e: Event<KeyboardData>| {
if e.key() == Key::Enter && !e.modifiers().shift() {
e.prevent_default();
do_send();
}
},
}
button {
class: "btn btn-primary",
style: "align-self: flex-end;",
disabled: *sending.read(),
onclick: move |_| do_send_click(),
"Send"
}
}
}
// Right: Findings / Attack Chain tabs
div { class: "card", style: "display: flex; flex-direction: column; overflow: hidden;",
// Tab bar
div { style: "display: flex; border-bottom: 1px solid var(--border-color); flex-shrink: 0;",
button {
style: if *right_tab.read() == "findings" {
"flex: 1; padding: 10px; background: none; border: none; border-bottom: 2px solid #2563eb; color: var(--text-primary); cursor: pointer; font-weight: 600;"
} else {
"flex: 1; padding: 10px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-secondary); cursor: pointer;"
},
onclick: move |_| right_tab.set("findings".to_string()),
Icon { icon: BsShieldExclamation, width: 14, height: 14 }
" Findings ({header_findings_count})"
}
button {
style: if *right_tab.read() == "chain" {
"flex: 1; padding: 10px; background: none; border: none; border-bottom: 2px solid #2563eb; color: var(--text-primary); cursor: pointer; font-weight: 600;"
} else {
"flex: 1; padding: 10px; background: none; border: none; border-bottom: 2px solid transparent; color: var(--text-secondary); cursor: pointer;"
},
onclick: move |_| right_tab.set("chain".to_string()),
Icon { icon: BsDiagram3, width: 14, height: 14 }
" Attack Chain"
}
}
// Tab content
div { style: "flex: 1; overflow-y: auto; padding: 12px;",
if *right_tab.read() == "findings" {
// Findings tab
match &*findings.read() {
Some(Some(data)) => {
let finding_list = &data.data;
if finding_list.is_empty() {
rsx! {
div { style: "text-align: center; color: var(--text-secondary); padding: 24px;",
p { "No findings yet." }
}
}
} else {
rsx! {
div { style: "display: flex; flex-direction: column; gap: 8px;",
for finding in finding_list {
{
let title = finding.get("title").and_then(|v| v.as_str()).unwrap_or("Untitled").to_string();
let severity = finding.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
let vuln_type = finding.get("vulnerability_type").and_then(|v| v.as_str()).unwrap_or("-").to_string();
let sev_style = match severity.as_str() {
"critical" => "background: #dc2626; color: #fff;",
"high" => "background: #ea580c; color: #fff;",
"medium" => "background: #d97706; color: #fff;",
"low" => "background: #2563eb; color: #fff;",
_ => "background: var(--bg-tertiary); color: var(--text-secondary);",
};
rsx! {
div { style: "padding: 10px; background: var(--bg-tertiary); border-radius: 8px;",
div { style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;",
span { style: "font-weight: 600; font-size: 0.85rem;", "{title}" }
span { class: "badge", style: "{sev_style}", "{severity}" }
}
div { style: "font-size: 0.8rem; color: var(--text-secondary);", "{vuln_type}" }
}
}
}
}
}
}
}
},
Some(None) => rsx! { p { style: "color: var(--text-secondary);", "Failed to load findings." } },
None => rsx! { p { style: "color: var(--text-secondary);", "Loading..." } },
}
} else {
// Attack chain tab
match &*attack_chain.read() {
Some(Some(data)) => {
let steps = &data.data;
if steps.is_empty() {
rsx! {
div { style: "text-align: center; color: var(--text-secondary); padding: 24px;",
p { "No attack chain steps yet." }
}
}
} else {
rsx! {
div { style: "display: flex; flex-direction: column; gap: 4px;",
for (i, step) in steps.iter().enumerate() {
{
let step_name = step.get("name").and_then(|v| v.as_str()).unwrap_or("Step").to_string();
let step_status = step.get("status").and_then(|v| v.as_str()).unwrap_or("pending").to_string();
let description = step.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
let step_num = i + 1;
let dot_color = match step_status.as_str() {
"completed" => "#16a34a",
"running" => "#d97706",
"failed" => "#dc2626",
_ => "var(--text-secondary)",
};
rsx! {
div { style: "display: flex; gap: 10px; padding: 8px 0;",
div { style: "display: flex; flex-direction: column; align-items: center;",
div { style: "width: 10px; height: 10px; border-radius: 50%; background: {dot_color}; flex-shrink: 0;" }
if i < steps.len() - 1 {
div { style: "width: 2px; flex: 1; background: var(--border-color); margin-top: 4px;" }
}
}
div {
div { style: "font-size: 0.85rem; font-weight: 600;", "{step_num}. {step_name}" }
if !description.is_empty() {
div { style: "font-size: 0.8rem; color: var(--text-secondary); margin-top: 2px;",
"{description}"
}
}
}
}
}
}
}
}
}
}
},
Some(None) => rsx! { p { style: "color: var(--text-secondary);", "Failed to load attack chain." } },
None => rsx! { p { style: "color: var(--text-secondary);", "Loading..." } },
}
}
}
}
}
}
}