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

@@ -28,9 +28,9 @@ pub use graph::{
pub use issue::{IssueStatus, TrackerIssue, TrackerType};
pub use mcp::{McpServerConfig, McpServerStatus, McpTransport};
pub use pentest::{
AttackChainNode, AttackNodeStatus, CodeContextHint, PentestEvent, PentestMessage,
PentestSession, PentestStats, PentestStatus, PentestStrategy, SeverityDistribution,
ToolCallRecord,
AttackChainNode, AttackNodeStatus, AuthMode, CodeContextHint, Environment, PentestAuthConfig,
PentestConfig, PentestEvent, PentestMessage, PentestSession, PentestStats, PentestStatus,
PentestStrategy, SeverityDistribution, TesterInfo, ToolCallRecord,
};
pub use repository::{ScanTrigger, TrackedRepository};
pub use sbom::{SbomEntry, VulnRef};

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@@ -50,6 +52,104 @@ impl std::fmt::Display for PentestStrategy {
}
}
/// Authentication mode for the pentest target
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AuthMode {
#[default]
None,
Manual,
AutoRegister,
}
/// Target environment classification
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Environment {
#[default]
Development,
Staging,
Production,
}
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Development => write!(f, "Development"),
Self::Staging => write!(f, "Staging"),
Self::Production => write!(f, "Production"),
}
}
}
/// Tester identity for the engagement record
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TesterInfo {
pub name: String,
pub email: String,
}
/// Authentication configuration for the pentest session
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PentestAuthConfig {
pub mode: AuthMode,
pub username: Option<String>,
pub password: Option<String>,
/// Optional — if omitted the orchestrator uses Playwright to discover it.
pub registration_url: Option<String>,
/// Base email for plus-addressing (e.g. `pentest@scanner.example.com`).
/// The orchestrator generates `base+{session_id}@domain` per session.
pub verification_email: Option<String>,
/// IMAP server to poll for verification emails (e.g. `imap.example.com`).
pub imap_host: Option<String>,
/// IMAP port (default 993 for TLS).
pub imap_port: Option<u16>,
/// IMAP username (defaults to `verification_email` if omitted).
pub imap_username: Option<String>,
/// IMAP password / app-specific password.
pub imap_password: Option<String>,
#[serde(default)]
pub cleanup_test_user: bool,
}
/// Full wizard configuration for a pentest session
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PentestConfig {
// Step 1: Target & Scope
pub app_url: String,
pub git_repo_url: Option<String>,
pub branch: Option<String>,
pub commit_hash: Option<String>,
pub app_type: Option<String>,
pub rate_limit: Option<u32>,
// Step 2: Authentication
#[serde(default)]
pub auth: PentestAuthConfig,
#[serde(default)]
pub custom_headers: HashMap<String, String>,
// Step 3: Strategy & Instructions
pub strategy: Option<String>,
#[serde(default)]
pub allow_destructive: bool,
pub initial_instructions: Option<String>,
#[serde(default)]
pub scope_exclusions: Vec<String>,
// Step 4: Disclaimer & Confirm
#[serde(default)]
pub disclaimer_accepted: bool,
pub disclaimer_accepted_at: Option<DateTime<Utc>>,
#[serde(default)]
pub environment: Environment,
#[serde(default)]
pub tester: TesterInfo,
pub max_duration_minutes: Option<u32>,
#[serde(default)]
pub skip_mode: bool,
}
/// A pentest session initiated via the chat interface
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PentestSession {
@@ -60,6 +160,8 @@ pub struct PentestSession {
pub repo_id: Option<String>,
pub status: PentestStatus,
pub strategy: PentestStrategy,
/// Wizard configuration (None for legacy sessions)
pub config: Option<PentestConfig>,
pub created_by: Option<String>,
/// Total number of tool invocations in this session
pub tool_invocations: u32,
@@ -83,6 +185,7 @@ impl PentestSession {
repo_id: None,
status: PentestStatus::Running,
strategy,
config: None,
created_by: None,
tool_invocations: 0,
tool_successes: 0,
@@ -261,6 +364,10 @@ pub enum PentestEvent {
Complete { summary: String },
/// Error occurred
Error { message: String },
/// Session paused
Paused,
/// Session resumed
Resumed,
}
/// Aggregated stats for the pentest dashboard

View File

@@ -436,6 +436,87 @@ fn pentest_event_serde_finding() {
}
}
// ─── PentestEvent Paused/Resumed ───
#[test]
fn pentest_event_serde_paused() {
let event = pentest::PentestEvent::Paused;
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"paused""#));
let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
assert!(matches!(back, pentest::PentestEvent::Paused));
}
#[test]
fn pentest_event_serde_resumed() {
let event = pentest::PentestEvent::Resumed;
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"resumed""#));
let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
assert!(matches!(back, pentest::PentestEvent::Resumed));
}
// ─── PentestConfig serde ───
#[test]
fn pentest_config_serde_roundtrip() {
let config = pentest::PentestConfig {
app_url: "https://example.com".into(),
git_repo_url: Some("https://github.com/org/repo".into()),
branch: Some("main".into()),
commit_hash: None,
app_type: Some("web".into()),
rate_limit: Some(10),
auth: pentest::PentestAuthConfig {
mode: pentest::AuthMode::Manual,
username: Some("admin".into()),
password: Some("pass123".into()),
registration_url: None,
verification_email: None,
imap_host: None,
imap_port: None,
imap_username: None,
imap_password: None,
cleanup_test_user: true,
},
custom_headers: [("X-Token".to_string(), "abc".to_string())]
.into_iter()
.collect(),
strategy: Some("comprehensive".into()),
allow_destructive: false,
initial_instructions: Some("Test the login flow".into()),
scope_exclusions: vec!["/admin".into()],
disclaimer_accepted: true,
disclaimer_accepted_at: Some(chrono::Utc::now()),
environment: pentest::Environment::Staging,
tester: pentest::TesterInfo {
name: "Alice".into(),
email: "alice@example.com".into(),
},
max_duration_minutes: Some(30),
skip_mode: false,
};
let json = serde_json::to_string(&config).unwrap();
let back: pentest::PentestConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.app_url, "https://example.com");
assert_eq!(back.auth.mode, pentest::AuthMode::Manual);
assert_eq!(back.auth.username, Some("admin".into()));
assert!(back.auth.cleanup_test_user);
assert_eq!(back.scope_exclusions, vec!["/admin".to_string()]);
assert_eq!(back.environment, pentest::Environment::Staging);
}
#[test]
fn pentest_auth_config_default() {
let auth = pentest::PentestAuthConfig::default();
assert_eq!(auth.mode, pentest::AuthMode::None);
assert!(auth.username.is_none());
assert!(auth.password.is_none());
assert!(auth.verification_email.is_none());
assert!(auth.imap_host.is_none());
assert!(!auth.cleanup_test_user);
}
// ─── Serde helpers (BSON datetime) ───
#[test]