use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OwaspCategory {
A01BrokenAccessControl,
A02SecurityMisconfiguration,
A03SoftwareSupplyChainFailures,
A04CryptographicFailures,
A05Injection,
A06InsecureDesign,
A07AuthenticationFailures,
A08SoftwareOrDataIntegrityFailures,
A09SecurityLoggingAlertingFailures,
A10MishandlingOfExceptionalConditions,
LLM01PromptInjection,
LLM02SensitiveInformationDisclosure,
LLM03SupplyChain,
LLM04DataModelPoisoning,
LLM05ImproperOutputHandling,
LLM06ExcessiveAgency,
LLM07SystemPromptLeakage,
LLM08VectorEmbeddingWeaknesses,
LLM09Misinformation,
LLM10UnboundedConsumption,
}
impl OwaspCategory {
pub fn id(&self) -> &'static str {
match self {
Self::A01BrokenAccessControl => "A01",
Self::A02SecurityMisconfiguration => "A02",
Self::A03SoftwareSupplyChainFailures => "A03",
Self::A04CryptographicFailures => "A04",
Self::A05Injection => "A05",
Self::A06InsecureDesign => "A06",
Self::A07AuthenticationFailures => "A07",
Self::A08SoftwareOrDataIntegrityFailures => "A08",
Self::A09SecurityLoggingAlertingFailures => "A09",
Self::A10MishandlingOfExceptionalConditions => "A10",
Self::LLM01PromptInjection => "LLM01",
Self::LLM02SensitiveInformationDisclosure => "LLM02",
Self::LLM03SupplyChain => "LLM03",
Self::LLM04DataModelPoisoning => "LLM04",
Self::LLM05ImproperOutputHandling => "LLM05",
Self::LLM06ExcessiveAgency => "LLM06",
Self::LLM07SystemPromptLeakage => "LLM07",
Self::LLM08VectorEmbeddingWeaknesses => "LLM08",
Self::LLM09Misinformation => "LLM09",
Self::LLM10UnboundedConsumption => "LLM10",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::A01BrokenAccessControl => "Broken Access Control",
Self::A02SecurityMisconfiguration => "Security Misconfiguration",
Self::A03SoftwareSupplyChainFailures => "Software Supply Chain Failures",
Self::A04CryptographicFailures => "Cryptographic Failures",
Self::A05Injection => "Injection",
Self::A06InsecureDesign => "Insecure Design",
Self::A07AuthenticationFailures => "Authentication Failures",
Self::A08SoftwareOrDataIntegrityFailures => "Software or Data Integrity Failures",
Self::A09SecurityLoggingAlertingFailures => "Security Logging & Alerting Failures",
Self::A10MishandlingOfExceptionalConditions => "Mishandling of Exceptional Conditions",
Self::LLM01PromptInjection => "Prompt Injection",
Self::LLM02SensitiveInformationDisclosure => "Sensitive Information Disclosure",
Self::LLM03SupplyChain => "Supply Chain",
Self::LLM04DataModelPoisoning => "Data and Model Poisoning",
Self::LLM05ImproperOutputHandling => "Improper Output Handling",
Self::LLM06ExcessiveAgency => "Excessive Agency",
Self::LLM07SystemPromptLeakage => "System Prompt Leakage",
Self::LLM08VectorEmbeddingWeaknesses => "Vector and Embedding Weaknesses",
Self::LLM09Misinformation => "Misinformation",
Self::LLM10UnboundedConsumption => "Unbounded Consumption",
}
}
pub fn reference_url(&self) -> String {
match self {
Self::A01BrokenAccessControl
| Self::A02SecurityMisconfiguration
| Self::A03SoftwareSupplyChainFailures
| Self::A04CryptographicFailures
| Self::A05Injection
| Self::A06InsecureDesign
| Self::A07AuthenticationFailures
| Self::A08SoftwareOrDataIntegrityFailures
| Self::A09SecurityLoggingAlertingFailures
| Self::A10MishandlingOfExceptionalConditions => {
format!("https://owasp.org/Top10/2025/{}_2025-{}/",
self.id(),
self.name().replace(" ", "_").replace("&", "and"))
}
Self::LLM01PromptInjection => "https://genai.owasp.org/llmrisk/llm01-prompt-injection/".to_string(),
Self::LLM02SensitiveInformationDisclosure => "https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/".to_string(),
Self::LLM03SupplyChain => "https://genai.owasp.org/llmrisk/llm032025-supply-chain/".to_string(),
Self::LLM04DataModelPoisoning => "https://genai.owasp.org/llmrisk/llm042025-data-and-model-poisoning/".to_string(),
Self::LLM05ImproperOutputHandling => "https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/".to_string(),
Self::LLM06ExcessiveAgency => "https://genai.owasp.org/llmrisk/llm062025-excessive-agency/".to_string(),
Self::LLM07SystemPromptLeakage => "https://genai.owasp.org/llmrisk/llm072025-system-prompt-leakage/".to_string(),
Self::LLM08VectorEmbeddingWeaknesses => "https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/".to_string(),
Self::LLM09Misinformation => "https://genai.owasp.org/llmrisk/llm092025-misinformation/".to_string(),
Self::LLM10UnboundedConsumption => "https://genai.owasp.org/llmrisk/llm102025-unbounded-consumption/".to_string(),
}
}
pub fn from_attack_type(attack_type: &str) -> Option<Self> {
match attack_type.to_lowercase().as_str() {
"xss" | "sqli" | "sql-injection" | "nosql-injection" | "command-injection"
| "ldap-injection" | "xxe" | "ssti" => Some(Self::A05Injection),
"ssrf" | "path-traversal" | "lfi" | "directory-traversal"
| "unauthorized-access" => Some(Self::A01BrokenAccessControl),
"rce" | "code-injection" => Some(Self::A08SoftwareOrDataIntegrityFailures),
"authentication" | "session" | "auth-bypass" => Some(Self::A07AuthenticationFailures),
"misconfiguration" | "default-credentials" => Some(Self::A02SecurityMisconfiguration),
"crypto" | "encryption" | "weak-crypto" => Some(Self::A04CryptographicFailures),
"error-handling" | "information-disclosure" => Some(Self::A10MishandlingOfExceptionalConditions),
"http2-bypass" | "http2" => Some(Self::A02SecurityMisconfiguration),
"adfs" | "windows-auth" => Some(Self::A07AuthenticationFailures),
"prompt-injection" | "jailbreak" | "instruction-hijacking"
| "context-confusion" | "role-reversal" => Some(Self::LLM01PromptInjection),
"training-data-leak" | "pii-exposure" | "model-inversion"
| "membership-inference" => Some(Self::LLM02SensitiveInformationDisclosure),
"plugin-vulnerability" | "model-backdoor" | "supply-chain-attack" => Some(Self::LLM03SupplyChain),
"data-poisoning" | "model-poisoning" | "backdoor-injection" => Some(Self::LLM04DataModelPoisoning),
"llm-xss" | "code-generation-injection" | "unsafe-output" => Some(Self::LLM05ImproperOutputHandling),
"excessive-permissions" | "function-calling-abuse" | "tool-misuse" => Some(Self::LLM06ExcessiveAgency),
"system-prompt-leak" | "instruction-disclosure" | "context-dumping" => Some(Self::LLM07SystemPromptLeakage),
"rag-poisoning" | "embedding-attack" | "semantic-manipulation" => Some(Self::LLM08VectorEmbeddingWeaknesses),
"hallucination" | "misinformation" | "false-facts" => Some(Self::LLM09Misinformation),
"dos" | "token-exhaustion" | "context-flooding" | "resource-exhaustion" => Some(Self::LLM10UnboundedConsumption),
_ => None,
}
}
}
impl fmt::Display for OwaspCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.id(), self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn color(&self) -> owo_colors::DynColors {
match self {
Severity::Critical => owo_colors::DynColors::Rgb(255, 0, 0),
Severity::High => owo_colors::DynColors::Rgb(255, 100, 0),
Severity::Medium => owo_colors::DynColors::Rgb(255, 255, 0),
Severity::Low => owo_colors::DynColors::Rgb(0, 150, 255),
Severity::Info => owo_colors::DynColors::Rgb(200, 200, 200),
}
}
pub fn from_cvss(score: f32) -> Self {
match score {
s if s >= 9.0 => Severity::Critical,
s if s >= 7.0 => Severity::High,
s if s >= 4.0 => Severity::Medium,
s if s > 0.0 => Severity::Low,
_ => Severity::Info,
}
}
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Critical => write!(f, "Critical"),
Severity::High => write!(f, "High"),
Severity::Medium => write!(f, "Medium"),
Severity::Low => write!(f, "Low"),
Severity::Info => write!(f, "Info"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub payload_id: String,
pub severity: Severity,
pub category: String,
pub owasp_category: Option<OwaspCategory>,
pub payload_value: String,
pub technique_used: Option<String>,
pub response_status: u16,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extracted_data: Option<ExtractedData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedData {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub info_disclosure: Vec<InfoDisclosure>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub exposed_paths: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub auth_tokens: Vec<AuthToken>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version_info: Option<VersionInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub internal_ips: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub adfs_metadata: Option<AdfsMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_snippet: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub system_prompts: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub model_info: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub training_data_leaked: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub rag_context: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub jailbreak_indicators: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InfoDisclosure {
pub disclosure_type: String,
pub value: String,
pub severity: Severity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthToken {
pub token_type: String,
pub name: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub framework: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub details: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdfsMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub service_identifier: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub endpoints: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub certificates: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub claims: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub relying_parties: Vec<String>,
}
impl ExtractedData {
pub fn new() -> Self {
Self {
info_disclosure: Vec::new(),
exposed_paths: Vec::new(),
auth_tokens: Vec::new(),
version_info: None,
internal_ips: Vec::new(),
adfs_metadata: None,
response_snippet: None,
system_prompts: Vec::new(),
model_info: Vec::new(),
training_data_leaked: Vec::new(),
rag_context: Vec::new(),
jailbreak_indicators: Vec::new(),
}
}
pub fn has_data(&self) -> bool {
!self.info_disclosure.is_empty()
|| !self.exposed_paths.is_empty()
|| !self.auth_tokens.is_empty()
|| self.version_info.is_some()
|| !self.internal_ips.is_empty()
|| self.adfs_metadata.is_some()
|| !self.system_prompts.is_empty()
|| !self.model_info.is_empty()
|| !self.training_data_leaked.is_empty()
|| !self.rag_context.is_empty()
|| !self.jailbreak_indicators.is_empty()
}
}
impl Default for ExtractedData {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanSummary {
pub total_payloads: usize,
pub successful_bypasses: usize,
pub techniques_effective: usize,
pub duration_secs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResults {
pub target: String,
pub timestamp: String,
pub waf_detected: Option<String>,
pub findings: Vec<Finding>,
pub summary: ScanSummary,
}
impl ScanResults {
pub fn new(target: String, waf_detected: Option<String>) -> Self {
Self {
target,
timestamp: chrono::Utc::now().to_rfc3339(),
waf_detected,
findings: Vec::new(),
summary: ScanSummary {
total_payloads: 0,
successful_bypasses: 0,
techniques_effective: 0,
duration_secs: 0.0,
},
}
}
pub fn add_finding(&mut self, finding: Finding) {
self.findings.push(finding);
}
pub fn sort_by_severity(&mut self) {
self.findings.sort_by(|a, b| b.severity.cmp(&a.severity));
}
}