osquery_rust_ng/plugin/logger/mod.rs
1//! Logger plugin support for osquery extensions.
2//!
3//! This module provides the infrastructure for creating logger plugins that integrate with osquery.
4//! Logger plugins receive log data from osquery in various formats (status logs, query results, snapshots)
5//! and are responsible for persisting or forwarding this data.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use osquery_rust_ng::plugin::{LoggerPlugin, LogStatus, Plugin};
11//! use osquery_rust_ng::prelude::*;
12//!
13//! struct ConsoleLogger;
14//!
15//! impl LoggerPlugin for ConsoleLogger {
16//! fn name(&self) -> String {
17//! "console_logger".to_string()
18//! }
19//!
20//! fn log_string(&self, message: &str) -> Result<(), String> {
21//! println!("{}", message);
22//! Ok(())
23//! }
24//!
25//! fn log_status(&self, status: &LogStatus) -> Result<(), String> {
26//! println!("[{}] {}:{} - {}",
27//! status.severity, status.filename, status.line, status.message);
28//! Ok(())
29//! }
30//! }
31//!
32//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
33//! let mut server = Server::new(None, "/path/to/socket").unwrap();
34//! server.register_plugin(Plugin::logger(ConsoleLogger));
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! # Protocol Details
40//!
41//! osquery sends log data to logger plugins in two main formats:
42//!
43//! 1. **Status logs**: `{"log": "[{\"s\":0,\"f\":\"file.cpp\",\"i\":123,\"m\":\"message\"}]", "status": "true"}`
44//! - `s`: severity (0=Info, 1=Warning, 2=Error)
45//! - `f`: filename
46//! - `i`: line number
47//! - `m`: message
48//!
49//! 2. **Query results**: `{"log": "{...query results as JSON..."}`
50//! - Contains the results of scheduled queries
51//! - Automatically pretty-printed by the framework
52//!
53//! The logger plugin framework handles parsing these formats and calls the appropriate methods on your implementation.
54
55use crate::_osquery::osquery::{ExtensionPluginRequest, ExtensionPluginResponse};
56use crate::_osquery::osquery::{ExtensionResponse, ExtensionStatus};
57use crate::plugin::OsqueryPlugin;
58use crate::plugin::_enums::response::ExtensionResponseEnum;
59use serde_json::Value;
60use std::fmt;
61
62/// Trait that logger plugins must implement.
63///
64/// # Example
65///
66/// ```no_run
67/// use osquery_rust_ng::plugin::{LoggerPlugin, LogStatus, LogSeverity};
68///
69/// struct MyLogger;
70///
71/// impl LoggerPlugin for MyLogger {
72/// fn name(&self) -> String {
73/// "my_logger".to_string()
74/// }
75///
76/// fn log_string(&self, message: &str) -> Result<(), String> {
77/// println!("Log: {}", message);
78/// Ok(())
79/// }
80/// }
81/// ```
82pub trait LoggerPlugin: Send + Sync + 'static {
83 /// Returns the name of the logger plugin
84 fn name(&self) -> String;
85
86 /// Log a raw string message.
87 ///
88 /// This is called for general log entries and query results.
89 fn log_string(&self, message: &str) -> Result<(), String>;
90
91 /// Log structured status information.
92 ///
93 /// Called when osquery sends status logs with severity, file, line, and message.
94 fn log_status(&self, status: &LogStatus) -> Result<(), String> {
95 // Default implementation converts to string
96 self.log_string(&status.to_string())
97 }
98
99 /// Log a snapshot (periodic state dump).
100 ///
101 /// Snapshots are periodic dumps of osquery's internal state.
102 fn log_snapshot(&self, snapshot: &str) -> Result<(), String> {
103 self.log_string(snapshot)
104 }
105
106 /// Initialize the logger.
107 ///
108 /// Called when the logger is first registered with osquery.
109 fn init(&self, _name: &str) -> Result<(), String> {
110 Ok(())
111 }
112
113 /// Health check for the logger.
114 ///
115 /// Called periodically to ensure the logger is still functioning.
116 fn health(&self) -> Result<(), String> {
117 Ok(())
118 }
119
120 /// Returns the features this logger supports.
121 ///
122 /// Override this method to advertise additional capabilities to osquery.
123 /// By default, loggers advertise support for status logs.
124 ///
125 /// # Example
126 ///
127 /// ```
128 /// use osquery_rust_ng::plugin::{LoggerPlugin, LoggerFeatures};
129 ///
130 /// struct MyLogger;
131 ///
132 /// impl LoggerPlugin for MyLogger {
133 /// fn name(&self) -> String { "my_logger".to_string() }
134 /// fn log_string(&self, _: &str) -> Result<(), String> { Ok(()) }
135 ///
136 /// fn features(&self) -> i32 {
137 /// // Support both status logs and event forwarding
138 /// LoggerFeatures::LOG_STATUS | LoggerFeatures::LOG_EVENT
139 /// }
140 /// }
141 /// ```
142 fn features(&self) -> i32 {
143 LoggerFeatures::LOG_STATUS
144 }
145
146 /// Shutdown the logger.
147 ///
148 /// Called when the extension is shutting down.
149 fn shutdown(&self) {}
150}
151
152/// Log status information from osquery.
153///
154/// Status logs contain structured information about osquery's internal state,
155/// including error messages, warnings, and informational messages.
156#[derive(Debug, Clone)]
157pub struct LogStatus {
158 /// The severity level of the log message
159 pub severity: LogSeverity,
160 /// The source file that generated the log
161 pub filename: String,
162 /// The line number in the source file
163 pub line: u32,
164 /// The log message text
165 pub message: String,
166}
167
168impl fmt::Display for LogStatus {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(
171 f,
172 "[{}] {}:{} - {}",
173 self.severity, self.filename, self.line, self.message
174 )
175 }
176}
177
178/// Feature flags that logger plugins can advertise to osquery.
179///
180/// These flags tell osquery which additional log types the plugin supports.
181/// When osquery sends a `{"action": "features"}` request, the plugin returns
182/// a bitmask of these values in the response status code.
183///
184/// # Example
185///
186/// ```
187/// use osquery_rust_ng::plugin::LoggerFeatures;
188///
189/// // Support both status logs and event forwarding
190/// let features = LoggerFeatures::LOG_STATUS | LoggerFeatures::LOG_EVENT;
191/// assert_eq!(features, 3);
192/// ```
193pub struct LoggerFeatures;
194
195impl LoggerFeatures {
196 /// No additional features - only query results are logged.
197 pub const BLANK: i32 = 0;
198
199 /// Plugin supports receiving osquery status logs (INFO/WARNING/ERROR).
200 ///
201 /// When enabled, osquery forwards its internal Glog status messages
202 /// to the logger plugin via `log_status()`.
203 pub const LOG_STATUS: i32 = 1;
204
205 /// Plugin supports receiving event logs.
206 ///
207 /// When enabled, event subscribers forward events directly to the logger.
208 pub const LOG_EVENT: i32 = 2;
209}
210
211/// Log severity levels used by osquery.
212///
213/// These map directly to osquery's internal severity levels.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum LogSeverity {
216 /// Informational messages (severity 0)
217 Info = 0,
218 /// Warning messages (severity 1)
219 Warning = 1,
220 /// Error messages (severity 2)
221 Error = 2,
222}
223
224impl fmt::Display for LogSeverity {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 match self {
227 LogSeverity::Info => write!(f, "INFO"),
228 LogSeverity::Warning => write!(f, "WARNING"),
229 LogSeverity::Error => write!(f, "ERROR"),
230 }
231 }
232}
233
234impl TryFrom<i64> for LogSeverity {
235 type Error = String;
236
237 fn try_from(value: i64) -> Result<Self, String> {
238 match value {
239 0 => Ok(LogSeverity::Info),
240 1 => Ok(LogSeverity::Warning),
241 2 => Ok(LogSeverity::Error),
242 _ => Err(format!("Invalid severity level: {value}")),
243 }
244 }
245}
246
247/// Types of log requests that can be received from osquery.
248///
249/// This enum represents the different types of logging operations
250/// that osquery can request from a logger plugin.
251#[derive(Debug)]
252enum LogRequestType {
253 /// Status log with array of status entries
254 StatusLog(Vec<StatusEntry>),
255 /// Query result log (formatted as JSON)
256 QueryResult(Value),
257 /// Raw string log
258 RawString(String),
259 /// Snapshot log (periodic state dump)
260 Snapshot(String),
261 /// Logger initialization request
262 Init(String),
263 /// Health check request
264 Health,
265 /// Features query - osquery asks what log types we support
266 Features,
267}
268
269/// A single status log entry from osquery
270#[derive(Debug)]
271struct StatusEntry {
272 severity: LogSeverity,
273 filename: String,
274 line: u32,
275 message: String,
276}
277
278/// Wrapper that adapts a LoggerPlugin to the OsqueryPlugin interface.
279///
280/// This wrapper handles the complexity of osquery's logger protocol,
281/// parsing different request formats and calling the appropriate methods
282/// on your LoggerPlugin implementation.
283///
284/// You typically don't need to interact with this directly - use
285/// `Plugin::logger()` to create plugins.
286pub struct LoggerPluginWrapper<L: LoggerPlugin> {
287 logger: L,
288}
289
290impl<L: LoggerPlugin> LoggerPluginWrapper<L> {
291 pub fn new(logger: L) -> Self {
292 Self { logger }
293 }
294
295 /// Parse an osquery request into a structured log request type
296 fn parse_request(&self, request: &ExtensionPluginRequest) -> LogRequestType {
297 // Check for status logs first (most common in daemon mode)
298 if let Some(log_data) = request.get("log") {
299 if request.get("status").map(|s| s == "true").unwrap_or(false) {
300 // Parse status log array
301 if let Ok(entries) = self.parse_status_entries(log_data) {
302 return LogRequestType::StatusLog(entries);
303 }
304 }
305
306 // Try to parse as JSON for pretty printing
307 if let Ok(value) = serde_json::from_str::<Value>(log_data) {
308 return LogRequestType::QueryResult(value);
309 }
310
311 // Fall back to raw string
312 return LogRequestType::RawString(log_data.to_string());
313 }
314
315 // Check for other request types
316 if let Some(snapshot) = request.get("snapshot") {
317 return LogRequestType::Snapshot(snapshot.to_string());
318 }
319
320 if let Some(init_name) = request.get("init") {
321 return LogRequestType::Init(init_name.to_string());
322 }
323
324 if request.contains_key("health") {
325 return LogRequestType::Health;
326 }
327
328 // Check for features query
329 if request
330 .get("action")
331 .map(|a| a == "features")
332 .unwrap_or(false)
333 {
334 return LogRequestType::Features;
335 }
336
337 // Fallback for unknown request
338 if let Some(string_log) = request.get("string") {
339 return LogRequestType::RawString(string_log.to_string());
340 }
341
342 LogRequestType::RawString(String::new())
343 }
344
345 /// Parse status entries from JSON array string
346 fn parse_status_entries(&self, log_data: &str) -> Result<Vec<StatusEntry>, String> {
347 let entries: Vec<Value> = serde_json::from_str(log_data)
348 .map_err(|e| format!("Failed to parse status log array: {e}"))?;
349
350 let mut status_entries = Vec::new();
351
352 for entry in entries {
353 if let Some(obj) = entry.as_object() {
354 let severity = obj
355 .get("s")
356 .and_then(|v| v.as_i64())
357 .unwrap_or(0)
358 .try_into()
359 .unwrap_or(LogSeverity::Info);
360
361 let filename = obj
362 .get("f")
363 .and_then(|v| v.as_str())
364 .unwrap_or("unknown")
365 .to_string();
366
367 let line = obj.get("i").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
368
369 let message = obj
370 .get("m")
371 .and_then(|v| v.as_str())
372 .unwrap_or("")
373 .to_string();
374
375 status_entries.push(StatusEntry {
376 severity,
377 filename,
378 line,
379 message,
380 });
381 }
382 }
383
384 Ok(status_entries)
385 }
386
387 /// Handle a parsed log request
388 fn handle_log_request(&self, request_type: LogRequestType) -> Result<(), String> {
389 match request_type {
390 LogRequestType::StatusLog(entries) => {
391 for entry in entries {
392 let status = LogStatus {
393 severity: entry.severity,
394 filename: entry.filename,
395 line: entry.line,
396 message: entry.message,
397 };
398 self.logger.log_status(&status)?;
399 }
400 Ok(())
401 }
402 LogRequestType::QueryResult(value) => {
403 let formatted =
404 serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
405 self.logger.log_string(&formatted)
406 }
407 LogRequestType::RawString(s) => self.logger.log_string(&s),
408 LogRequestType::Snapshot(s) => self.logger.log_snapshot(&s),
409 LogRequestType::Init(name) => self.logger.init(&name),
410 LogRequestType::Health => self.logger.health(),
411 // Features is handled specially in handle_call before this is called
412 LogRequestType::Features => Ok(()),
413 }
414 }
415}
416
417impl<L: LoggerPlugin> OsqueryPlugin for LoggerPluginWrapper<L> {
418 fn name(&self) -> String {
419 self.logger.name()
420 }
421
422 fn registry(&self) -> crate::plugin::Registry {
423 crate::plugin::Registry::Logger
424 }
425
426 fn routes(&self) -> ExtensionPluginResponse {
427 // Logger plugins don't expose routes like table plugins do
428 ExtensionPluginResponse::new()
429 }
430
431 fn ping(&self) -> ExtensionStatus {
432 // Health check - always return OK for now
433 ExtensionStatus::default()
434 }
435
436 fn handle_call(&self, request: crate::_osquery::ExtensionPluginRequest) -> ExtensionResponse {
437 // Parse the request into a structured type
438 let request_type = self.parse_request(&request);
439
440 // Features request needs special handling - return features as status code
441 if matches!(request_type, LogRequestType::Features) {
442 return ExtensionResponseEnum::SuccessWithCode(self.logger.features()).into();
443 }
444
445 // Handle the request and return the appropriate response
446 match self.handle_log_request(request_type) {
447 Ok(()) => ExtensionResponseEnum::Success().into(),
448 Err(e) => ExtensionResponseEnum::Failure(e).into(),
449 }
450 }
451
452 fn shutdown(&self) {
453 self.logger.shutdown();
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460 use crate::plugin::OsqueryPlugin;
461 use std::collections::BTreeMap;
462
463 /// A minimal logger for testing
464 struct TestLogger {
465 custom_features: Option<i32>,
466 }
467
468 impl TestLogger {
469 fn new() -> Self {
470 Self {
471 custom_features: None,
472 }
473 }
474
475 fn with_features(features: i32) -> Self {
476 Self {
477 custom_features: Some(features),
478 }
479 }
480 }
481
482 impl LoggerPlugin for TestLogger {
483 fn name(&self) -> String {
484 "test_logger".to_string()
485 }
486
487 fn log_string(&self, _message: &str) -> Result<(), String> {
488 Ok(())
489 }
490
491 fn features(&self) -> i32 {
492 self.custom_features.unwrap_or(LoggerFeatures::LOG_STATUS)
493 }
494 }
495
496 #[test]
497 fn test_features_request_returns_default_log_status() {
498 let logger = TestLogger::new();
499 let wrapper = LoggerPluginWrapper::new(logger);
500
501 // Simulate osquery sending {"action": "features"}
502 let mut request: BTreeMap<String, String> = BTreeMap::new();
503 request.insert("action".to_string(), "features".to_string());
504
505 let response = wrapper.handle_call(request);
506
507 // The status code should be LOG_STATUS (1)
508 let status = response.status.as_ref();
509 assert!(status.is_some(), "response should have status");
510 assert_eq!(
511 status.and_then(|s| s.code),
512 Some(LoggerFeatures::LOG_STATUS)
513 );
514 }
515
516 #[test]
517 fn test_features_request_returns_custom_features() {
518 // Logger that supports both status logs and event forwarding
519 let features = LoggerFeatures::LOG_STATUS | LoggerFeatures::LOG_EVENT;
520 let logger = TestLogger::with_features(features);
521 let wrapper = LoggerPluginWrapper::new(logger);
522
523 let mut request: BTreeMap<String, String> = BTreeMap::new();
524 request.insert("action".to_string(), "features".to_string());
525
526 let response = wrapper.handle_call(request);
527
528 // The status code should be 3 (LOG_STATUS | LOG_EVENT)
529 let status = response.status.as_ref();
530 assert!(status.is_some(), "response should have status");
531 assert_eq!(status.and_then(|s| s.code), Some(3));
532 }
533
534 #[test]
535 fn test_features_request_returns_blank_when_no_features() {
536 let logger = TestLogger::with_features(LoggerFeatures::BLANK);
537 let wrapper = LoggerPluginWrapper::new(logger);
538
539 let mut request: BTreeMap<String, String> = BTreeMap::new();
540 request.insert("action".to_string(), "features".to_string());
541
542 let response = wrapper.handle_call(request);
543
544 // The status code should be 0 (BLANK)
545 let status = response.status.as_ref();
546 assert!(status.is_some(), "response should have status");
547 assert_eq!(status.and_then(|s| s.code), Some(LoggerFeatures::BLANK));
548 }
549
550 #[test]
551 fn test_parse_request_recognizes_features_action() {
552 let logger = TestLogger::new();
553 let wrapper = LoggerPluginWrapper::new(logger);
554
555 let mut request: BTreeMap<String, String> = BTreeMap::new();
556 request.insert("action".to_string(), "features".to_string());
557
558 let request_type = wrapper.parse_request(&request);
559 assert!(matches!(request_type, LogRequestType::Features));
560 }
561
562 #[test]
563 fn test_parse_request_ignores_other_actions() {
564 let logger = TestLogger::new();
565 let wrapper = LoggerPluginWrapper::new(logger);
566
567 let mut request: BTreeMap<String, String> = BTreeMap::new();
568 request.insert("action".to_string(), "unknown".to_string());
569
570 let request_type = wrapper.parse_request(&request);
571 // Should fall through to default (RawString)
572 assert!(matches!(request_type, LogRequestType::RawString(_)));
573 }
574}