cleanlib_client/
derive_status.rs1use serde_json::Value;
38
39use crate::envelope::{ReasonCode, Status, VerdictEnvelopeV1};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct DerivedStatus {
44 pub status: Status,
45 pub reason_code: ReasonCode,
46}
47
48pub fn derive_status(envelope: &VerdictEnvelopeV1) -> DerivedStatus {
54 let rich = envelope.rich_data.as_ref();
55 let rem = envelope.remediation.as_ref();
56 let exp = envelope.exploitability.as_ref();
57 let avail = envelope.availability.as_ref();
58
59 let mut status: Status;
60 let mut reason: ReasonCode;
61 let mut freshness_signal: Option<String> = None;
65
66 if as_str(exp, "exploitation_likelihood") == Some("CRITICAL") {
67 status = Status::Deny;
68 reason = ReasonCode::VerdictExploitationCritical;
69 freshness_signal = as_str(avail, "exploitation_fusion").map(str::to_string);
70 } else if as_bool(exp, "in_kev")
71 && as_str(avail, "kev") == Some("available")
72 && as_f64(exp, "exploit_risk_score") >= 70.0
73 {
74 status = Status::Deny;
75 reason = ReasonCode::VerdictKevListed;
76 freshness_signal = as_str(avail, "kev").map(str::to_string);
77 } else if as_bool(rich, "has_obfuscation") {
78 status = Status::Deny;
79 reason = ReasonCode::VerdictObfuscated;
80 } else if get(rem, "fix").is_some() || get(rem, "recommended_version").is_some() {
81 status = Status::Warn;
82 reason = ReasonCode::VerdictHasRemediation;
83 let block = get(rem, "fix").or_else(|| get(rem, "recommended_version"));
85 if let Some(b) = block {
86 if let Some(s) = b.get("availability").and_then(Value::as_str) {
87 freshness_signal = Some(s.to_string());
88 }
89 }
90 } else if as_f64(rich, "abandonment_score") >= 0.7 {
91 status = Status::Warn;
92 reason = ReasonCode::VerdictAbandoned;
93 } else if get(rich, "recommended_version").is_some() {
94 status = Status::Allow;
95 reason = ReasonCode::VerdictRecommendedVersionNewer;
96 } else {
97 status = Status::Allow;
98 reason = ReasonCode::VerdictClean;
99 }
100
101 if freshness_signal.as_deref() == Some("degraded_stale") {
103 reason = ReasonCode::VerdictDegradedStale;
104 }
105
106 let _ = &mut status;
110 let _ = &mut reason;
111
112 DerivedStatus { status, reason_code: reason }
113}
114
115fn get<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a Value> {
118 let v = obj?.get(key)?;
119 if v.is_null() {
120 None
121 } else {
122 Some(v)
123 }
124}
125
126fn as_str<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a str> {
127 get(obj, key).and_then(Value::as_str)
128}
129
130fn as_bool(obj: Option<&Value>, key: &str) -> bool {
131 get(obj, key).and_then(Value::as_bool).unwrap_or(false)
132}
133
134fn as_f64(obj: Option<&Value>, key: &str) -> f64 {
135 get(obj, key).and_then(Value::as_f64).unwrap_or(0.0)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn parse(json: &str) -> VerdictEnvelopeV1 {
143 serde_json::from_str(json).unwrap()
144 }
145
146 #[test]
147 fn clean_envelope_yields_allow_clean() {
148 let env = parse(
149 r#"{
150 "status":"ALLOW","reason_code":"VERDICT_CLEAN",
151 "human_message":"ok","as_of":"2026-05-28"
152 }"#,
153 );
154 let d = derive_status(&env);
155 assert_eq!(d.status, Status::Allow);
156 assert_eq!(d.reason_code, ReasonCode::VerdictClean);
157 }
158
159 #[test]
160 fn exploitation_critical_wins_over_kev_and_remediation() {
161 let env = parse(
163 r#"{
164 "status":"DENY","reason_code":"VERDICT_EXPLOITATION_CRITICAL",
165 "human_message":"e","as_of":"2026-05-28",
166 "exploitability":{
167 "exploitation_likelihood":"CRITICAL",
168 "in_kev":true,
169 "exploit_risk_score":99
170 },
171 "availability":{"kev":"available","exploitation_fusion":"available"},
172 "remediation":{"fix":{"availability":"available"}}
173 }"#,
174 );
175 let d = derive_status(&env);
176 assert_eq!(d.status, Status::Deny);
177 assert_eq!(d.reason_code, ReasonCode::VerdictExploitationCritical);
178 }
179
180 #[test]
181 fn freshness_override_preserves_tier_only_rewrites_reason() {
182 let env = parse(
185 r#"{
186 "status":"WARN","reason_code":"VERDICT_DEGRADED_STALE",
187 "human_message":"stale","as_of":"2026-05-28",
188 "remediation":{"fix":{"availability":"degraded_stale"}}
189 }"#,
190 );
191 let d = derive_status(&env);
192 assert_eq!(d.status, Status::Warn);
193 assert_eq!(d.reason_code, ReasonCode::VerdictDegradedStale);
194 }
195
196 #[test]
197 fn kev_requires_all_three_conditions() {
198 let env = parse(
200 r#"{
201 "status":"ALLOW","reason_code":"VERDICT_CLEAN",
202 "human_message":"x","as_of":"2026-05-28",
203 "exploitability":{"in_kev":true,"exploit_risk_score":50},
204 "availability":{"kev":"available"}
205 }"#,
206 );
207 let d = derive_status(&env);
208 assert_eq!(d.status, Status::Allow);
209 assert_eq!(d.reason_code, ReasonCode::VerdictClean);
210 }
211}