feat: 添加新插件支持及多项功能改进
- 新增磁盘加密、打印审计和剪贴板管控插件支持 - 优化水印插件显示效果,支持中文及更多Unicode字符 - 改进硬件资产收集逻辑,更准确获取磁盘和显卡信息 - 增强API错误处理,添加详细日志记录 - 完善前端界面,新增插件管理页面 - 修复多个UI问题,优化页面过渡效果 - 添加环境变量覆盖配置功能 - 实现插件状态管理API - 更新文档和变更日志 - 添加安装程序脚本支持
This commit is contained in:
200
crates/client/src/disk_encryption/mod.rs
Normal file
200
crates/client/src/disk_encryption/mod.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
use tracing::{info, debug, warn};
|
||||
use csm_protocol::{Frame, MessageType, DiskEncryptionStatusPayload, DriveEncryptionInfo, DiskEncryptionConfigPayload};
|
||||
|
||||
/// Disk encryption configuration pushed from server
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiskEncryptionConfig {
|
||||
pub enabled: bool,
|
||||
pub report_interval_secs: u64,
|
||||
}
|
||||
|
||||
impl From<DiskEncryptionConfigPayload> for DiskEncryptionConfig {
|
||||
fn from(payload: DiskEncryptionConfigPayload) -> Self {
|
||||
Self {
|
||||
enabled: payload.enabled,
|
||||
report_interval_secs: payload.report_interval_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the disk encryption detection plugin.
|
||||
/// On startup and periodically, collects BitLocker volume status via PowerShell
|
||||
/// and sends results to the server.
|
||||
pub async fn start(
|
||||
mut config_rx: watch::Receiver<DiskEncryptionConfig>,
|
||||
data_tx: tokio::sync::mpsc::Sender<Frame>,
|
||||
device_uid: String,
|
||||
) {
|
||||
info!("Disk encryption plugin started");
|
||||
|
||||
let mut config = DiskEncryptionConfig::default();
|
||||
let default_interval_secs: u64 = 3600;
|
||||
let mut report_interval = tokio::time::interval(Duration::from_secs(default_interval_secs));
|
||||
report_interval.tick().await;
|
||||
|
||||
// Collect and report once on startup if enabled
|
||||
if config.enabled {
|
||||
collect_and_report(&data_tx, &device_uid).await;
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = config_rx.changed() => {
|
||||
if result.is_err() {
|
||||
break;
|
||||
}
|
||||
let new_config = config_rx.borrow_and_update().clone();
|
||||
if new_config.enabled != config.enabled {
|
||||
info!("Disk encryption enabled: {}", new_config.enabled);
|
||||
}
|
||||
config = new_config;
|
||||
if config.enabled {
|
||||
let secs = if config.report_interval_secs > 0 {
|
||||
config.report_interval_secs
|
||||
} else {
|
||||
default_interval_secs
|
||||
};
|
||||
report_interval = tokio::time::interval(Duration::from_secs(secs));
|
||||
report_interval.tick().await;
|
||||
}
|
||||
}
|
||||
_ = report_interval.tick() => {
|
||||
if !config.enabled {
|
||||
continue;
|
||||
}
|
||||
collect_and_report(&data_tx, &device_uid).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_and_report(
|
||||
data_tx: &tokio::sync::mpsc::Sender<Frame>,
|
||||
device_uid: &str,
|
||||
) {
|
||||
let uid = device_uid.to_string();
|
||||
match tokio::task::spawn_blocking(move || collect_bitlocker_status()).await {
|
||||
Ok(drives) => {
|
||||
if drives.is_empty() {
|
||||
debug!("No BitLocker volumes found for device {}", uid);
|
||||
return;
|
||||
}
|
||||
let payload = DiskEncryptionStatusPayload {
|
||||
device_uid: uid,
|
||||
drives,
|
||||
};
|
||||
if let Ok(frame) = Frame::new_json(MessageType::DiskEncryptionStatus, &payload) {
|
||||
if data_tx.send(frame).await.is_err() {
|
||||
warn!("Failed to send disk encryption status: channel closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to collect disk encryption status: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect BitLocker volume information via PowerShell.
|
||||
/// Runs: Get-BitLockerVolume | ConvertTo-Json
|
||||
fn collect_bitlocker_status() -> Vec<DriveEncryptionInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let output = std::process::Command::new("powershell")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
"Get-BitLockerVolume | Select-Object MountPoint, VolumeName, EncryptionMethod, ProtectionStatus, EncryptionPercentage, LockStatus | ConvertTo-Json -Compress",
|
||||
])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let trimmed = stdout.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// PowerShell returns a single object (not array) when there is exactly one volume
|
||||
let json_str = if trimmed.starts_with('{') {
|
||||
format!("[{}]", trimmed)
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
};
|
||||
match serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
|
||||
Ok(entries) => entries.into_iter().map(|e| parse_bitlocker_entry(&e)).collect(),
|
||||
Err(e) => {
|
||||
warn!("Failed to parse BitLocker output: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
warn!("PowerShell BitLocker query failed: {}", stderr);
|
||||
Vec::new()
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to run PowerShell for BitLocker status: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bitlocker_entry(entry: &serde_json::Value) -> DriveEncryptionInfo {
|
||||
let mount_point = entry.get("MountPoint")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Unknown:")
|
||||
.to_string();
|
||||
|
||||
let volume_name = entry.get("VolumeName")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from);
|
||||
|
||||
let encryption_method = entry.get("EncryptionMethod")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty() && *s != "None")
|
||||
.map(String::from);
|
||||
|
||||
let protection_status = match entry.get("ProtectionStatus") {
|
||||
Some(v) if v.is_number() => match v.as_i64().unwrap_or(0) {
|
||||
1 => "On".to_string(),
|
||||
0 => "Off".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
},
|
||||
Some(v) if v.is_string() => v.as_str().unwrap_or("Unknown").to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
let encryption_percentage = entry.get("EncryptionPercentage")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let lock_status = match entry.get("LockStatus") {
|
||||
Some(v) if v.is_number() => match v.as_i64().unwrap_or(0) {
|
||||
1 => "Locked".to_string(),
|
||||
0 => "Unlocked".to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
},
|
||||
Some(v) if v.is_string() => v.as_str().unwrap_or("Unknown").to_string(),
|
||||
_ => "Unknown".to_string(),
|
||||
};
|
||||
|
||||
DriveEncryptionInfo {
|
||||
drive_letter: mount_point,
|
||||
volume_name,
|
||||
encryption_method,
|
||||
protection_status,
|
||||
encryption_percentage,
|
||||
lock_status,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user