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>
150 lines
5.5 KiB
Rust
150 lines
5.5 KiB
Rust
use compliance_core::models::auth::AuthInfo;
|
|
use dioxus::prelude::*;
|
|
use dioxus_free_icons::icons::bs_icons::*;
|
|
use dioxus_free_icons::Icon;
|
|
|
|
use crate::app::Route;
|
|
|
|
struct NavItem {
|
|
label: &'static str,
|
|
route: Route,
|
|
icon: Element,
|
|
}
|
|
|
|
#[component]
|
|
pub fn Sidebar() -> Element {
|
|
let current_route = use_route::<Route>();
|
|
let mut collapsed = use_signal(|| true);
|
|
|
|
let nav_items = [
|
|
NavItem {
|
|
label: "Overview",
|
|
route: Route::OverviewPage {},
|
|
icon: rsx! { Icon { icon: BsSpeedometer2, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Repositories",
|
|
route: Route::RepositoriesPage {},
|
|
icon: rsx! { Icon { icon: BsFolder2Open, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Findings",
|
|
route: Route::FindingsPage {},
|
|
icon: rsx! { Icon { icon: BsShieldExclamation, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "SBOM",
|
|
route: Route::SbomPage {},
|
|
icon: rsx! { Icon { icon: BsBoxSeam, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Issues",
|
|
route: Route::IssuesPage {},
|
|
icon: rsx! { Icon { icon: BsListTask, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "DAST",
|
|
route: Route::DastOverviewPage {},
|
|
icon: rsx! { Icon { icon: BsBug, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Pentest",
|
|
route: Route::PentestDashboardPage {},
|
|
icon: rsx! { Icon { icon: BsLightningCharge, width: 18, height: 18 } },
|
|
},
|
|
NavItem {
|
|
label: "Settings",
|
|
route: Route::SettingsPage {},
|
|
icon: rsx! { Icon { icon: BsGear, width: 18, height: 18 } },
|
|
},
|
|
];
|
|
|
|
let docs_url = option_env!("DOCS_URL").unwrap_or("/docs");
|
|
|
|
let sidebar_class = if collapsed() {
|
|
"sidebar collapsed"
|
|
} else {
|
|
"sidebar"
|
|
};
|
|
|
|
rsx! {
|
|
nav { class: "{sidebar_class}",
|
|
div { class: "sidebar-header",
|
|
Icon { icon: BsShieldCheck, width: 24, height: 24 }
|
|
if !collapsed() {
|
|
h1 { "Compliance Scanner" }
|
|
}
|
|
}
|
|
div { class: "sidebar-nav",
|
|
for item in nav_items {
|
|
{
|
|
let is_active = match (¤t_route, &item.route) {
|
|
(Route::FindingDetailPage { .. }, Route::FindingsPage {}) => true,
|
|
(Route::DastTargetsPage {}, Route::DastOverviewPage {}) => true,
|
|
(Route::DastFindingsPage {}, Route::DastOverviewPage {}) => true,
|
|
(Route::DastFindingDetailPage { .. }, Route::DastOverviewPage {}) => true,
|
|
(Route::PentestSessionPage { .. }, Route::PentestDashboardPage {}) => true,
|
|
(a, b) => a == b,
|
|
};
|
|
let class = if is_active { "nav-item active" } else { "nav-item" };
|
|
rsx! {
|
|
Link {
|
|
to: item.route.clone(),
|
|
class: class,
|
|
{item.icon}
|
|
if !collapsed() {
|
|
span { "{item.label}" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
a {
|
|
href: "{docs_url}",
|
|
target: "_blank",
|
|
class: "nav-item",
|
|
Icon { icon: BsBook, width: 18, height: 18 }
|
|
if !collapsed() {
|
|
span { "Docs" }
|
|
}
|
|
}
|
|
button {
|
|
class: "sidebar-toggle",
|
|
onclick: move |_| collapsed.set(!collapsed()),
|
|
if collapsed() {
|
|
Icon { icon: BsChevronRight, width: 14, height: 14 }
|
|
} else {
|
|
Icon { icon: BsChevronLeft, width: 14, height: 14 }
|
|
}
|
|
}
|
|
{
|
|
let auth_info = use_context::<Signal<AuthInfo>>();
|
|
let info = auth_info();
|
|
let initials = info.name.chars().next().unwrap_or('U').to_uppercase().to_string();
|
|
let user_class = if collapsed() { "sidebar-user sidebar-user-collapsed" } else { "sidebar-user" };
|
|
rsx! {
|
|
div { class: "{user_class}",
|
|
div { class: "user-avatar",
|
|
if info.avatar_url.is_empty() {
|
|
span { class: "avatar-initials", "{initials}" }
|
|
} else {
|
|
img { src: "{info.avatar_url}", alt: "avatar", class: "avatar-img" }
|
|
}
|
|
}
|
|
if !collapsed() {
|
|
span { class: "user-name", "{info.name}" }
|
|
}
|
|
a {
|
|
href: "/logout",
|
|
class: if collapsed() { "logout-btn logout-btn-collapsed" } else { "logout-btn" },
|
|
title: "Sign out",
|
|
Icon { icon: BsBoxArrowRight, width: 16, height: 16 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|