1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DashboardOverview {
11 pub total_keys: usize,
13 pub healthy_keys: usize,
15 pub warning_keys: usize,
17 pub urgent_keys: usize,
19 pub avg_ttl_percent: u32,
21 pub system_health: String,
23}
24
25impl DashboardOverview {
26 pub fn new() -> Self {
28 Self {
29 total_keys: 0,
30 healthy_keys: 0,
31 warning_keys: 0,
32 urgent_keys: 0,
33 avg_ttl_percent: 0,
34 system_health: "healthy".to_string(),
35 }
36 }
37
38 pub fn recalculate_health(&mut self) {
40 if self.urgent_keys > 0 {
41 self.system_health = "critical".to_string();
42 } else if self.warning_keys > 0 {
43 self.system_health = "warning".to_string();
44 } else {
45 self.system_health = "healthy".to_string();
46 }
47 }
48}
49
50impl Default for DashboardOverview {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct KeyStatusCard {
59 pub key_id: String,
61 pub current_version: u16,
63 pub ttl_percent: u32,
65 pub status: String,
67 pub last_rotation: Option<DateTime<Utc>>,
69 pub next_rotation: Option<DateTime<Utc>>,
71 pub versions_count: usize,
73 pub urgency_score: u32,
75 pub recommended_action: String,
77}
78
79impl KeyStatusCard {
80 pub fn new(key_id: impl Into<String>, current_version: u16, ttl_percent: u32) -> Self {
82 let (status, urgency_score, recommended_action) = match ttl_percent {
83 0..=40 => ("healthy".to_string(), 10, "Monitor key health".to_string()),
84 41..=70 => ("healthy".to_string(), 30, "Monitor key health".to_string()),
85 71..=85 => ("warning".to_string(), 60, "Prepare for upcoming rotation".to_string()),
86 86..=99 => ("urgent".to_string(), 85, "Trigger manual rotation".to_string()),
87 _ => ("overdue".to_string(), 100, "CRITICAL: Rotate immediately".to_string()),
88 };
89
90 Self {
91 key_id: key_id.into(),
92 current_version,
93 ttl_percent,
94 status,
95 last_rotation: None,
96 next_rotation: None,
97 versions_count: 1,
98 urgency_score,
99 recommended_action,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct RotationMetricsPoint {
107 pub timestamp: DateTime<Utc>,
109 pub rotations_total: u64,
111 pub rotations_manual: u64,
113 pub rotations_auto: u64,
115 pub rotation_duration_avg_ms: u64,
117 pub success_rate_percent: u32,
119}
120
121impl RotationMetricsPoint {
122 pub fn new(timestamp: DateTime<Utc>) -> Self {
124 Self {
125 timestamp,
126 rotations_total: 0,
127 rotations_manual: 0,
128 rotations_auto: 0,
129 rotation_duration_avg_ms: 0,
130 success_rate_percent: 100,
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct MetricsTimeSeries {
138 pub period: String,
140 pub data_points: Vec<RotationMetricsPoint>,
142}
143
144impl MetricsTimeSeries {
145 pub fn new(period: impl Into<String>) -> Self {
147 Self {
148 period: period.into(),
149 data_points: Vec::new(),
150 }
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156pub enum ComplianceStatus {
157 Compliant,
159 Partial,
161 NonCompliant,
163}
164
165impl std::fmt::Display for ComplianceStatus {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 match self {
168 Self::Compliant => write!(f, "compliant"),
169 Self::Partial => write!(f, "partial"),
170 Self::NonCompliant => write!(f, "non_compliant"),
171 }
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ComplianceRequirement {
178 pub name: String,
180 pub met: bool,
182 pub details: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ComplianceDashboard {
189 pub hipaa: ComplianceStatus,
191 pub pci_dss: ComplianceStatus,
193 pub gdpr: ComplianceStatus,
195 pub soc2: ComplianceStatus,
197 pub overall: ComplianceStatus,
199}
200
201impl ComplianceDashboard {
202 pub fn new() -> Self {
204 Self {
205 hipaa: ComplianceStatus::Compliant,
206 pci_dss: ComplianceStatus::Compliant,
207 gdpr: ComplianceStatus::Compliant,
208 soc2: ComplianceStatus::Compliant,
209 overall: ComplianceStatus::Compliant,
210 }
211 }
212
213 pub fn recalculate_overall(&mut self) {
215 if matches!(self.hipaa, ComplianceStatus::NonCompliant)
217 || matches!(self.pci_dss, ComplianceStatus::NonCompliant)
218 || matches!(self.gdpr, ComplianceStatus::NonCompliant)
219 || matches!(self.soc2, ComplianceStatus::NonCompliant)
220 {
221 self.overall = ComplianceStatus::NonCompliant;
222 } else if matches!(self.hipaa, ComplianceStatus::Partial)
223 || matches!(self.pci_dss, ComplianceStatus::Partial)
224 || matches!(self.gdpr, ComplianceStatus::Partial)
225 || matches!(self.soc2, ComplianceStatus::Partial)
226 {
227 self.overall = ComplianceStatus::Partial;
228 } else {
229 self.overall = ComplianceStatus::Compliant;
230 }
231 }
232}
233
234impl Default for ComplianceDashboard {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
242#[serde(rename_all = "lowercase")]
243pub enum AlertSeverity {
244 Info,
246 Warning,
248 Error,
250 Critical,
252}
253
254impl std::fmt::Display for AlertSeverity {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 match self {
257 Self::Info => write!(f, "info"),
258 Self::Warning => write!(f, "warning"),
259 Self::Error => write!(f, "error"),
260 Self::Critical => write!(f, "critical"),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct Alert {
268 pub id: String,
270 pub alert_type: String,
272 pub severity: AlertSeverity,
274 pub key_id: Option<String>,
276 pub timestamp: DateTime<Utc>,
278 pub message: String,
280 pub action: Option<String>,
282 pub acknowledged: bool,
284}
285
286impl Alert {
287 pub fn new(
289 alert_type: impl Into<String>,
290 severity: AlertSeverity,
291 message: impl Into<String>,
292 ) -> Self {
293 Self {
294 id: uuid::Uuid::new_v4().to_string(),
295 alert_type: alert_type.into(),
296 severity,
297 key_id: None,
298 timestamp: Utc::now(),
299 message: message.into(),
300 action: None,
301 acknowledged: false,
302 }
303 }
304
305 pub fn with_key_id(mut self, key_id: impl Into<String>) -> Self {
307 self.key_id = Some(key_id.into());
308 self
309 }
310
311 pub fn with_action(mut self, action: impl Into<String>) -> Self {
313 self.action = Some(action.into());
314 self
315 }
316
317 pub fn acknowledge(&mut self) {
319 self.acknowledged = true;
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct AlertsWidget {
326 pub active_alerts: Vec<Alert>,
328 pub historical_alerts: Vec<Alert>,
330 pub unacknowledged_count: usize,
332}
333
334impl AlertsWidget {
335 pub fn new() -> Self {
337 Self {
338 active_alerts: Vec::new(),
339 historical_alerts: Vec::new(),
340 unacknowledged_count: 0,
341 }
342 }
343
344 pub fn add_alert(&mut self, alert: Alert) {
346 if !alert.acknowledged {
347 self.unacknowledged_count += 1;
348 }
349 self.active_alerts.push(alert);
350 }
351
352 pub fn clear_old_alerts(&mut self, days: i64) {
354 let cutoff = Utc::now() - chrono::Duration::days(days);
355 self.active_alerts.retain(|a| a.timestamp > cutoff || !a.acknowledged);
356 }
357}
358
359impl Default for AlertsWidget {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct TrendAnalysis {
368 pub rotation_frequency: String,
370 pub rotation_duration: String,
372 pub failure_rate: String,
374 pub compliance_status: String,
376}
377
378impl TrendAnalysis {
379 pub fn new() -> Self {
381 Self {
382 rotation_frequency: "stable".to_string(),
383 rotation_duration: "stable".to_string(),
384 failure_rate: "stable".to_string(),
385 compliance_status: "stable".to_string(),
386 }
387 }
388}
389
390impl Default for TrendAnalysis {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
398pub enum ExportFormat {
399 Json,
401 Csv,
403 Pdf,
405}
406
407impl std::fmt::Display for ExportFormat {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 match self {
410 Self::Json => write!(f, "json"),
411 Self::Csv => write!(f, "csv"),
412 Self::Pdf => write!(f, "pdf"),
413 }
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct DashboardSnapshot {
420 pub snapshot_time: DateTime<Utc>,
422 pub overview: DashboardOverview,
424 pub key_cards: Vec<KeyStatusCard>,
426 pub active_alerts_count: usize,
428 pub compliance: ComplianceDashboard,
430}
431
432impl DashboardSnapshot {
433 pub fn new(
435 overview: DashboardOverview,
436 key_cards: Vec<KeyStatusCard>,
437 compliance: ComplianceDashboard,
438 active_alerts_count: usize,
439 ) -> Self {
440 Self {
441 snapshot_time: Utc::now(),
442 overview,
443 key_cards,
444 active_alerts_count,
445 compliance,
446 }
447 }
448
449 pub fn urgency_summary(&self) -> (usize, usize, usize, usize) {
451 let mut healthy = 0;
452 let mut warning = 0;
453 let mut urgent = 0;
454 let mut overdue = 0;
455
456 for card in &self.key_cards {
457 match card.status.as_str() {
458 "healthy" => healthy += 1,
459 "warning" => warning += 1,
460 "urgent" => urgent += 1,
461 "overdue" => overdue += 1,
462 _ => {},
463 }
464 }
465
466 (healthy, warning, urgent, overdue)
467 }
468
469 pub fn average_urgency_score(&self) -> u32 {
471 if self.key_cards.is_empty() {
472 return 0;
473 }
474
475 let total: u32 = self.key_cards.iter().map(|c| c.urgency_score).sum();
476 total / self.key_cards.len() as u32
477 }
478}
479
480#[derive(Debug, Clone)]
482pub struct AlertFilter {
483 pub severity: Option<AlertSeverity>,
485 pub alert_type: Option<String>,
487 pub key_id: Option<String>,
489 pub acknowledged: Option<bool>,
491}
492
493impl AlertFilter {
494 pub fn new() -> Self {
496 Self {
497 severity: None,
498 alert_type: None,
499 key_id: None,
500 acknowledged: None,
501 }
502 }
503
504 pub fn apply(&self, alerts: &[Alert]) -> Vec<Alert> {
506 alerts
507 .iter()
508 .filter(|alert| {
509 if let Some(sev) = self.severity {
510 if alert.severity != sev {
511 return false;
512 }
513 }
514 if let Some(ref alert_type) = self.alert_type {
515 if alert.alert_type != *alert_type {
516 return false;
517 }
518 }
519 if let Some(ref key_id) = self.key_id {
520 if alert.key_id.as_ref() != Some(key_id) {
521 return false;
522 }
523 }
524 if let Some(ack) = self.acknowledged {
525 if alert.acknowledged != ack {
526 return false;
527 }
528 }
529 true
530 })
531 .cloned()
532 .collect()
533 }
534}
535
536impl Default for AlertFilter {
537 fn default() -> Self {
538 Self::new()
539 }
540}
541
542#[derive(Debug, Clone)]
544pub struct ComplianceChecker {
545 pub framework: String,
547 pub required_ttl_days: u32,
549 pub required_check_interval_hours: u32,
551}
552
553impl ComplianceChecker {
554 pub fn hipaa() -> Self {
556 Self {
557 framework: "HIPAA".to_string(),
558 required_ttl_days: 365,
559 required_check_interval_hours: 24,
560 }
561 }
562
563 pub fn pci_dss() -> Self {
565 Self {
566 framework: "PCI-DSS".to_string(),
567 required_ttl_days: 365,
568 required_check_interval_hours: 24,
569 }
570 }
571
572 pub fn gdpr() -> Self {
574 Self {
575 framework: "GDPR".to_string(),
576 required_ttl_days: 90,
577 required_check_interval_hours: 24,
578 }
579 }
580
581 pub fn soc2() -> Self {
583 Self {
584 framework: "SOC 2".to_string(),
585 required_ttl_days: 365,
586 required_check_interval_hours: 24,
587 }
588 }
589
590 pub fn check_compliance(&self, card: &KeyStatusCard) -> bool {
592 !matches!(card.status.as_str(), "urgent" | "overdue")
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_dashboard_overview_creation() {
603 let overview = DashboardOverview::new();
604 assert_eq!(overview.total_keys, 0);
605 assert_eq!(overview.system_health, "healthy");
606 }
607
608 #[test]
609 fn test_dashboard_overview_health_critical() {
610 let mut overview = DashboardOverview {
611 total_keys: 1,
612 healthy_keys: 0,
613 warning_keys: 0,
614 urgent_keys: 1,
615 avg_ttl_percent: 95,
616 system_health: "healthy".to_string(),
617 };
618 overview.recalculate_health();
619 assert_eq!(overview.system_health, "critical");
620 }
621
622 #[test]
623 fn test_key_status_card_healthy() {
624 let card = KeyStatusCard::new("primary", 1, 50);
625 assert_eq!(card.status, "healthy");
626 assert_eq!(card.urgency_score, 30);
627 }
628
629 #[test]
630 fn test_key_status_card_urgent() {
631 let card = KeyStatusCard::new("primary", 1, 90);
632 assert_eq!(card.status, "urgent");
633 assert_eq!(card.urgency_score, 85);
634 }
635
636 #[test]
637 fn test_key_status_card_overdue() {
638 let card = KeyStatusCard::new("primary", 1, 105);
639 assert_eq!(card.status, "overdue");
640 assert_eq!(card.urgency_score, 100);
641 }
642
643 #[test]
644 fn test_rotation_metrics_point_creation() {
645 let now = Utc::now();
646 let point = RotationMetricsPoint::new(now);
647 assert_eq!(point.timestamp, now);
648 assert_eq!(point.rotations_total, 0);
649 assert_eq!(point.success_rate_percent, 100);
650 }
651
652 #[test]
653 fn test_metrics_time_series_creation() {
654 let series = MetricsTimeSeries::new("30d");
655 assert_eq!(series.period, "30d");
656 assert_eq!(series.data_points.len(), 0);
657 }
658
659 #[test]
660 fn test_compliance_dashboard_creation() {
661 let dashboard = ComplianceDashboard::new();
662 assert_eq!(dashboard.overall, ComplianceStatus::Compliant);
663 }
664
665 #[test]
666 fn test_compliance_dashboard_recalculate_non_compliant() {
667 let mut dashboard = ComplianceDashboard::new();
668 dashboard.hipaa = ComplianceStatus::NonCompliant;
669 dashboard.recalculate_overall();
670 assert_eq!(dashboard.overall, ComplianceStatus::NonCompliant);
671 }
672
673 #[test]
674 fn test_compliance_dashboard_recalculate_partial() {
675 let mut dashboard = ComplianceDashboard::new();
676 dashboard.gdpr = ComplianceStatus::Partial;
677 dashboard.recalculate_overall();
678 assert_eq!(dashboard.overall, ComplianceStatus::Partial);
679 }
680
681 #[test]
682 fn test_alert_creation() {
683 let alert = Alert::new("rotation_failed", AlertSeverity::Critical, "Rotation failed");
684 assert_eq!(alert.alert_type, "rotation_failed");
685 assert_eq!(alert.severity, AlertSeverity::Critical);
686 assert!(!alert.acknowledged);
687 }
688
689 #[test]
690 fn test_alert_with_key_id() {
691 let alert = Alert::new("rotation_failed", AlertSeverity::Error, "Rotation failed")
692 .with_key_id("primary");
693 assert_eq!(alert.key_id, Some("primary".to_string()));
694 }
695
696 #[test]
697 fn test_alert_acknowledge() {
698 let mut alert = Alert::new("rotation_failed", AlertSeverity::Error, "Rotation failed");
699 assert!(!alert.acknowledged);
700 alert.acknowledge();
701 assert!(alert.acknowledged);
702 }
703
704 #[test]
705 fn test_alerts_widget_creation() {
706 let widget = AlertsWidget::new();
707 assert_eq!(widget.active_alerts.len(), 0);
708 assert_eq!(widget.unacknowledged_count, 0);
709 }
710
711 #[test]
712 fn test_alerts_widget_add_alert() {
713 let mut widget = AlertsWidget::new();
714 let alert = Alert::new("rotation_failed", AlertSeverity::Error, "Failed");
715 widget.add_alert(alert);
716 assert_eq!(widget.active_alerts.len(), 1);
717 assert_eq!(widget.unacknowledged_count, 1);
718 }
719
720 #[test]
721 fn test_trend_analysis_creation() {
722 let trend = TrendAnalysis::new();
723 assert_eq!(trend.rotation_frequency, "stable");
724 assert_eq!(trend.compliance_status, "stable");
725 }
726
727 #[test]
728 fn test_dashboard_snapshot_creation() {
729 let overview = DashboardOverview::new();
730 let key_cards = vec![
731 KeyStatusCard::new("primary", 1, 50),
732 KeyStatusCard::new("secondary", 1, 80),
733 ];
734 let compliance = ComplianceDashboard::new();
735 let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
736 assert_eq!(snapshot.key_cards.len(), 2);
737 }
738
739 #[test]
740 fn test_dashboard_snapshot_urgency_summary() {
741 let overview = DashboardOverview::new();
742 let key_cards = vec![
743 KeyStatusCard::new("primary", 1, 30), KeyStatusCard::new("secondary", 1, 50), KeyStatusCard::new("tertiary", 1, 75), KeyStatusCard::new("quaternary", 1, 90), ];
748 let compliance = ComplianceDashboard::new();
749 let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
750 let (healthy, warning, urgent, overdue) = snapshot.urgency_summary();
751 assert_eq!(healthy, 2);
752 assert_eq!(warning, 1);
753 assert_eq!(urgent, 1);
754 assert_eq!(overdue, 0);
755 }
756
757 #[test]
758 fn test_dashboard_snapshot_average_urgency_score() {
759 let overview = DashboardOverview::new();
760 let key_cards = vec![
761 KeyStatusCard::new("primary", 1, 50), KeyStatusCard::new("secondary", 1, 80), ];
764 let compliance = ComplianceDashboard::new();
765 let snapshot = DashboardSnapshot::new(overview, key_cards, compliance, 0);
766 assert_eq!(snapshot.average_urgency_score(), 45);
767 }
768
769 #[test]
770 fn test_alert_filter_by_severity() {
771 let mut filter = AlertFilter::new();
772 filter.severity = Some(AlertSeverity::Critical);
773
774 let alerts = vec![
775 Alert::new("rotation_failed", AlertSeverity::Critical, "Failed"),
776 Alert::new("rotation_warning", AlertSeverity::Warning, "Warning"),
777 ];
778
779 let filtered = filter.apply(&alerts);
780 assert_eq!(filtered.len(), 1);
781 assert_eq!(filtered[0].severity, AlertSeverity::Critical);
782 }
783
784 #[test]
785 fn test_alert_filter_by_acknowledged() {
786 let mut filter = AlertFilter::new();
787 filter.acknowledged = Some(false);
788
789 let alert1 = Alert::new("rotation_failed", AlertSeverity::Error, "Failed");
790 let mut alert2 = Alert::new("rotation_warning", AlertSeverity::Warning, "Warning");
791 alert2.acknowledge();
792
793 let alerts = vec![alert1, alert2];
794 let filtered = filter.apply(&alerts);
795 assert_eq!(filtered.len(), 1);
796 assert!(!filtered[0].acknowledged);
797 }
798
799 #[test]
800 fn test_compliance_checker_hipaa() {
801 let checker = ComplianceChecker::hipaa();
802 assert_eq!(checker.framework, "HIPAA");
803 assert_eq!(checker.required_ttl_days, 365);
804 }
805
806 #[test]
807 fn test_compliance_checker_gdpr() {
808 let checker = ComplianceChecker::gdpr();
809 assert_eq!(checker.framework, "GDPR");
810 assert_eq!(checker.required_ttl_days, 90);
811 }
812
813 #[test]
814 fn test_compliance_checker_validates_key_status() {
815 let checker = ComplianceChecker::hipaa();
816 let healthy_card = KeyStatusCard::new("primary", 1, 50);
817 let urgent_card = KeyStatusCard::new("primary", 1, 90);
818
819 assert!(checker.check_compliance(&healthy_card));
820 assert!(!checker.check_compliance(&urgent_card));
821 }
822
823 #[test]
824 fn test_export_format_display() {
825 assert_eq!(ExportFormat::Json.to_string(), "json");
826 assert_eq!(ExportFormat::Csv.to_string(), "csv");
827 assert_eq!(ExportFormat::Pdf.to_string(), "pdf");
828 }
829}