use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum Status {
#[serde(rename = "ALLOW")]
Allow,
#[serde(rename = "WARN")]
Warn,
#[serde(rename = "DENY")]
Deny,
}
impl Status {
pub fn as_str(&self) -> &'static str {
match self {
Status::Allow => "ALLOW",
Status::Warn => "WARN",
Status::Deny => "DENY",
}
}
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum ReasonCode {
#[serde(rename = "VERDICT_CLEAN")]
VerdictClean,
#[serde(rename = "VERDICT_RECOMMENDED_VERSION_NEWER")]
VerdictRecommendedVersionNewer,
#[serde(rename = "VERDICT_ABANDONED")]
VerdictAbandoned,
#[serde(rename = "VERDICT_LOW_TRUST")]
VerdictLowTrust,
#[serde(rename = "VERDICT_CVE_FOUND")]
VerdictCveFound,
#[serde(rename = "VERDICT_NOT_YET_ASSESSED")]
VerdictNotYetAssessed,
#[serde(rename = "VERDICT_DEGRADED_STALE")]
VerdictDegradedStale,
#[serde(rename = "VERDICT_HAS_REMEDIATION")]
VerdictHasRemediation,
#[serde(rename = "VERDICT_KEV_LISTED")]
VerdictKevListed,
#[serde(rename = "VERDICT_RANSOMWARE_LISTED")]
VerdictRansomwareListed,
#[serde(rename = "VERDICT_EXPLOITATION_CRITICAL")]
VerdictExploitationCritical,
#[serde(rename = "VERDICT_MALICIOUS")]
VerdictMalicious,
#[serde(rename = "VERDICT_OBFUSCATED")]
VerdictObfuscated,
#[serde(rename = "VERDICT_DENY_LIST")]
VerdictDenyList,
#[serde(rename = "VERDICT_RANGE_NOT_RESOLVED")]
VerdictRangeNotResolved,
#[serde(rename = "CLIENT_NETWORK_UNREACHABLE")]
ClientNetworkUnreachable,
#[serde(rename = "CLIENT_AUTH_FAILED")]
ClientAuthFailed,
#[serde(rename = "CLIENT_BEARER_MISSING")]
ClientBearerMissing,
#[serde(rename = "CLIENT_RATE_LIMITED")]
ClientRateLimited,
#[serde(rename = "REMEDIATION_NOT_FOUND")]
RemediationNotFound,
}
impl ReasonCode {
pub fn as_str(&self) -> &'static str {
match self {
ReasonCode::VerdictClean => "VERDICT_CLEAN",
ReasonCode::VerdictRecommendedVersionNewer => "VERDICT_RECOMMENDED_VERSION_NEWER",
ReasonCode::VerdictAbandoned => "VERDICT_ABANDONED",
ReasonCode::VerdictLowTrust => "VERDICT_LOW_TRUST",
ReasonCode::VerdictCveFound => "VERDICT_CVE_FOUND",
ReasonCode::VerdictNotYetAssessed => "VERDICT_NOT_YET_ASSESSED",
ReasonCode::VerdictDegradedStale => "VERDICT_DEGRADED_STALE",
ReasonCode::VerdictHasRemediation => "VERDICT_HAS_REMEDIATION",
ReasonCode::VerdictKevListed => "VERDICT_KEV_LISTED",
ReasonCode::VerdictRansomwareListed => "VERDICT_RANSOMWARE_LISTED",
ReasonCode::VerdictExploitationCritical => "VERDICT_EXPLOITATION_CRITICAL",
ReasonCode::VerdictMalicious => "VERDICT_MALICIOUS",
ReasonCode::VerdictObfuscated => "VERDICT_OBFUSCATED",
ReasonCode::VerdictDenyList => "VERDICT_DENY_LIST",
ReasonCode::VerdictRangeNotResolved => "VERDICT_RANGE_NOT_RESOLVED",
ReasonCode::ClientNetworkUnreachable => "CLIENT_NETWORK_UNREACHABLE",
ReasonCode::ClientAuthFailed => "CLIENT_AUTH_FAILED",
ReasonCode::ClientBearerMissing => "CLIENT_BEARER_MISSING",
ReasonCode::ClientRateLimited => "CLIENT_RATE_LIMITED",
ReasonCode::RemediationNotFound => "REMEDIATION_NOT_FOUND",
}
}
}
impl std::fmt::Display for ReasonCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub const ALL_REASON_CODES: &[ReasonCode] = &[
ReasonCode::VerdictClean,
ReasonCode::VerdictRecommendedVersionNewer,
ReasonCode::VerdictAbandoned,
ReasonCode::VerdictLowTrust,
ReasonCode::VerdictCveFound,
ReasonCode::VerdictNotYetAssessed,
ReasonCode::VerdictDegradedStale,
ReasonCode::VerdictHasRemediation,
ReasonCode::VerdictKevListed,
ReasonCode::VerdictRansomwareListed,
ReasonCode::VerdictExploitationCritical,
ReasonCode::VerdictMalicious,
ReasonCode::VerdictObfuscated,
ReasonCode::VerdictDenyList,
ReasonCode::VerdictRangeNotResolved,
ReasonCode::ClientNetworkUnreachable,
ReasonCode::ClientAuthFailed,
ReasonCode::ClientBearerMissing,
ReasonCode::ClientRateLimited,
ReasonCode::RemediationNotFound,
];
pub const ALL_VERDICT_SOURCES: &[&str] = &[
"ALLOWED_NO_FINDINGS",
"VECTOR_VERDICT",
"DM_THRESHOLD_BLOCK",
"INSUFFICIENT_DATA",
"CVE_FINDING",
"CVE_FINDING_ON_KEV",
"CVE_FINDING_ON_RANSOMWARE",
"MALICIOUS_TRIAGE",
];
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VerdictEnvelopeV1 {
pub status: String,
pub reason_code: String,
pub human_message: String,
pub as_of: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as_of_ns: Option<String>,
#[serde(default)]
pub rich_data: Option<serde_json::Value>,
#[serde(default)]
pub remediation: Option<serde_json::Value>,
#[serde(default)]
pub exploitability: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub availability: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_minimal_envelope() {
let json = r#"{
"status": "ALLOW",
"reason_code": "VERDICT_CLEAN",
"human_message": "ok",
"as_of": "2026-05-28"
}"#;
let env: VerdictEnvelopeV1 = serde_json::from_str(json).unwrap();
assert_eq!(env.status, "ALLOW");
assert_eq!(env.reason_code, "VERDICT_CLEAN");
assert!(env.rich_data.is_none());
assert!(env.remediation.is_none());
assert!(env.exploitability.is_none());
assert!(env.availability.is_null());
}
#[test]
fn all_20_reason_codes_present() {
assert_eq!(ALL_REASON_CODES.len(), 20);
assert!(ALL_REASON_CODES.contains(&ReasonCode::VerdictCveFound));
assert!(ALL_REASON_CODES.contains(&ReasonCode::VerdictNotYetAssessed));
assert!(ALL_REASON_CODES.contains(&ReasonCode::VerdictRangeNotResolved));
}
#[test]
fn all_verdict_sources_recognized() {
assert_eq!(ALL_VERDICT_SOURCES.len(), 8);
assert!(ALL_VERDICT_SOURCES.contains(&"CVE_FINDING_ON_KEV"));
assert!(ALL_VERDICT_SOURCES.contains(&"CVE_FINDING_ON_RANSOMWARE"));
assert!(ALL_VERDICT_SOURCES.contains(&"MALICIOUS_TRIAGE"));
}
#[test]
fn reason_code_string_roundtrip() {
for rc in ALL_REASON_CODES {
let s = serde_json::to_string(rc).unwrap();
let back: ReasonCode = serde_json::from_str(&s).unwrap();
assert_eq!(&back, rc);
assert_eq!(format!("\"{}\"", rc.as_str()), s);
}
}
#[test]
fn status_string_roundtrip() {
for st in [Status::Allow, Status::Warn, Status::Deny] {
let s = serde_json::to_string(&st).unwrap();
let back: Status = serde_json::from_str(&s).unwrap();
assert_eq!(back, st);
assert_eq!(format!("\"{}\"", st.as_str()), s);
}
}
}