1#![deny(clippy::all)]
8#![warn(clippy::pedantic)]
9#![deny(missing_docs)]
10
11mod endpoint_stop;
12mod endpoint_verdict;
13mod exchange;
14mod finding_id;
15mod observability;
16mod observability_compute;
17mod outcome;
18mod response_class;
19mod scoring;
20mod serde_helpers;
21mod signal;
22mod technique;
23
24pub use endpoint_stop::StrategyMetaForStop;
25pub use endpoint_verdict::{
26 posterior_to_verdict, verdict_to_severity, ContributingFinding, EndpointStopReason,
27 EndpointVerdict, StrategyOutcomeKind,
28};
29pub use exchange::{DifferentialSet, ProbeExchange};
30pub use finding_id::finding_id;
31pub use observability::{BlockFamily, BlockSummary, ObservabilityStatus};
32pub use observability_compute::compute_observability;
33pub use outcome::StrategyOutcome;
34pub use response_class::ResponseClass;
35pub use scoring::{ScoringDimension, ScoringReason};
36pub use signal::{ImpactClass, Signal, SignalKind};
37pub use technique::{
38 always_applicable, Applicability, NormativeStrength, RequestAuthState, SignalSurface,
39 Technique, Vector,
40};
41
42use bytes::Bytes;
43use http::{HeaderMap, Method, StatusCode};
44use serde::{Deserialize, Serialize};
45use serde_helpers::{
46 bytes_serde, header_map_serde, method_serde, opt_bytes_serde, status_code_serde,
47};
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ResponseSurface {
52 #[serde(with = "status_code_serde")]
54 pub status: StatusCode,
55 #[serde(with = "header_map_serde")]
57 pub headers: HeaderMap,
58 #[serde(with = "bytes_serde")]
60 pub body: Bytes,
61 pub timing_ns: u64,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ProbeDefinition {
72 pub url: String,
74 #[serde(with = "method_serde")]
76 pub method: Method,
77 #[serde(with = "header_map_serde")]
79 pub headers: HeaderMap,
80 #[serde(with = "opt_bytes_serde")]
82 pub body: Option<Bytes>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[non_exhaustive]
91pub enum OracleClass {
92 Existence,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
99pub enum OracleVerdict {
100 Confirmed,
103 Likely,
106 Inconclusive,
108 NotPresent,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117pub enum Severity {
118 High,
120 Medium,
122 Low,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132pub struct OracleResult {
133 pub class: OracleClass,
135 pub verdict: OracleVerdict,
137 pub severity: Option<Severity>,
139 #[serde(default)]
141 pub confidence: u8,
142 #[serde(skip_serializing_if = "Option::is_none", default)]
144 pub impact_class: Option<ImpactClass>,
145 #[serde(skip_serializing_if = "Vec::is_empty", default)]
147 pub reasons: Vec<ScoringReason>,
148 #[serde(skip_serializing_if = "Vec::is_empty", default)]
150 pub signals: Vec<Signal>,
151 #[serde(skip_serializing_if = "Option::is_none", default)]
153 pub technique_id: Option<String>,
154 #[serde(skip_serializing_if = "Option::is_none", default)]
156 pub vector: Option<Vector>,
157 #[serde(skip_serializing_if = "Option::is_none", default)]
159 pub normative_strength: Option<NormativeStrength>,
160 #[serde(skip_serializing_if = "Option::is_none", default)]
162 pub label: Option<String>,
163 #[serde(skip_serializing_if = "Option::is_none", default)]
165 pub leaks: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none", default)]
168 pub rfc_basis: Option<String>,
169}
170
171impl OracleResult {
172 #[must_use]
174 pub fn primary_evidence(&self) -> &str {
175 self.signals
176 .iter()
177 .find(|s| s.kind == SignalKind::StatusCodeDiff)
178 .or_else(|| self.signals.first())
179 .map_or("—", |s| s.evidence.as_str())
180 }
181
182 #[must_use]
184 pub fn from_technique(
185 verdict: OracleVerdict,
186 severity: Option<Severity>,
187 evidence: String,
188 signal_kind: SignalKind,
189 confidence: u8,
190 technique: &Technique,
191 ) -> Self {
192 Self {
193 class: technique.oracle_class,
194 verdict,
195 severity,
196 confidence,
197 impact_class: None,
198 reasons: vec![],
199 signals: vec![Signal {
200 kind: signal_kind,
201 evidence,
202 rfc_basis: None,
203 }],
204 technique_id: Some(technique.id.to_string()),
205 vector: Some(technique.vector),
206 normative_strength: Some(technique.strength),
207 label: None,
208 leaks: None,
209 rfc_basis: None,
210 }
211 }
212
213 #[must_use]
215 pub fn into_outcome(self) -> StrategyOutcome {
216 if self.verdict == OracleVerdict::Confirmed {
217 StrategyOutcome::Positive(self)
218 } else {
219 StrategyOutcome::NoSignal(self)
220 }
221 }
222}
223
224#[derive(Debug, thiserror::Error)]
226#[non_exhaustive]
227pub enum Error {
228 #[error("http error: {0}")]
230 Http(String),
231 #[error("cli error: {0}")]
233 Cli(String),
234 #[error("analysis error: {0}")]
236 Analysis(String),
237 #[error("serialization error: {0}")]
239 Serialization(#[from] serde_json::Error),
240}
241
242impl std::fmt::Display for OracleClass {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Self::Existence => write!(f, "Existence"),
246 }
247 }
248}
249
250impl std::fmt::Display for OracleVerdict {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 Self::Confirmed => write!(f, "Confirmed"),
254 Self::Likely => write!(f, "Likely"),
255 Self::Inconclusive => write!(f, "Inconclusive"),
256 Self::NotPresent => write!(f, "NotPresent"),
257 }
258 }
259}
260
261impl std::fmt::Display for Severity {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 match self {
264 Self::High => write!(f, "High"),
265 Self::Medium => write!(f, "Medium"),
266 Self::Low => write!(f, "Low"),
267 }
268 }
269}
270
271#[cfg(test)]
272#[path = "lib_tests.rs"]
273mod tests;