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

This commit was merged in pull request #12.
This commit is contained in:
2026-03-12 14:42:54 +00:00
parent 3ec1456b0d
commit acc5b86aa4
52 changed files with 11729 additions and 98 deletions

View File

@@ -176,6 +176,16 @@ pub enum DastVulnType {
InformationDisclosure,
SecurityMisconfiguration,
BrokenAuth,
DnsMisconfiguration,
EmailSecurity,
TlsMisconfiguration,
CookieSecurity,
CspIssue,
CorsMisconfiguration,
RateLimitAbsent,
ConsoleLogLeakage,
SecurityHeaderMissing,
KnownCveExploit,
Other,
}
@@ -192,6 +202,16 @@ impl std::fmt::Display for DastVulnType {
Self::InformationDisclosure => write!(f, "information_disclosure"),
Self::SecurityMisconfiguration => write!(f, "security_misconfiguration"),
Self::BrokenAuth => write!(f, "broken_auth"),
Self::DnsMisconfiguration => write!(f, "dns_misconfiguration"),
Self::EmailSecurity => write!(f, "email_security"),
Self::TlsMisconfiguration => write!(f, "tls_misconfiguration"),
Self::CookieSecurity => write!(f, "cookie_security"),
Self::CspIssue => write!(f, "csp_issue"),
Self::CorsMisconfiguration => write!(f, "cors_misconfiguration"),
Self::RateLimitAbsent => write!(f, "rate_limit_absent"),
Self::ConsoleLogLeakage => write!(f, "console_log_leakage"),
Self::SecurityHeaderMissing => write!(f, "security_header_missing"),
Self::KnownCveExploit => write!(f, "known_cve_exploit"),
Self::Other => write!(f, "other"),
}
}
@@ -244,6 +264,8 @@ pub struct DastFinding {
pub remediation: Option<String>,
/// Linked SAST finding ID (if correlated)
pub linked_sast_finding_id: Option<String>,
/// Pentest session that produced this finding (if AI-driven)
pub session_id: Option<String>,
#[serde(with = "super::serde_helpers::bson_datetime")]
pub created_at: DateTime<Utc>,
}
@@ -276,6 +298,7 @@ impl DastFinding {
evidence: Vec::new(),
remediation: None,
linked_sast_finding_id: None,
session_id: None,
created_at: Utc::now(),
}
}

View File

@@ -7,6 +7,7 @@ pub mod finding;
pub mod graph;
pub mod issue;
pub mod mcp;
pub mod pentest;
pub mod repository;
pub mod sbom;
pub mod scan;
@@ -26,6 +27,11 @@ 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,
};
pub use repository::{ScanTrigger, TrackedRepository};
pub use sbom::{SbomEntry, VulnRef};
pub use scan::{ScanPhase, ScanRun, ScanRunStatus, ScanType};

View File

@@ -0,0 +1,294 @@
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"),
}
}
}
/// 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,
pub created_by: Option<String>,
/// 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,
created_by: 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 },
}
/// 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>,
}