From 2ca543fd667d6994e8936e57b91e3b25d3cb4cb9 Mon Sep 17 00:00:00 2001 From: Warren Date: Sun, 21 Jun 2026 11:29:04 +0800 Subject: [PATCH] Add SSH Structured Logging (Phase 1-5): ssh_audit_log.rs module with JSON tracing Features: - SshAuditLog: Structured audit logging using tracing crate - 16 audit event types (connection/auth/command/file/port_forward) - JSON output format via tracing-subscriber json layer - 10 unit tests for all audit events Files: - markbase-core/src/ssh_server/ssh_audit_log.rs (289 lines) - markbase-core/Cargo.toml (tracing + json layer) - markbase-core/src/ssh_server/mod.rs (export module) Tests: 298 passed (+10) --- Cargo.lock | 14 + markbase-core/Cargo.toml | 3 +- markbase-core/src/ssh_server/mod.rs | 1 + markbase-core/src/ssh_server/ssh_audit_log.rs | 395 ++++++++++++++++++ 4 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 markbase-core/src/ssh_server/ssh_audit_log.rs diff --git a/Cargo.lock b/Cargo.lock index bc0ca13..c21c811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2919,6 +2919,7 @@ dependencies = [ "tokio-postgres", "tokio-util", "toml", + "tracing", "tracing-subscriber", "unrar", "ureq", @@ -5868,6 +5869,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -5878,12 +5889,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/markbase-core/Cargo.toml b/markbase-core/Cargo.toml index f93f477..593e045 100644 --- a/markbase-core/Cargo.toml +++ b/markbase-core/Cargo.toml @@ -76,7 +76,8 @@ smb2 = { path = "../vendor/smb2" } # Pure-Rust SMB2/3 client library with pipel # === SMB/CIFS Server (Phase 2) — optional (vendored) === smb-server = { path = "../vendor/smb-server", optional = true, default-features = false } async-trait = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } [features] default = [] # 默认不启用可选格式 diff --git a/markbase-core/src/ssh_server/mod.rs b/markbase-core/src/ssh_server/mod.rs index f8b716c..838a1e7 100644 --- a/markbase-core/src/ssh_server/mod.rs +++ b/markbase-core/src/ssh_server/mod.rs @@ -21,6 +21,7 @@ pub mod rsync_handler; pub mod scp_handler; pub mod server; pub mod sftp_handler; +pub mod ssh_audit_log; pub mod ssh_config; pub mod ssh_security_config; pub mod sshbuf; diff --git a/markbase-core/src/ssh_server/ssh_audit_log.rs b/markbase-core/src/ssh_server/ssh_audit_log.rs new file mode 100644 index 0000000..f4cd768 --- /dev/null +++ b/markbase-core/src/ssh_server/ssh_audit_log.rs @@ -0,0 +1,395 @@ +use serde::Serialize; +use std::net::IpAddr; +use std::time::SystemTime; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::fmt::FormatEvent; +use tracing_subscriber::fmt::format::{Format, Json}; +use tracing_subscriber::layer::Layer; + +pub struct SshAuditLog; + +#[derive(Debug, Clone, Serialize)] +pub struct SshAuditEvent { + pub timestamp: String, + pub event_type: SshEventType, + pub session_id: Option, + pub client_ip: Option, + pub user: Option, + pub channel_id: Option, + pub command: Option, + pub file_path: Option, + pub port: Option, + pub success: bool, + pub error_message: Option, + pub duration_ms: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq)] +pub enum SshEventType { + ConnectionStart, + ConnectionEnd, + AuthAttempt, + AuthSuccess, + AuthFailure, + ChannelOpen, + ChannelClose, + CommandExec, + FileUpload, + FileDownload, + PortForwardRequest, + PortForwardBind, + PortForwardConnect, + HostKeyVerify, + RateLimitHit, + SecurityViolation, +} + +impl std::fmt::Display for SshEventType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SshEventType::ConnectionStart => write!(f, "ConnectionStart"), + SshEventType::ConnectionEnd => write!(f, "ConnectionEnd"), + SshEventType::AuthAttempt => write!(f, "AuthAttempt"), + SshEventType::AuthSuccess => write!(f, "AuthSuccess"), + SshEventType::AuthFailure => write!(f, "AuthFailure"), + SshEventType::ChannelOpen => write!(f, "ChannelOpen"), + SshEventType::ChannelClose => write!(f, "ChannelClose"), + SshEventType::CommandExec => write!(f, "CommandExec"), + SshEventType::FileUpload => write!(f, "FileUpload"), + SshEventType::FileDownload => write!(f, "FileDownload"), + SshEventType::PortForwardRequest => write!(f, "PortForwardRequest"), + SshEventType::PortForwardBind => write!(f, "PortForwardBind"), + SshEventType::PortForwardConnect => write!(f, "PortForwardConnect"), + SshEventType::HostKeyVerify => write!(f, "HostKeyVerify"), + SshEventType::RateLimitHit => write!(f, "RateLimitHit"), + SshEventType::SecurityViolation => write!(f, "SecurityViolation"), + } + } +} + +impl SshAuditLog { + pub fn log_connection_start(client_ip: IpAddr) { + tracing::info!( + event_type = "ConnectionStart", + client_ip = %client_ip, + success = true, + "SSH connection started" + ); + } + + pub fn log_connection_end(client_ip: IpAddr, session_id: &str, duration_ms: u64) { + tracing::info!( + event_type = "ConnectionEnd", + client_ip = %client_ip, + session_id = %session_id, + duration_ms = duration_ms, + success = true, + "SSH connection ended" + ); + } + + pub fn log_auth_attempt(client_ip: IpAddr, user: &str, method: &str) { + tracing::info!( + event_type = "AuthAttempt", + client_ip = %client_ip, + user = %user, + auth_method = %method, + "SSH authentication attempt" + ); + } + + pub fn log_auth_success(client_ip: IpAddr, user: &str, method: &str) { + tracing::info!( + event_type = "AuthSuccess", + client_ip = %client_ip, + user = %user, + auth_method = %method, + success = true, + "SSH authentication successful" + ); + } + + pub fn log_auth_failure(client_ip: IpAddr, user: &str, method: &str, reason: &str) { + tracing::warn!( + event_type = "AuthFailure", + client_ip = %client_ip, + user = %user, + auth_method = %method, + success = false, + error_message = %reason, + "SSH authentication failed" + ); + } + + pub fn log_channel_open(session_id: &str, channel_id: u32, channel_type: &str) { + tracing::info!( + event_type = "ChannelOpen", + session_id = %session_id, + channel_id = channel_id, + channel_type = %channel_type, + success = true, + "SSH channel opened" + ); + } + + pub fn log_channel_close(session_id: &str, channel_id: u32) { + tracing::info!( + event_type = "ChannelClose", + session_id = %session_id, + channel_id = channel_id, + success = true, + "SSH channel closed" + ); + } + + pub fn log_command_exec(session_id: &str, user: &str, channel_id: u32, command: &str, success: bool) { + tracing::info!( + event_type = "CommandExec", + session_id = %session_id, + user = %user, + channel_id = channel_id, + command = %command, + success = success, + "SSH command executed" + ); + } + + pub fn log_file_upload(session_id: &str, user: &str, file_path: &str, size_bytes: u64, success: bool) { + tracing::info!( + event_type = "FileUpload", + session_id = %session_id, + user = %user, + file_path = %file_path, + size_bytes = size_bytes, + success = success, + "SSH file uploaded" + ); + } + + pub fn log_file_download(session_id: &str, user: &str, file_path: &str, size_bytes: u64, success: bool) { + tracing::info!( + event_type = "FileDownload", + session_id = %session_id, + user = %user, + file_path = %file_path, + size_bytes = size_bytes, + success = success, + "SSH file downloaded" + ); + } + + pub fn log_port_forward_request(client_ip: IpAddr, user: &str, bind_port: u16, success: bool) { + tracing::info!( + event_type = "PortForwardRequest", + client_ip = %client_ip, + user = %user, + port = bind_port, + success = success, + "SSH port forward requested" + ); + } + + pub fn log_port_forward_bind(bind_port: u16, success: bool) { + tracing::info!( + event_type = "PortForwardBind", + port = bind_port, + success = success, + "SSH port forward bound" + ); + } + + pub fn log_port_forward_connect(session_id: &str, target_host: &str, target_port: u16, success: bool) { + tracing::info!( + event_type = "PortForwardConnect", + session_id = %session_id, + target_host = %target_host, + port = target_port, + success = success, + "SSH port forward connection" + ); + } + + pub fn log_host_key_verify(client_ip: IpAddr, fingerprint: &str, accepted: bool) { + tracing::info!( + event_type = "HostKeyVerify", + client_ip = %client_ip, + fingerprint = %fingerprint, + success = accepted, + "SSH host key verification" + ); + } + + pub fn log_rate_limit_hit(client_ip: IpAddr, limit_type: &str, current_count: u32) { + tracing::warn!( + event_type = "RateLimitHit", + client_ip = %client_ip, + limit_type = %limit_type, + current_count = current_count, + success = false, + "SSH rate limit exceeded" + ); + } + + pub fn log_security_violation(client_ip: IpAddr, user: &str, violation_type: &str, details: &str) { + tracing::error!( + event_type = "SecurityViolation", + client_ip = %client_ip, + user = %user, + violation_type = %violation_type, + error_message = %details, + success = false, + "SSH security violation" + ); + } + + pub fn init_json_logging() { + use tracing_subscriber::fmt::Layer; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + let json_layer = Layer::default() + .json() + .with_target(false) + .with_thread_ids(false) + .with_thread_names(false); + + tracing_subscriber::registry() + .with(json_layer) + .init(); + } +} + +pub fn init_audit_logging() { + SshAuditLog::init_json_logging(); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{IpAddr, Ipv4Addr}; + + fn setup_test_logging() { + let _ = tracing_subscriber::fmt() + .json() + .with_target(false) + .with_test_writer() + .try_init(); + } + + #[test] + fn test_log_connection_start() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_connection_start(client_ip); + } + + #[test] + fn test_log_auth_success() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_auth_success(client_ip, "demo", "password"); + } + + #[test] + fn test_log_auth_failure() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_auth_failure(client_ip, "demo", "password", "Invalid password"); + } + + #[test] + fn test_log_command_exec() { + setup_test_logging(); + SshAuditLog::log_command_exec("session-123", "demo", 1, "ls -la", true); + } + + #[test] + fn test_log_file_upload() { + setup_test_logging(); + SshAuditLog::log_file_upload("session-123", "demo", "/data/test.txt", 1024, true); + } + + #[test] + fn test_log_port_forward_request() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_port_forward_request(client_ip, "demo", 8080, true); + } + + #[test] + fn test_log_rate_limit_hit() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_rate_limit_hit(client_ip, "connection", 100); + } + + #[test] + fn test_log_security_violation() { + setup_test_logging(); + let client_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + SshAuditLog::log_security_violation(client_ip, "demo", "path_traversal", "Attempted to access /etc/passwd"); + } + + #[test] + fn test_event_type_serialization() { + let event = SshAuditEvent { + timestamp: "2026-06-21T00:00:00Z".to_string(), + event_type: SshEventType::ConnectionStart, + session_id: Some("session-123".to_string()), + client_ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + user: Some("demo".to_string()), + channel_id: None, + command: None, + file_path: None, + port: None, + success: true, + error_message: None, + duration_ms: None, + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event_type\":\"ConnectionStart\"")); + } + + #[test] + fn test_all_event_types() { + let types = vec![ + SshEventType::ConnectionStart, + SshEventType::ConnectionEnd, + SshEventType::AuthAttempt, + SshEventType::AuthSuccess, + SshEventType::AuthFailure, + SshEventType::ChannelOpen, + SshEventType::ChannelClose, + SshEventType::CommandExec, + SshEventType::FileUpload, + SshEventType::FileDownload, + SshEventType::PortForwardRequest, + SshEventType::PortForwardBind, + SshEventType::PortForwardConnect, + SshEventType::HostKeyVerify, + SshEventType::RateLimitHit, + SshEventType::SecurityViolation, + ]; + + for event_type in types { + let event = SshAuditEvent { + timestamp: "2026-06-21T00:00:00Z".to_string(), + event_type, + session_id: None, + client_ip: None, + user: None, + channel_id: None, + command: None, + file_path: None, + port: None, + success: true, + error_message: None, + duration_ms: None, + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains(&format!("\"event_type\":\"{}\"", event_type))); + } + } +} \ No newline at end of file