Skip to content

Commit 4ed85a2

Browse files
committed
fix: Address security and quality issues in audit infrastructure
1 parent 80e7187 commit 4ed85a2

3 files changed

Lines changed: 166 additions & 45 deletions

File tree

src/server/audit/event.rs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ pub struct AuditEvent {
5959
pub result: EventResult,
6060

6161
/// Additional details about the event
62+
///
63+
/// # Security Warning
64+
///
65+
/// This field may contain sensitive information such as:
66+
/// - Error messages with file paths or system information
67+
/// - Command arguments that may include passwords or tokens
68+
/// - User-supplied data that hasn't been sanitized
69+
///
70+
/// When implementing exporters, ensure this field is handled securely:
71+
/// - Apply appropriate access controls to audit logs
72+
/// - Consider redacting or filtering sensitive patterns
73+
/// - Use encryption when transmitting over networks
74+
/// - Comply with data retention and privacy policies
6275
pub details: Option<String>,
6376

6477
/// Protocol used (ssh, sftp, scp)
@@ -217,6 +230,11 @@ impl AuditEvent {
217230
}
218231

219232
/// Set additional details.
233+
///
234+
/// # Security Warning
235+
///
236+
/// Be cautious when including sensitive information in this field.
237+
/// See the `details` field documentation for security considerations.
220238
pub fn with_details(mut self, details: String) -> Self {
221239
self.details = Some(details);
222240
self
@@ -229,11 +247,9 @@ impl AuditEvent {
229247
}
230248
}
231249

232-
impl Default for AuditEvent {
233-
fn default() -> Self {
234-
Self::new(EventType::CommandExecuted, String::new(), String::new())
235-
}
236-
}
250+
// Note: Default implementation removed as it creates sentinel values with empty
251+
// user and session_id fields, which are semantically invalid for audit events.
252+
// Use AuditEvent::new() to create audit events with meaningful values.
237253

238254
#[cfg(test)]
239255
mod tests {
@@ -319,15 +335,6 @@ mod tests {
319335
assert_eq!(deserialized.protocol, event.protocol);
320336
}
321337

322-
#[test]
323-
fn test_default_event() {
324-
let event = AuditEvent::default();
325-
assert_eq!(event.event_type, EventType::CommandExecuted);
326-
assert_eq!(event.result, EventResult::Success);
327-
assert!(event.user.is_empty());
328-
assert!(event.session_id.is_empty());
329-
}
330-
331338
#[test]
332339
fn test_event_with_dest_path() {
333340
let event = AuditEvent::new(

src/server/audit/exporter.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,14 @@ pub trait AuditExporter: Send + Sync {
4646
///
4747
/// # Arguments
4848
///
49-
/// * `events` - Vector of audit events to export
49+
/// * `events` - Slice of audit events to export
5050
///
5151
/// # Errors
5252
///
5353
/// Returns an error if any event fails to export.
54-
async fn export_batch(&self, events: Vec<AuditEvent>) -> Result<()> {
54+
async fn export_batch(&self, events: &[AuditEvent]) -> Result<()> {
5555
for event in events {
56-
self.export(event).await?;
56+
self.export(event.clone()).await?;
5757
}
5858
Ok(())
5959
}
@@ -98,7 +98,7 @@ impl AuditExporter for NullExporter {
9898
Ok(())
9999
}
100100

101-
async fn export_batch(&self, _events: Vec<AuditEvent>) -> Result<()> {
101+
async fn export_batch(&self, _events: &[AuditEvent]) -> Result<()> {
102102
// Discard all events
103103
Ok(())
104104
}
@@ -149,7 +149,7 @@ mod tests {
149149
.with_result(EventResult::Failure),
150150
];
151151

152-
let result = exporter.export_batch(events).await;
152+
let result = exporter.export_batch(&events).await;
153153
assert!(result.is_ok());
154154
}
155155

@@ -185,7 +185,7 @@ mod tests {
185185
"bob".to_string(),
186186
"session-456".to_string(),
187187
)];
188-
exporter.export_batch(events).await.unwrap();
188+
exporter.export_batch(&events).await.unwrap();
189189

190190
// Flush and close
191191
exporter.flush().await.unwrap();

src/server/audit/mod.rs

Lines changed: 139 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use anyhow::Result;
5353
use std::sync::Arc;
5454
use std::time::Duration;
5555
use tokio::sync::mpsc;
56+
use tokio::task::JoinHandle;
5657

5758
pub use event::{AuditEvent, EventResult, EventType};
5859
pub use exporter::{AuditExporter, NullExporter};
@@ -128,19 +129,34 @@ impl AuditConfig {
128129
}
129130

130131
/// Set the buffer size.
132+
///
133+
/// # Panics
134+
///
135+
/// Panics if size is 0.
131136
pub fn with_buffer_size(mut self, size: usize) -> Self {
137+
assert!(size >= 1, "buffer_size must be at least 1");
132138
self.buffer_size = size;
133139
self
134140
}
135141

136142
/// Set the batch size.
143+
///
144+
/// # Panics
145+
///
146+
/// Panics if size is 0.
137147
pub fn with_batch_size(mut self, size: usize) -> Self {
148+
assert!(size >= 1, "batch_size must be at least 1");
138149
self.batch_size = size;
139150
self
140151
}
141152

142153
/// Set the flush interval.
154+
///
155+
/// # Panics
156+
///
157+
/// Panics if secs is 0.
143158
pub fn with_flush_interval(mut self, secs: u64) -> Self {
159+
assert!(secs >= 1, "flush_interval_secs must be at least 1");
144160
self.flush_interval_secs = secs;
145161
self
146162
}
@@ -166,6 +182,9 @@ pub struct AuditManager {
166182

167183
/// Whether audit logging is enabled
168184
enabled: bool,
185+
186+
/// Handle to the background worker task
187+
worker_handle: Option<JoinHandle<()>>,
169188
}
170189

171190
impl AuditManager {
@@ -208,23 +227,26 @@ impl AuditManager {
208227
exporters.push(exporter);
209228
}
210229

211-
let manager = Self {
212-
exporters: exporters.clone(),
213-
sender,
214-
enabled: config.enabled,
215-
};
216-
217230
// Start background worker
218-
if config.enabled {
231+
let worker_handle = if config.enabled {
219232
let batch_size = config.batch_size;
220233
let flush_interval = Duration::from_secs(config.flush_interval_secs);
221-
tokio::spawn(Self::worker(
234+
Some(tokio::spawn(Self::worker(
222235
receiver,
223-
exporters,
236+
exporters.clone(),
224237
batch_size,
225238
flush_interval,
226-
));
227-
}
239+
)))
240+
} else {
241+
None
242+
};
243+
244+
let manager = Self {
245+
exporters,
246+
sender,
247+
enabled: config.enabled,
248+
worker_handle,
249+
};
228250

229251
Ok(manager)
230252
}
@@ -263,26 +285,32 @@ impl AuditManager {
263285

264286
loop {
265287
tokio::select! {
266-
Some(event) = receiver.recv() => {
267-
buffer.push(event);
268-
269-
// Flush if buffer is full
270-
if buffer.len() >= batch_size {
271-
Self::flush_buffer(&exporters, &mut buffer).await;
288+
biased;
289+
290+
event_opt = receiver.recv() => {
291+
match event_opt {
292+
Some(event) => {
293+
buffer.push(event);
294+
295+
// Flush if buffer is full
296+
if buffer.len() >= batch_size {
297+
Self::flush_buffer(&exporters, &mut buffer).await;
298+
}
299+
}
300+
None => {
301+
// Channel closed, flush remaining events and exit
302+
if !buffer.is_empty() {
303+
Self::flush_buffer(&exporters, &mut buffer).await;
304+
}
305+
break;
306+
}
272307
}
273308
}
274309
_ = flush_timer.tick() => {
275310
if !buffer.is_empty() {
276311
Self::flush_buffer(&exporters, &mut buffer).await;
277312
}
278313
}
279-
else => {
280-
// Channel closed, flush remaining events and exit
281-
if !buffer.is_empty() {
282-
Self::flush_buffer(&exporters, &mut buffer).await;
283-
}
284-
break;
285-
}
286314
}
287315
}
288316

@@ -297,7 +325,7 @@ impl AuditManager {
297325
/// Flush the event buffer to all exporters.
298326
async fn flush_buffer(exporters: &[Arc<dyn AuditExporter>], buffer: &mut Vec<AuditEvent>) {
299327
for exporter in exporters {
300-
if let Err(e) = exporter.export_batch(buffer.clone()).await {
328+
if let Err(e) = exporter.export_batch(buffer).await {
301329
tracing::error!("Audit export failed: {}", e);
302330
}
303331
}
@@ -319,6 +347,33 @@ impl AuditManager {
319347
pub fn is_enabled(&self) -> bool {
320348
self.enabled
321349
}
350+
351+
/// Gracefully shut down the audit manager.
352+
///
353+
/// This method:
354+
/// 1. Drops the sender to signal the worker to stop accepting new events
355+
/// 2. Waits for the worker to finish processing buffered events
356+
/// 3. Ensures all exporters are properly closed
357+
///
358+
/// After calling this method, the AuditManager should not be used.
359+
///
360+
/// # Errors
361+
///
362+
/// Returns an error if the worker task panicked or if there was an issue
363+
/// waiting for the worker to complete.
364+
pub async fn shutdown(mut self) -> Result<()> {
365+
// Drop the sender to signal the worker to exit
366+
drop(self.sender);
367+
368+
// Wait for the worker to finish
369+
if let Some(handle) = self.worker_handle.take() {
370+
handle
371+
.await
372+
.map_err(|e| anyhow::anyhow!("Worker task panicked: {}", e))?;
373+
}
374+
375+
Ok(())
376+
}
322377
}
323378

324379
#[cfg(test)]
@@ -448,4 +503,63 @@ mod tests {
448503
// Wait for flush interval
449504
tokio::time::sleep(Duration::from_millis(1100)).await;
450505
}
506+
507+
#[tokio::test(flavor = "multi_thread")]
508+
async fn test_audit_manager_shutdown() {
509+
let config = AuditConfig::new()
510+
.with_enabled(true)
511+
.with_buffer_size(10)
512+
.with_batch_size(5)
513+
.with_flush_interval(1);
514+
515+
let manager = AuditManager::new(&config).unwrap();
516+
517+
// Send some events
518+
for i in 0..3 {
519+
let event = AuditEvent::new(
520+
EventType::FileUploaded,
521+
format!("user{}", i),
522+
format!("session-{}", i),
523+
);
524+
manager.log(event).await;
525+
}
526+
527+
// Give a small amount of time for events to be queued
528+
tokio::time::sleep(Duration::from_millis(50)).await;
529+
530+
// Shutdown should wait for all events to be processed
531+
let result = tokio::time::timeout(Duration::from_secs(10), manager.shutdown()).await;
532+
assert!(result.is_ok(), "Shutdown timed out");
533+
assert!(result.unwrap().is_ok(), "Shutdown failed");
534+
}
535+
536+
#[test]
537+
#[should_panic(expected = "buffer_size must be at least 1")]
538+
fn test_audit_config_invalid_buffer_size() {
539+
let _config = AuditConfig::new().with_buffer_size(0);
540+
}
541+
542+
#[test]
543+
#[should_panic(expected = "batch_size must be at least 1")]
544+
fn test_audit_config_invalid_batch_size() {
545+
let _config = AuditConfig::new().with_batch_size(0);
546+
}
547+
548+
#[test]
549+
#[should_panic(expected = "flush_interval_secs must be at least 1")]
550+
fn test_audit_config_invalid_flush_interval() {
551+
let _config = AuditConfig::new().with_flush_interval(0);
552+
}
553+
554+
#[test]
555+
fn test_audit_config_valid_minimum_values() {
556+
let config = AuditConfig::new()
557+
.with_buffer_size(1)
558+
.with_batch_size(1)
559+
.with_flush_interval(1);
560+
561+
assert_eq!(config.buffer_size, 1);
562+
assert_eq!(config.batch_size, 1);
563+
assert_eq!(config.flush_interval_secs, 1);
564+
}
451565
}

0 commit comments

Comments
 (0)