pub mod api;
pub mod behavior;
pub mod chain;
pub mod cloud;
pub mod cmdi;
pub mod combo;
pub mod evasion;
pub mod severity;
pub mod signals;
pub mod sqli;
pub mod ssti;
pub mod tech;
pub mod temporal;
pub mod traversal;
pub mod xss;
use crate::types::Vulnerability;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum ParamLocation {
Query,
Body,
Header,
Path,
Cookie,
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: String,
pub body_bytes: usize,
pub response_time_ms: u64,
}
impl HttpResponse {
pub fn from_client_response(resp: &crate::http_client::HttpResponse) -> Self {
Self {
status: resp.status_code,
headers: resp.headers.clone(),
body: resp.body.clone(),
body_bytes: resp.body.len(),
response_time_ms: resp.duration_ms,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AuthContext {
pub has_auth_header: bool,
pub auth_type: Option<String>, pub user_role: Option<String>, }
#[derive(Debug, Clone)]
pub struct ProbeContext {
pub probe_payload: String,
pub probe_category: String,
pub param_name: String,
pub param_location: ParamLocation,
pub request_url: String,
pub request_method: String,
pub response: HttpResponse,
pub baseline: HttpResponse,
pub injected_delay: Option<f64>,
pub probe_sequence: Option<Vec<HttpResponse>>,
pub timing_samples: Option<Vec<u64>>,
pub encoding_used: Option<String>,
pub raw_payload_blocked: bool,
pub request_headers: Option<HashMap<String, String>>,
pub auth_context: Option<AuthContext>,
}
pub fn extract_features(ctx: &ProbeContext) -> HashMap<String, f64> {
let mut features = HashMap::new();
match ctx.probe_category.as_str() {
"sqli" => sqli::extract_sqli_features(ctx, &mut features),
"xss" => xss::extract_xss_features(ctx, &mut features),
"ssti" => ssti::extract_ssti_features(ctx, &mut features),
"cmdi" => cmdi::extract_cmdi_features(ctx, &mut features),
"traversal" => traversal::extract_traversal_features(ctx, &mut features),
_ => {}
}
signals::extract_signal_features(ctx, &mut features);
features
}
pub fn extract_features_v3(
ctx: &ProbeContext,
tech_features: &HashMap<String, f64>,
is_authenticated: bool,
is_admin: bool,
retry_results: Option<&[HashMap<String, f64>]>,
) -> HashMap<String, f64> {
let mut features = extract_features(ctx);
features.extend(tech_features.clone());
let severity_features = severity::extract_severity_features(
&ctx.request_url,
&ctx.request_method,
&ctx.param_name,
&ctx.response,
is_authenticated,
is_admin,
);
features.extend(severity_features);
let combo_features = combo::extract_combo_features(&features, retry_results);
features.extend(combo_features);
features
}
pub fn extract_features_v4(
ctx: &ProbeContext,
tech_features: &HashMap<String, f64>,
is_authenticated: bool,
is_admin: bool,
retry_results: Option<&[HashMap<String, f64>]>,
) -> HashMap<String, f64> {
let mut features = HashMap::new();
match ctx.probe_category.as_str() {
"sqli" => sqli::extract_sqli_features(ctx, &mut features),
"xss" => xss::extract_xss_features(ctx, &mut features),
"ssti" => ssti::extract_ssti_features(ctx, &mut features),
"cmdi" => cmdi::extract_cmdi_features(ctx, &mut features),
"traversal" => traversal::extract_traversal_features(ctx, &mut features),
_ => {}
}
signals::extract_signal_features(ctx, &mut features);
behavior::extract_behavior_features(ctx, &mut features);
evasion::extract_evasion_features(ctx, &mut features);
api::extract_api_features(ctx, &mut features);
cloud::extract_cloud_features(ctx, &mut features);
temporal::extract_temporal_features(ctx, &mut features);
features.extend(tech_features.clone());
let severity_features = severity::extract_severity_features(
&ctx.request_url,
&ctx.request_method,
&ctx.param_name,
&ctx.response,
is_authenticated,
is_admin,
);
features.extend(severity_features);
let combo_features = combo::extract_combo_features(&features, retry_results);
features.extend(combo_features);
features
}
pub fn extract_chain_features(vulns: &[Vulnerability]) -> HashMap<String, f64> {
chain::extract_chain_features(vulns)
}
#[cfg(test)]
mod tests {
use super::*;
pub fn make_baseline() -> HttpResponse {
HttpResponse {
status: 200,
headers: HashMap::new(),
body: "<html><body>Normal page</body></html>".to_string(),
body_bytes: 36,
response_time_ms: 100,
}
}
pub fn make_response(body: &str, status: u16) -> HttpResponse {
HttpResponse {
status,
headers: HashMap::new(),
body: body.to_string(),
body_bytes: body.len(),
response_time_ms: 100,
}
}
pub fn make_ctx(category: &str, payload: &str, response: HttpResponse) -> ProbeContext {
ProbeContext {
probe_payload: payload.to_string(),
probe_category: category.to_string(),
param_name: "id".to_string(),
param_location: ParamLocation::Query,
request_url: "https://example.com/test".to_string(),
request_method: "GET".to_string(),
response,
baseline: make_baseline(),
injected_delay: None,
probe_sequence: None,
timing_samples: None,
encoding_used: None,
raw_payload_blocked: false,
request_headers: None,
auth_context: None,
}
}
#[test]
fn test_extract_features_sqli() {
let response = make_response(
"You have an error in your SQL syntax near '1'",
500,
);
let ctx = make_ctx("sqli", "'", response);
let features = extract_features(&ctx);
assert!(features.contains_key("sqli:error_mysql_syntax"));
assert!(features.contains_key("signal:error_triggered"));
}
#[test]
fn test_extract_features_unknown_category() {
let response = make_response("OK", 200);
let ctx = make_ctx("unknown", "test", response);
let features = extract_features(&ctx);
assert!(!features.contains_key("sqli:error_mysql_syntax"));
}
#[test]
fn test_extract_features_v3_full_pipeline() {
let response = make_response(
"You have an error in your SQL syntax near '1'",
500,
);
let ctx = make_ctx("sqli", "'", response);
let mut tech_features = HashMap::new();
tech_features.insert("tech:runtime_php".to_string(), 1.0);
tech_features.insert("tech:db_mysql".to_string(), 1.0);
let features = extract_features_v3(&ctx, &tech_features, false, false, None);
assert!(features.contains_key("sqli:error_mysql_syntax"));
assert!(features.contains_key("signal:error_triggered"));
assert!(features.contains_key("tech:runtime_php"));
assert!(features.contains_key("tech:db_mysql"));
assert!(features.contains_key("severity:production_environment"));
assert!(features.contains_key("severity:endpoint_is_public"));
assert!(features.contains_key("combo:sqli_error_plus_input_char"));
}
#[test]
fn test_extract_features_v3_with_retries() {
let response = make_response(
"You have an error in your SQL syntax near '1'",
500,
);
let ctx = make_ctx("sqli", "'", response);
let tech_features = HashMap::new();
let retry1: HashMap<String, f64> = [
("sqli:error_mysql_syntax".to_string(), 0.95),
("sqli:single_quote_triggers_error".to_string(), 1.0),
]
.into_iter()
.collect();
let retry2: HashMap<String, f64> = [
("sqli:error_mysql_syntax".to_string(), 0.95),
("sqli:single_quote_triggers_error".to_string(), 1.0),
]
.into_iter()
.collect();
let retries = vec![retry1, retry2];
let features =
extract_features_v3(&ctx, &tech_features, false, false, Some(&retries));
assert!(features.contains_key("sqli:error_mysql_syntax"));
assert!(features.contains_key("combo:consistent_across_retries"));
}
}