use serde_json::Value;
use crate::envelope::{ReasonCode, Status, VerdictEnvelopeV1};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DerivedStatus {
pub status: Status,
pub reason_code: ReasonCode,
}
pub fn derive_status(envelope: &VerdictEnvelopeV1) -> DerivedStatus {
let rich = envelope.rich_data.as_ref();
let rem = envelope.remediation.as_ref();
let exp = envelope.exploitability.as_ref();
let avail = envelope.availability.as_ref();
let mut status: Status;
let mut reason: ReasonCode;
let mut freshness_signal: Option<String> = None;
if as_str(exp, "exploitation_likelihood") == Some("CRITICAL") {
status = Status::Deny;
reason = ReasonCode::VerdictExploitationCritical;
freshness_signal = as_str(avail, "exploitation_fusion").map(str::to_string);
} else if as_bool(exp, "in_kev")
&& as_str(avail, "kev") == Some("available")
&& as_f64(exp, "exploit_risk_score") >= 70.0
{
status = Status::Deny;
reason = ReasonCode::VerdictKevListed;
freshness_signal = as_str(avail, "kev").map(str::to_string);
} else if as_bool(rich, "has_obfuscation") {
status = Status::Deny;
reason = ReasonCode::VerdictObfuscated;
} else if get(rem, "fix").is_some() || get(rem, "recommended_version").is_some() {
status = Status::Warn;
reason = ReasonCode::VerdictHasRemediation;
let block = get(rem, "fix").or_else(|| get(rem, "recommended_version"));
if let Some(b) = block {
if let Some(s) = b.get("availability").and_then(Value::as_str) {
freshness_signal = Some(s.to_string());
}
}
} else if as_f64(rich, "abandonment_score") >= 0.7 {
status = Status::Warn;
reason = ReasonCode::VerdictAbandoned;
} else if get(rich, "recommended_version").is_some() {
status = Status::Allow;
reason = ReasonCode::VerdictRecommendedVersionNewer;
} else {
status = Status::Allow;
reason = ReasonCode::VerdictClean;
}
if freshness_signal.as_deref() == Some("degraded_stale") {
reason = ReasonCode::VerdictDegradedStale;
}
let _ = &mut status;
let _ = &mut reason;
DerivedStatus { status, reason_code: reason }
}
fn get<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a Value> {
let v = obj?.get(key)?;
if v.is_null() {
None
} else {
Some(v)
}
}
fn as_str<'a>(obj: Option<&'a Value>, key: &str) -> Option<&'a str> {
get(obj, key).and_then(Value::as_str)
}
fn as_bool(obj: Option<&Value>, key: &str) -> bool {
get(obj, key).and_then(Value::as_bool).unwrap_or(false)
}
fn as_f64(obj: Option<&Value>, key: &str) -> f64 {
get(obj, key).and_then(Value::as_f64).unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(json: &str) -> VerdictEnvelopeV1 {
serde_json::from_str(json).unwrap()
}
#[test]
fn clean_envelope_yields_allow_clean() {
let env = parse(
r#"{
"status":"ALLOW","reason_code":"VERDICT_CLEAN",
"human_message":"ok","as_of":"2026-05-28"
}"#,
);
let d = derive_status(&env);
assert_eq!(d.status, Status::Allow);
assert_eq!(d.reason_code, ReasonCode::VerdictClean);
}
#[test]
fn exploitation_critical_wins_over_kev_and_remediation() {
let env = parse(
r#"{
"status":"DENY","reason_code":"VERDICT_EXPLOITATION_CRITICAL",
"human_message":"e","as_of":"2026-05-28",
"exploitability":{
"exploitation_likelihood":"CRITICAL",
"in_kev":true,
"exploit_risk_score":99
},
"availability":{"kev":"available","exploitation_fusion":"available"},
"remediation":{"fix":{"availability":"available"}}
}"#,
);
let d = derive_status(&env);
assert_eq!(d.status, Status::Deny);
assert_eq!(d.reason_code, ReasonCode::VerdictExploitationCritical);
}
#[test]
fn freshness_override_preserves_tier_only_rewrites_reason() {
let env = parse(
r#"{
"status":"WARN","reason_code":"VERDICT_DEGRADED_STALE",
"human_message":"stale","as_of":"2026-05-28",
"remediation":{"fix":{"availability":"degraded_stale"}}
}"#,
);
let d = derive_status(&env);
assert_eq!(d.status, Status::Warn);
assert_eq!(d.reason_code, ReasonCode::VerdictDegradedStale);
}
#[test]
fn kev_requires_all_three_conditions() {
let env = parse(
r#"{
"status":"ALLOW","reason_code":"VERDICT_CLEAN",
"human_message":"x","as_of":"2026-05-28",
"exploitability":{"in_kev":true,"exploit_risk_score":50},
"availability":{"kev":"available"}
}"#,
);
let d = derive_status(&env);
assert_eq!(d.status, Status::Allow);
assert_eq!(d.reason_code, ReasonCode::VerdictClean);
}
}