Files
csm/crates/client/src/disk_encryption/mod.rs
iven b5333d8c93 feat: 添加新插件支持及多项功能改进
- 新增磁盘加密、打印审计和剪贴板管控插件支持
- 优化水印插件显示效果,支持中文及更多Unicode字符
- 改进硬件资产收集逻辑,更准确获取磁盘和显卡信息
- 增强API错误处理,添加详细日志记录
- 完善前端界面,新增插件管理页面
- 修复多个UI问题,优化页面过渡效果
- 添加环境变量覆盖配置功能
- 实现插件状态管理API
- 更新文档和变更日志
- 添加安装程序脚本支持
2026-04-10 22:21:05 +08:00

201 lines
6.8 KiB
Rust

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,
}
}