use http::{HeaderMap, Method};
use parlov_core::{
always_applicable, NormativeStrength, OracleClass, ResponseClass, SignalSurface, Technique,
Vector,
};
use crate::chain::{Consumer, Producer, ProducerOutput, ProducerOutputKind};
use crate::context::ScanContext;
use crate::types::{ProbePair, ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::substitute_url;
use parlov_core::ProbeDefinition;
static METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "scd-resource-id-chain",
strategy_name: "Resource-ID Chain (POST/PUT → GET/PATCH/DELETE)",
risk: RiskLevel::MethodDestructive,
};
static TECHNIQUE: Technique = Technique {
id: "resource-id-chain",
name: "Resource-identifier producer/consumer chain",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::Should,
normalization_weight: Some(0.12),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
};
pub(super) struct ScdResourceIdProducer;
impl Producer for ScdResourceIdProducer {
fn admits(&self, class: ResponseClass) -> bool {
matches!(class, ResponseClass::Success)
}
fn extract(&self, _class: ResponseClass, headers: &HeaderMap) -> Option<ProducerOutput> {
let location = headers.get(http::header::LOCATION)?.to_str().ok()?;
let path = location.split('?').next().unwrap_or(location);
let segment = path.rsplit('/').next()?;
if segment.is_empty() {
return None;
}
Some(ProducerOutput::ResourceId(segment.to_owned()))
}
}
pub(super) struct ScdResourceIdConsumer;
impl Consumer for ScdResourceIdConsumer {
fn needs(&self) -> ProducerOutputKind {
ProducerOutputKind::ResourceId
}
fn generate(&self, ctx: &ScanContext, output: &ProducerOutput) -> Vec<ProbeSpec> {
let ProducerOutput::ResourceId(ref id) = output else {
return vec![];
};
let baseline_url = substitute_url(&ctx.target, id);
let probe_url = substitute_url(&ctx.target, &ctx.probe_id);
let mut specs = Vec::with_capacity(3);
for method in [Method::GET, Method::PATCH, Method::DELETE] {
let body = if method == Method::PATCH {
Some(bytes::Bytes::from_static(b"{}"))
} else {
None
};
let pair = ProbePair {
baseline: ProbeDefinition {
url: baseline_url.clone(),
method: method.clone(),
headers: ctx.headers.clone(),
body: body.clone(),
},
probe: ProbeDefinition {
url: probe_url.clone(),
method,
headers: ctx.headers.clone(),
body,
},
canonical_baseline: None,
metadata: METADATA.clone(),
technique: TECHNIQUE,
chain_provenance: None,
};
specs.push(ProbeSpec::Pair(pair));
}
specs
}
}
#[cfg(test)]
#[path = "resource_id_tests.rs"]
mod tests;