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    /// Shutdown the logger.
121    ///
122    /// Called when osquery is shutting down.
123    fn shutdown(&self) {}
124}
125
126/// Log status information from osquery.
127///
128/// Status logs contain structured information about osquery's internal state,
129/// including error messages, warnings, and informational messages.
130#[derive(Debug, Clone)]
131pub struct LogStatus {
132    /// The severity level of the log message
133    pub severity: LogSeverity,
134    /// The source file that generated the log
135    pub filename: String,
136    /// The line number in the source file
137    pub line: u32,
138    /// The log message text
139    pub message: String,
140}
141
142impl fmt::Display for LogStatus {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(
145            f,
146            "[{}] {}:{} - {}",
147            self.severity, self.filename, self.line, self.message
148        )
149    }
150}
151
152/// Log severity levels used by osquery.
153///
154/// These map directly to osquery's internal severity levels.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum LogSeverity {
157    /// Informational messages (severity 0)
158    Info = 0,
159    /// Warning messages (severity 1)
160    Warning = 1,
161    /// Error messages (severity 2)
162    Error = 2,
163}
164
165impl fmt::Display for LogSeverity {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            LogSeverity::Info => write!(f, "INFO"),
169            LogSeverity::Warning => write!(f, "WARNING"),
170            LogSeverity::Error => write!(f, "ERROR"),
171        }
172    }
173}
174
175impl TryFrom<i64> for LogSeverity {
176    type Error = String;
177
178    fn try_from(value: i64) -> Result<Self, String> {
179        match value {
180            0 => Ok(LogSeverity::Info),
181            1 => Ok(LogSeverity::Warning),
182            2 => Ok(LogSeverity::Error),
183            _ => Err(format!("Invalid severity level: {value}")),
184        }
185    }
186}
187
188/// Types of log requests that can be received from osquery.
189///
190/// This enum represents the different types of logging operations
191/// that osquery can request from a logger plugin.
192#[derive(Debug)]
193enum LogRequestType {
194    /// Status log with array of status entries
195    StatusLog(Vec<StatusEntry>),
196    /// Query result log (formatted as JSON)
197    QueryResult(Value),
198    /// Raw string log
199    RawString(String),
200    /// Snapshot log (periodic state dump)
201    Snapshot(String),
202    /// Logger initialization request
203    Init(String),
204    /// Health check request
205    Health,
206}
207
208/// A single status log entry from osquery
209#[derive(Debug)]
210struct StatusEntry {
211    severity: LogSeverity,
212    filename: String,
213    line: u32,
214    message: String,
215}
216
217/// Wrapper that adapts a LoggerPlugin to the OsqueryPlugin interface.
218///
219/// This wrapper handles the complexity of osquery's logger protocol,
220/// parsing different request formats and calling the appropriate methods
221/// on your LoggerPlugin implementation.
222///
223/// You typically don't need to interact with this directly - use
224/// `Plugin::logger()` to create plugins.
225pub struct LoggerPluginWrapper<L: LoggerPlugin> {
226    logger: L,
227}
228
229impl<L: LoggerPlugin> LoggerPluginWrapper<L> {
230    pub fn new(logger: L) -> Self {
231        Self { logger }
232    }
233
234    /// Parse an osquery request into a structured log request type
235    fn parse_request(&self, request: &ExtensionPluginRequest) -> LogRequestType {
236        // Check for status logs first (most common in daemon mode)
237        if let Some(log_data) = request.get("log") {
238            if request.get("status").map(|s| s == "true").unwrap_or(false) {
239                // Parse status log array
240                if let Ok(entries) = self.parse_status_entries(log_data) {
241                    return LogRequestType::StatusLog(entries);
242                }
243            }
244
245            // Try to parse as JSON for pretty printing
246            if let Ok(value) = serde_json::from_str::<Value>(log_data) {
247                return LogRequestType::QueryResult(value);
248            }
249
250            // Fall back to raw string
251            return LogRequestType::RawString(log_data.to_string());
252        }
253
254        // Check for other request types
255        if let Some(snapshot) = request.get("snapshot") {
256            return LogRequestType::Snapshot(snapshot.to_string());
257        }
258
259        if let Some(init_name) = request.get("init") {
260            return LogRequestType::Init(init_name.to_string());
261        }
262
263        if request.contains_key("health") {
264            return LogRequestType::Health;
265        }
266
267        // Fallback for unknown request
268        if let Some(string_log) = request.get("string") {
269            return LogRequestType::RawString(string_log.to_string());
270        }
271
272        LogRequestType::RawString(String::new())
273    }
274
275    /// Parse status entries from JSON array string
276    fn parse_status_entries(&self, log_data: &str) -> Result<Vec<StatusEntry>, String> {
277        let entries: Vec<Value> = serde_json::from_str(log_data)
278            .map_err(|e| format!("Failed to parse status log array: {e}"))?;
279
280        let mut status_entries = Vec::new();
281
282        for entry in entries {
283            if let Some(obj) = entry.as_object() {
284                let severity = obj
285                    .get("s")
286                    .and_then(|v| v.as_i64())
287                    .unwrap_or(0)
288                    .try_into()
289                    .unwrap_or(LogSeverity::Info);
290
291                let filename = obj
292                    .get("f")
293                    .and_then(|v| v.as_str())
294                    .unwrap_or("unknown")
295                    .to_string();
296
297                let line = obj.get("i").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
298
299                let message = obj
300                    .get("m")
301                    .and_then(|v| v.as_str())
302                    .unwrap_or("")
303                    .to_string();
304
305                status_entries.push(StatusEntry {
306                    severity,
307                    filename,
308                    line,
309                    message,
310                });
311            }
312        }
313
314        Ok(status_entries)
315    }
316
317    /// Handle a parsed log request
318    fn handle_log_request(&self, request_type: LogRequestType) -> Result<(), String> {
319        match request_type {
320            LogRequestType::StatusLog(entries) => {
321                for entry in entries {
322                    let status = LogStatus {
323                        severity: entry.severity,
324                        filename: entry.filename,
325                        line: entry.line,
326                        message: entry.message,
327                    };
328                    self.logger.log_status(&status)?;
329                }
330                Ok(())
331            }
332            LogRequestType::QueryResult(value) => {
333                let formatted =
334                    serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
335                self.logger.log_string(&formatted)
336            }
337            LogRequestType::RawString(s) => self.logger.log_string(&s),
338            LogRequestType::Snapshot(s) => self.logger.log_snapshot(&s),
339            LogRequestType::Init(name) => self.logger.init(&name),
340            LogRequestType::Health => self.logger.health(),
341        }
342    }
343}
344
345impl<L: LoggerPlugin> OsqueryPlugin for LoggerPluginWrapper<L> {
346    fn name(&self) -> String {
347        self.logger.name()
348    }
349
350    fn registry(&self) -> crate::plugin::Registry {
351        crate::plugin::Registry::Logger
352    }
353
354    fn routes(&self) -> ExtensionPluginResponse {
355        // Logger plugins don't expose routes like table plugins do
356        ExtensionPluginResponse::new()
357    }
358
359    fn ping(&self) -> ExtensionStatus {
360        // Health check - always return OK for now
361        ExtensionStatus::default()
362    }
363
364    fn handle_call(&self, request: crate::_osquery::ExtensionPluginRequest) -> ExtensionResponse {
365        // Parse the request into a structured type
366        let request_type = self.parse_request(&request);
367
368        // Handle the request and return the appropriate response
369        match self.handle_log_request(request_type) {
370            Ok(()) => ExtensionResponseEnum::Success().into(),
371            Err(e) => ExtensionResponseEnum::Failure(e).into(),
372        }
373    }
374
375    fn shutdown(&self) {
376        self.logger.shutdown();
377    }
378}