feat: 新增补丁管理和异常检测插件及相关功能
feat(protocol): 添加补丁管理和行为指标协议类型 feat(client): 实现补丁管理插件采集功能 feat(server): 添加补丁管理和异常检测API feat(database): 新增补丁状态和异常检测相关表 feat(web): 添加补丁管理和异常检测前端页面 fix(security): 增强输入验证和防注入保护 refactor(auth): 重构认证检查逻辑 perf(service): 优化Windows服务恢复策略 style: 统一健康评分显示样式 docs: 更新知识库文档
This commit is contained in:
@@ -4,6 +4,28 @@ use sqlx::Row;
|
||||
use crate::AppState;
|
||||
use super::{ApiResponse, Pagination};
|
||||
|
||||
/// GET /api/devices/:uid/health-score
|
||||
pub async fn get_health_score(
|
||||
State(state): State<AppState>,
|
||||
Path(uid): Path<String>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
match crate::health::get_device_score(&state.db, &uid).await {
|
||||
Ok(Some(score)) => Json(ApiResponse::ok(score)),
|
||||
Ok(None) => Json(ApiResponse::error("No health score available")),
|
||||
Err(e) => Json(ApiResponse::internal_error("health score", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/dashboard/health-overview
|
||||
pub async fn health_overview(
|
||||
State(state): State<AppState>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
match crate::health::get_health_overview(&state.db).await {
|
||||
Ok(overview) => Json(ApiResponse::ok(overview)),
|
||||
Err(e) => Json(ApiResponse::internal_error("health overview", e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DeviceListParams {
|
||||
pub status: Option<String>,
|
||||
@@ -26,6 +48,10 @@ pub struct DeviceRow {
|
||||
pub last_heartbeat: Option<String>,
|
||||
pub registered_at: Option<String>,
|
||||
pub group_name: Option<String>,
|
||||
#[sqlx(default)]
|
||||
pub health_score: Option<i32>,
|
||||
#[sqlx(default)]
|
||||
pub health_level: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
@@ -41,13 +67,16 @@ pub async fn list(
|
||||
let search = params.search.as_deref().filter(|s| !s.is_empty()).map(String::from);
|
||||
|
||||
let devices = sqlx::query_as::<_, DeviceRow>(
|
||||
"SELECT id, device_uid, hostname, ip_address, mac_address, os_version, client_version,
|
||||
status, last_heartbeat, registered_at, group_name
|
||||
FROM devices WHERE 1=1
|
||||
AND (? IS NULL OR status = ?)
|
||||
AND (? IS NULL OR group_name = ?)
|
||||
AND (? IS NULL OR hostname LIKE '%' || ? || '%' OR ip_address LIKE '%' || ? || '%')
|
||||
ORDER BY registered_at DESC LIMIT ? OFFSET ?"
|
||||
"SELECT d.id, d.device_uid, d.hostname, d.ip_address, d.mac_address, d.os_version, d.client_version,
|
||||
d.status, d.last_heartbeat, d.registered_at, d.group_name,
|
||||
h.score as health_score, h.level as health_level
|
||||
FROM devices d
|
||||
LEFT JOIN device_health_scores h ON h.device_uid = d.device_uid
|
||||
WHERE 1=1
|
||||
AND (? IS NULL OR d.status = ?)
|
||||
AND (? IS NULL OR d.group_name = ?)
|
||||
AND (? IS NULL OR d.hostname LIKE '%' || ? || '%' OR d.ip_address LIKE '%' || ? || '%')
|
||||
ORDER BY d.registered_at DESC LIMIT ? OFFSET ?"
|
||||
)
|
||||
.bind(&status).bind(&status)
|
||||
.bind(&group).bind(&group)
|
||||
@@ -187,16 +216,6 @@ pub async fn remove(
|
||||
State(state): State<AppState>,
|
||||
Path(uid): Path<String>,
|
||||
) -> Json<ApiResponse<()>> {
|
||||
// If client is connected, send self-destruct command
|
||||
let frame = csm_protocol::Frame::new_json(
|
||||
csm_protocol::MessageType::ConfigUpdate,
|
||||
&serde_json::json!({"type": "SelfDestruct"}),
|
||||
).ok();
|
||||
|
||||
if let Some(frame) = frame {
|
||||
state.clients.send_to(&uid, frame.encode()).await;
|
||||
}
|
||||
|
||||
// Delete device and all associated data in a transaction
|
||||
let mut tx = match state.db.begin().await {
|
||||
Ok(tx) => tx,
|
||||
@@ -224,6 +243,8 @@ pub async fn remove(
|
||||
// Delete plugin-related data
|
||||
let cleanup_tables = [
|
||||
"hardware_assets",
|
||||
"software_assets",
|
||||
"asset_changes",
|
||||
"usb_events",
|
||||
"usb_file_operations",
|
||||
"usage_daily",
|
||||
@@ -231,8 +252,20 @@ pub async fn remove(
|
||||
"software_violations",
|
||||
"web_access_log",
|
||||
"popup_block_stats",
|
||||
"disk_encryption_status",
|
||||
"disk_encryption_alerts",
|
||||
"print_events",
|
||||
"clipboard_violations",
|
||||
"behavior_metrics",
|
||||
"anomaly_alerts",
|
||||
"device_health_scores",
|
||||
"patch_status",
|
||||
];
|
||||
for table in &cleanup_tables {
|
||||
// Safety: table names are hardcoded constants above, not user input.
|
||||
// Parameterized ? is used for device_uid.
|
||||
debug_assert!(table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
|
||||
"BUG: table name contains unexpected characters: {}", table);
|
||||
if let Err(e) = sqlx::query(&format!("DELETE FROM {} WHERE device_uid = ?", table))
|
||||
.bind(&uid)
|
||||
.execute(&mut *tx)
|
||||
@@ -253,6 +286,17 @@ pub async fn remove(
|
||||
if let Err(e) = tx.commit().await {
|
||||
return Json(ApiResponse::internal_error("commit device deletion", e));
|
||||
}
|
||||
|
||||
// Send self-destruct command AFTER successful commit
|
||||
let frame = csm_protocol::Frame::new_json(
|
||||
csm_protocol::MessageType::ConfigUpdate,
|
||||
&serde_json::json!({"type": "SelfDestruct"}),
|
||||
).ok();
|
||||
|
||||
if let Some(frame) = frame {
|
||||
state.clients.send_to(&uid, frame.encode()).await;
|
||||
}
|
||||
|
||||
state.clients.unregister(&uid).await;
|
||||
tracing::info!(device_uid = %uid, "Device and all associated data deleted");
|
||||
Json(ApiResponse::ok(()))
|
||||
|
||||
Reference in New Issue
Block a user