1use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
27pub enum Status {
28 #[serde(rename = "ALLOW")]
29 Allow,
30 #[serde(rename = "WARN")]
31 Warn,
32 #[serde(rename = "DENY")]
33 Deny,
34}
35
36impl Status {
37 pub fn as_str(&self) -> &'static str {
40 match self {
41 Status::Allow => "ALLOW",
42 Status::Warn => "WARN",
43 Status::Deny => "DENY",
44 }
45 }
46}
47
48impl std::fmt::Display for Status {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
57pub enum ReasonCode {
58 #[serde(rename = "VERDICT_CLEAN")]
60 VerdictClean,
61 #[serde(rename = "VERDICT_RECOMMENDED_VERSION_NEWER")]
62 VerdictRecommendedVersionNewer,
63 #[serde(rename = "VERDICT_ABANDONED")]
64 VerdictAbandoned,
65 #[serde(rename = "VERDICT_LOW_TRUST")]
66 VerdictLowTrust,
67 #[serde(rename = "VERDICT_DEGRADED_STALE")]
71 VerdictDegradedStale,
72 #[serde(rename = "VERDICT_HAS_REMEDIATION")]
73 VerdictHasRemediation,
74 #[serde(rename = "VERDICT_KEV_LISTED")]
75 VerdictKevListed,
76 #[serde(rename = "VERDICT_EXPLOITATION_CRITICAL")]
77 VerdictExploitationCritical,
78 #[serde(rename = "VERDICT_OBFUSCATED")]
79 VerdictObfuscated,
80 #[serde(rename = "VERDICT_DENY_LIST")]
81 VerdictDenyList,
82
83 #[serde(rename = "CLIENT_NETWORK_UNREACHABLE")]
85 ClientNetworkUnreachable,
86 #[serde(rename = "CLIENT_AUTH_FAILED")]
87 ClientAuthFailed,
88 #[serde(rename = "CLIENT_BEARER_MISSING")]
89 ClientBearerMissing,
90 #[serde(rename = "CLIENT_RATE_LIMITED")]
91 ClientRateLimited,
92
93 #[serde(rename = "REMEDIATION_NOT_FOUND")]
95 RemediationNotFound,
96}
97
98impl ReasonCode {
99 pub fn as_str(&self) -> &'static str {
102 match self {
103 ReasonCode::VerdictClean => "VERDICT_CLEAN",
104 ReasonCode::VerdictRecommendedVersionNewer => "VERDICT_RECOMMENDED_VERSION_NEWER",
105 ReasonCode::VerdictAbandoned => "VERDICT_ABANDONED",
106 ReasonCode::VerdictLowTrust => "VERDICT_LOW_TRUST",
107 ReasonCode::VerdictDegradedStale => "VERDICT_DEGRADED_STALE",
108 ReasonCode::VerdictHasRemediation => "VERDICT_HAS_REMEDIATION",
109 ReasonCode::VerdictKevListed => "VERDICT_KEV_LISTED",
110 ReasonCode::VerdictExploitationCritical => "VERDICT_EXPLOITATION_CRITICAL",
111 ReasonCode::VerdictObfuscated => "VERDICT_OBFUSCATED",
112 ReasonCode::VerdictDenyList => "VERDICT_DENY_LIST",
113 ReasonCode::ClientNetworkUnreachable => "CLIENT_NETWORK_UNREACHABLE",
114 ReasonCode::ClientAuthFailed => "CLIENT_AUTH_FAILED",
115 ReasonCode::ClientBearerMissing => "CLIENT_BEARER_MISSING",
116 ReasonCode::ClientRateLimited => "CLIENT_RATE_LIMITED",
117 ReasonCode::RemediationNotFound => "REMEDIATION_NOT_FOUND",
118 }
119 }
120}
121
122impl std::fmt::Display for ReasonCode {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.write_str(self.as_str())
125 }
126}
127
128pub const ALL_REASON_CODES: &[ReasonCode] = &[
130 ReasonCode::VerdictClean,
131 ReasonCode::VerdictRecommendedVersionNewer,
132 ReasonCode::VerdictAbandoned,
133 ReasonCode::VerdictLowTrust,
134 ReasonCode::VerdictDegradedStale,
135 ReasonCode::VerdictHasRemediation,
136 ReasonCode::VerdictKevListed,
137 ReasonCode::VerdictExploitationCritical,
138 ReasonCode::VerdictObfuscated,
139 ReasonCode::VerdictDenyList,
140 ReasonCode::ClientNetworkUnreachable,
141 ReasonCode::ClientAuthFailed,
142 ReasonCode::ClientBearerMissing,
143 ReasonCode::ClientRateLimited,
144 ReasonCode::RemediationNotFound,
145];
146
147#[derive(Debug, Clone, Deserialize, Serialize)]
157pub struct VerdictEnvelopeV1 {
158 pub status: String,
159 pub reason_code: String,
160 pub human_message: String,
161 pub as_of: String,
162
163 #[serde(default)]
164 pub rich_data: Option<serde_json::Value>,
165 #[serde(default)]
166 pub remediation: Option<serde_json::Value>,
167 #[serde(default)]
168 pub exploitability: Option<serde_json::Value>,
169 #[serde(default)]
170 pub availability: Option<serde_json::Value>,
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn parses_minimal_envelope() {
179 let json = r#"{
180 "status": "ALLOW",
181 "reason_code": "VERDICT_CLEAN",
182 "human_message": "ok",
183 "as_of": "2026-05-28"
184 }"#;
185 let env: VerdictEnvelopeV1 = serde_json::from_str(json).unwrap();
186 assert_eq!(env.status, "ALLOW");
187 assert_eq!(env.reason_code, "VERDICT_CLEAN");
188 assert!(env.rich_data.is_none());
189 assert!(env.remediation.is_none());
190 assert!(env.exploitability.is_none());
191 assert!(env.availability.is_none());
192 }
193
194 #[test]
195 fn all_15_reason_codes_present() {
196 assert_eq!(ALL_REASON_CODES.len(), 15);
198 }
199
200 #[test]
201 fn reason_code_string_roundtrip() {
202 for rc in ALL_REASON_CODES {
203 let s = serde_json::to_string(rc).unwrap();
204 let back: ReasonCode = serde_json::from_str(&s).unwrap();
205 assert_eq!(&back, rc);
206 assert_eq!(format!("\"{}\"", rc.as_str()), s);
208 }
209 }
210
211 #[test]
212 fn status_string_roundtrip() {
213 for st in [Status::Allow, Status::Warn, Status::Deny] {
214 let s = serde_json::to_string(&st).unwrap();
215 let back: Status = serde_json::from_str(&s).unwrap();
216 assert_eq!(back, st);
217 assert_eq!(format!("\"{}\"", st.as_str()), s);
218 }
219 }
220}