1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
8pub enum OwaspCategory {
9 A01BrokenAccessControl,
12 A02SecurityMisconfiguration,
15 A03SoftwareSupplyChainFailures,
18 A04CryptographicFailures,
21 A05Injection,
24 A06InsecureDesign,
27 A07AuthenticationFailures,
30 A08SoftwareOrDataIntegrityFailures,
33 A09SecurityLoggingAlertingFailures,
36 A10MishandlingOfExceptionalConditions,
39
40 LLM01PromptInjection,
46 LLM02SensitiveInformationDisclosure,
49 LLM03SupplyChain,
52 LLM04DataModelPoisoning,
55 LLM05ImproperOutputHandling,
58 LLM06ExcessiveAgency,
61 LLM07SystemPromptLeakage,
64 LLM08VectorEmbeddingWeaknesses,
67 LLM09Misinformation,
70 LLM10UnboundedConsumption,
73}
74
75impl OwaspCategory {
76 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 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 pub fn reference_url(&self) -> String {
130 match self {
131 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 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 pub fn from_attack_type(attack_type: &str) -> Option<Self> {
162 match attack_type.to_lowercase().as_str() {
163 "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 "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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
203#[serde(rename_all = "lowercase")]
204pub enum Severity {
205 Info,
207 Low,
209 Medium,
211 High,
213 Critical,
215}
216
217impl Severity {
218 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Finding {
256 pub payload_id: String,
258 pub severity: Severity,
260 pub category: String,
262 pub owasp_category: Option<OwaspCategory>,
264 pub payload_value: String,
266 pub technique_used: Option<String>,
268 pub response_status: u16,
270 pub description: String,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub http_version: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub extracted_data: Option<ExtractedData>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct ExtractedData {
283 #[serde(skip_serializing_if = "Vec::is_empty")]
285 pub info_disclosure: Vec<InfoDisclosure>,
286 #[serde(skip_serializing_if = "Vec::is_empty")]
288 pub exposed_paths: Vec<String>,
289 #[serde(skip_serializing_if = "Vec::is_empty")]
291 pub auth_tokens: Vec<AuthToken>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub version_info: Option<VersionInfo>,
295 #[serde(skip_serializing_if = "Vec::is_empty")]
297 pub internal_ips: Vec<String>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub adfs_metadata: Option<AdfsMetadata>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub response_snippet: Option<String>,
304
305 #[serde(skip_serializing_if = "Vec::is_empty")]
308 pub system_prompts: Vec<String>,
309 #[serde(skip_serializing_if = "Vec::is_empty")]
311 pub model_info: Vec<String>,
312 #[serde(skip_serializing_if = "Vec::is_empty")]
314 pub training_data_leaked: Vec<String>,
315 #[serde(skip_serializing_if = "Vec::is_empty")]
317 pub rag_context: Vec<String>,
318 #[serde(skip_serializing_if = "Vec::is_empty")]
320 pub jailbreak_indicators: Vec<String>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct InfoDisclosure {
326 pub disclosure_type: String,
328 pub value: String,
330 pub severity: Severity,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct AuthToken {
337 pub token_type: String,
339 pub name: String,
341 pub value: String,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub attributes: Option<String>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct VersionInfo {
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub server: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub framework: Option<String>,
357 #[serde(skip_serializing_if = "Vec::is_empty")]
359 pub details: Vec<String>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AdfsMetadata {
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub service_identifier: Option<String>,
368 #[serde(skip_serializing_if = "Vec::is_empty")]
370 pub endpoints: Vec<String>,
371 #[serde(skip_serializing_if = "Vec::is_empty")]
373 pub certificates: Vec<String>,
374 #[serde(skip_serializing_if = "Vec::is_empty")]
376 pub claims: Vec<String>,
377 #[serde(skip_serializing_if = "Vec::is_empty")]
379 pub relying_parties: Vec<String>,
380}
381
382impl ExtractedData {
383 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct ScanSummary {
426 pub total_payloads: usize,
428 pub successful_bypasses: usize,
430 pub techniques_effective: usize,
432 pub duration_secs: f64,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct ScanResults {
439 pub target: String,
441 pub timestamp: String,
443 pub waf_detected: Option<String>,
445 pub findings: Vec<Finding>,
447 pub summary: ScanSummary,
449}
450
451impl ScanResults {
452 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 pub fn add_finding(&mut self, finding: Finding) {
470 self.findings.push(finding);
471 }
472
473 pub fn sort_by_severity(&mut self) {
475 self.findings.sort_by(|a, b| b.severity.cmp(&a.severity));
476 }
477}