Skip to main content

simple_waf_scanner/
types.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// OWASP Top 10:2025 Categories
5/// Reference: https://owasp.org/Top10/2025/
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
8pub enum OwaspCategory {
9    /// A01:2025 - Broken Access Control
10    /// Includes SSRF, path traversal, unauthorized access
11    A01BrokenAccessControl,
12    /// A02:2025 - Security Misconfiguration
13    /// Default credentials, unnecessary features, insecure defaults
14    A02SecurityMisconfiguration,
15    /// A03:2025 - Software Supply Chain Failures
16    /// Vulnerable dependencies, unsigned components
17    A03SoftwareSupplyChainFailures,
18    /// A04:2025 - Cryptographic Failures
19    /// Weak encryption, cleartext storage of sensitive data
20    A04CryptographicFailures,
21    /// A05:2025 - Injection
22    /// SQL, NoSQL, Command, LDAP, XSS, XXE injection
23    A05Injection,
24    /// A06:2025 - Insecure Design
25    /// Missing security controls, threat modeling failures
26    A06InsecureDesign,
27    /// A07:2025 - Authentication Failures
28    /// Broken session management, weak credentials
29    A07AuthenticationFailures,
30    /// A08:2025 - Software or Data Integrity Failures
31    /// Insecure CI/CD, untrusted updates
32    A08SoftwareOrDataIntegrityFailures,
33    /// A09:2025 - Security Logging & Alerting Failures
34    /// Insufficient logging, missing alerting
35    A09SecurityLoggingAlertingFailures,
36    /// A10:2025 - Mishandling of Exceptional Conditions
37    /// Poor error handling, information disclosure
38    A10MishandlingOfExceptionalConditions,
39    
40    // OWASP Top 10 for LLM Applications 2025
41    // Reference: https://genai.owasp.org/llm-top-10/
42    
43    /// LLM01:2025 - Prompt Injection
44    /// Direct/indirect prompt injections, jailbreaks, instruction hijacking
45    LLM01PromptInjection,
46    /// LLM02:2025 - Sensitive Information Disclosure
47    /// Training data leakage, PII exposure, API key disclosure
48    LLM02SensitiveInformationDisclosure,
49    /// LLM03:2025 - Supply Chain
50    /// Vulnerable plugins, compromised models, backdoors
51    LLM03SupplyChain,
52    /// LLM04:2025 - Data and Model Poisoning
53    /// Training data manipulation, backdoor injection
54    LLM04DataModelPoisoning,
55    /// LLM05:2025 - Improper Output Handling
56    /// XSS via LLM output, code injection through generated content
57    LLM05ImproperOutputHandling,
58    /// LLM06:2025 - Excessive Agency
59    /// Over-permissioned function calling, tool misuse
60    LLM06ExcessiveAgency,
61    /// LLM07:2025 - System Prompt Leakage
62    /// System instruction disclosure, context dumping
63    LLM07SystemPromptLeakage,
64    /// LLM08:2025 - Vector and Embedding Weaknesses
65    /// RAG poisoning, semantic search manipulation
66    LLM08VectorEmbeddingWeaknesses,
67    /// LLM09:2025 - Misinformation
68    /// Hallucinations, false information generation
69    LLM09Misinformation,
70    /// LLM10:2025 - Unbounded Consumption
71    /// DoS via context exhaustion, token flooding
72    LLM10UnboundedConsumption,
73}
74
75impl OwaspCategory {
76    /// Get the category identifier (e.g., "A01")
77    pub fn id(&self) -> &'static str {
78        match self {
79            Self::A01BrokenAccessControl => "A01",
80            Self::A02SecurityMisconfiguration => "A02",
81            Self::A03SoftwareSupplyChainFailures => "A03",
82            Self::A04CryptographicFailures => "A04",
83            Self::A05Injection => "A05",
84            Self::A06InsecureDesign => "A06",
85            Self::A07AuthenticationFailures => "A07",
86            Self::A08SoftwareOrDataIntegrityFailures => "A08",
87            Self::A09SecurityLoggingAlertingFailures => "A09",
88            Self::A10MishandlingOfExceptionalConditions => "A10",
89            Self::LLM01PromptInjection => "LLM01",
90            Self::LLM02SensitiveInformationDisclosure => "LLM02",
91            Self::LLM03SupplyChain => "LLM03",
92            Self::LLM04DataModelPoisoning => "LLM04",
93            Self::LLM05ImproperOutputHandling => "LLM05",
94            Self::LLM06ExcessiveAgency => "LLM06",
95            Self::LLM07SystemPromptLeakage => "LLM07",
96            Self::LLM08VectorEmbeddingWeaknesses => "LLM08",
97            Self::LLM09Misinformation => "LLM09",
98            Self::LLM10UnboundedConsumption => "LLM10",
99        }
100    }
101
102    /// Get the full category name
103    pub fn name(&self) -> &'static str {
104        match self {
105            Self::A01BrokenAccessControl => "Broken Access Control",
106            Self::A02SecurityMisconfiguration => "Security Misconfiguration",
107            Self::A03SoftwareSupplyChainFailures => "Software Supply Chain Failures",
108            Self::A04CryptographicFailures => "Cryptographic Failures",
109            Self::A05Injection => "Injection",
110            Self::A06InsecureDesign => "Insecure Design",
111            Self::A07AuthenticationFailures => "Authentication Failures",
112            Self::A08SoftwareOrDataIntegrityFailures => "Software or Data Integrity Failures",
113            Self::A09SecurityLoggingAlertingFailures => "Security Logging & Alerting Failures",
114            Self::A10MishandlingOfExceptionalConditions => "Mishandling of Exceptional Conditions",
115            Self::LLM01PromptInjection => "Prompt Injection",
116            Self::LLM02SensitiveInformationDisclosure => "Sensitive Information Disclosure",
117            Self::LLM03SupplyChain => "Supply Chain",
118            Self::LLM04DataModelPoisoning => "Data and Model Poisoning",
119            Self::LLM05ImproperOutputHandling => "Improper Output Handling",
120            Self::LLM06ExcessiveAgency => "Excessive Agency",
121            Self::LLM07SystemPromptLeakage => "System Prompt Leakage",
122            Self::LLM08VectorEmbeddingWeaknesses => "Vector and Embedding Weaknesses",
123            Self::LLM09Misinformation => "Misinformation",
124            Self::LLM10UnboundedConsumption => "Unbounded Consumption",
125        }
126    }
127
128    /// Get OWASP reference URL
129    pub fn reference_url(&self) -> String {
130        match self {
131            // OWASP Top 10:2025 for Web Applications
132            Self::A01BrokenAccessControl
133            | Self::A02SecurityMisconfiguration
134            | Self::A03SoftwareSupplyChainFailures
135            | Self::A04CryptographicFailures
136            | Self::A05Injection
137            | Self::A06InsecureDesign
138            | Self::A07AuthenticationFailures
139            | Self::A08SoftwareOrDataIntegrityFailures
140            | Self::A09SecurityLoggingAlertingFailures
141            | Self::A10MishandlingOfExceptionalConditions => {
142                format!("https://owasp.org/Top10/2025/{}_2025-{}/", 
143                    self.id(), 
144                    self.name().replace(" ", "_").replace("&", "and"))
145            }
146            // OWASP Top 10 for LLM Applications 2025
147            Self::LLM01PromptInjection => "https://genai.owasp.org/llmrisk/llm01-prompt-injection/".to_string(),
148            Self::LLM02SensitiveInformationDisclosure => "https://genai.owasp.org/llmrisk/llm022025-sensitive-information-disclosure/".to_string(),
149            Self::LLM03SupplyChain => "https://genai.owasp.org/llmrisk/llm032025-supply-chain/".to_string(),
150            Self::LLM04DataModelPoisoning => "https://genai.owasp.org/llmrisk/llm042025-data-and-model-poisoning/".to_string(),
151            Self::LLM05ImproperOutputHandling => "https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/".to_string(),
152            Self::LLM06ExcessiveAgency => "https://genai.owasp.org/llmrisk/llm062025-excessive-agency/".to_string(),
153            Self::LLM07SystemPromptLeakage => "https://genai.owasp.org/llmrisk/llm072025-system-prompt-leakage/".to_string(),
154            Self::LLM08VectorEmbeddingWeaknesses => "https://genai.owasp.org/llmrisk/llm082025-vector-and-embedding-weaknesses/".to_string(),
155            Self::LLM09Misinformation => "https://genai.owasp.org/llmrisk/llm092025-misinformation/".to_string(),
156            Self::LLM10UnboundedConsumption => "https://genai.owasp.org/llmrisk/llm102025-unbounded-consumption/".to_string(),
157        }
158    }
159
160    /// Map traditional attack category to OWASP Top 10:2025
161    pub fn from_attack_type(attack_type: &str) -> Option<Self> {
162        match attack_type.to_lowercase().as_str() {
163            // Web Application Security (OWASP Top 10:2025)
164            "xss" | "sqli" | "sql-injection" | "nosql-injection" | "command-injection" 
165            | "ldap-injection" | "xxe" | "ssti" => Some(Self::A05Injection),
166            "ssrf" | "path-traversal" | "lfi" | "directory-traversal" 
167            | "unauthorized-access" => Some(Self::A01BrokenAccessControl),
168            "rce" | "code-injection" => Some(Self::A08SoftwareOrDataIntegrityFailures),
169            "authentication" | "session" | "auth-bypass" => Some(Self::A07AuthenticationFailures),
170            "misconfiguration" | "default-credentials" => Some(Self::A02SecurityMisconfiguration),
171            "crypto" | "encryption" | "weak-crypto" => Some(Self::A04CryptographicFailures),
172            "error-handling" | "information-disclosure" => Some(Self::A10MishandlingOfExceptionalConditions),
173            "http2-bypass" | "http2" => Some(Self::A02SecurityMisconfiguration),
174            "adfs" | "windows-auth" => Some(Self::A07AuthenticationFailures),
175            
176            // LLM/GenAI Security (OWASP LLM Top 10:2025)
177            "prompt-injection" | "jailbreak" | "instruction-hijacking" 
178            | "context-confusion" | "role-reversal" => Some(Self::LLM01PromptInjection),
179            "training-data-leak" | "pii-exposure" | "model-inversion" 
180            | "membership-inference" => Some(Self::LLM02SensitiveInformationDisclosure),
181            "plugin-vulnerability" | "model-backdoor" | "supply-chain-attack" => Some(Self::LLM03SupplyChain),
182            "data-poisoning" | "model-poisoning" | "backdoor-injection" => Some(Self::LLM04DataModelPoisoning),
183            "llm-xss" | "code-generation-injection" | "unsafe-output" => Some(Self::LLM05ImproperOutputHandling),
184            "excessive-permissions" | "function-calling-abuse" | "tool-misuse" => Some(Self::LLM06ExcessiveAgency),
185            "system-prompt-leak" | "instruction-disclosure" | "context-dumping" => Some(Self::LLM07SystemPromptLeakage),
186            "rag-poisoning" | "embedding-attack" | "semantic-manipulation" => Some(Self::LLM08VectorEmbeddingWeaknesses),
187            "hallucination" | "misinformation" | "false-facts" => Some(Self::LLM09Misinformation),
188            "dos" | "token-exhaustion" | "context-flooding" | "resource-exhaustion" => Some(Self::LLM10UnboundedConsumption),
189            
190            _ => None,
191        }
192    }
193}
194
195impl fmt::Display for OwaspCategory {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(f, "{}: {}", self.id(), self.name())
198    }
199}
200
201/// Severity levels for security findings
202#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
203#[serde(rename_all = "lowercase")]
204pub enum Severity {
205    /// CVSS 0.0 - Informational findings
206    Info,
207    /// CVSS 0.1-3.9 - Low severity issues
208    Low,
209    /// CVSS 4.0-6.9 - Medium severity issues
210    Medium,
211    /// CVSS 7.0-8.9 - High severity issues
212    High,
213    /// CVSS 9.0-10.0 - Critical severity issues
214    Critical,
215}
216
217impl Severity {
218    /// Get terminal color for the severity level
219    pub fn color(&self) -> owo_colors::DynColors {
220        match self {
221            Severity::Critical => owo_colors::DynColors::Rgb(255, 0, 0),
222            Severity::High => owo_colors::DynColors::Rgb(255, 100, 0),
223            Severity::Medium => owo_colors::DynColors::Rgb(255, 255, 0),
224            Severity::Low => owo_colors::DynColors::Rgb(0, 150, 255),
225            Severity::Info => owo_colors::DynColors::Rgb(200, 200, 200),
226        }
227    }
228
229    /// Convert CVSS score to severity level
230    pub fn from_cvss(score: f32) -> Self {
231        match score {
232            s if s >= 9.0 => Severity::Critical,
233            s if s >= 7.0 => Severity::High,
234            s if s >= 4.0 => Severity::Medium,
235            s if s > 0.0 => Severity::Low,
236            _ => Severity::Info,
237        }
238    }
239}
240
241impl fmt::Display for Severity {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        match self {
244            Severity::Critical => write!(f, "Critical"),
245            Severity::High => write!(f, "High"),
246            Severity::Medium => write!(f, "Medium"),
247            Severity::Low => write!(f, "Low"),
248            Severity::Info => write!(f, "Info"),
249        }
250    }
251}
252
253/// Security finding from a scan
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Finding {
256    /// ID of the payload that triggered this finding
257    pub payload_id: String,
258    /// Severity of the finding
259    pub severity: Severity,
260    /// Category of the vulnerability
261    pub category: String,
262    /// OWASP Top 10:2025 mapping (if applicable)
263    pub owasp_category: Option<OwaspCategory>,
264    /// The actual payload value used
265    pub payload_value: String,
266    /// The evasion technique that worked (if any)
267    pub technique_used: Option<String>,
268    /// HTTP response status code
269    pub response_status: u16,
270    /// Description of the finding
271    pub description: String,
272    /// HTTP version used (HTTP/1.1, HTTP/2, etc.)
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub http_version: Option<String>,
275    /// Extracted sensitive data from response
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub extracted_data: Option<ExtractedData>,
278}
279
280/// Sensitive data extracted from responses
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct ExtractedData {
283    /// Information disclosure findings
284    #[serde(skip_serializing_if = "Vec::is_empty")]
285    pub info_disclosure: Vec<InfoDisclosure>,
286    /// Exposed paths and directories
287    #[serde(skip_serializing_if = "Vec::is_empty")]
288    pub exposed_paths: Vec<String>,
289    /// Authentication tokens found
290    #[serde(skip_serializing_if = "Vec::is_empty")]
291    pub auth_tokens: Vec<AuthToken>,
292    /// Server/application version info
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub version_info: Option<VersionInfo>,
295    /// Internal IP addresses exposed
296    #[serde(skip_serializing_if = "Vec::is_empty")]
297    pub internal_ips: Vec<String>,
298    /// ADFS-specific metadata
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub adfs_metadata: Option<AdfsMetadata>,
301    /// Response body snippet (first 500 chars)
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub response_snippet: Option<String>,
304    
305    // LLM/GenAI-specific extractions
306    /// System prompts or instructions leaked
307    #[serde(skip_serializing_if = "Vec::is_empty")]
308    pub system_prompts: Vec<String>,
309    /// LLM model information detected
310    #[serde(skip_serializing_if = "Vec::is_empty")]
311    pub model_info: Vec<String>,
312    /// Training data leakage detected
313    #[serde(skip_serializing_if = "Vec::is_empty")]
314    pub training_data_leaked: Vec<String>,
315    /// RAG/retrieval context exposed
316    #[serde(skip_serializing_if = "Vec::is_empty")]
317    pub rag_context: Vec<String>,
318    /// Jailbreak success indicators
319    #[serde(skip_serializing_if = "Vec::is_empty")]
320    pub jailbreak_indicators: Vec<String>,
321}
322
323/// Information disclosure types
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct InfoDisclosure {
326    /// Type of disclosure
327    pub disclosure_type: String,
328    /// The actual disclosed information
329    pub value: String,
330    /// Severity of this specific disclosure
331    pub severity: Severity,
332}
333
334/// Authentication token information
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct AuthToken {
337    /// Token type (cookie, JWT, bearer, etc.)
338    pub token_type: String,
339    /// Token name/key
340    pub name: String,
341    /// Token value (potentially redacted)
342    pub value: String,
343    /// Additional attributes
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub attributes: Option<String>,
346}
347
348/// Version information
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct VersionInfo {
351    /// Server software and version
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub server: Option<String>,
354    /// Framework/platform version
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub framework: Option<String>,
357    /// Additional version details
358    #[serde(skip_serializing_if = "Vec::is_empty")]
359    pub details: Vec<String>,
360}
361
362/// ADFS-specific metadata
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AdfsMetadata {
365    /// Federation service identifier
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub service_identifier: Option<String>,
368    /// Exposed endpoints
369    #[serde(skip_serializing_if = "Vec::is_empty")]
370    pub endpoints: Vec<String>,
371    /// Certificate information
372    #[serde(skip_serializing_if = "Vec::is_empty")]
373    pub certificates: Vec<String>,
374    /// Claim types exposed
375    #[serde(skip_serializing_if = "Vec::is_empty")]
376    pub claims: Vec<String>,
377    /// Relying party trusts
378    #[serde(skip_serializing_if = "Vec::is_empty")]
379    pub relying_parties: Vec<String>,
380}
381
382impl ExtractedData {
383    /// Create a new empty extracted data instance
384    pub fn new() -> Self {
385        Self {
386            info_disclosure: Vec::new(),
387            exposed_paths: Vec::new(),
388            auth_tokens: Vec::new(),
389            version_info: None,
390            internal_ips: Vec::new(),
391            adfs_metadata: None,
392            response_snippet: None,
393            system_prompts: Vec::new(),
394            model_info: Vec::new(),
395            training_data_leaked: Vec::new(),
396            rag_context: Vec::new(),
397            jailbreak_indicators: Vec::new(),
398        }
399    }
400
401    /// Check if any data was extracted
402    pub fn has_data(&self) -> bool {
403        !self.info_disclosure.is_empty()
404            || !self.exposed_paths.is_empty()
405            || !self.auth_tokens.is_empty()
406            || self.version_info.is_some()
407            || !self.internal_ips.is_empty()
408            || self.adfs_metadata.is_some()
409            || !self.system_prompts.is_empty()
410            || !self.model_info.is_empty()
411            || !self.training_data_leaked.is_empty()
412            || !self.rag_context.is_empty()
413            || !self.jailbreak_indicators.is_empty()
414    }
415}
416
417impl Default for ExtractedData {
418    fn default() -> Self {
419        Self::new()
420    }
421}
422
423/// Summary statistics from a scan
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct ScanSummary {
426    /// Total number of payloads tested
427    pub total_payloads: usize,
428    /// Number of successful bypasses detected
429    pub successful_bypasses: usize,
430    /// Number of techniques that were effective
431    pub techniques_effective: usize,
432    /// Scan duration in seconds
433    pub duration_secs: f64,
434}
435
436/// Results from a WAF scan
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct ScanResults {
439    /// Target URL that was scanned
440    pub target: String,
441    /// Timestamp of the scan
442    pub timestamp: String,
443    /// Detected WAF name (if any)
444    pub waf_detected: Option<String>,
445    /// List of findings
446    pub findings: Vec<Finding>,
447    /// Summary statistics
448    pub summary: ScanSummary,
449}
450
451impl ScanResults {
452    /// Create a new scan results instance
453    pub fn new(target: String, waf_detected: Option<String>) -> Self {
454        Self {
455            target,
456            timestamp: chrono::Utc::now().to_rfc3339(),
457            waf_detected,
458            findings: Vec::new(),
459            summary: ScanSummary {
460                total_payloads: 0,
461                successful_bypasses: 0,
462                techniques_effective: 0,
463                duration_secs: 0.0,
464            },
465        }
466    }
467
468    /// Add a finding to the results
469    pub fn add_finding(&mut self, finding: Finding) {
470        self.findings.push(finding);
471    }
472
473    /// Sort findings by severity (critical first)
474    pub fn sort_by_severity(&mut self) {
475        self.findings.sort_by(|a, b| b.severity.cmp(&a.severity));
476    }
477}