- 新增磁盘加密、打印审计和剪贴板管控插件支持 - 优化水印插件显示效果,支持中文及更多Unicode字符 - 改进硬件资产收集逻辑,更准确获取磁盘和显卡信息 - 增强API错误处理,添加详细日志记录 - 完善前端界面,新增插件管理页面 - 修复多个UI问题,优化页面过渡效果 - 添加环境变量覆盖配置功能 - 实现插件状态管理API - 更新文档和变更日志 - 添加安装程序脚本支持
201 lines
6.8 KiB
Rust
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,
|
|
}
|
|
}
|