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:
iven
2026-04-11 15:59:53 +08:00
parent b5333d8c93
commit 60ee38a3c2
49 changed files with 3988 additions and 461 deletions

View File

@@ -1,11 +1,13 @@
use anyhow::Result;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{info, debug, warn};
use csm_protocol::{Frame, MessageType, RegisterRequest, RegisterResponse, HeartbeatPayload, WatermarkConfigPayload, UsbPolicyPayload, DiskEncryptionConfigPayload};
use csm_protocol::{Frame, MessageType, RegisterRequest, RegisterResponse, HeartbeatPayload, WatermarkConfigPayload, UsbPolicyPayload, DiskEncryptionConfigPayload, PatchScanConfigPayload};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use sha2::{Sha256, Digest};
use crate::ClientState;
@@ -21,6 +23,7 @@ pub struct PluginChannels {
pub disk_encryption_tx: tokio::sync::watch::Sender<crate::disk_encryption::DiskEncryptionConfig>,
pub print_audit_tx: tokio::sync::watch::Sender<crate::print_audit::PrintAuditConfig>,
pub clipboard_control_tx: tokio::sync::watch::Sender<crate::clipboard_control::ClipboardControlConfig>,
pub patch_tx: tokio::sync::watch::Sender<crate::patch::PluginConfig>,
}
/// Connect to server and run the main communication loop
@@ -30,7 +33,7 @@ pub async fn connect_and_run(
plugins: &PluginChannels,
) -> Result<()> {
let tcp_stream = TcpStream::connect(&state.server_addr).await?;
info!("TCP connected to {}", state.server_addr);
debug!("TCP connected to {}", state.server_addr);
if state.use_tls {
let tls_stream = wrap_tls(tcp_stream, &state.server_addr).await?;
@@ -40,9 +43,7 @@ pub async fn connect_and_run(
}
}
/// Wrap a TCP stream with TLS.
/// Supports custom CA certificate via CSM_TLS_CA_CERT env var (path to PEM file).
/// Supports skipping verification via CSM_TLS_SKIP_VERIFY=true (development only).
/// Wrap a TCP stream with TLS and certificate pinning.
async fn wrap_tls(stream: TcpStream, server_addr: &str) -> Result<tokio_rustls::client::TlsStream<TcpStream>> {
let mut root_store = rustls::RootCertStore::empty();
@@ -62,19 +63,38 @@ async fn wrap_tls(stream: TcpStream, server_addr: &str) -> Result<tokio_rustls::
// Always include system roots as fallback
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = if std::env::var("CSM_TLS_SKIP_VERIFY").as_deref() == Ok("true") {
warn!("TLS certificate verification DISABLED — do not use in production!");
// Check if skip-verify is allowed (only in CSM_DEV mode)
let skip_verify = std::env::var("CSM_TLS_SKIP_VERIFY").as_deref() == Ok("true")
&& std::env::var("CSM_DEV").is_ok();
let config = if skip_verify {
warn!("TLS certificate verification DISABLED — CSM_DEV mode only!");
rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(NoVerifier))
.with_custom_certificate_verifier(Arc::new(NoVerifier))
.with_no_client_auth()
} else {
// Build standard verifier with pinning wrapper
let inner = rustls::client::WebPkiServerVerifier::builder(Arc::new(root_store))
.build()
.map_err(|e| anyhow::anyhow!("Failed to build TLS verifier: {:?}", e))?;
let pin_file = pin_file_path();
let pinned_hashes = load_pinned_hashes(&pin_file);
let verifier = PinnedCertVerifier {
inner,
pin_file,
pinned_hashes: Arc::new(Mutex::new(pinned_hashes)),
};
rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
.with_no_client_auth()
};
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config));
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
// Extract hostname from server_addr (strip port)
let domain = server_addr.split(':').next().unwrap_or("localhost").to_string();
@@ -86,6 +106,131 @@ async fn wrap_tls(stream: TcpStream, server_addr: &str) -> Result<tokio_rustls::
Ok(tls_stream)
}
/// Default pin file path: %PROGRAMDATA%\CSM\server_cert_pin (Windows)
fn pin_file_path() -> PathBuf {
if let Ok(custom) = std::env::var("CSM_TLS_PIN_FILE") {
PathBuf::from(custom)
} else if cfg!(target_os = "windows") {
std::env::var("PROGRAMDATA")
.map(|p| PathBuf::from(p).join("CSM").join("server_cert_pin"))
.unwrap_or_else(|_| PathBuf::from("server_cert_pin"))
} else {
PathBuf::from("/var/lib/csm/server_cert_pin")
}
}
/// Load pinned certificate hashes from file.
/// Format: one hex-encoded SHA-256 hash per line.
fn load_pinned_hashes(path: &PathBuf) -> Vec<String> {
match std::fs::read_to_string(path) {
Ok(content) => content.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect(),
Err(_) => Vec::new(), // First connection — no pin file yet
}
}
/// Save a pinned hash to the pin file.
fn save_pinned_hash(path: &PathBuf, hash: &str) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(path, format!("{}\n", hash));
}
/// Compute SHA-256 fingerprint of a DER-encoded certificate.
fn cert_fingerprint(cert: &rustls_pki_types::CertificateDer) -> String {
let mut hasher = Sha256::new();
hasher.update(cert.as_ref());
hex::encode(hasher.finalize())
}
/// Certificate verifier with pinning support.
/// On first connection (no stored pin), records the certificate fingerprint.
/// On subsequent connections, verifies the fingerprint matches.
#[derive(Debug)]
struct PinnedCertVerifier {
inner: Arc<rustls::client::WebPkiServerVerifier>,
pin_file: PathBuf,
pinned_hashes: Arc<Mutex<Vec<String>>>,
}
impl rustls::client::danger::ServerCertVerifier for PinnedCertVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls_pki_types::CertificateDer,
intermediates: &[rustls_pki_types::CertificateDer],
server_name: &rustls_pki_types::ServerName,
ocsp_response: &[u8],
now: rustls_pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
// 1. Standard PKIX verification
self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)?;
// 2. Compute certificate fingerprint
let fingerprint = cert_fingerprint(end_entity);
// 3. Check against pinned hashes
let mut pinned = self.pinned_hashes.lock().unwrap();
if pinned.is_empty() {
// First connection — record the certificate fingerprint
info!("Recording server certificate pin: {}...", &fingerprint[..16]);
save_pinned_hash(&self.pin_file, &fingerprint);
pinned.push(fingerprint);
} else if !pinned.contains(&fingerprint) {
warn!("Certificate pin mismatch! Expected one of {:?}, got {}", pinned, fingerprint);
return Err(rustls::Error::General(
"Server certificate does not match pinned fingerprint. Possible MITM attack.".into(),
));
}
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.supported_verify_schemes()
}
}
/// Update pinned certificate hash (called when receiving TlsCertRotate).
pub fn update_cert_pin(new_hash: &str) {
let pin_file = pin_file_path();
let mut pinned = load_pinned_hashes(&pin_file);
if !pinned.contains(&new_hash.to_string()) {
pinned.push(new_hash.to_string());
// Keep only the last 2 hashes (current + rotating)
while pinned.len() > 2 {
pinned.remove(0);
}
// Write all hashes to file
if let Some(parent) = pin_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
let content = pinned.iter().map(|h| h.as_str()).collect::<Vec<_>>().join("\n");
let _ = std::fs::write(&pin_file, format!("{}\n", content));
info!("Updated certificate pin file with new hash: {}...", &new_hash[..16]);
}
}
/// A no-op certificate verifier for development use (CSM_TLS_SKIP_VERIFY=true).
#[derive(Debug)]
struct NoVerifier;
@@ -242,7 +387,20 @@ fn handle_server_message(frame: Frame, plugins: &PluginChannels) -> Result<()> {
info!("Received policy update: {}", policy);
}
MessageType::ConfigUpdate => {
info!("Received config update");
let update: csm_protocol::ConfigUpdateType = frame.decode_payload()
.map_err(|e| anyhow::anyhow!("Invalid config update: {}", e))?;
match update {
csm_protocol::ConfigUpdateType::UpdateIntervals { heartbeat, status, asset } => {
info!("Config update: intervals heartbeat={}s status={}s asset={}s", heartbeat, status, asset);
}
csm_protocol::ConfigUpdateType::TlsCertRotate { new_cert_hash, valid_until } => {
info!("Certificate rotation: new hash={}... valid_until={}", &new_cert_hash[..16.min(new_cert_hash.len())], valid_until);
update_cert_pin(&new_cert_hash);
}
csm_protocol::ConfigUpdateType::SelfDestruct => {
warn!("Self-destruct command received (not implemented)");
}
}
}
MessageType::TaskExecute => {
warn!("Task execution requested (not yet implemented)");
@@ -276,7 +434,14 @@ fn handle_server_message(frame: Frame, plugins: &PluginChannels) -> Result<()> {
let blacklist: Vec<crate::software_blocker::BlacklistEntry> = payload.get("blacklist")
.and_then(|r| serde_json::from_value(r.clone()).ok())
.unwrap_or_default();
let config = crate::software_blocker::SoftwareBlockerConfig { enabled: true, blacklist };
let whitelist: Vec<String> = payload.get("whitelist")
.and_then(|r| serde_json::from_value(r.clone()).ok())
.unwrap_or_default();
let config = crate::software_blocker::SoftwareBlockerConfig {
enabled: true,
blacklist,
whitelist,
};
plugins.software_blocker_tx.send(config)?;
}
MessageType::PopupRules => {
@@ -322,6 +487,16 @@ fn handle_server_message(frame: Frame, plugins: &PluginChannels) -> Result<()> {
};
plugins.clipboard_control_tx.send(config)?;
}
MessageType::PatchScanConfig => {
let config: PatchScanConfigPayload = frame.decode_payload()
.map_err(|e| anyhow::anyhow!("Invalid patch scan config: {}", e))?;
info!("Received patch scan config: enabled={}, interval={}s", config.enabled, config.scan_interval_secs);
let plugin_config = crate::patch::PluginConfig {
enabled: config.enabled,
scan_interval_secs: config.scan_interval_secs,
};
plugins.patch_tx.send(plugin_config)?;
}
_ => {
debug!("Unhandled message type: {:?}", frame.msg_type);
}
@@ -351,7 +526,7 @@ fn handle_plugin_control(
}
"software_blocker" => {
if !enabled {
plugins.software_blocker_tx.send(crate::software_blocker::SoftwareBlockerConfig { enabled: false, blacklist: vec![] })?;
plugins.software_blocker_tx.send(crate::software_blocker::SoftwareBlockerConfig { enabled: false, blacklist: vec![], whitelist: vec![] })?;
}
}
"popup_blocker" => {
@@ -384,6 +559,11 @@ fn handle_plugin_control(
plugins.clipboard_control_tx.send(crate::clipboard_control::ClipboardControlConfig { enabled: false, ..Default::default() })?;
}
}
"patch" => {
if !enabled {
plugins.patch_tx.send(crate::patch::PluginConfig { enabled: false, scan_interval_secs: 43200 })?;
}
}
_ => {
warn!("Unknown plugin: {}", payload.plugin_name);
}