use http::Method;
use parlov_core::{NormativeStrength, OracleClass, ProbeDefinition, Technique, Vector};
use crate::context::ScanContext;
use crate::strategy::Strategy;
use crate::types::{ProbePair, ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::substitute_url;
fn metadata() -> StrategyMetadata {
StrategyMetadata {
strategy_id: "emg-app-vs-server-404",
strategy_name: "EMG: App vs Server 404",
risk: RiskLevel::Safe,
}
}
fn technique() -> Technique {
Technique {
id: "emg-app-vs-server-404",
name: "Application vs server 404 body differential",
oracle_class: OracleClass::Existence,
vector: Vector::ErrorMessageGranularity,
strength: NormativeStrength::May,
}
}
fn garble_url(target: &str) -> String {
let path_start = target
.find("://")
.map_or(0, |i| i + 3 + target[i + 3..].find('/').unwrap_or(0));
if let Some(rel_pos) = target[path_start..].rfind('/') {
let pos = path_start + rel_pos;
let base = &target[..pos];
if let Some(rel_pos2) = base[path_start..].rfind('/') {
let pos2 = path_start + rel_pos2;
return format!("{}/_parlov_no_route/0", &target[..pos2]);
}
}
target.replace("{id}", "_parlov_no_route")
}
pub struct EmgAppVsServer404;
impl Strategy for EmgAppVsServer404 {
fn id(&self) -> &'static str {
"emg-app-vs-server-404"
}
fn name(&self) -> &'static str {
"EMG: App vs Server 404"
}
fn risk(&self) -> RiskLevel {
RiskLevel::Safe
}
fn methods(&self) -> &[Method] {
&[Method::GET]
}
fn is_applicable(&self, _ctx: &ScanContext) -> bool {
true
}
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
let baseline_url = substitute_url(&ctx.target, &ctx.baseline_id);
let probe_url = garble_url(&ctx.target);
let pair = ProbePair {
baseline: ProbeDefinition {
url: baseline_url,
method: Method::GET,
headers: ctx.headers.clone(),
body: None,
},
probe: ProbeDefinition {
url: probe_url,
method: Method::GET,
headers: ctx.headers.clone(),
body: None,
},
metadata: metadata(),
technique: technique(),
};
vec![ProbeSpec::Pair(pair)]
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderMap;
fn make_ctx() -> ScanContext {
ScanContext {
target: "https://api.example.com/api/users/{id}".to_string(),
baseline_id: "1001".to_string(),
probe_id: "9999".to_string(),
headers: HeaderMap::new(),
max_risk: RiskLevel::Safe,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
}
}
#[test]
fn generates_correct_technique_vector() {
let specs = EmgAppVsServer404.generate(&make_ctx());
assert_eq!(specs[0].technique().vector, Vector::ErrorMessageGranularity);
}
#[test]
fn generates_correct_technique_strength() {
let specs = EmgAppVsServer404.generate(&make_ctx());
assert_eq!(specs[0].technique().strength, NormativeStrength::May);
}
#[test]
fn is_always_applicable() {
assert!(EmgAppVsServer404.is_applicable(&make_ctx()));
}
#[test]
fn risk_is_safe() {
assert_eq!(EmgAppVsServer404.risk(), RiskLevel::Safe);
}
#[test]
fn generates_get_method() {
let specs = EmgAppVsServer404.generate(&make_ctx());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair variant")
};
assert_eq!(pair.baseline.method, Method::GET);
assert_eq!(pair.probe.method, Method::GET);
}
#[test]
fn baseline_url_has_baseline_id() {
let specs = EmgAppVsServer404.generate(&make_ctx());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair variant")
};
assert!(
pair.baseline.url.contains("1001"),
"baseline URL must contain baseline_id: {}",
pair.baseline.url
);
}
#[test]
fn probe_url_is_garbled() {
let specs = EmgAppVsServer404.generate(&make_ctx());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair variant")
};
assert!(
!pair.probe.url.contains("{id}"),
"probe URL must not contain {{id}}: {}",
pair.probe.url
);
assert!(
pair.probe.url.contains("_parlov_no_route"),
"probe URL must contain _parlov_no_route: {}",
pair.probe.url
);
}
#[test]
fn garble_url_replaces_parent_segment() {
let result = garble_url("https://api.example.com/api/users/{id}");
assert_eq!(result, "https://api.example.com/api/_parlov_no_route/0");
}
#[test]
fn garble_url_fallback_when_single_segment() {
let result = garble_url("https://api.example.com/{id}");
assert_eq!(result, "https://api.example.com/_parlov_no_route");
}
}