use thiserror::Error;
use crate::customer_state::{CustomerState, Tier};
use crate::errors::CleanLibraryError;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Assessment {
state: CustomerState,
}
impl Assessment {
pub fn new(state: CustomerState) -> Self {
Self { state }
}
pub fn from_wire(source: &str) -> Self {
Self::new(CustomerState::from_wire(source))
}
pub fn from_wire_with_source_state(source: &str, source_state: Option<&str>) -> Self {
Self::new(CustomerState::from_wire_with_source_state(source, source_state))
}
pub fn from_wire_with_source_state_and_origin(
source: &str,
source_state: Option<&str>,
matched_rule_id: Option<&str>,
) -> Self {
Self::new(CustomerState::from_wire_with_source_state_and_origin(
source,
source_state,
matched_rule_id,
))
}
pub fn state(&self) -> CustomerState {
self.state
}
pub fn tier(&self) -> Tier {
self.state.tier()
}
pub fn exit_code(&self) -> i32 {
self.state.exit_code()
}
pub fn is_allowed(&self) -> bool {
matches!(self.tier(), Tier::Clean)
}
pub fn enforce(&self) -> Result<(), GateError> {
if self.is_allowed() {
Ok(())
} else {
Err(GateError::Blocked {
state: self.state,
exit_code: self.exit_code(),
})
}
}
}
pub fn verdict(
outcome: Result<CustomerState, CleanLibraryError>,
) -> Result<Assessment, CleanLibraryError> {
outcome.map(Assessment::new)
}
pub fn enforce(outcome: Result<CustomerState, CleanLibraryError>) -> Result<(), GateError> {
match verdict(outcome) {
Ok(assessment) => assessment.enforce(),
Err(e) => Err(GateError::NotAssessed(e)),
}
}
#[derive(Debug, Error)]
pub enum GateError {
#[error("gate refused: {} (exit {exit_code})", .state.as_str())]
Blocked { state: CustomerState, exit_code: i32 },
#[error("gate could not evaluate: {0}")]
NotAssessed(#[from] CleanLibraryError),
}
impl GateError {
pub fn exit_code(&self) -> i32 {
match self {
GateError::Blocked { exit_code, .. } => *exit_code,
GateError::NotAssessed(_) => Tier::Block.exit_code(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verdict_returns_block_tier_as_value_not_error() {
for state in [
CustomerState::Malicious,
CustomerState::Compromised,
CustomerState::BlockedByPolicy,
CustomerState::ActivelyExploited,
CustomerState::RansomwareLinked,
] {
let a = verdict(Ok(state)).expect("a completed block is Ok, not Err");
assert_eq!(a.state(), state);
assert_eq!(a.tier(), Tier::Block);
assert!(!a.is_allowed());
}
}
#[test]
fn verdict_returns_clean_as_allowed_value() {
let a = verdict(Ok(CustomerState::Clean)).unwrap();
assert!(a.is_allowed());
assert_eq!(a.exit_code(), 0);
}
#[test]
fn verdict_propagates_couldnt_assess_as_err() {
let outcome = Err(CleanLibraryError::CoverageIncomplete {
reason_code: "SCAN_ABORTED".into(),
message: "3 of 40 coordinates unreachable".into(),
});
assert!(verdict(outcome).is_err());
}
#[test]
fn enforce_ok_only_for_clean() {
assert!(enforce(Ok(CustomerState::Clean)).is_ok());
}
#[test]
fn enforce_refuses_every_non_clean_completed_state() {
for state in CustomerState::all() {
if matches!(state, CustomerState::Clean) {
continue;
}
let err = enforce(Ok(state)).expect_err("non-clean must refuse the gate");
match err {
GateError::Blocked { state: s, .. } => assert_eq!(s, state),
other => panic!("expected Blocked for {state:?}, got {other:?}"),
}
}
}
#[test]
fn enforce_not_yet_assessed_refuses_and_never_exits_zero() {
let err = enforce(Ok(CustomerState::NotYetAssessed)).unwrap_err();
assert!(matches!(err, GateError::Blocked { .. }));
assert_ne!(err.exit_code(), 0, "not-assessed must never exit clean");
assert_eq!(err.exit_code(), 2, "not-assessed is warn-tier (exit 2)");
}
#[test]
fn enforce_couldnt_assess_fails_closed_to_block_exit() {
let outcome = Err(CleanLibraryError::AttestationInvalid {
reason_code: "SIG_MISMATCH".into(),
message: "attestation signature did not verify".into(),
});
let err = enforce(outcome).unwrap_err();
assert!(matches!(err, GateError::NotAssessed(_)));
assert_eq!(err.exit_code(), 1, "couldn't-assess fails closed to block exit");
}
#[test]
fn blocked_and_not_assessed_are_distinguishable() {
let blocked = enforce(Ok(CustomerState::Malicious)).unwrap_err();
let not_assessed = enforce(Err(CleanLibraryError::CoverageIncomplete {
reason_code: "X".into(),
message: "y".into(),
}))
.unwrap_err();
assert!(matches!(blocked, GateError::Blocked { .. }));
assert!(matches!(not_assessed, GateError::NotAssessed(_)));
}
#[test]
fn assessment_method_enforce_matches_free_fn_on_completed() {
for state in CustomerState::all() {
let a = Assessment::new(state);
assert_eq!(a.enforce().is_ok(), enforce(Ok(state)).is_ok(), "{state:?}");
}
}
#[test]
fn matches_cx8_gate_conformance_fixture() {
use std::path::PathBuf;
let tier_str = |t: Tier| match t {
Tier::Block => "block",
Tier::Warn => "warn",
Tier::Clean => "clean",
};
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/contract-fixtures/CX8_GATE_EXPECTED.json");
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {path:?}: {e}"));
let g: serde_json::Value =
serde_json::from_str(&raw).expect("CX8_GATE_EXPECTED.json must parse");
let completed = g["completed"].as_object().expect("completed object");
assert!(!completed.is_empty(), "fixture has no completed cases");
for (wire, row) in completed {
let source = if wire == "__UNKNOWN__" {
"SOME_FUTURE_VARIANT"
} else {
wire.as_str()
};
let state = CustomerState::from_wire(source);
let a = verdict(Ok(state)).expect("a completed assessment is Ok, never Err");
assert_eq!(a.state().as_str(), row["state"].as_str().unwrap(), "{wire} state");
assert_eq!(tier_str(a.tier()), row["tier"].as_str().unwrap(), "{wire} tier");
assert_eq!(
i64::from(a.exit_code()),
row["exit_code"].as_i64().unwrap(),
"{wire} exit_code"
);
assert_eq!(a.is_allowed(), row["is_allowed"].as_bool().unwrap(), "{wire} is_allowed");
assert!(row["verdict_ok"].as_bool().unwrap(), "{wire} completed verdict_ok must be true");
let en = enforce(Ok(state));
assert_eq!(en.is_ok(), row["enforce_ok"].as_bool().unwrap(), "{wire} enforce_ok");
match (&en, row["enforce_error"].as_str()) {
(Ok(()), None) => {}
(Err(GateError::Blocked { .. }), Some("blocked")) => {}
(got, want) => {
panic!("{wire} enforce_error mismatch: got {got:?}, want {want:?}")
}
}
}
let mk = |kind: &str| -> CleanLibraryError {
match kind {
"coverage_incomplete" => CleanLibraryError::CoverageIncomplete {
reason_code: "SCAN_ABORTED".into(),
message: "coverage incomplete".into(),
},
"attestation_invalid" => CleanLibraryError::AttestationInvalid {
reason_code: "SIG_MISMATCH".into(),
message: "attestation invalid".into(),
},
other => panic!("unknown couldnt_assess kind in fixture: {other}"),
}
};
let couldnt = g["couldnt_assess"].as_object().expect("couldnt_assess object");
assert!(!couldnt.is_empty(), "fixture has no couldnt_assess cases");
for (kind, row) in couldnt {
assert_eq!(
verdict(Err(mk(kind))).is_ok(),
row["verdict_ok"].as_bool().unwrap(),
"{kind} verdict_ok"
);
assert!(!row["verdict_ok"].as_bool().unwrap(), "{kind} couldnt_assess verdict_ok must be false");
let en = enforce(Err(mk(kind)));
assert_eq!(en.is_ok(), row["enforce_ok"].as_bool().unwrap(), "{kind} enforce_ok");
let err = en.expect_err("couldnt_assess must refuse the gate");
assert!(matches!(err, GateError::NotAssessed(_)), "{kind} must be NotAssessed");
assert_eq!(row["enforce_error"].as_str(), Some("not_assessed"), "{kind} fixture arm");
assert_eq!(
i64::from(err.exit_code()),
row["exit_code"].as_i64().unwrap(),
"{kind} fail-closed exit"
);
assert_ne!(err.exit_code(), 0, "{kind} couldnt-assess must never exit clean");
}
}
}