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 = Some(&envelope.availability);
63
64 let mut status: Status;
65 let mut reason: ReasonCode;
66 let mut freshness_signal: Option<String> = None;
70
71 if as_str(exp, "exploitation_likelihood") == Some("CRITICAL") {
72 status = Status::Deny;
73 reason = ReasonCode::VerdictExploitationCritical;
74 freshness_signal = as_str(avail, "exploitation_fusion").map(str::to_string);
75 } else if as_bool(exp, "in_kev")
76 && as_str(avail, "kev") == Some("available")
77 && as_f64(exp, "exploit_risk_score") >= 70.0
78 {
79 status = Status::Deny;
80 reason = ReasonCode::VerdictKevListed;
81 freshness_signal = as_str(avail, "kev").map(str::to_string);
82 } else if as_bool(rich, "has_obfuscation") {
83 status = Status::Deny;
84 reason = ReasonCode::VerdictObfuscated;
85 } else if get(rem, "fix").is_some() || get(rem, "recommended_version").is_some() {
86 status = Status::Warn;
87 reason = ReasonCode::VerdictHasRemediation;
88 let block = get(rem, "fix").or_else(|| get(rem, "recommended_version"));
90 if let Some(b) = block {
91 if let Some(s) = b.get("availability").and_then(Value::as_str) {
92 freshness_signal = Some(s.to_string());
93 }
94 }
95 } else if as_f64(rich, "abandonment_score") >= 0.7 {
96 status = Status::Warn;
97 reason = ReasonCode::VerdictAbandoned;
98 } else if get(rich, "recommended_version").is_some() {
99 status = Status::Allow;
100 reason = ReasonCode::VerdictRecommendedVersionNewer;
101 } else {
102 status = Status::Allow;
103 reason = ReasonCode::VerdictClean;
104 }
105
106 if freshness_signal.as_deref() == Some("degraded_stale") {
108 reason = ReasonCode::VerdictDegradedStale;
109 }
110
111 let _ = &mut status;
115 let _ = &mut reason;
116
117 DerivedStatus { status, reason_code: reason }
118}
119
120fn get<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a Value> {
123 let v = obj?.get(key)?;
124 if v.is_null() {
125 None
126 } else {
127 Some(v)
128 }
129}
130
131fn as_str<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a str> {
132 get(obj, key).and_then(Value::as_str)
133}
134
135fn as_bool(obj: Option<&Value>, key: &str) -> bool {
136 get(obj, key).and_then(Value::as_bool).unwrap_or(false)
137}
138
139fn as_f64(obj: Option<&Value>, key: &str) -> f64 {
140 get(obj, key).and_then(Value::as_f64).unwrap_or(0.0)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 fn parse(json: &str) -> VerdictEnvelopeV1 {
148 serde_json::from_str(json).unwrap()
149 }
150
151 #[test]
152 fn clean_envelope_yields_allow_clean() {
153 let env = parse(
154 r#"{
155 "status":"ALLOW","reason_code":"VERDICT_CLEAN",
156 "human_message":"ok","as_of":"2026-05-28"
157 }"#,
158 );
159 let d = derive_status(&env);
160 assert_eq!(d.status, Status::Allow);
161 assert_eq!(d.reason_code, ReasonCode::VerdictClean);
162 }
163
164 #[test]
165 fn exploitation_critical_wins_over_kev_and_remediation() {
166 let env = parse(
168 r#"{
169 "status":"DENY","reason_code":"VERDICT_EXPLOITATION_CRITICAL",
170 "human_message":"e","as_of":"2026-05-28",
171 "exploitability":{
172 "exploitation_likelihood":"CRITICAL",
173 "in_kev":true,
174 "exploit_risk_score":99
175 },
176 "availability":{"kev":"available","exploitation_fusion":"available"},
177 "remediation":{"fix":{"availability":"available"}}
178 }"#,
179 );
180 let d = derive_status(&env);
181 assert_eq!(d.status, Status::Deny);
182 assert_eq!(d.reason_code, ReasonCode::VerdictExploitationCritical);
183 }
184
185 #[test]
186 fn freshness_override_preserves_tier_only_rewrites_reason() {
187 let env = parse(
190 r#"{
191 "status":"WARN","reason_code":"VERDICT_DEGRADED_STALE",
192 "human_message":"stale","as_of":"2026-05-28",
193 "remediation":{"fix":{"availability":"degraded_stale"}}
194 }"#,
195 );
196 let d = derive_status(&env);
197 assert_eq!(d.status, Status::Warn);
198 assert_eq!(d.reason_code, ReasonCode::VerdictDegradedStale);
199 }
200
201 #[test]
202 fn kev_requires_all_three_conditions() {
203 let env = parse(
205 r#"{
206 "status":"ALLOW","reason_code":"VERDICT_CLEAN",
207 "human_message":"x","as_of":"2026-05-28",
208 "exploitability":{"in_kev":true,"exploit_risk_score":50},
209 "availability":{"kev":"available"}
210 }"#,
211 );
212 let d = derive_status(&env);
213 assert_eq!(d.status, Status::Allow);
214 assert_eq!(d.reason_code, ReasonCode::VerdictClean);
215 }
216}