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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user