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}