1use 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#[derive(Clone)]
22pub struct ComplianceDashboardState {
23 pub engine: Arc<RwLock<ComplianceDashboardEngine>>,
25}
26
27#[derive(Debug, Deserialize)]
29pub struct AddGapRequest {
30 pub description: String,
32 pub severity: GapSeverity,
34 pub standard: ComplianceStandard,
36 pub control_id: Option<String>,
38 pub target_remediation_date: Option<chrono::DateTime<chrono::Utc>>,
40}
41
42#[derive(Debug, Deserialize)]
44pub struct AddAlertRequest {
45 pub alert_type: AlertType,
47 pub severity: GapSeverity,
49 pub message: String,
51 pub standard: Option<ComplianceStandard>,
53 pub control_id: Option<String>,
55}
56
57#[derive(Debug, Deserialize)]
59pub struct UpdateGapStatusRequest {
60 pub status: mockforge_core::security::compliance_dashboard::GapStatus,
62}
63
64pub 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 Ok(Json(serde_json::to_value(&dashboard).unwrap()))
77}
78
79pub async fn get_gaps(
83 State(state): State<ComplianceDashboardState>,
84 Query(params): Query<HashMap<String, String>>,
85) -> Result<Json<serde_json::Value>, StatusCode> {
86 let engine = state.engine.read().await;
87
88 let gaps = if let Some(severity_str) = params.get("severity") {
89 let severity = match severity_str.as_str() {
90 "critical" => GapSeverity::Critical,
91 "high" => GapSeverity::High,
92 "medium" => GapSeverity::Medium,
93 "low" => GapSeverity::Low,
94 _ => return Err(StatusCode::BAD_REQUEST),
95 };
96 engine.get_gaps_by_severity(severity).await.map_err(|e| {
97 error!("Failed to get gaps by severity: {}", e);
98 StatusCode::INTERNAL_SERVER_ERROR
99 })?
100 } else {
101 engine.get_all_gaps().await.map_err(|e| {
102 error!("Failed to get all gaps: {}", e);
103 StatusCode::INTERNAL_SERVER_ERROR
104 })?
105 };
106
107 Ok(Json(serde_json::to_value(&gaps).unwrap()))
108}
109
110pub async fn add_gap(
114 State(state): State<ComplianceDashboardState>,
115 Json(request): Json<AddGapRequest>,
116) -> Result<Json<serde_json::Value>, StatusCode> {
117 let gap_id = format!("GAP-{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
118
119 let engine = state.engine.write().await;
120 engine
121 .add_gap(
122 gap_id.clone(),
123 request.description,
124 request.severity,
125 request.standard,
126 request.control_id,
127 request.target_remediation_date,
128 )
129 .await
130 .map_err(|e| {
131 error!("Failed to add gap: {}", e);
132 StatusCode::INTERNAL_SERVER_ERROR
133 })?;
134
135 info!("Compliance gap added: {}", gap_id);
136
137 Ok(Json(serde_json::json!({
138 "gap_id": gap_id,
139 "status": "created"
140 })))
141}
142
143pub async fn update_gap_status(
147 State(state): State<ComplianceDashboardState>,
148 Path(gap_id): Path<String>,
149 Json(request): Json<UpdateGapStatusRequest>,
150) -> Result<Json<serde_json::Value>, StatusCode> {
151 let engine = state.engine.write().await;
152 engine.update_gap_status(&gap_id, request.status).await.map_err(|e| {
153 error!("Failed to update gap status: {}", e);
154 StatusCode::BAD_REQUEST
155 })?;
156
157 info!("Gap status updated: {}", gap_id);
158
159 Ok(Json(serde_json::json!({
160 "gap_id": gap_id,
161 "status": "updated"
162 })))
163}
164
165pub async fn get_alerts(
169 State(state): State<ComplianceDashboardState>,
170 Query(params): Query<HashMap<String, String>>,
171) -> Result<Json<serde_json::Value>, StatusCode> {
172 let engine = state.engine.read().await;
173
174 let alerts = if let Some(severity_str) = params.get("severity") {
175 let severity = match severity_str.as_str() {
176 "critical" => GapSeverity::Critical,
177 "high" => GapSeverity::High,
178 "medium" => GapSeverity::Medium,
179 "low" => GapSeverity::Low,
180 _ => return Err(StatusCode::BAD_REQUEST),
181 };
182 engine.get_alerts_by_severity(severity).await.map_err(|e| {
183 error!("Failed to get alerts by severity: {}", e);
184 StatusCode::INTERNAL_SERVER_ERROR
185 })?
186 } else {
187 engine.get_all_alerts().await.map_err(|e| {
188 error!("Failed to get all alerts: {}", e);
189 StatusCode::INTERNAL_SERVER_ERROR
190 })?
191 };
192
193 Ok(Json(serde_json::to_value(&alerts).unwrap()))
194}
195
196pub async fn add_alert(
200 State(state): State<ComplianceDashboardState>,
201 Json(request): Json<AddAlertRequest>,
202) -> Result<Json<serde_json::Value>, StatusCode> {
203 let alert_id = format!("ALERT-{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
204
205 let engine = state.engine.write().await;
206 engine
207 .add_alert(
208 alert_id.clone(),
209 request.alert_type,
210 request.severity,
211 request.message,
212 request.standard,
213 request.control_id,
214 )
215 .await
216 .map_err(|e| {
217 error!("Failed to add alert: {}", e);
218 StatusCode::INTERNAL_SERVER_ERROR
219 })?;
220
221 info!("Compliance alert added: {}", alert_id);
222
223 Ok(Json(serde_json::json!({
224 "alert_id": alert_id,
225 "status": "created"
226 })))
227}
228
229pub async fn get_compliance_status(
233 State(state): State<ComplianceDashboardState>,
234) -> Result<Json<serde_json::Value>, StatusCode> {
235 let engine = state.engine.read().await;
236 let dashboard = engine.get_dashboard_data().await.map_err(|e| {
237 error!("Failed to get dashboard data: {}", e);
238 StatusCode::INTERNAL_SERVER_ERROR
239 })?;
240
241 let mut by_area = serde_json::Map::new();
243 for (category, effectiveness) in &dashboard.control_effectiveness {
244 let category_name = match category {
245 mockforge_core::security::compliance_dashboard::ControlCategory::AccessControl => "access_control",
246 mockforge_core::security::compliance_dashboard::ControlCategory::Encryption => "encryption",
247 mockforge_core::security::compliance_dashboard::ControlCategory::Monitoring => "monitoring",
248 mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement => "change_management",
249 mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse => "incident_response",
250 };
251 by_area.insert(category_name.to_string(), serde_json::Value::Number(effectiveness.effectiveness.into()));
252 }
253
254 Ok(Json(serde_json::json!({
255 "overall_compliance": dashboard.overall_compliance,
256 "soc2_compliance": dashboard.soc2_compliance,
257 "iso27001_compliance": dashboard.iso27001_compliance,
258 "by_area": by_area,
259 "gaps": dashboard.gaps.total,
260 "remediation_in_progress": dashboard.remediation.in_progress
261 })))
262}
263
264pub async fn get_compliance_report(
268 State(state): State<ComplianceDashboardState>,
269 Path(period): Path<String>,
270 Query(params): Query<HashMap<String, String>>,
271) -> Result<Json<serde_json::Value>, StatusCode> {
272 let engine = state.engine.read().await;
273 let dashboard = engine.get_dashboard_data().await.map_err(|e| {
274 error!("Failed to get dashboard data: {}", e);
275 StatusCode::INTERNAL_SERVER_ERROR
276 })?;
277
278 let report_period = params.get("month")
280 .or_else(|| params.get("period"))
281 .cloned()
282 .unwrap_or_else(|| {
283 let now = chrono::Utc::now();
285 now.format("%Y-%m").to_string()
286 });
287
288 let all_gaps = engine.get_all_gaps().await.map_err(|e| {
290 error!("Failed to get gaps: {}", e);
291 StatusCode::INTERNAL_SERVER_ERROR
292 })?;
293
294 let mut recommendations = Vec::new();
296 for gap in &all_gaps {
297 match gap.severity {
298 mockforge_core::security::compliance_dashboard::GapSeverity::Critical => {
299 recommendations.push(format!("Urgent: {}", gap.description));
300 }
301 mockforge_core::security::compliance_dashboard::GapSeverity::High => {
302 recommendations.push(format!("High priority: {}", gap.description));
303 }
304 _ => {}
305 }
306 }
307
308 if recommendations.is_empty() {
310 if dashboard.control_effectiveness.get(&mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement)
311 .map(|e| e.effectiveness < 95)
312 .unwrap_or(false) {
313 recommendations.push("Enhance change management procedures".to_string());
314 }
315 if dashboard.control_effectiveness.get(&mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse)
316 .map(|e| e.effectiveness < 95)
317 .unwrap_or(false) {
318 recommendations.push("Improve incident response time".to_string());
319 }
320 }
321
322 let gaps_summary: Vec<serde_json::Value> = all_gaps.iter().take(10).map(|gap| {
324 serde_json::json!({
325 "id": gap.gap_id,
326 "severity": format!("{:?}", gap.severity).to_lowercase(),
327 "remediation_status": format!("{:?}", gap.status).to_lowercase()
328 })
329 }).collect();
330
331 let mut control_effectiveness = serde_json::Map::new();
333 for (category, effectiveness) in &dashboard.control_effectiveness {
334 let category_name = match category {
335 mockforge_core::security::compliance_dashboard::ControlCategory::AccessControl => "access_control",
336 mockforge_core::security::compliance_dashboard::ControlCategory::Encryption => "encryption",
337 mockforge_core::security::compliance_dashboard::ControlCategory::Monitoring => "monitoring",
338 mockforge_core::security::compliance_dashboard::ControlCategory::ChangeManagement => "change_management",
339 mockforge_core::security::compliance_dashboard::ControlCategory::IncidentResponse => "incident_response",
340 };
341 control_effectiveness.insert(category_name.to_string(), serde_json::Value::Number(effectiveness.effectiveness.into()));
342 }
343
344 Ok(Json(serde_json::json!({
345 "report_period": report_period,
346 "overall_compliance": dashboard.overall_compliance,
347 "control_effectiveness": control_effectiveness,
348 "gaps": gaps_summary,
349 "recommendations": recommendations
350 })))
351}
352
353pub fn compliance_dashboard_router(state: ComplianceDashboardState) -> axum::Router {
355 use axum::routing::{get, patch, post};
356
357 axum::Router::new()
358 .route("/dashboard", get(get_dashboard))
359 .route("/status", get(get_compliance_status))
360 .route("/reports/:period", get(get_compliance_report))
361 .route("/gaps", get(get_gaps))
362 .route("/gaps", post(add_gap))
363 .route("/gaps/{gap_id}/status", patch(update_gap_status))
364 .route("/alerts", get(get_alerts))
365 .route("/alerts", post(add_alert))
366 .with_state(state)
367}