feat: pentest onboarding — streaming, browser automation, reports, user cleanup (#16)
All checks were successful
All checks were successful
Complete pentest feature overhaul: SSE streaming, session-persistent browser tool (CDP), AES-256 credential encryption, auto-screenshots in reports, code-level remediation correlation, SAST triage chunking, context window optimization, test user cleanup (Keycloak/Auth0/Okta), wizard dropdowns, attack chain improvements, architecture docs with Mermaid diagrams. Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
238
compliance-agent/src/pentest/report/html/executive_summary.rs
Normal file
238
compliance-agent/src/pentest/report/html/executive_summary.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use super::html_escape;
|
||||
use compliance_core::models::dast::DastFinding;
|
||||
|
||||
pub(super) fn executive_summary(
|
||||
findings: &[DastFinding],
|
||||
target_name: &str,
|
||||
target_url: &str,
|
||||
tool_count: usize,
|
||||
tool_invocations: u32,
|
||||
success_rate: f64,
|
||||
) -> String {
|
||||
let critical = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == "critical")
|
||||
.count();
|
||||
let high = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == "high")
|
||||
.count();
|
||||
let medium = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == "medium")
|
||||
.count();
|
||||
let low = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == "low")
|
||||
.count();
|
||||
let info = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == "info")
|
||||
.count();
|
||||
let exploitable = findings.iter().filter(|f| f.exploitable).count();
|
||||
let total = findings.len();
|
||||
|
||||
let overall_risk = if critical > 0 {
|
||||
"CRITICAL"
|
||||
} else if high > 0 {
|
||||
"HIGH"
|
||||
} else if medium > 0 {
|
||||
"MEDIUM"
|
||||
} else if low > 0 {
|
||||
"LOW"
|
||||
} else {
|
||||
"INFORMATIONAL"
|
||||
};
|
||||
|
||||
let risk_color = match overall_risk {
|
||||
"CRITICAL" => "#991b1b",
|
||||
"HIGH" => "#c2410c",
|
||||
"MEDIUM" => "#a16207",
|
||||
"LOW" => "#1d4ed8",
|
||||
_ => "#4b5563",
|
||||
};
|
||||
|
||||
let risk_score: usize =
|
||||
std::cmp::min(100, critical * 25 + high * 15 + medium * 8 + low * 3 + info);
|
||||
|
||||
let severity_bar = build_severity_bar(critical, high, medium, low, info, total);
|
||||
|
||||
// Table of contents finding sub-entries
|
||||
let severity_order = ["critical", "high", "medium", "low", "info"];
|
||||
let toc_findings_sub = if !findings.is_empty() {
|
||||
let mut sub = String::new();
|
||||
let mut fnum = 0usize;
|
||||
for &sev_key in severity_order.iter() {
|
||||
let count = findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == sev_key)
|
||||
.count();
|
||||
if count == 0 {
|
||||
continue;
|
||||
}
|
||||
for f in findings
|
||||
.iter()
|
||||
.filter(|f| f.severity.to_string() == sev_key)
|
||||
{
|
||||
fnum += 1;
|
||||
sub.push_str(&format!(
|
||||
r#"<div class="toc-sub">F-{:03} — {}</div>"#,
|
||||
fnum,
|
||||
html_escape(&f.title),
|
||||
));
|
||||
}
|
||||
}
|
||||
sub
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let critical_high_str = format!("{} / {}", critical, high);
|
||||
let escaped_target_name = html_escape(target_name);
|
||||
let escaped_target_url = html_escape(target_url);
|
||||
|
||||
format!(
|
||||
r##"<!-- ═══════════════ TABLE OF CONTENTS ═══════════════ -->
|
||||
<div class="report-body">
|
||||
|
||||
<div class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<div class="toc-entry"><span class="toc-num">1</span><span class="toc-label">Executive Summary</span></div>
|
||||
<div class="toc-entry"><span class="toc-num">2</span><span class="toc-label">Scope & Methodology</span></div>
|
||||
<div class="toc-entry"><span class="toc-num">3</span><span class="toc-label">Findings ({total_findings})</span></div>
|
||||
{toc_findings_sub}
|
||||
<div class="toc-entry"><span class="toc-num">4</span><span class="toc-label">Attack Chain Timeline</span></div>
|
||||
<div class="toc-entry"><span class="toc-num">5</span><span class="toc-label">Appendix</span></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════ 1. EXECUTIVE SUMMARY ═══════════════ -->
|
||||
<h2><span class="section-num">1.</span> Executive Summary</h2>
|
||||
|
||||
<div class="risk-gauge">
|
||||
<div class="risk-gauge-meter">
|
||||
<div class="risk-gauge-track">
|
||||
<div class="risk-gauge-fill" style="width: {risk_score}%; background: {risk_color};"></div>
|
||||
</div>
|
||||
<div class="risk-gauge-score" style="color: {risk_color};">{risk_score} / 100</div>
|
||||
</div>
|
||||
<div class="risk-gauge-text">
|
||||
<div class="risk-gauge-label" style="color: {risk_color};">Overall Risk: {overall_risk}</div>
|
||||
<div class="risk-gauge-desc">
|
||||
Based on {total_findings} finding{findings_plural} identified across the target application.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exec-grid">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value">{total_findings}</div>
|
||||
<div class="kpi-label">Total Findings</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" style="color: var(--sev-critical);">{critical_high}</div>
|
||||
<div class="kpi-label">Critical / High</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" style="color: var(--sev-critical);">{exploitable_count}</div>
|
||||
<div class="kpi-label">Exploitable</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value">{tool_count}</div>
|
||||
<div class="kpi-label">Tools Used</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Severity Distribution</h3>
|
||||
{severity_bar}
|
||||
|
||||
<p>
|
||||
This report presents the results of an automated penetration test conducted against
|
||||
<strong>{target_name}</strong> (<code>{target_url}</code>) using the Compliance Scanner
|
||||
AI-powered testing engine. A total of <strong>{total_findings} vulnerabilities</strong> were
|
||||
identified, of which <strong>{exploitable_count}</strong> were confirmed exploitable with
|
||||
working proof-of-concept payloads. The assessment employed <strong>{tool_count} security tools</strong>
|
||||
across <strong>{tool_invocations} invocations</strong> ({success_rate:.0}% success rate).
|
||||
</p>"##,
|
||||
total_findings = total,
|
||||
findings_plural = if total == 1 { "" } else { "s" },
|
||||
critical_high = critical_high_str,
|
||||
exploitable_count = exploitable,
|
||||
target_name = escaped_target_name,
|
||||
target_url = escaped_target_url,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_severity_bar(
|
||||
critical: usize,
|
||||
high: usize,
|
||||
medium: usize,
|
||||
low: usize,
|
||||
info: usize,
|
||||
total: usize,
|
||||
) -> String {
|
||||
if total == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let crit_pct = (critical as f64 / total as f64 * 100.0) as usize;
|
||||
let high_pct = (high as f64 / total as f64 * 100.0) as usize;
|
||||
let med_pct = (medium as f64 / total as f64 * 100.0) as usize;
|
||||
let low_pct = (low as f64 / total as f64 * 100.0) as usize;
|
||||
let info_pct = 100_usize.saturating_sub(crit_pct + high_pct + med_pct + low_pct);
|
||||
|
||||
let mut bar = String::from(r#"<div class="sev-bar">"#);
|
||||
if critical > 0 {
|
||||
bar.push_str(&format!(
|
||||
r#"<div class="sev-bar-seg sev-bar-critical" style="width:{}%"><span>{}</span></div>"#,
|
||||
std::cmp::max(crit_pct, 4),
|
||||
critical
|
||||
));
|
||||
}
|
||||
if high > 0 {
|
||||
bar.push_str(&format!(
|
||||
r#"<div class="sev-bar-seg sev-bar-high" style="width:{}%"><span>{}</span></div>"#,
|
||||
std::cmp::max(high_pct, 4),
|
||||
high
|
||||
));
|
||||
}
|
||||
if medium > 0 {
|
||||
bar.push_str(&format!(
|
||||
r#"<div class="sev-bar-seg sev-bar-medium" style="width:{}%"><span>{}</span></div>"#,
|
||||
std::cmp::max(med_pct, 4),
|
||||
medium
|
||||
));
|
||||
}
|
||||
if low > 0 {
|
||||
bar.push_str(&format!(
|
||||
r#"<div class="sev-bar-seg sev-bar-low" style="width:{}%"><span>{}</span></div>"#,
|
||||
std::cmp::max(low_pct, 4),
|
||||
low
|
||||
));
|
||||
}
|
||||
if info > 0 {
|
||||
bar.push_str(&format!(
|
||||
r#"<div class="sev-bar-seg sev-bar-info" style="width:{}%"><span>{}</span></div>"#,
|
||||
std::cmp::max(info_pct, 4),
|
||||
info
|
||||
));
|
||||
}
|
||||
bar.push_str("</div>");
|
||||
bar.push_str(r#"<div class="sev-bar-legend">"#);
|
||||
if critical > 0 {
|
||||
bar.push_str(r#"<span><i class="sev-dot" style="background:#991b1b"></i> Critical</span>"#);
|
||||
}
|
||||
if high > 0 {
|
||||
bar.push_str(r#"<span><i class="sev-dot" style="background:#c2410c"></i> High</span>"#);
|
||||
}
|
||||
if medium > 0 {
|
||||
bar.push_str(r#"<span><i class="sev-dot" style="background:#a16207"></i> Medium</span>"#);
|
||||
}
|
||||
if low > 0 {
|
||||
bar.push_str(r#"<span><i class="sev-dot" style="background:#1d4ed8"></i> Low</span>"#);
|
||||
}
|
||||
if info > 0 {
|
||||
bar.push_str(r#"<span><i class="sev-dot" style="background:#4b5563"></i> Info</span>"#);
|
||||
}
|
||||
bar.push_str("</div>");
|
||||
bar
|
||||
}
|
||||
Reference in New Issue
Block a user