use http::Method;
use parlov_core::{
always_applicable, NormativeStrength, OracleClass, ProbeDefinition, SignalSurface, Technique,
Vector,
};
use crate::context::ScanContext;
use crate::strategy::Strategy;
use crate::types::{ProbePair, ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::{garble_path_segment, substitute_url};
static METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "emg-app-vs-server-404",
strategy_name: "EMG: App vs Server 404",
risk: RiskLevel::Safe,
};
static 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,
normalization_weight: None,
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Body,
};
pub struct EmgAppVsServer404;
impl Strategy for EmgAppVsServer404 {
fn metadata(&self) -> &'static StrategyMetadata {
&METADATA
}
fn technique_def(&self) -> &'static Technique {
&TECHNIQUE
}
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_path_segment(&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,
},
canonical_baseline: None,
metadata: METADATA.clone(),
technique: TECHNIQUE,
chain_provenance: None,
};
vec![ProbeSpec::Pair(pair)]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::minimal_ctx;
fn make_ctx() -> ScanContext {
ScanContext {
target: "https://api.example.com/api/users/{id}".to_owned(),
..minimal_ctx()
}
}
#[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_replaces_parent_segment() {
let result = garble_path_segment("https://api.example.com/api/users/{id}");
assert_eq!(result, "https://api.example.com/api/_parlov_no_route/0");
}
#[test]
fn garble_fallback_when_single_segment() {
let result = garble_path_segment("https://api.example.com/{id}");
assert_eq!(result, "https://api.example.com/_parlov_no_route");
}
}