mockforge_http/handlers/
compliance_dashboard.rs

1//! HTTP handlers for compliance dashboard
2//!
3//! This module provides REST API endpoints for accessing compliance
4//! dashboard data, gaps, alerts, and control effectiveness metrics.
5
6use axum::{
7    extract::{Path, Query, State},
8    http::StatusCode,
9    response::Json,
10};
11use mockforge_core::security::compliance_dashboard::{
12    AlertType, ComplianceDashboardEngine, ComplianceStandard, GapSeverity,
13};
14use serde::Deserialize;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::{error, info};
19
20/// State for compliance dashboard handlers
21#[derive(Clone)]
22pub struct ComplianceDashboardState {
23    /// Compliance dashboard engine
24    pub engine: Arc<RwLock<ComplianceDashboardEngine>>,
25}
26
27/// Request to add a compliance gap
28#[derive(Debug, Deserialize)]
29pub struct AddGapRequest {
30    /// Gap description
31    pub description: String,
32    /// Severity
33    pub severity: GapSeverity,
34    /// Standard
35    pub standard: ComplianceStandard,
36    /// Control ID (optional)
37    pub control_id: Option<String>,
38    /// Target remediation date (optional)
39    pub target_remediation_date: Option<chrono::DateTime<chrono::Utc>>,
40}
41
42/// Request to add a compliance alert
43#[derive(Debug, Deserialize)]
44pub struct AddAlertRequest {
45    /// Alert type
46    pub alert_type: AlertType,
47    /// Severity
48    pub severity: GapSeverity,
49    /// Message
50    pub message: String,
51    /// Standard (optional)
52    pub standard: Option<ComplianceStandard>,
53    /// Control ID (optional)
54    pub control_id: Option<String>,
55}
56
57/// Request to update gap status
58#[derive(Debug, Deserialize)]
59pub struct UpdateGapStatusRequest {
60    /// New status
61    pub status: mockforge_core::security::compliance_dashboard::GapStatus,
62}
63
64/// Get dashboard data
65///
66/// GET /api/v1/compliance/dashboard
67pub async fn get_dashboard(
68    State(state): State<ComplianceDashboardState>,
69) -> Result<Json<serde_json::Value>, StatusCode> {
70    let engine = state.engine.read().await;
71    let dashboard = engine.get_dashboard_data().await.map_err(|e| {
72        error!("Failed to get dashboard data: {}", e);
73        StatusCode::INTERNAL_SERVER_ERROR
74    })?;
75
76    serde_json::to_value(&dashboard).map(Json).map_err(|e| {
77        error!("Failed to serialize dashboard: {}", e);
78        StatusCode::INTERNAL_SERVER_ERROR
79    })
80}
81
82/// Get all compliance gaps
83///
84/// GET /api/v1/compliance/gaps
85pub async fn get_gaps(
86    State(state): State<ComplianceDashboardState>,
87    Query(params): Query<HashMap<String, String>>,
88) -> Result<Json<serde_json::Value>, StatusCode> {
89    let engine = state.engine.read().await;
90
91    let gaps = if let Some(severity_str) = params.get("severity") {
92        let severity = match severity_str.as_str() {
93            "critical" => GapSeverity::Critical,
94            "high" => GapSeverity::High,
95            "medium" => GapSeverity::Medium,
96            "low" => GapSeverity::Low,
97            _ => return Err(StatusCode::BAD_REQUEST),
98        };
99        engine.get_gaps_by_severity(severity).await.map_err(|e| {
100            error!("Failed to get gaps by severity: {}", e);
101            StatusCode::INTERNAL_SERVER_ERROR
102        })?
103    } else {
104        engine.get_all_gaps().await.map_err(|e| {
105            error!("Failed to get all gaps: {}", e);
106            StatusCode::INTERNAL_SERVER_ERROR
107        })?
108    };
109
110    serde_json::to_value(&gaps).map(Json).map_err(|e| {
111        error!("Failed to serialize gaps: {}", e);
112        StatusCode::INTERNAL_SERVER_ERROR
113    })
114}
115
116/// Add a compliance gap
117///
118/// POST /api/v1/compliance/gaps
119pub async fn add_gap(
120    State(state): State<ComplianceDashboardState>,
121    Json(request): Json<AddGapRequest>,
122) -> Result<Json<serde_json::Value>, StatusCode> {
123    let gap_id = format!(
124        "GAP-{}",
125        uuid::Uuid::new_v4().simple().to_string().get(..8).unwrap_or("00000000")
126    );
127
128    let engine = state.engine.write().await;
129    engine
130        .add_gap(
131            gap_id.clone(),
132            request.description,
133            request.severity,
134            request.standard,
135            request.control_id,
136            request.target_remediation_date,
137        )
138        .await
139        .map_err(|e| {
140            error!("Failed to add gap: {}", e);
141            StatusCode::INTERNAL_SERVER_ERROR
142        })?;
143
144    info!("Compliance gap added: {}", gap_id);
145
146    Ok(Json(serde_json::json!({
147        "gap_id": gap_id,
148        "status": "created"
149    })))
150}
151
152/// Update gap status
153///
154/// PATCH /api/v1/compliance/gaps/{gap_id}/status
155pub async fn update_gap_status(
156    State(state): State<ComplianceDashboardState>,
157    Path(gap_id): Path<String>,
158    Json(request): Json<UpdateGapStatusRequest>,
159) -> Result<Json<serde_json::Value>, StatusCode> {
160    let engine = state.engine.write().await;
161    engine.update_gap_status(&gap_id, request.status).await.map_err(|e| {
162        error!("Failed to update gap status: {}", e);
163        StatusCode::BAD_REQUEST
164    })?;
165
166    info!("Gap status updated: {}", gap_id);
167
168    Ok(Json(serde_json::json!({
169        "gap_id": gap_id,
170        "status": "updated"
171    })))
172}
173
174/// Get all compliance alerts
175///
176/// GET /api/v1/compliance/alerts
177pub async fn get_alerts(
178    State(state): State<ComplianceDashboardState>,
179    Query(params): Query<HashMap<String, String>>,
180) -> Result<Json<serde_json::Value>, StatusCode> {
181    let engine = state.engine.read().await;
182
183    let alerts = if let Some(severity_str) = params.get("severity") {
184        let severity = match severity_str.as_str() {
185            "critical" => GapSeverity::Critical,
186            "high" => GapSeverity::High,
187            "medium" => GapSeverity::Medium,
188            "low" => GapSeverity::Low,
189            _ => return Err(StatusCode::BAD_REQUEST),
190        };
191        engine.get_alerts_by_severity(severity).await.map_err(|e| {
192            error!("Failed to get alerts by severity: {}", e);
193            StatusCode::INTERNAL_SERVER_ERROR
194        })?
195    } else {
196        engine.get_all_alerts().await.map_err(|e| {
197            error!("Failed to get all alerts: {}", e);
198            StatusCode::INTERNAL_SERVER_ERROR
199        })?
200    };
201
202    serde_json::to_value(&alerts).map(Json).map_err(|e| {
203        error!("Failed to serialize alerts: {}", e);
204        StatusCode::INTERNAL_SERVER_ERROR
205    })
206}
207
208/// Add a compliance alert
209///
210/// POST /api/v1/compliance/alerts
211pub async fn add_alert(
212    State(state): State<ComplianceDashboardState>,
213    Json(request): Json<AddAlertRequest>,
214) -> Result<Json<serde_json::Value>, StatusCode> {
215    let alert_id = format!(
216        "ALERT-{}",
217        uuid::Uuid::new_v4().simple().to_string().get(..8).unwrap_or("00000000")
218    );
219
220    let engine = state.engine.write().await;
221    engine
222        .add_alert(
223            alert_id.clone(),
224            request.alert_type,
225            request.severity,
226            request.message,
227            request.standard,
228            request.control_id,
229        )
230        .await
231        .map_err(|e| {
232            error!("Failed to add alert: {}", e);
233            StatusCode::INTERNAL_SERVER_ERROR
234        })?;
235
236    info!("Compliance alert added: {}", alert_id);
237
238    Ok(Json(serde_json::json!({
239        "alert_id": alert_id,
240        "status": "created"
241    })))
242}
243
244/// Get compliance status
245///
246/// GET /api/v1/compliance/status
247pub async fn get_compliance_status(
248    State(state): State<ComplianceDashboardState>,
249) -> Result<Json<serde_json::Value>, StatusCode> {
250    let engine = state.engine.read().await;
251    let dashboard = engine.get_dashboard_data().await.map_err(|e| {
252        error!("Failed to get dashboard data: {}", e);
253        StatusCode::INTERNAL_SERVER_ERROR
254    })?;
255
256    // Extract control effectiveness by area
257    let mut by_area = serde_json::Map::new();
258    for (category, effectiveness) in &dashboard.control_effectiveness {
259        let category_name = match category {
260            mockforge_core::security::compliance_dashboard::ControlCategory::AccessControl => {
261                "access_control"
262            }
263            mockforge_core::security::compliance_dashboard::ControlCategory::Encryption => {
264                "encryption"
265            }
266            mockforge_core::security::compliance_dashboard::ControlCategory::Monitoring => {
267                "monitoring"
268            }
269            mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement => {
270                "change_management"
271            }
272            mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse => {
273                "incident_response"
274            }
275        };
276        by_area.insert(
277            category_name.to_string(),
278            serde_json::Value::Number(effectiveness.effectiveness.into()),
279        );
280    }
281
282    Ok(Json(serde_json::json!({
283        "overall_compliance": dashboard.overall_compliance,
284        "soc2_compliance": dashboard.soc2_compliance,
285        "iso27001_compliance": dashboard.iso27001_compliance,
286        "by_area": by_area,
287        "gaps": dashboard.gaps.total,
288        "remediation_in_progress": dashboard.remediation.in_progress
289    })))
290}
291
292/// Get compliance report
293///
294/// GET /api/v1/compliance/reports/{period}
295pub async fn get_compliance_report(
296    State(state): State<ComplianceDashboardState>,
297    Path(period): Path<String>,
298    Query(params): Query<HashMap<String, String>>,
299) -> Result<Json<serde_json::Value>, StatusCode> {
300    let engine = state.engine.read().await;
301    let dashboard = engine.get_dashboard_data().await.map_err(|e| {
302        error!("Failed to get dashboard data: {}", e);
303        StatusCode::INTERNAL_SERVER_ERROR
304    })?;
305
306    // Extract report period from query or use provided period
307    let report_period = params
308        .get("month")
309        .or_else(|| params.get("period"))
310        .cloned()
311        .unwrap_or_else(|| {
312            // Default to current month
313            let now = chrono::Utc::now();
314            now.format("%Y-%m").to_string()
315        });
316
317    // Get all gaps for recommendations
318    let all_gaps = engine.get_all_gaps().await.map_err(|e| {
319        error!("Failed to get gaps: {}", e);
320        StatusCode::INTERNAL_SERVER_ERROR
321    })?;
322
323    // Generate recommendations based on gaps
324    let mut recommendations = Vec::new();
325    for gap in &all_gaps {
326        match gap.severity {
327            mockforge_core::security::compliance_dashboard::GapSeverity::Critical => {
328                recommendations.push(format!("Urgent: {}", gap.description));
329            }
330            mockforge_core::security::compliance_dashboard::GapSeverity::High => {
331                recommendations.push(format!("High priority: {}", gap.description));
332            }
333            _ => {}
334        }
335    }
336
337    // Add generic recommendations if no gaps
338    if recommendations.is_empty() {
339        if dashboard
340            .control_effectiveness
341            .get(&mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement)
342            .map(|e| e.effectiveness < 95)
343            .unwrap_or(false)
344        {
345            recommendations.push("Enhance change management procedures".to_string());
346        }
347        if dashboard
348            .control_effectiveness
349            .get(&mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse)
350            .map(|e| e.effectiveness < 95)
351            .unwrap_or(false)
352        {
353            recommendations.push("Improve incident response time".to_string());
354        }
355    }
356
357    // Format gaps for report
358    let gaps_summary: Vec<serde_json::Value> = all_gaps
359        .iter()
360        .take(10)
361        .map(|gap| {
362            serde_json::json!({
363                "id": gap.gap_id,
364                "severity": format!("{:?}", gap.severity).to_lowercase(),
365                "remediation_status": format!("{:?}", gap.status).to_lowercase()
366            })
367        })
368        .collect();
369
370    // Format control effectiveness
371    let mut control_effectiveness = serde_json::Map::new();
372    for (category, effectiveness) in &dashboard.control_effectiveness {
373        let category_name = match category {
374            mockforge_core::security::compliance_dashboard::ControlCategory::AccessControl => {
375                "access_control"
376            }
377            mockforge_core::security::compliance_dashboard::ControlCategory::Encryption => {
378                "encryption"
379            }
380            mockforge_core::security::compliance_dashboard::ControlCategory::Monitoring => {
381                "monitoring"
382            }
383            mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement => {
384                "change_management"
385            }
386            mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse => {
387                "incident_response"
388            }
389        };
390        control_effectiveness.insert(
391            category_name.to_string(),
392            serde_json::Value::Number(effectiveness.effectiveness.into()),
393        );
394    }
395
396    Ok(Json(serde_json::json!({
397        "report_period": report_period,
398        "overall_compliance": dashboard.overall_compliance,
399        "control_effectiveness": control_effectiveness,
400        "gaps": gaps_summary,
401        "recommendations": recommendations
402    })))
403}
404
405/// Create compliance dashboard router
406pub fn compliance_dashboard_router(state: ComplianceDashboardState) -> axum::Router {
407    use axum::routing::{get, patch, post};
408
409    axum::Router::new()
410        .route("/dashboard", get(get_dashboard))
411        .route("/status", get(get_compliance_status))
412        .route("/reports/:period", get(get_compliance_report))
413        .route("/gaps", get(get_gaps))
414        .route("/gaps", post(add_gap))
415        .route("/gaps/{gap_id}/status", patch(update_gap_status))
416        .route("/alerts", get(get_alerts))
417        .route("/alerts", post(add_alert))
418        .with_state(state)
419}