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    ComplianceDashboardEngine, ComplianceGap, ComplianceAlert, GapSeverity, AlertType, ComplianceStandard,
13};
14use serde::{Deserialize, Serialize};
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
72        .get_dashboard_data()
73        .await
74        .map_err(|e| {
75            error!("Failed to get dashboard data: {}", e);
76            StatusCode::INTERNAL_SERVER_ERROR
77        })?;
78
79    Ok(Json(serde_json::to_value(&dashboard).unwrap()))
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
100            .get_gaps_by_severity(severity)
101            .await
102            .map_err(|e| {
103                error!("Failed to get gaps by severity: {}", e);
104                StatusCode::INTERNAL_SERVER_ERROR
105            })?
106    } else {
107        engine
108            .get_all_gaps()
109            .await
110            .map_err(|e| {
111                error!("Failed to get all gaps: {}", e);
112                StatusCode::INTERNAL_SERVER_ERROR
113            })?
114    };
115
116    Ok(Json(serde_json::to_value(&gaps).unwrap()))
117}
118
119/// Add a compliance gap
120///
121/// POST /api/v1/compliance/gaps
122pub async fn add_gap(
123    State(state): State<ComplianceDashboardState>,
124    Json(request): Json<AddGapRequest>,
125) -> Result<Json<serde_json::Value>, StatusCode> {
126    let gap_id = format!("GAP-{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
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
162        .update_gap_status(&gap_id, request.status)
163        .await
164        .map_err(|e| {
165            error!("Failed to update gap status: {}", e);
166            StatusCode::BAD_REQUEST
167        })?;
168
169    info!("Gap status updated: {}", gap_id);
170
171    Ok(Json(serde_json::json!({
172        "gap_id": gap_id,
173        "status": "updated"
174    })))
175}
176
177/// Get all compliance alerts
178///
179/// GET /api/v1/compliance/alerts
180pub async fn get_alerts(
181    State(state): State<ComplianceDashboardState>,
182    Query(params): Query<HashMap<String, String>>,
183) -> Result<Json<serde_json::Value>, StatusCode> {
184    let engine = state.engine.read().await;
185
186    let alerts = if let Some(severity_str) = params.get("severity") {
187        let severity = match severity_str.as_str() {
188            "critical" => GapSeverity::Critical,
189            "high" => GapSeverity::High,
190            "medium" => GapSeverity::Medium,
191            "low" => GapSeverity::Low,
192            _ => return Err(StatusCode::BAD_REQUEST),
193        };
194        engine
195            .get_alerts_by_severity(severity)
196            .await
197            .map_err(|e| {
198                error!("Failed to get alerts by severity: {}", e);
199                StatusCode::INTERNAL_SERVER_ERROR
200            })?
201    } else {
202        engine
203            .get_all_alerts()
204            .await
205            .map_err(|e| {
206                error!("Failed to get all alerts: {}", e);
207                StatusCode::INTERNAL_SERVER_ERROR
208            })?
209    };
210
211    Ok(Json(serde_json::to_value(&alerts).unwrap()))
212}
213
214/// Add a compliance alert
215///
216/// POST /api/v1/compliance/alerts
217pub async fn add_alert(
218    State(state): State<ComplianceDashboardState>,
219    Json(request): Json<AddAlertRequest>,
220) -> Result<Json<serde_json::Value>, StatusCode> {
221    let alert_id = format!("ALERT-{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
222
223    let engine = state.engine.write().await;
224    engine
225        .add_alert(
226            alert_id.clone(),
227            request.alert_type,
228            request.severity,
229            request.message,
230            request.standard,
231            request.control_id,
232        )
233        .await
234        .map_err(|e| {
235            error!("Failed to add alert: {}", e);
236            StatusCode::INTERNAL_SERVER_ERROR
237        })?;
238
239    info!("Compliance alert added: {}", alert_id);
240
241    Ok(Json(serde_json::json!({
242        "alert_id": alert_id,
243        "status": "created"
244    })))
245}
246
247/// Create compliance dashboard router
248pub fn compliance_dashboard_router(state: ComplianceDashboardState) -> axum::Router {
249    use axum::routing::{get, post, patch};
250
251    axum::Router::new()
252        .route("/dashboard", get(get_dashboard))
253        .route("/gaps", get(get_gaps))
254        .route("/gaps", post(add_gap))
255        .route("/gaps/{gap_id}/status", patch(update_gap_status))
256        .route("/alerts", get(get_alerts))
257        .route("/alerts", post(add_alert))
258        .with_state(state)
259}