use crate::context::ScanContext;
use crate::existence::cache_probing::{
CpAccept, CpIfMatch, CpIfModifiedSince, CpIfNoneMatch, CpIfRange,
CpIfUnmodifiedSince, CpRangeSatisfiable, CpRangeUnsatisfiable,
};
use crate::existence::error_message_granularity::{
EmgAppVsServer404, EmgBola, EmgFkViolation, EmgQueryValidation,
EmgSchemaValidationPatch, EmgSchemaValidationPut, EmgStateConflict,
};
use crate::existence::status_code_diff::{
AcceptElicitation, AuthStripElicitation, CaseNormalizeElicitation,
ContentTypeElicitation, DependencyDeleteElicitation, EmptyBodyElicitation,
IfMatchElicitation, IfNoneMatchElicitation, LongUriElicitation,
LowPrivilegeElicitation, OversizedBodyElicitation, RateLimitBurstElicitation,
RateLimitHeadersElicitation, ScopeManipulationElicitation,
StateTransitionElicitation, TrailingSlashElicitation, UniquenessElicitation,
};
use crate::strategy::Strategy;
use crate::types::ProbeSpec;
#[must_use]
pub fn all_strategies() -> Vec<Box<dyn Strategy>> {
vec![
Box::new(AcceptElicitation),
Box::new(IfNoneMatchElicitation),
Box::new(TrailingSlashElicitation),
Box::new(CaseNormalizeElicitation),
Box::new(LongUriElicitation),
Box::new(AuthStripElicitation),
Box::new(LowPrivilegeElicitation),
Box::new(ScopeManipulationElicitation),
Box::new(RateLimitHeadersElicitation),
Box::new(CpIfNoneMatch),
Box::new(CpIfModifiedSince),
Box::new(CpIfMatch),
Box::new(CpIfUnmodifiedSince),
Box::new(CpRangeSatisfiable),
Box::new(CpRangeUnsatisfiable),
Box::new(CpIfRange),
Box::new(CpAccept),
Box::new(EmgBola),
Box::new(EmgQueryValidation),
Box::new(EmgAppVsServer404),
Box::new(ContentTypeElicitation),
Box::new(IfMatchElicitation),
Box::new(EmptyBodyElicitation),
Box::new(OversizedBodyElicitation),
Box::new(StateTransitionElicitation),
Box::new(EmgSchemaValidationPatch),
Box::new(EmgSchemaValidationPut),
Box::new(EmgStateConflict),
Box::new(EmgFkViolation),
Box::new(UniquenessElicitation),
Box::new(DependencyDeleteElicitation),
Box::new(RateLimitBurstElicitation),
]
}
#[must_use]
pub fn generate_plan(ctx: &ScanContext) -> Vec<ProbeSpec> {
all_strategies()
.iter()
.filter(|s| s.risk() <= ctx.max_risk)
.filter(|s| s.is_applicable(ctx))
.flat_map(|s| s.generate(ctx))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::RiskLevel;
use crate::{KnownDuplicate, ScanContext, StateField};
use http::{HeaderMap, HeaderValue};
use parlov_core::Vector;
fn make_minimal_ctx(max_risk: RiskLevel) -> ScanContext {
ScanContext {
target: "https://api.example.com/users/{id}".to_string(),
baseline_id: "1001".to_string(),
probe_id: "9999".to_string(),
headers: HeaderMap::new(),
max_risk,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
}
}
fn make_full_ctx() -> ScanContext {
let mut headers = HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer token123"),
);
let mut alt = HeaderMap::new();
alt.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer under-scoped"),
);
ScanContext {
target: "https://api.example.com/users/{id}".to_string(),
baseline_id: "1001".to_string(),
probe_id: "9999".to_string(),
headers,
max_risk: RiskLevel::OperationDestructive,
known_duplicate: Some(KnownDuplicate {
field: "email".to_string(),
value: "alice@example.com".to_string(),
}),
state_field: Some(StateField {
field: "status".to_string(),
value: "invalid_state".to_string(),
}),
alt_credential: Some(alt),
body_template: None,
}
}
fn spec_strategy_id(spec: &ProbeSpec) -> &str {
match spec {
ProbeSpec::Pair(p) | ProbeSpec::HeaderDiff(p) => p.metadata.strategy_id,
ProbeSpec::Burst(b) => b.metadata.strategy_id,
}
}
#[test]
fn all_strategies_returns_exactly_32() {
assert_eq!(all_strategies().len(), 32);
}
#[test]
fn risk_distribution_is_20_9_3() {
let s = all_strategies();
let count = |r: RiskLevel| s.iter().filter(|x| x.risk() == r).count();
assert_eq!(count(RiskLevel::Safe), 20);
assert_eq!(count(RiskLevel::MethodDestructive), 9);
assert_eq!(count(RiskLevel::OperationDestructive), 3);
}
#[test]
fn all_strategy_ids_are_unique() {
let strategies = all_strategies();
let mut ids: Vec<&str> = strategies.iter().map(|s| s.id()).collect();
ids.sort_unstable();
let before = ids.len();
ids.dedup();
assert_eq!(before, ids.len(), "duplicate strategy IDs detected");
}
#[test]
fn safe_filter_excludes_method_and_op_destructive() {
let ctx = make_minimal_ctx(RiskLevel::Safe);
let plan = generate_plan(&ctx);
let safe_ids: std::collections::HashSet<&str> = all_strategies()
.iter()
.filter(|s| s.risk() == RiskLevel::Safe)
.map(|s| s.id())
.collect();
for spec in &plan {
assert!(
safe_ids.contains(spec_strategy_id(spec)),
"non-Safe strategy '{}' appeared in Safe plan",
spec_strategy_id(spec)
);
}
}
#[test]
fn method_destructive_ceiling_excludes_op_destructive() {
let ctx = make_minimal_ctx(RiskLevel::MethodDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(
spec_strategy_id(spec),
"uniqueness-elicit",
"OperationDestructive strategy appeared in MethodDestructive plan"
);
assert_ne!(
spec_strategy_id(spec),
"dependency-delete-elicit",
"OperationDestructive strategy appeared in MethodDestructive plan"
);
assert_ne!(
spec_strategy_id(spec),
"rate-limit-burst-elicit",
"OperationDestructive strategy appeared in MethodDestructive plan"
);
}
}
#[test]
fn no_auth_excludes_auth_strip_and_low_privilege() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), "auth-strip-elicit");
assert_ne!(spec_strategy_id(spec), "low-privilege-elicit");
}
}
#[test]
fn no_alt_credential_excludes_scope_manipulation() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), "scope-manipulation-elicit");
}
}
#[test]
fn no_state_field_excludes_state_transition() {
let ctx = make_minimal_ctx(RiskLevel::MethodDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), "state-transition-elicit");
}
}
#[test]
fn no_known_duplicate_excludes_uniqueness() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), "uniqueness-elicit");
}
}
#[test]
fn full_plan_returns_at_least_31_specs() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
assert!(
plan.len() >= 31,
"expected >= 31 specs, got {}",
plan.len()
);
}
#[test]
fn all_specs_have_known_vector() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
for spec in &plan {
let v = spec.technique().vector;
assert!(
v == Vector::StatusCodeDiff
|| v == Vector::CacheProbing
|| v == Vector::ErrorMessageGranularity,
"strategy '{}' has unexpected vector: {:?}",
spec_strategy_id(spec),
v,
);
}
}
#[test]
fn all_specs_have_populated_technique_id() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
for spec in &plan {
assert!(
!spec.technique().id.is_empty(),
"strategy '{}' has empty technique id",
spec_strategy_id(spec),
);
}
}
}