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}