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
438 lines
13 KiB
Rust
438 lines
13 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Status of a pentest session
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PentestStatus {
|
|
Running,
|
|
Paused,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
impl std::fmt::Display for PentestStatus {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Running => write!(f, "running"),
|
|
Self::Paused => write!(f, "paused"),
|
|
Self::Completed => write!(f, "completed"),
|
|
Self::Failed => write!(f, "failed"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Strategy for the AI pentest orchestrator
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PentestStrategy {
|
|
/// Quick scan focusing on common vulnerabilities
|
|
Quick,
|
|
/// Standard comprehensive scan
|
|
Comprehensive,
|
|
/// Focus on specific vulnerability types guided by SAST/SBOM
|
|
Targeted,
|
|
/// Aggressive testing with more payloads and deeper exploitation
|
|
Aggressive,
|
|
/// Stealth mode with slower rate and fewer noisy payloads
|
|
Stealth,
|
|
}
|
|
|
|
impl std::fmt::Display for PentestStrategy {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Quick => write!(f, "quick"),
|
|
Self::Comprehensive => write!(f, "comprehensive"),
|
|
Self::Targeted => write!(f, "targeted"),
|
|
Self::Aggressive => write!(f, "aggressive"),
|
|
Self::Stealth => write!(f, "stealth"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}
|
|
|
|
/// Identity provider type for cleanup routing
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum IdentityProvider {
|
|
Keycloak,
|
|
Auth0,
|
|
Okta,
|
|
Firebase,
|
|
Custom,
|
|
}
|
|
|
|
/// Details of a test user created during a pentest session.
|
|
/// Stored so the cleanup step knows exactly what to delete and where.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct TestUserRecord {
|
|
/// Username used to register
|
|
pub username: Option<String>,
|
|
/// Email used to register
|
|
pub email: Option<String>,
|
|
/// User ID returned by the identity provider (if known)
|
|
pub provider_user_id: Option<String>,
|
|
/// Which identity provider holds this user
|
|
pub provider: Option<IdentityProvider>,
|
|
/// Whether cleanup has been completed
|
|
#[serde(default)]
|
|
pub cleaned_up: bool,
|
|
}
|
|
|
|
/// A pentest session initiated via the chat interface
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PentestSession {
|
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<bson::oid::ObjectId>,
|
|
pub target_id: String,
|
|
/// Linked repository for code-aware testing
|
|
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>,
|
|
/// Test user created during auto-register (for cleanup)
|
|
#[serde(default)]
|
|
pub test_user: Option<TestUserRecord>,
|
|
/// Total number of tool invocations in this session
|
|
pub tool_invocations: u32,
|
|
/// Total successful tool invocations
|
|
pub tool_successes: u32,
|
|
/// Number of findings discovered
|
|
pub findings_count: u32,
|
|
/// Number of confirmed exploitable findings
|
|
pub exploitable_count: u32,
|
|
#[serde(with = "super::serde_helpers::bson_datetime")]
|
|
pub started_at: DateTime<Utc>,
|
|
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
impl PentestSession {
|
|
pub fn new(target_id: String, strategy: PentestStrategy) -> Self {
|
|
Self {
|
|
id: None,
|
|
target_id,
|
|
repo_id: None,
|
|
status: PentestStatus::Running,
|
|
strategy,
|
|
config: None,
|
|
created_by: None,
|
|
test_user: None,
|
|
tool_invocations: 0,
|
|
tool_successes: 0,
|
|
findings_count: 0,
|
|
exploitable_count: 0,
|
|
started_at: Utc::now(),
|
|
completed_at: None,
|
|
}
|
|
}
|
|
|
|
pub fn success_rate(&self) -> f64 {
|
|
if self.tool_invocations == 0 {
|
|
return 100.0;
|
|
}
|
|
(self.tool_successes as f64 / self.tool_invocations as f64) * 100.0
|
|
}
|
|
}
|
|
|
|
/// Status of a node in the attack chain
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AttackNodeStatus {
|
|
Pending,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
Skipped,
|
|
}
|
|
|
|
/// A single step in the LLM-driven attack chain DAG
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AttackChainNode {
|
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<bson::oid::ObjectId>,
|
|
pub session_id: String,
|
|
/// Unique ID for DAG references
|
|
pub node_id: String,
|
|
/// Parent node IDs (multiple for merge nodes)
|
|
pub parent_node_ids: Vec<String>,
|
|
/// Tool that was invoked
|
|
pub tool_name: String,
|
|
/// Input parameters passed to the tool
|
|
pub tool_input: serde_json::Value,
|
|
/// Output from the tool
|
|
pub tool_output: Option<serde_json::Value>,
|
|
pub status: AttackNodeStatus,
|
|
/// LLM's reasoning for choosing this action
|
|
pub llm_reasoning: String,
|
|
/// IDs of DastFindings produced by this step
|
|
pub findings_produced: Vec<String>,
|
|
/// Risk score (0-100) assigned by the LLM
|
|
pub risk_score: Option<u8>,
|
|
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
|
pub started_at: Option<DateTime<Utc>>,
|
|
#[serde(default, with = "super::serde_helpers::opt_bson_datetime")]
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
impl AttackChainNode {
|
|
pub fn new(
|
|
session_id: String,
|
|
node_id: String,
|
|
tool_name: String,
|
|
tool_input: serde_json::Value,
|
|
llm_reasoning: String,
|
|
) -> Self {
|
|
Self {
|
|
id: None,
|
|
session_id,
|
|
node_id,
|
|
parent_node_ids: Vec::new(),
|
|
tool_name,
|
|
tool_input,
|
|
tool_output: None,
|
|
status: AttackNodeStatus::Pending,
|
|
llm_reasoning,
|
|
findings_produced: Vec::new(),
|
|
risk_score: None,
|
|
started_at: None,
|
|
completed_at: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Chat message within a pentest session
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PentestMessage {
|
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<bson::oid::ObjectId>,
|
|
pub session_id: String,
|
|
/// "user", "assistant", "tool_result", "system"
|
|
pub role: String,
|
|
pub content: String,
|
|
/// Tool calls made by the assistant in this message
|
|
pub tool_calls: Option<Vec<ToolCallRecord>>,
|
|
/// Link to the attack chain node (for tool_result messages)
|
|
pub attack_node_id: Option<String>,
|
|
#[serde(with = "super::serde_helpers::bson_datetime")]
|
|
pub created_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl PentestMessage {
|
|
pub fn user(session_id: String, content: String) -> Self {
|
|
Self {
|
|
id: None,
|
|
session_id,
|
|
role: "user".to_string(),
|
|
content,
|
|
tool_calls: None,
|
|
attack_node_id: None,
|
|
created_at: Utc::now(),
|
|
}
|
|
}
|
|
|
|
pub fn assistant(session_id: String, content: String) -> Self {
|
|
Self {
|
|
id: None,
|
|
session_id,
|
|
role: "assistant".to_string(),
|
|
content,
|
|
tool_calls: None,
|
|
attack_node_id: None,
|
|
created_at: Utc::now(),
|
|
}
|
|
}
|
|
|
|
pub fn tool_result(session_id: String, content: String, node_id: String) -> Self {
|
|
Self {
|
|
id: None,
|
|
session_id,
|
|
role: "tool_result".to_string(),
|
|
content,
|
|
tool_calls: None,
|
|
attack_node_id: Some(node_id),
|
|
created_at: Utc::now(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Record of a tool call made by the LLM
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ToolCallRecord {
|
|
pub call_id: String,
|
|
pub tool_name: String,
|
|
pub arguments: serde_json::Value,
|
|
pub result: Option<serde_json::Value>,
|
|
}
|
|
|
|
/// SSE event types for real-time pentest streaming
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum PentestEvent {
|
|
/// LLM is thinking/reasoning
|
|
Thinking { reasoning: String },
|
|
/// A tool execution has started
|
|
ToolStart {
|
|
node_id: String,
|
|
tool_name: String,
|
|
input: serde_json::Value,
|
|
},
|
|
/// A tool execution completed
|
|
ToolComplete {
|
|
node_id: String,
|
|
summary: String,
|
|
findings_count: u32,
|
|
},
|
|
/// A new finding was discovered
|
|
Finding {
|
|
finding_id: String,
|
|
title: String,
|
|
severity: String,
|
|
},
|
|
/// Assistant message (streaming text)
|
|
Message { content: String },
|
|
/// Session completed
|
|
Complete { summary: String },
|
|
/// Error occurred
|
|
Error { message: String },
|
|
/// Session paused
|
|
Paused,
|
|
/// Session resumed
|
|
Resumed,
|
|
}
|
|
|
|
/// Aggregated stats for the pentest dashboard
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PentestStats {
|
|
pub running_sessions: u32,
|
|
pub total_vulnerabilities: u32,
|
|
pub total_tool_invocations: u32,
|
|
pub tool_success_rate: f64,
|
|
pub severity_distribution: SeverityDistribution,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SeverityDistribution {
|
|
pub critical: u32,
|
|
pub high: u32,
|
|
pub medium: u32,
|
|
pub low: u32,
|
|
pub info: u32,
|
|
}
|
|
|
|
/// Code context hint linking a discovered endpoint to source code
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CodeContextHint {
|
|
/// HTTP route pattern (e.g., "GET /api/users/:id")
|
|
pub endpoint_pattern: String,
|
|
/// Handler function name
|
|
pub handler_function: String,
|
|
/// Source file path
|
|
pub file_path: String,
|
|
/// Relevant code snippet
|
|
pub code_snippet: String,
|
|
/// SAST findings associated with this code
|
|
pub known_vulnerabilities: Vec<String>,
|
|
}
|