feat: AI-driven automated penetration testing (#12)
Some checks failed
CI / Clippy (push) Failing after 1m51s
CI / Security Audit (push) Successful in 2m1s
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Format (push) Failing after 42s
CI / Deploy MCP (push) Has been skipped
Some checks failed
CI / Clippy (push) Failing after 1m51s
CI / Security Audit (push) Successful in 2m1s
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Format (push) Failing after 42s
CI / Deploy MCP (push) Has been skipped
This commit was merged in pull request #12.
This commit is contained in:
@@ -2767,3 +2767,467 @@ tbody tr:last-child td {
|
||||
.sbom-diff-row-changed {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════
|
||||
ATTACK CHAIN VISUALIZATION
|
||||
═══════════════════════════════════ */
|
||||
|
||||
/* KPI bar */
|
||||
.ac-kpi-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.ac-kpi-card {
|
||||
flex: 1;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px 14px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ac-kpi-card:first-child { border-radius: 10px 0 0 10px; }
|
||||
.ac-kpi-card:last-child { border-radius: 0 10px 10px 0; }
|
||||
.ac-kpi-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
.ac-kpi-card:nth-child(1)::before { background: var(--accent, #3b82f6); opacity: 0.4; }
|
||||
.ac-kpi-card:nth-child(2)::before { background: var(--danger, #dc2626); opacity: 0.5; }
|
||||
.ac-kpi-card:nth-child(3)::before { background: var(--success, #16a34a); opacity: 0.4; }
|
||||
.ac-kpi-card:nth-child(4)::before { background: var(--warning, #d97706); opacity: 0.4; }
|
||||
|
||||
.ac-kpi-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.ac-kpi-label {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Phase progress rail */
|
||||
.ac-phase-rail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.ac-phase-rail::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
height: 2px;
|
||||
background: var(--border-color);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ac-rail-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
min-width: 56px;
|
||||
flex: 1;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ac-rail-node:hover .ac-rail-dot { transform: scale(1.25); }
|
||||
.ac-rail-node.active .ac-rail-label { color: var(--accent, #3b82f6); }
|
||||
.ac-rail-node.active .ac-rail-dot { box-shadow: 0 0 0 3px rgba(59,130,246,0.2), 0 0 12px rgba(59,130,246,0.15); }
|
||||
|
||||
.ac-rail-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2.5px solid var(--bg-primary, #0f172a);
|
||||
transition: transform 0.2s cubic-bezier(0.16,1,0.3,1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-rail-dot.done { background: var(--success, #16a34a); box-shadow: 0 0 8px rgba(22,163,74,0.25); }
|
||||
.ac-rail-dot.running { background: var(--warning, #d97706); box-shadow: 0 0 10px rgba(217,119,6,0.35); animation: ac-dot-pulse 2s ease-in-out infinite; }
|
||||
.ac-rail-dot.pending { background: var(--text-tertiary, #6b7280); opacity: 0.5; }
|
||||
.ac-rail-dot.mixed { background: conic-gradient(var(--success, #16a34a) 0deg 270deg, var(--danger, #dc2626) 270deg 360deg); box-shadow: 0 0 8px rgba(22,163,74,0.2); }
|
||||
|
||||
@keyframes ac-dot-pulse {
|
||||
0%, 100% { box-shadow: 0 0 8px rgba(217,119,6,0.35); }
|
||||
50% { box-shadow: 0 0 18px rgba(217,119,6,0.55); }
|
||||
}
|
||||
|
||||
.ac-rail-label {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
margin-top: 5px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.ac-rail-findings {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.ac-rail-findings.has { color: var(--danger, #dc2626); }
|
||||
.ac-rail-findings.none { color: var(--text-tertiary, #6b7280); opacity: 0.4; }
|
||||
|
||||
.ac-rail-heatmap {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.ac-hm-cell {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 1.5px;
|
||||
}
|
||||
.ac-hm-cell.ok { background: var(--success, #16a34a); opacity: 0.5; }
|
||||
.ac-hm-cell.fail { background: var(--danger, #dc2626); opacity: 0.65; }
|
||||
.ac-hm-cell.run { background: var(--warning, #d97706); opacity: 0.5; animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-hm-cell.wait { background: var(--text-tertiary, #6b7280); opacity: 0.15; }
|
||||
|
||||
.ac-rail-bar {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
margin-top: 7px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.ac-rail-bar-inner {
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.ac-rail-bar-inner.done { background: var(--success, #16a34a); opacity: 0.35; }
|
||||
.ac-rail-bar-inner.running { background: linear-gradient(to right, var(--success, #16a34a), var(--warning, #d97706)); opacity: 0.35; }
|
||||
|
||||
/* Progress track */
|
||||
.ac-progress-track {
|
||||
height: 3px;
|
||||
background: var(--border-color);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ac-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--success, #16a34a) 0%, var(--accent, #3b82f6) 100%);
|
||||
transition: width 0.6s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
|
||||
/* Expand all controls */
|
||||
.ac-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ac-btn-toggle {
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--accent, #3b82f6);
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.ac-btn-toggle:hover {
|
||||
background: rgba(59,130,246,0.08);
|
||||
border-color: rgba(59,130,246,0.12);
|
||||
}
|
||||
|
||||
/* Phase accordion */
|
||||
.ac-phases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ac-phase {
|
||||
animation: ac-phase-in 0.35s cubic-bezier(0.16,1,0.3,1) both;
|
||||
}
|
||||
@keyframes ac-phase-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ac-phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.ac-phase.open .ac-phase-header {
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.ac-phase-header:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.ac-phase-num {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--accent, #3b82f6);
|
||||
background: rgba(59,130,246,0.08);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
border: 1px solid rgba(59,130,246,0.1);
|
||||
}
|
||||
|
||||
.ac-phase-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ac-phase-dots {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
.ac-phase-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-phase-dot.completed { background: var(--success, #16a34a); }
|
||||
.ac-phase-dot.failed { background: var(--danger, #dc2626); }
|
||||
.ac-phase-dot.running { background: var(--warning, #d97706); animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-phase-dot.pending { background: var(--text-tertiary, #6b7280); opacity: 0.4; }
|
||||
|
||||
@keyframes ac-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.ac-phase-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-phase-meta .findings-ct { color: var(--danger, #dc2626); font-weight: 600; }
|
||||
.ac-phase-meta .running-ct { color: var(--warning, #d97706); font-weight: 500; }
|
||||
|
||||
.ac-phase-chevron {
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
font-size: 11px;
|
||||
transition: transform 0.25s cubic-bezier(0.16,1,0.3,1);
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.ac-phase.open .ac-phase-chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.ac-phase-body {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.35s cubic-bezier(0.16,1,0.3,1);
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
.ac-phase.open .ac-phase-body {
|
||||
max-height: 2000px;
|
||||
}
|
||||
.ac-phase-body-inner {
|
||||
padding: 4px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
/* Tool rows */
|
||||
.ac-tool-row {
|
||||
display: grid;
|
||||
grid-template-columns: 5px 26px 1fr auto auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.ac-tool-row:hover {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
.ac-tool-row.expanded {
|
||||
background: rgba(59,130,246,0.03);
|
||||
}
|
||||
.ac-tool-row.is-pending {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ac-status-bar {
|
||||
width: 4px;
|
||||
height: 26px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ac-status-bar.completed { background: var(--success, #16a34a); }
|
||||
.ac-status-bar.failed { background: var(--danger, #dc2626); }
|
||||
.ac-status-bar.running { background: var(--warning, #d97706); animation: ac-pulse 1.5s ease-in-out infinite; }
|
||||
.ac-status-bar.pending { background: var(--text-tertiary, #6b7280); opacity: 0.25; }
|
||||
|
||||
.ac-tool-icon {
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ac-tool-info { min-width: 0; }
|
||||
.ac-tool-name {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Category chips */
|
||||
.ac-cat-chip {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.ac-cat-chip.recon { color: #38bdf8; background: rgba(56,189,248,0.1); }
|
||||
.ac-cat-chip.api { color: #818cf8; background: rgba(129,140,248,0.1); }
|
||||
.ac-cat-chip.headers { color: #06b6d4; background: rgba(6,182,212,0.1); }
|
||||
.ac-cat-chip.csp { color: #d946ef; background: rgba(217,70,239,0.1); }
|
||||
.ac-cat-chip.cookies { color: #f59e0b; background: rgba(245,158,11,0.1); }
|
||||
.ac-cat-chip.logs { color: #78716c; background: rgba(120,113,108,0.1); }
|
||||
.ac-cat-chip.ratelimit { color: #64748b; background: rgba(100,116,139,0.1); }
|
||||
.ac-cat-chip.cors { color: #8b5cf6; background: rgba(139,92,246,0.1); }
|
||||
.ac-cat-chip.tls { color: #14b8a6; background: rgba(20,184,166,0.1); }
|
||||
.ac-cat-chip.redirect { color: #fb923c; background: rgba(251,146,60,0.1); }
|
||||
.ac-cat-chip.email { color: #0ea5e9; background: rgba(14,165,233,0.1); }
|
||||
.ac-cat-chip.auth { color: #f43f5e; background: rgba(244,63,94,0.1); }
|
||||
.ac-cat-chip.xss { color: #f97316; background: rgba(249,115,22,0.1); }
|
||||
.ac-cat-chip.sqli { color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||
.ac-cat-chip.ssrf { color: #a855f7; background: rgba(168,85,247,0.1); }
|
||||
.ac-cat-chip.idor { color: #ec4899; background: rgba(236,72,153,0.1); }
|
||||
.ac-cat-chip.fuzzer { color: #a78bfa; background: rgba(167,139,250,0.1); }
|
||||
.ac-cat-chip.cve { color: #dc2626; background: rgba(220,38,38,0.1); }
|
||||
.ac-cat-chip.default { color: #94a3b8; background: rgba(148,163,184,0.1); }
|
||||
|
||||
.ac-tool-duration {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
.ac-tool-duration.running-text {
|
||||
color: var(--warning, #d97706);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ac-findings-pill {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
padding: 1px 7px;
|
||||
border-radius: 9px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
.ac-findings-pill.has { background: rgba(220,38,38,0.12); color: var(--danger, #dc2626); }
|
||||
.ac-findings-pill.zero { background: transparent; color: var(--text-tertiary, #6b7280); font-weight: 400; opacity: 0.5; }
|
||||
|
||||
.ac-risk-val {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
.ac-risk-val.high { color: var(--danger, #dc2626); }
|
||||
.ac-risk-val.medium { color: var(--warning, #d97706); }
|
||||
.ac-risk-val.low { color: var(--text-secondary); }
|
||||
.ac-risk-val.none { color: transparent; }
|
||||
|
||||
/* Tool detail (expanded) */
|
||||
.ac-tool-detail {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.28s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.ac-tool-detail.open {
|
||||
max-height: 300px;
|
||||
}
|
||||
.ac-tool-detail-inner {
|
||||
padding: 6px 10px 10px 49px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-reasoning-block {
|
||||
background: rgba(59,130,246,0.03);
|
||||
border-left: 2px solid var(--accent, #3b82f6);
|
||||
padding: 7px 12px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
font-style: italic;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.ac-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 3px 14px;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 10px;
|
||||
}
|
||||
.ac-detail-label {
|
||||
color: var(--text-tertiary, #6b7280);
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.ac-detail-value {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ pub enum Route {
|
||||
DastFindingsPage {},
|
||||
#[route("/dast/findings/:id")]
|
||||
DastFindingDetailPage { id: String },
|
||||
#[route("/pentest")]
|
||||
PentestDashboardPage {},
|
||||
#[route("/pentest/:session_id")]
|
||||
PentestSessionPage { session_id: String },
|
||||
#[route("/mcp-servers")]
|
||||
McpServersPage {},
|
||||
#[route("/settings")]
|
||||
@@ -49,7 +53,6 @@ const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||
const VIS_NETWORK_JS: Asset = asset!("/assets/vis-network.min.js");
|
||||
const GRAPH_VIZ_JS: Asset = asset!("/assets/graph-viz.js");
|
||||
|
||||
#[component]
|
||||
pub fn App() -> Element {
|
||||
rsx! {
|
||||
|
||||
@@ -47,6 +47,11 @@ pub fn Sidebar() -> Element {
|
||||
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 {},
|
||||
@@ -78,6 +83,7 @@ pub fn Sidebar() -> Element {
|
||||
(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" };
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod findings;
|
||||
pub mod graph;
|
||||
pub mod issues;
|
||||
pub mod mcp;
|
||||
pub mod pentest;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
|
||||
308
compliance-dashboard/src/infrastructure/pentest.rs
Normal file
308
compliance-dashboard/src/infrastructure/pentest.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::dast::DastFindingsResponse;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PentestSessionsResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PentestSessionResponse {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PentestMessagesResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PentestStatsResponse {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct AttackChainResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_pentest_sessions() -> Result<PentestSessionsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
// Fetch sessions
|
||||
let url = format!("{}/api/v1/pentest/sessions", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let mut body: PentestSessionsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
// Fetch DAST targets to resolve target names
|
||||
let targets_url = format!("{}/api/v1/dast/targets", state.agent_api_url);
|
||||
if let Ok(tresp) = reqwest::get(&targets_url).await {
|
||||
if let Ok(tbody) = tresp.json::<serde_json::Value>().await {
|
||||
let targets = tbody.get("data").and_then(|v| v.as_array());
|
||||
if let Some(targets) = targets {
|
||||
// Build target_id -> name lookup
|
||||
let target_map: std::collections::HashMap<String, String> = targets
|
||||
.iter()
|
||||
.filter_map(|t| {
|
||||
let id = t.get("_id")?.get("$oid")?.as_str()?.to_string();
|
||||
let name = t.get("name")?.as_str()?.to_string();
|
||||
Some((id, name))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Enrich sessions with target_name
|
||||
for session in body.data.iter_mut() {
|
||||
if let Some(tid) = session.get("target_id").and_then(|v| v.as_str()) {
|
||||
if let Some(name) = target_map.get(tid) {
|
||||
session.as_object_mut().map(|obj| {
|
||||
obj.insert(
|
||||
"target_name".to_string(),
|
||||
serde_json::Value::String(name.clone()),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_pentest_session(id: String) -> Result<PentestSessionResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/pentest/sessions/{id}", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let mut body: PentestSessionResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
// Resolve target name from targets list
|
||||
if let Some(tid) = body.data.get("target_id").and_then(|v| v.as_str()) {
|
||||
let targets_url = format!("{}/api/v1/dast/targets", state.agent_api_url);
|
||||
if let Ok(tresp) = reqwest::get(&targets_url).await {
|
||||
if let Ok(tbody) = tresp.json::<serde_json::Value>().await {
|
||||
if let Some(targets) = tbody.get("data").and_then(|v| v.as_array()) {
|
||||
for t in targets {
|
||||
let t_id = t.get("_id").and_then(|v| v.get("$oid")).and_then(|v| v.as_str()).unwrap_or("");
|
||||
if t_id == tid {
|
||||
if let Some(name) = t.get("name").and_then(|v| v.as_str()) {
|
||||
body.data.as_object_mut().map(|obj| {
|
||||
obj.insert("target_name".to_string(), serde_json::Value::String(name.to_string()))
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_pentest_messages(
|
||||
session_id: String,
|
||||
) -> Result<PentestMessagesResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/messages",
|
||||
state.agent_api_url
|
||||
);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: PentestMessagesResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_pentest_stats() -> Result<PentestStatsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/pentest/stats", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: PentestStatsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_attack_chain(
|
||||
session_id: String,
|
||||
) -> Result<AttackChainResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/attack-chain",
|
||||
state.agent_api_url
|
||||
);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: AttackChainResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn create_pentest_session(
|
||||
target_id: String,
|
||||
strategy: String,
|
||||
message: String,
|
||||
) -> Result<PentestSessionResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/pentest/sessions", state.agent_api_url);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"target_id": target_id,
|
||||
"strategy": strategy,
|
||||
"message": message,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: PentestSessionResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn send_pentest_message(
|
||||
session_id: String,
|
||||
message: String,
|
||||
) -> Result<PentestMessagesResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/chat",
|
||||
state.agent_api_url
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"message": message,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: PentestMessagesResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn stop_pentest_session(session_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/stop",
|
||||
state.agent_api_url
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_pentest_findings(
|
||||
session_id: String,
|
||||
) -> Result<DastFindingsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/findings",
|
||||
state.agent_api_url
|
||||
);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: DastFindingsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ExportReportResponse {
|
||||
pub archive_base64: String,
|
||||
pub sha256: String,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn export_pentest_report(
|
||||
session_id: String,
|
||||
password: String,
|
||||
requester_name: String,
|
||||
requester_email: String,
|
||||
) -> Result<ExportReportResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/pentest/sessions/{session_id}/export",
|
||||
state.agent_api_url
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"password": password,
|
||||
"requester_name": requester_name,
|
||||
"requester_email": requester_email,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
if !resp.status().is_success() {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
return Err(ServerFnError::new(format!("Export failed: {text}")));
|
||||
}
|
||||
let body: ExportReportResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
@@ -11,6 +11,11 @@ use crate::infrastructure::dast::fetch_dast_findings;
|
||||
pub fn DastFindingsPage() -> Element {
|
||||
let findings = use_resource(|| async { fetch_dast_findings().await.ok() });
|
||||
|
||||
let mut filter_severity = use_signal(|| "all".to_string());
|
||||
let mut filter_vuln_type = use_signal(|| "all".to_string());
|
||||
let mut filter_exploitable = use_signal(|| "all".to_string());
|
||||
let mut search_text = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
div { class: "back-nav",
|
||||
button {
|
||||
@@ -26,14 +31,105 @@ pub fn DastFindingsPage() -> Element {
|
||||
description: "Vulnerabilities discovered through dynamic application security testing",
|
||||
}
|
||||
|
||||
// Filter bar
|
||||
div { style: "display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; align-items: center;",
|
||||
// Search
|
||||
div { style: "flex: 1; min-width: 180px;",
|
||||
input {
|
||||
class: "chat-input",
|
||||
style: "width: 100%; padding: 6px 10px; font-size: 0.85rem;",
|
||||
placeholder: "Search title or endpoint...",
|
||||
value: "{search_text}",
|
||||
oninput: move |e| search_text.set(e.value()),
|
||||
}
|
||||
}
|
||||
// Severity
|
||||
select {
|
||||
style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;",
|
||||
value: "{filter_severity}",
|
||||
onchange: move |e| filter_severity.set(e.value()),
|
||||
option { value: "all", "All Severities" }
|
||||
option { value: "critical", "Critical" }
|
||||
option { value: "high", "High" }
|
||||
option { value: "medium", "Medium" }
|
||||
option { value: "low", "Low" }
|
||||
option { value: "info", "Info" }
|
||||
}
|
||||
// Vuln type
|
||||
select {
|
||||
style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;",
|
||||
value: "{filter_vuln_type}",
|
||||
onchange: move |e| filter_vuln_type.set(e.value()),
|
||||
option { value: "all", "All Types" }
|
||||
option { value: "sql_injection", "SQL Injection" }
|
||||
option { value: "xss", "XSS" }
|
||||
option { value: "auth_bypass", "Auth Bypass" }
|
||||
option { value: "ssrf", "SSRF" }
|
||||
option { value: "api_misconfiguration", "API Misconfiguration" }
|
||||
option { value: "open_redirect", "Open Redirect" }
|
||||
option { value: "idor", "IDOR" }
|
||||
option { value: "information_disclosure", "Information Disclosure" }
|
||||
option { value: "security_misconfiguration", "Security Misconfiguration" }
|
||||
option { value: "broken_auth", "Broken Auth" }
|
||||
option { value: "dns_misconfiguration", "DNS Misconfiguration" }
|
||||
option { value: "email_security", "Email Security" }
|
||||
option { value: "tls_misconfiguration", "TLS Misconfiguration" }
|
||||
option { value: "cookie_security", "Cookie Security" }
|
||||
option { value: "csp_issue", "CSP Issue" }
|
||||
option { value: "cors_misconfiguration", "CORS Misconfiguration" }
|
||||
option { value: "rate_limit_absent", "Rate Limit Absent" }
|
||||
option { value: "console_log_leakage", "Console Log Leakage" }
|
||||
option { value: "security_header_missing", "Security Header Missing" }
|
||||
option { value: "known_cve_exploit", "Known CVE Exploit" }
|
||||
option { value: "other", "Other" }
|
||||
}
|
||||
// Exploitable
|
||||
select {
|
||||
style: "padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); font-size: 0.85rem;",
|
||||
value: "{filter_exploitable}",
|
||||
onchange: move |e| filter_exploitable.set(e.value()),
|
||||
option { value: "all", "All" }
|
||||
option { value: "yes", "Exploitable" }
|
||||
option { value: "no", "Unconfirmed" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
match &*findings.read() {
|
||||
Some(Some(data)) => {
|
||||
let finding_list = &data.data;
|
||||
if finding_list.is_empty() {
|
||||
rsx! { p { "No DAST findings yet. Run a scan to discover vulnerabilities." } }
|
||||
let sev_filter = filter_severity.read().clone();
|
||||
let vt_filter = filter_vuln_type.read().clone();
|
||||
let exp_filter = filter_exploitable.read().clone();
|
||||
let search = search_text.read().to_lowercase();
|
||||
|
||||
let filtered: Vec<_> = data.data.iter().filter(|f| {
|
||||
let severity = f.get("severity").and_then(|v| v.as_str()).unwrap_or("info");
|
||||
let vuln_type = f.get("vuln_type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let exploitable = f.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let title = f.get("title").and_then(|v| v.as_str()).unwrap_or("").to_lowercase();
|
||||
let endpoint = f.get("endpoint").and_then(|v| v.as_str()).unwrap_or("").to_lowercase();
|
||||
|
||||
(sev_filter == "all" || severity == sev_filter)
|
||||
&& (vt_filter == "all" || vuln_type == vt_filter)
|
||||
&& match exp_filter.as_str() {
|
||||
"yes" => exploitable,
|
||||
"no" => !exploitable,
|
||||
_ => true,
|
||||
}
|
||||
&& (search.is_empty() || title.contains(&search) || endpoint.contains(&search))
|
||||
}).collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
if data.data.is_empty() {
|
||||
rsx! { p { style: "padding: 16px;", "No DAST findings yet. Run a scan to discover vulnerabilities." } }
|
||||
} else {
|
||||
rsx! { p { style: "padding: 16px; color: var(--text-secondary);", "No findings match the current filters." } }
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
div { style: "padding: 8px 16px; font-size: 0.8rem; color: var(--text-secondary);",
|
||||
"Showing {filtered.len()} of {data.data.len()} findings"
|
||||
}
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
@@ -46,7 +142,7 @@ pub fn DastFindingsPage() -> Element {
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for finding in finding_list {
|
||||
for finding in filtered {
|
||||
{
|
||||
let id = finding.get("_id").and_then(|v| v.get("$oid")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let severity = finding.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
|
||||
|
||||
@@ -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;
|
||||
|
||||
398
compliance-dashboard/src/pages/pentest_dashboard.rs
Normal file
398
compliance-dashboard/src/pages/pentest_dashboard.rs
Normal file
@@ -0,0 +1,398 @@
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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_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.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 stop_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 {
|
||||
div { style: "margin-top: 8px; display: flex; justify-content: flex-end;",
|
||||
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..." } },
|
||||
}
|
||||
}
|
||||
|
||||
// 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1141
compliance-dashboard/src/pages/pentest_session.rs
Normal file
1141
compliance-dashboard/src/pages/pentest_session.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user