use axum::{ extract::Path, http::StatusCode, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use std::sync::Arc; use tokio::sync::RwLock; use tracing::warn; use super::manager::{ProtectionConfig, ProtectionManager, ProtectionTool, ScanResult, ToolStatus}; static PROTECTION_MANAGER: OnceLock>> = OnceLock::new(); fn get_manager() -> &'static Arc> { PROTECTION_MANAGER.get_or_init(|| { Arc::new(RwLock::new(ProtectionManager::new(ProtectionConfig::default()))) }) } #[derive(Debug, Serialize)] struct ApiResponse { success: bool, data: Option, error: Option, } impl ApiResponse { fn success(data: T) -> Self { Self { success: true, data: Some(data), error: None, } } } impl ApiResponse<()> { fn error(message: impl Into) -> Self { Self { success: false, data: None, error: Some(message.into()), } } } #[derive(Debug, Serialize)] struct AllStatusResponse { tools: Vec, } #[derive(Debug, Deserialize)] struct AutoToggleRequest { enabled: bool, setting: Option, } #[derive(Debug, Serialize)] struct ActionResponse { success: bool, message: String, } pub fn configure_protection_routes() -> Router { Router::new() .route("/api/v1/security/protection/status", get(get_all_status)) .route( "/api/v1/security/protection/:tool/status", get(get_tool_status), ) .route( "/api/v1/security/protection/:tool/install", post(install_tool), ) .route( "/api/v1/security/protection/:tool/uninstall", post(uninstall_tool), ) .route("/api/v1/security/protection/:tool/start", post(start_service)) .route("/api/v1/security/protection/:tool/stop", post(stop_service)) .route( "/api/v1/security/protection/:tool/enable", post(enable_service), ) .route( "/api/v1/security/protection/:tool/disable", post(disable_service), ) .route("/api/v1/security/protection/:tool/run", post(run_scan)) .route("/api/v1/security/protection/:tool/report", get(get_report)) .route( "/api/v1/security/protection/:tool/update", post(update_definitions), ) .route("/api/v1/security/protection/:tool/auto", post(toggle_auto)) .route( "/api/v1/security/protection/clamav/quarantine", get(get_quarantine), ) .route( "/api/v1/security/protection/clamav/quarantine/:id", post(remove_from_quarantine), ) } fn parse_tool(tool_name: &str) -> Result>)> { ProtectionTool::from_str(tool_name).ok_or_else(|| { ( StatusCode::BAD_REQUEST, Json(ApiResponse::error(format!("Unknown tool: {tool_name}"))), ) }) } async fn get_all_status() -> Result>, (StatusCode, Json>)> { let manager = get_manager().read().await; let status_map = manager.get_all_status().await; let tools: Vec = status_map.into_values().collect(); Ok(Json(ApiResponse::success(AllStatusResponse { tools }))) } async fn get_tool_status( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.check_tool_status(tool).await { Ok(status) => Ok(Json(ApiResponse::success(status))), Err(e) => { warn!(error = %e, "Failed to get tool status"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to get tool status")))) } } } async fn install_tool( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.install_tool(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} installed successfully"), }))), Err(e) => { warn!(error = %e, "Failed to install tool"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to install tool")))) } } } async fn uninstall_tool( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; Err(( StatusCode::NOT_IMPLEMENTED, Json(ApiResponse::error(format!( "Uninstall not yet implemented for {tool}" ))), )) } async fn start_service( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.start_service(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} service started"), }))), Err(e) => { warn!(error = %e, "Failed to start service"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to start service")))) } } } async fn stop_service( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.stop_service(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} service stopped"), }))), Err(e) => { warn!(error = %e, "Failed to stop service"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to stop service")))) } } } async fn enable_service( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.enable_service(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} service enabled"), }))), Err(e) => { warn!(error = %e, "Failed to enable service"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to enable service")))) } } } async fn disable_service( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.disable_service(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} service disabled"), }))), Err(e) => { warn!(error = %e, "Failed to disable service"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to disable service")))) } } } async fn run_scan( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.run_scan(tool).await { Ok(result) => Ok(Json(ApiResponse::success(result))), Err(e) => { warn!(error = %e, "Failed to run scan"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to run scan")))) } } } async fn get_report( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.get_report(tool).await { Ok(report) => Ok(Json(ApiResponse::success(report))), Err(e) => { warn!(error = %e, "Failed to get report"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to get report")))) } } } async fn update_definitions( Path(tool_name): Path, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().read().await; match manager.update_definitions(tool).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} definitions updated"), }))), Err(e) => { warn!(error = %e, "Failed to update definitions"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to update definitions")))) } } } async fn toggle_auto( Path(tool_name): Path, Json(request): Json, ) -> Result>, (StatusCode, Json>)> { let tool = parse_tool(&tool_name)?; let manager = get_manager().write().await; let setting = request.setting.as_deref().unwrap_or("update"); let result = match setting { "update" => manager.set_auto_update(tool, request.enabled).await, "remediate" => manager.set_auto_remediate(tool, request.enabled).await, _ => manager.set_auto_update(tool, request.enabled).await, }; match result { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("{tool} {setting} set to {}", request.enabled), }))), Err(e) => { warn!(error = %e, "Failed to toggle auto setting"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to toggle auto setting")))) } } } async fn get_quarantine() -> Result>>, (StatusCode, Json>)> { match super::lmd::list_quarantined().await { Ok(files) => Ok(Json(ApiResponse::success(files))), Err(e) => { warn!(error = %e, "Failed to get quarantine list"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to get quarantine list")))) } } } async fn remove_from_quarantine( Path(file_id): Path, ) -> Result>, (StatusCode, Json>)> { match super::lmd::restore_file(&file_id).await { Ok(()) => Ok(Json(ApiResponse::success(ActionResponse { success: true, message: format!("File {file_id} restored from quarantine"), }))), Err(e) => { warn!(error = %e, "Failed to restore file from quarantine"); Err((StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::error("Failed to restore file from quarantine")))) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_tool_valid() { assert!(parse_tool("lynis").is_ok()); assert!(parse_tool("rkhunter").is_ok()); assert!(parse_tool("clamav").is_ok()); assert!(parse_tool("LYNIS").is_ok()); } #[test] fn test_parse_tool_invalid() { assert!(parse_tool("unknown").is_err()); assert!(parse_tool("").is_err()); } #[test] fn test_api_response_success() { let response = ApiResponse::success("test data"); assert!(response.success); assert!(response.data.is_some()); assert!(response.error.is_none()); } #[test] fn test_api_response_error() { let response: ApiResponse<()> = ApiResponse::error("test error".to_string()); assert!(!response.success); assert!(response.data.is_none()); assert!(response.error.is_some()); } #[test] fn test_action_response() { let response = ActionResponse { success: true, message: "Test message".to_string(), }; assert!(response.success); assert_eq!(response.message, "Test message"); } #[test] fn test_auto_toggle_request_deserialize() { let json = r#"{"enabled": true, "setting": "update"}"#; let request: AutoToggleRequest = serde_json::from_str(json).unwrap(); assert!(request.enabled); assert_eq!(request.setting, Some("update".to_string())); } #[test] fn test_auto_toggle_request_minimal() { let json = r#"{"enabled": false}"#; let request: AutoToggleRequest = serde_json::from_str(json).unwrap(); assert!(!request.enabled); assert!(request.setting.is_none()); } }