use http::Method;
use parlov_core::{
Applicability, NormativeStrength, OracleClass, ResponseSurface, SignalSurface, Technique,
Vector,
};
use crate::strategy::Strategy;
use crate::types::{ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::{build_pair, clone_headers_static, json_body};
use crate::ScanContext;
static METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "empty-body-elicit",
strategy_name: "Empty Body Elicitation",
risk: RiskLevel::MethodDestructive,
};
fn empty_body_applicable(baseline: &ResponseSurface, probe: &ResponseSurface) -> Applicability {
let is_strong = |s: u16| matches!(s, 400 | 415 | 422);
if is_strong(baseline.status.as_u16()) || is_strong(probe.status.as_u16()) {
return Applicability::Strong;
}
Applicability::Weak
}
static TECHNIQUE: Technique = Technique {
id: "empty-body",
name: "Empty JSON body validation probe",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::May,
normalization_weight: Some(0.05),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: true,
applicability: empty_body_applicable,
contradiction_surface: SignalSurface::Status,
};
pub struct EmptyBodyElicitation;
impl Strategy for EmptyBodyElicitation {
fn metadata(&self) -> &'static StrategyMetadata {
&METADATA
}
fn technique_def(&self) -> &'static Technique {
&TECHNIQUE
}
fn methods(&self) -> &[Method] {
&[Method::POST, Method::PUT, Method::PATCH]
}
fn is_applicable(&self, _ctx: &ScanContext) -> bool {
true
}
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
let body = Some(json_body(&[]));
let mut specs = Vec::with_capacity(3);
for method in [Method::POST, Method::PUT, Method::PATCH] {
let headers = clone_headers_static(&ctx.headers, "content-type", "application/json");
let pair = build_pair(
ctx,
method,
headers.clone(),
headers,
body.clone(),
METADATA.clone(),
TECHNIQUE,
);
specs.push(ProbeSpec::Pair(pair));
}
specs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::ctx_method_destructive;
#[test]
fn risk_is_method_destructive() {
assert_eq!(EmptyBodyElicitation.risk(), RiskLevel::MethodDestructive);
}
#[test]
fn generate_returns_three_items() {
assert_eq!(
EmptyBodyElicitation
.generate(&ctx_method_destructive())
.len(),
3
);
}
#[test]
fn both_have_application_json_content_type() {
let specs = EmptyBodyElicitation.generate(&ctx_method_destructive());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair")
};
assert_eq!(
pair.baseline.headers.get("content-type").unwrap(),
"application/json"
);
assert_eq!(
pair.probe.headers.get("content-type").unwrap(),
"application/json"
);
}
#[test]
fn both_body_is_empty_json_object() {
let specs = EmptyBodyElicitation.generate(&ctx_method_destructive());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair")
};
assert_eq!(pair.baseline.body.as_deref().unwrap(), b"{}");
assert_eq!(pair.probe.body.as_deref().unwrap(), b"{}");
}
#[test]
fn technique_strength_is_may() {
let specs = EmptyBodyElicitation.generate(&ctx_method_destructive());
assert_eq!(specs[0].technique().strength, NormativeStrength::May);
}
#[test]
fn normalization_weight_is_0_05() {
assert_eq!(TECHNIQUE.normalization_weight, Some(0.05));
}
#[test]
fn inverted_signal_weight_is_none() {
assert_eq!(TECHNIQUE.inverted_signal_weight, None);
}
}