#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![deny(missing_docs)]
mod endpoint_stop;
mod endpoint_verdict;
mod exchange;
mod finding_id;
mod observability;
mod observability_compute;
mod outcome;
mod response_class;
mod scoring;
mod serde_helpers;
mod signal;
mod technique;
pub use endpoint_stop::StrategyMetaForStop;
pub use endpoint_verdict::{
posterior_to_verdict, verdict_to_severity, ContributingFinding, EndpointStopReason,
EndpointVerdict, StrategyOutcomeKind,
};
pub use exchange::{DifferentialSet, ProbeExchange};
pub use finding_id::finding_id;
pub use observability::{BlockFamily, BlockSummary, ObservabilityStatus};
pub use observability_compute::compute_observability;
pub use outcome::StrategyOutcome;
pub use response_class::ResponseClass;
pub use scoring::{ScoringDimension, ScoringReason};
pub use signal::{ImpactClass, Signal, SignalKind};
pub use technique::{
always_applicable, Applicability, NormativeStrength, RequestAuthState, SignalSurface,
Technique, Vector,
};
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode};
use serde::{Deserialize, Serialize};
use serde_helpers::{
bytes_serde, header_map_serde, method_serde, opt_bytes_serde, status_code_serde,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseSurface {
#[serde(with = "status_code_serde")]
pub status: StatusCode,
#[serde(with = "header_map_serde")]
pub headers: HeaderMap,
#[serde(with = "bytes_serde")]
pub body: Bytes,
pub timing_ns: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeDefinition {
pub url: String,
#[serde(with = "method_serde")]
pub method: Method,
#[serde(with = "header_map_serde")]
pub headers: HeaderMap,
#[serde(with = "opt_bytes_serde")]
pub body: Option<Bytes>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum OracleClass {
Existence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum OracleVerdict {
Confirmed,
Likely,
Inconclusive,
NotPresent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Severity {
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct OracleResult {
pub class: OracleClass,
pub verdict: OracleVerdict,
pub severity: Option<Severity>,
#[serde(default)]
pub confidence: u8,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub impact_class: Option<ImpactClass>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub reasons: Vec<ScoringReason>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub signals: Vec<Signal>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub technique_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub vector: Option<Vector>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub normative_strength: Option<NormativeStrength>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub leaks: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub rfc_basis: Option<String>,
}
impl OracleResult {
#[must_use]
pub fn primary_evidence(&self) -> &str {
self.signals
.iter()
.find(|s| s.kind == SignalKind::StatusCodeDiff)
.or_else(|| self.signals.first())
.map_or("—", |s| s.evidence.as_str())
}
#[must_use]
pub fn from_technique(
verdict: OracleVerdict,
severity: Option<Severity>,
evidence: String,
signal_kind: SignalKind,
confidence: u8,
technique: &Technique,
) -> Self {
Self {
class: technique.oracle_class,
verdict,
severity,
confidence,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: signal_kind,
evidence,
rfc_basis: None,
}],
technique_id: Some(technique.id.to_string()),
vector: Some(technique.vector),
normative_strength: Some(technique.strength),
label: None,
leaks: None,
rfc_basis: None,
}
}
#[must_use]
pub fn into_outcome(self) -> StrategyOutcome {
if self.verdict == OracleVerdict::Confirmed {
StrategyOutcome::Positive(self)
} else {
StrategyOutcome::NoSignal(self)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("http error: {0}")]
Http(String),
#[error("cli error: {0}")]
Cli(String),
#[error("analysis error: {0}")]
Analysis(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl std::fmt::Display for OracleClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Existence => write!(f, "Existence"),
}
}
}
impl std::fmt::Display for OracleVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Confirmed => write!(f, "Confirmed"),
Self::Likely => write!(f, "Likely"),
Self::Inconclusive => write!(f, "Inconclusive"),
Self::NotPresent => write!(f, "NotPresent"),
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::High => write!(f, "High"),
Self::Medium => write!(f, "Medium"),
Self::Low => write!(f, "Low"),
}
}
}
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;