use parlov_analysis::{EvidenceAccumulator, StopDecision};
use parlov_core::StrategyOutcome;
use parlov_output::ScanFinding;
pub(crate) struct ScanPipelineState {
pub findings: Vec<(ScanFinding, StrategyOutcome)>,
pub accumulator: EvidenceAccumulator,
pub stop_decision: Option<StopDecision>,
pub strategies_run: usize,
pub strategies_total: usize,
pub first_threshold_crossed_by: Option<String>,
}
impl ScanPipelineState {
pub fn new(total: usize) -> Self {
Self {
findings: Vec::new(),
accumulator: EvidenceAccumulator::new(),
stop_decision: None,
strategies_run: 0,
strategies_total: total,
first_threshold_crossed_by: None,
}
}
pub fn findings_only(&self) -> impl Iterator<Item = &ScanFinding> {
self.findings.iter().map(|(f, _)| f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use parlov_core::{NormativeStrength, OracleClass, OracleResult, OracleVerdict, Vector};
use parlov_output::ScanFinding;
fn make_oracle_result() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
confidence: 0,
impact_class: None,
reasons: vec![],
signals: vec![],
technique_id: None,
vector: Some(Vector::StatusCodeDiff),
normative_strength: Some(NormativeStrength::Must),
label: None,
leaks: None,
rfc_basis: None,
}
}
fn make_finding(strategy_id: &str) -> ScanFinding {
use parlov_output::{ExchangeContext, ProbeContext};
ScanFinding {
target_url: "https://example.com/{id}".to_owned(),
strategy_id: strategy_id.to_owned(),
strategy_name: "Test Strategy".to_owned(),
method: "GET".to_owned(),
result: make_oracle_result(),
repro: None,
probe: ProbeContext {
baseline_url: "https://example.com/1".to_owned(),
probe_url: "https://example.com/9999".to_owned(),
method: "GET".to_owned(),
headers: None,
},
exchange: ExchangeContext {
baseline_status: 200,
probe_status: 404,
headers: None,
body_samples: None,
},
chain_provenance: None,
}
}
#[test]
fn pipeline_state_new_initialises_correctly() {
let state = ScanPipelineState::new(10);
assert!(state.findings.is_empty());
assert!(state.stop_decision.is_none());
assert_eq!(state.strategies_total, 10);
assert_eq!(state.strategies_run, 0);
}
#[test]
fn findings_only_discards_outcomes() {
let mut state = ScanPipelineState::new(2);
let outcome = StrategyOutcome::NoSignal(make_oracle_result());
state
.findings
.push((make_finding("strategy-a"), outcome.clone()));
state.findings.push((make_finding("strategy-b"), outcome));
let only: Vec<&ScanFinding> = state.findings_only().collect();
assert_eq!(only.len(), 2);
assert_eq!(only[0].strategy_id, "strategy-a");
assert_eq!(only[1].strategy_id, "strategy-b");
}
#[test]
fn findings_only_returns_references_not_clones() {
let mut state = ScanPipelineState::new(1);
let outcome = StrategyOutcome::NoSignal(make_oracle_result());
state.findings.push((make_finding("strategy-x"), outcome));
let collected: Vec<&ScanFinding> = state.findings_only().collect();
assert!(std::ptr::eq(collected[0], &raw const state.findings[0].0));
}
}