feat(protocol): 添加补丁管理和行为指标协议类型 feat(client): 实现补丁管理插件采集功能 feat(server): 添加补丁管理和异常检测API feat(database): 新增补丁状态和异常检测相关表 feat(web): 添加补丁管理和异常检测前端页面 fix(security): 增强输入验证和防注入保护 refactor(auth): 重构认证检查逻辑 perf(service): 优化Windows服务恢复策略 style: 统一健康评分显示样式 docs: 更新知识库文档
252 lines
9.3 KiB
Rust
252 lines
9.3 KiB
Rust
use axum::{extract::{State, Path, Query, Json}, http::StatusCode};
|
|
use serde::Deserialize;
|
|
use sqlx::Row;
|
|
|
|
use crate::AppState;
|
|
use super::ApiResponse;
|
|
use crate::tcp::push_to_targets;
|
|
use csm_protocol::{MessageType, UsbPolicyPayload, UsbDeviceRule};
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UsbEventListParams {
|
|
pub device_uid: Option<String>,
|
|
pub event_type: Option<String>,
|
|
pub page: Option<u32>,
|
|
pub page_size: Option<u32>,
|
|
}
|
|
|
|
pub async fn list_events(
|
|
State(state): State<AppState>,
|
|
Query(params): Query<UsbEventListParams>,
|
|
) -> Json<ApiResponse<serde_json::Value>> {
|
|
let limit = params.page_size.unwrap_or(20).min(100);
|
|
let offset = params.page.unwrap_or(1).saturating_sub(1) * limit;
|
|
|
|
// Normalize empty strings to None
|
|
let device_uid = params.device_uid.as_deref().filter(|s| !s.is_empty()).map(String::from);
|
|
let event_type = params.event_type.as_deref().filter(|s| !s.is_empty()).map(String::from);
|
|
|
|
let rows = sqlx::query(
|
|
"SELECT id, device_uid, vendor_id, product_id, serial_number, device_name, event_type, event_time
|
|
FROM usb_events WHERE 1=1
|
|
AND (? IS NULL OR device_uid = ?)
|
|
AND (? IS NULL OR event_type = ?)
|
|
ORDER BY event_time DESC LIMIT ? OFFSET ?"
|
|
)
|
|
.bind(&device_uid).bind(&device_uid)
|
|
.bind(&event_type).bind(&event_type)
|
|
.bind(limit).bind(offset)
|
|
.fetch_all(&state.db)
|
|
.await;
|
|
|
|
match rows {
|
|
Ok(records) => {
|
|
let items: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
|
|
"id": r.get::<i64, _>("id"),
|
|
"device_uid": r.get::<String, _>("device_uid"),
|
|
"vendor_id": r.get::<Option<String>, _>("vendor_id"),
|
|
"product_id": r.get::<Option<String>, _>("product_id"),
|
|
"serial_number": r.get::<Option<String>, _>("serial_number"),
|
|
"device_name": r.get::<Option<String>, _>("device_name"),
|
|
"event_type": r.get::<String, _>("event_type"),
|
|
"event_time": r.get::<String, _>("event_time"),
|
|
})).collect();
|
|
Json(ApiResponse::ok(serde_json::json!({
|
|
"events": items,
|
|
"page": params.page.unwrap_or(1),
|
|
"page_size": limit,
|
|
})))
|
|
}
|
|
Err(e) => Json(ApiResponse::internal_error("query usb events", e)),
|
|
}
|
|
}
|
|
|
|
pub async fn list_policies(
|
|
State(state): State<AppState>,
|
|
) -> Json<ApiResponse<serde_json::Value>> {
|
|
let rows = sqlx::query(
|
|
"SELECT id, name, policy_type, target_group, rules, enabled, created_at, updated_at
|
|
FROM usb_policies ORDER BY created_at DESC LIMIT 500"
|
|
)
|
|
.fetch_all(&state.db)
|
|
.await;
|
|
|
|
match rows {
|
|
Ok(records) => {
|
|
let items: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
|
|
"id": r.get::<i64, _>("id"),
|
|
"name": r.get::<String, _>("name"),
|
|
"policy_type": r.get::<String, _>("policy_type"),
|
|
"target_group": r.get::<Option<String>, _>("target_group"),
|
|
"rules": r.get::<String, _>("rules"),
|
|
"enabled": r.get::<i32, _>("enabled"),
|
|
"created_at": r.get::<String, _>("created_at"),
|
|
"updated_at": r.get::<String, _>("updated_at"),
|
|
})).collect();
|
|
Json(ApiResponse::ok(serde_json::json!({
|
|
"policies": items,
|
|
})))
|
|
}
|
|
Err(e) => Json(ApiResponse::internal_error("query usb policies", e)),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreatePolicyRequest {
|
|
pub name: String,
|
|
pub policy_type: String,
|
|
pub target_group: Option<String>,
|
|
pub rules: String,
|
|
pub enabled: Option<i32>,
|
|
}
|
|
|
|
pub async fn create_policy(
|
|
State(state): State<AppState>,
|
|
Json(body): Json<CreatePolicyRequest>,
|
|
) -> (StatusCode, Json<ApiResponse<serde_json::Value>>) {
|
|
let enabled = body.enabled.unwrap_or(1);
|
|
|
|
// Input validation
|
|
if body.name.trim().is_empty() || body.name.len() > 100 {
|
|
return (StatusCode::BAD_REQUEST, Json(ApiResponse::error("name must be 1-100 chars")));
|
|
}
|
|
|
|
let result = sqlx::query(
|
|
"INSERT INTO usb_policies (name, policy_type, target_group, rules, enabled) VALUES (?, ?, ?, ?, ?)"
|
|
)
|
|
.bind(&body.name)
|
|
.bind(&body.policy_type)
|
|
.bind(&body.target_group)
|
|
.bind(&body.rules)
|
|
.bind(enabled)
|
|
.execute(&state.db)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(r) => {
|
|
let new_id = r.last_insert_rowid();
|
|
// Push USB policy to matching online clients
|
|
if enabled == 1 {
|
|
let payload = build_usb_policy_payload(&body.policy_type, true, &body.rules);
|
|
let target_group = body.target_group.as_deref();
|
|
push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &payload, "group", target_group).await;
|
|
}
|
|
(StatusCode::CREATED, Json(ApiResponse::ok(serde_json::json!({
|
|
"id": new_id,
|
|
}))))
|
|
}
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse::internal_error("create usb policy", e))),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdatePolicyRequest {
|
|
pub name: Option<String>,
|
|
pub policy_type: Option<String>,
|
|
pub target_group: Option<String>,
|
|
pub rules: Option<String>,
|
|
pub enabled: Option<i32>,
|
|
}
|
|
|
|
pub async fn update_policy(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
Json(body): Json<UpdatePolicyRequest>,
|
|
) -> Json<ApiResponse<serde_json::Value>> {
|
|
// Fetch existing policy
|
|
let existing = sqlx::query("SELECT * FROM usb_policies WHERE id = ?")
|
|
.bind(id)
|
|
.fetch_optional(&state.db)
|
|
.await;
|
|
|
|
let existing = match existing {
|
|
Ok(Some(row)) => row,
|
|
Ok(None) => return Json(ApiResponse::error("Policy not found")),
|
|
Err(e) => return Json(ApiResponse::internal_error("query usb policy", e)),
|
|
};
|
|
|
|
let name = body.name.unwrap_or_else(|| existing.get::<String, _>("name"));
|
|
let policy_type = body.policy_type.unwrap_or_else(|| existing.get::<String, _>("policy_type"));
|
|
let target_group = body.target_group.or_else(|| existing.get::<Option<String>, _>("target_group"));
|
|
let rules = body.rules.unwrap_or_else(|| existing.get::<String, _>("rules"));
|
|
let enabled = body.enabled.unwrap_or_else(|| existing.get::<i32, _>("enabled"));
|
|
|
|
let result = sqlx::query(
|
|
"UPDATE usb_policies SET name = ?, policy_type = ?, target_group = ?, rules = ?, enabled = ?, updated_at = datetime('now') WHERE id = ?"
|
|
)
|
|
.bind(&name)
|
|
.bind(&policy_type)
|
|
.bind(&target_group)
|
|
.bind(&rules)
|
|
.bind(enabled)
|
|
.bind(id)
|
|
.execute(&state.db)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
// Push updated USB policy to matching online clients
|
|
let payload = build_usb_policy_payload(&policy_type, enabled == 1, &rules);
|
|
let target_group = target_group.as_deref();
|
|
push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &payload, "group", target_group).await;
|
|
Json(ApiResponse::ok(serde_json::json!({"updated": true})))
|
|
}
|
|
Err(e) => Json(ApiResponse::internal_error("update usb policy", e)),
|
|
}
|
|
}
|
|
|
|
pub async fn delete_policy(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
) -> Json<ApiResponse<serde_json::Value>> {
|
|
// Fetch existing policy to get target info for push
|
|
let existing = sqlx::query("SELECT target_group FROM usb_policies WHERE id = ?")
|
|
.bind(id)
|
|
.fetch_optional(&state.db)
|
|
.await;
|
|
|
|
let target_group = match existing {
|
|
Ok(Some(row)) => row.get::<Option<String>, _>("target_group"),
|
|
_ => return Json(ApiResponse::error("Policy not found")),
|
|
};
|
|
|
|
let result = sqlx::query("DELETE FROM usb_policies WHERE id = ?")
|
|
.bind(id)
|
|
.execute(&state.db)
|
|
.await;
|
|
|
|
match result {
|
|
Ok(r) => {
|
|
if r.rows_affected() > 0 {
|
|
// Push disabled policy to clients
|
|
let disabled = UsbPolicyPayload {
|
|
policy_type: String::new(),
|
|
enabled: false,
|
|
rules: vec![],
|
|
};
|
|
push_to_targets(&state.db, &state.clients, MessageType::UsbPolicyUpdate, &disabled, "group", target_group.as_deref()).await;
|
|
Json(ApiResponse::ok(serde_json::json!({"deleted": true})))
|
|
} else {
|
|
Json(ApiResponse::error("Policy not found"))
|
|
}
|
|
}
|
|
Err(e) => Json(ApiResponse::internal_error("delete usb policy", e)),
|
|
}
|
|
}
|
|
|
|
/// Build a UsbPolicyPayload from raw policy fields
|
|
fn build_usb_policy_payload(policy_type: &str, enabled: bool, rules_json: &str) -> UsbPolicyPayload {
|
|
let raw_rules: Vec<serde_json::Value> = serde_json::from_str(rules_json).unwrap_or_default();
|
|
let rules: Vec<UsbDeviceRule> = raw_rules.iter().map(|r| UsbDeviceRule {
|
|
vendor_id: r.get("vendor_id").and_then(|v| v.as_str().map(String::from)),
|
|
product_id: r.get("product_id").and_then(|v| v.as_str().map(String::from)),
|
|
serial: r.get("serial").and_then(|v| v.as_str().map(String::from)),
|
|
device_name: r.get("device_name").and_then(|v| v.as_str().map(String::from)),
|
|
}).collect();
|
|
UsbPolicyPayload {
|
|
policy_type: policy_type.to_string(),
|
|
enabled,
|
|
rules,
|
|
}
|
|
}
|