use std::fmt;
use serde::{Deserialize, Serialize};
use crate::Request;
use crate::Technique;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvasionResult {
pub request: Request,
pub techniques: Vec<Technique>,
pub description: String,
pub confidence: f64,
}
impl EvasionResult {
#[must_use]
pub fn new(request: Request, techniques: Vec<Technique>, description: String) -> Self {
let confidence = Self::estimate_confidence(&techniques);
Self {
request,
techniques,
description,
confidence,
}
}
#[must_use]
pub fn with_confidence(
request: Request,
techniques: Vec<Technique>,
description: String,
confidence: f64,
) -> Self {
Self {
request,
techniques,
description,
confidence: confidence.clamp(0.0, 1.0),
}
}
fn estimate_confidence(techniques: &[Technique]) -> f64 {
if techniques.is_empty() {
return 0.0;
}
let mut score: f64 = 0.0;
for t in techniques {
score += match t {
Technique::PayloadEncoding(_) | Technique::BoundaryManipulation => 0.15,
Technique::ContentTypeSwitch(_) => 0.20,
Technique::JsonUnicodeEscape
| Technique::TlsFingerprint(_)
| Technique::HeaderObfuscation(_) => 0.10,
Technique::UserAgentRotation | Technique::Http2Settings => 0.05,
Technique::GrammarMutation(_) => 0.30,
Technique::RequestSmuggling(_) => 0.35,
Technique::H2Evasion(_) => 0.25,
Technique::DifferentialProbe => 0.0,
};
}
if techniques.len() >= 3 {
score += 0.10;
}
score.min(1.0)
}
#[must_use]
pub fn technique_count(&self) -> usize {
self.techniques.len()
}
#[must_use]
pub fn uses_grammar(&self) -> bool {
self.techniques
.iter()
.any(|t| matches!(t, Technique::GrammarMutation(_)))
}
#[must_use]
pub fn uses_smuggling(&self) -> bool {
self.techniques
.iter()
.any(|t| matches!(t, Technique::RequestSmuggling(_)))
}
#[must_use]
pub fn uses_header_obfuscation(&self) -> bool {
self.techniques
.iter()
.any(|t| matches!(t, Technique::HeaderObfuscation(_)))
}
}
impl fmt::Display for EvasionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{:.0}%] {} technique(s): {}",
self.confidence * 100.0,
self.techniques.len(),
self.description
)
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn evasion_result_confidence() {
let req = Request::get("https://example.com");
let result = EvasionResult::new(
req,
vec![
Technique::GrammarMutation("sql_tautology".into()),
Technique::PayloadEncoding("UrlEncode".into()),
],
"grammar + encoding".into(),
);
assert!(
result.confidence > 0.3,
"grammar + encoding should have decent confidence"
);
assert!(result.uses_grammar());
assert!(!result.uses_smuggling());
}
#[test]
fn evasion_result_empty_zero_confidence() {
let result = EvasionResult::new(
Request::get("https://example.com"),
vec![],
"no evasion".into(),
);
assert_eq!(result.confidence, 0.0);
}
#[test]
fn evasion_result_display() {
let result = EvasionResult::new(
Request::get("https://example.com"),
vec![Technique::GrammarMutation("xss_polyglot".into())],
"polyglot XSS".into(),
);
let s = result.to_string();
assert!(s.contains('%'));
assert!(s.contains("polyglot XSS"));
}
#[test]
fn with_confidence_clamps() {
let result = EvasionResult::with_confidence(
Request::get("https://example.com"),
vec![],
"test".into(),
1.5,
);
assert_eq!(result.confidence, 1.0);
let result2 = EvasionResult::with_confidence(
Request::get("https://example.com"),
vec![],
"test".into(),
-0.5,
);
assert_eq!(result2.confidence, 0.0);
}
}