Skip to main content

libverify_core/controls/
deployment_health.rs

1use crate::control::{Control, ControlFinding, ControlId, builtin};
2use crate::evidence::{EvidenceBundle, EvidenceState};
3
4/// Simple binary check that deployment maintains healthy service metrics.
5pub struct DeploymentHealthControl;
6
7impl Control for DeploymentHealthControl {
8    fn id(&self) -> ControlId {
9        builtin::id(builtin::DEPLOYMENT_HEALTH)
10    }
11
12    fn description(&self) -> &'static str {
13        "Deployment must maintain healthy service metrics"
14    }
15
16    fn evaluate(&self, evidence: &EvidenceBundle) -> Vec<ControlFinding> {
17        let id = self.id();
18
19        let diff = match &evidence.behavioral_diff {
20            EvidenceState::NotApplicable => {
21                return vec![ControlFinding::not_applicable(
22                    id,
23                    "Behavioral diff evidence is not applicable",
24                )];
25            }
26            EvidenceState::Missing { gaps } => {
27                return vec![ControlFinding::indeterminate(
28                    id,
29                    "Behavioral diff evidence is unavailable",
30                    vec![],
31                    gaps.clone(),
32                )];
33            }
34            EvidenceState::Complete { value } | EvidenceState::Partial { value, .. } => value,
35        };
36
37        let mut violations: Vec<String> = Vec::new();
38
39        for metric in &diff.metrics {
40            let lower = metric.name.to_lowercase();
41
42            // Error rate metrics: current > 5.0 (5%) is critical
43            if (lower.contains("error_rate") || lower.contains("_5xx")) && metric.current > 5.0 {
44                violations.push(format!(
45                    "{}: {:.2}% (threshold: 5%)",
46                    metric.name, metric.current,
47                ));
48            }
49
50            // Availability/uptime metrics: current < 99.0 is critical
51            if (lower.contains("availability") || lower.contains("uptime")) && metric.current < 99.0
52            {
53                violations.push(format!(
54                    "{}: {:.2}% (threshold: 99%)",
55                    metric.name, metric.current,
56                ));
57            }
58        }
59
60        if violations.is_empty() {
61            vec![ControlFinding::satisfied(
62                id,
63                format!(
64                    "Deployment {} is healthy — no critical thresholds breached",
65                    diff.deployment_id
66                ),
67                vec![diff.deployment_id.clone()],
68            )]
69        } else {
70            vec![ControlFinding::violated(
71                id,
72                format!("Service health degraded: {}", violations.join("; ")),
73                violations,
74            )]
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::control::ControlStatus;
83    use crate::evidence::{BehavioralDiff, EvidenceGap, MetricObservation};
84
85    fn metric(name: &str, baseline: f64, current: f64) -> MetricObservation {
86        MetricObservation {
87            name: name.to_string(),
88            current,
89            baseline,
90            unit: None,
91            window_secs: None,
92        }
93    }
94
95    fn make_bundle(metrics: Vec<MetricObservation>) -> EvidenceBundle {
96        EvidenceBundle {
97            behavioral_diff: EvidenceState::complete(BehavioralDiff {
98                deployment_id: "deploy-42".to_string(),
99                environment: Some("production".to_string()),
100                metrics,
101                observed_at: None,
102            }),
103            ..Default::default()
104        }
105    }
106
107    #[test]
108    fn healthy_metrics_is_satisfied() {
109        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
110            metric("error_rate_5xx", 0.5, 1.0),
111            metric("service_availability", 99.99, 99.95),
112        ]));
113        assert_eq!(findings.len(), 1);
114        assert_eq!(findings[0].status, ControlStatus::Satisfied);
115    }
116
117    #[test]
118    fn high_error_rate_is_violated() {
119        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
120            metric("error_rate_5xx", 0.5, 6.0), // 6% > 5% threshold
121        ]));
122        assert_eq!(findings.len(), 1);
123        assert_eq!(findings[0].status, ControlStatus::Violated);
124        assert!(findings[0].rationale.contains("error_rate_5xx"));
125    }
126
127    #[test]
128    fn low_availability_is_violated() {
129        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
130            metric("service_availability", 99.99, 98.5), // 98.5% < 99% threshold
131        ]));
132        assert_eq!(findings.len(), 1);
133        assert_eq!(findings[0].status, ControlStatus::Violated);
134        assert!(findings[0].rationale.contains("availability"));
135    }
136
137    #[test]
138    fn unrelated_metrics_are_not_checked() {
139        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
140            metric("cpu_usage", 50.0, 95.0), // not an error/availability metric
141            metric("requests_rps", 1000.0, 500.0), // throughput, not health
142        ]));
143        assert_eq!(findings.len(), 1);
144        assert_eq!(findings[0].status, ControlStatus::Satisfied);
145    }
146
147    #[test]
148    fn missing_evidence_is_indeterminate() {
149        let bundle = EvidenceBundle {
150            behavioral_diff: EvidenceState::missing(vec![EvidenceGap::CollectionFailed {
151                source: "datadog".to_string(),
152                subject: "metrics".to_string(),
153                detail: "auth failed".to_string(),
154            }]),
155            ..Default::default()
156        };
157        let findings = DeploymentHealthControl.evaluate(&bundle);
158        assert_eq!(findings.len(), 1);
159        assert_eq!(findings[0].status, ControlStatus::Indeterminate);
160    }
161
162    #[test]
163    fn not_applicable_when_evidence_not_applicable() {
164        let bundle = EvidenceBundle::default();
165        let findings = DeploymentHealthControl.evaluate(&bundle);
166        assert_eq!(findings.len(), 1);
167        assert_eq!(findings[0].status, ControlStatus::NotApplicable);
168    }
169
170    #[test]
171    fn multiple_violations_are_all_reported() {
172        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![
173            metric("error_rate_5xx", 0.5, 7.0),
174            metric("service_availability", 99.99, 95.0),
175        ]));
176        assert_eq!(findings.len(), 1);
177        assert_eq!(findings[0].status, ControlStatus::Violated);
178        assert_eq!(findings[0].subjects.len(), 2);
179    }
180
181    #[test]
182    fn error_rate_at_boundary_is_satisfied() {
183        // Exactly 5.0 should NOT trigger (threshold is >5.0)
184        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![metric(
185            "error_rate_5xx",
186            0.5,
187            5.0,
188        )]));
189        assert_eq!(findings.len(), 1);
190        assert_eq!(findings[0].status, ControlStatus::Satisfied);
191    }
192
193    #[test]
194    fn uptime_metric_also_detected() {
195        let findings = DeploymentHealthControl.evaluate(&make_bundle(vec![metric(
196            "system_uptime",
197            99.99,
198            97.0,
199        )]));
200        assert_eq!(findings.len(), 1);
201        assert_eq!(findings[0].status, ControlStatus::Violated);
202    }
203}