use http::Method;
use crate::context::ScanContext;
use crate::types::{ProbeSpec, RiskLevel, StrategyMetadata};
use parlov_core::Technique;
pub trait Strategy: Send + Sync {
fn metadata(&self) -> &'static StrategyMetadata;
fn technique_def(&self) -> &'static Technique;
fn id(&self) -> &'static str {
self.metadata().strategy_id
}
fn name(&self) -> &'static str {
self.metadata().strategy_name
}
fn risk(&self) -> RiskLevel {
self.metadata().risk
}
fn methods(&self) -> &[Method];
fn is_applicable(&self, ctx: &ScanContext) -> bool;
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::ScanContext;
use crate::types::{ProbePair, ProbeSpec, RiskLevel, StrategyMetadata};
use http::{HeaderMap, Method};
use parlov_core::{
always_applicable, NormativeStrength, OracleClass, ProbeDefinition, SignalSurface,
Technique, Vector,
};
static TEST_METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "test",
strategy_name: "Test Strategy",
risk: RiskLevel::Safe,
};
static TEST_TECHNIQUE: Technique = Technique {
id: "test",
name: "Test technique",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::Should,
normalization_weight: Some(0.2),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
};
struct TestStrategy;
impl Strategy for TestStrategy {
fn metadata(&self) -> &'static StrategyMetadata {
&TEST_METADATA
}
fn technique_def(&self) -> &'static Technique {
&TEST_TECHNIQUE
}
fn methods(&self) -> &[Method] {
&[Method::GET]
}
fn is_applicable(&self, _ctx: &ScanContext) -> bool {
true
}
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
let def = ProbeDefinition {
url: ctx.target.clone(),
method: Method::GET,
headers: HeaderMap::new(),
body: None,
};
vec![ProbeSpec::Pair(ProbePair {
baseline: def.clone(),
probe: def,
canonical_baseline: None,
metadata: self.metadata().clone(),
technique: *self.technique_def(),
chain_provenance: None,
})]
}
}
#[test]
fn strategy_is_object_safe() {
let _: Box<dyn Strategy> = Box::new(TestStrategy);
}
#[test]
fn strategy_id_accessible_via_dyn() {
let s: Box<dyn Strategy> = Box::new(TestStrategy);
assert_eq!(s.id(), "test");
}
#[test]
fn strategy_name_accessible_via_dyn() {
let s: Box<dyn Strategy> = Box::new(TestStrategy);
assert_eq!(s.name(), "Test Strategy");
}
#[test]
fn strategy_risk_accessible_via_dyn() {
let s: Box<dyn Strategy> = Box::new(TestStrategy);
assert_eq!(s.risk(), RiskLevel::Safe);
}
#[test]
fn test_strategy_is_applicable_returns_true() {
let ctx = ScanContext {
target: "https://example.com/{id}".to_owned(),
baseline_id: "1".to_owned(),
probe_id: "999".to_owned(),
headers: HeaderMap::new(),
max_risk: RiskLevel::Safe,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
};
assert!(TestStrategy.is_applicable(&ctx));
}
#[test]
fn test_strategy_generate_returns_pair() {
let ctx = ScanContext {
target: "https://example.com/{id}".to_owned(),
baseline_id: "1".to_owned(),
probe_id: "999".to_owned(),
headers: HeaderMap::new(),
max_risk: RiskLevel::Safe,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
};
let specs = TestStrategy.generate(&ctx);
assert_eq!(specs.len(), 1);
assert!(matches!(specs[0], ProbeSpec::Pair(_)));
}
}