use crate::_osquery::osquery::{ExtensionPluginRequest, ExtensionPluginResponse};
use crate::_osquery::osquery::{ExtensionResponse, ExtensionStatus};
use crate::plugin::OsqueryPlugin;
use crate::plugin::_enums::response::ExtensionResponseEnum;
use serde_json::Value;
use std::fmt;
pub trait LoggerPlugin: Send + Sync + 'static {
fn name(&self) -> String;
fn log_string(&self, message: &str) -> Result<(), String>;
fn log_status(&self, status: &LogStatus) -> Result<(), String> {
self.log_string(&status.to_string())
}
fn log_snapshot(&self, snapshot: &str) -> Result<(), String> {
self.log_string(snapshot)
}
fn init(&self, _name: &str) -> Result<(), String> {
Ok(())
}
fn health(&self) -> Result<(), String> {
Ok(())
}
fn features(&self) -> i32 {
LoggerFeatures::LOG_STATUS
}
fn shutdown(&self) {}
}
#[derive(Debug, Clone)]
pub struct LogStatus {
pub severity: LogSeverity,
pub filename: String,
pub line: u32,
pub message: String,
}
impl fmt::Display for LogStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}:{} - {}",
self.severity, self.filename, self.line, self.message
)
}
}
pub struct LoggerFeatures;
impl LoggerFeatures {
pub const BLANK: i32 = 0;
pub const LOG_STATUS: i32 = 1;
pub const LOG_EVENT: i32 = 2;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogSeverity {
Info = 0,
Warning = 1,
Error = 2,
}
impl fmt::Display for LogSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogSeverity::Info => write!(f, "INFO"),
LogSeverity::Warning => write!(f, "WARNING"),
LogSeverity::Error => write!(f, "ERROR"),
}
}
}
impl TryFrom<i64> for LogSeverity {
type Error = String;
fn try_from(value: i64) -> Result<Self, String> {
match value {
0 => Ok(LogSeverity::Info),
1 => Ok(LogSeverity::Warning),
2 => Ok(LogSeverity::Error),
_ => Err(format!("Invalid severity level: {value}")),
}
}
}
#[derive(Debug)]
enum LogRequestType {
StatusLog(Vec<StatusEntry>),
QueryResult(Value),
RawString(String),
Snapshot(String),
Init(String),
Health,
Features,
}
#[derive(Debug)]
struct StatusEntry {
severity: LogSeverity,
filename: String,
line: u32,
message: String,
}
pub struct LoggerPluginWrapper<L: LoggerPlugin> {
logger: L,
}
impl<L: LoggerPlugin> LoggerPluginWrapper<L> {
pub fn new(logger: L) -> Self {
Self { logger }
}
fn parse_request(&self, request: &ExtensionPluginRequest) -> LogRequestType {
if let Some(log_data) = request.get("log") {
if request.get("status").map(|s| s == "true").unwrap_or(false) {
if let Ok(entries) = self.parse_status_entries(log_data) {
return LogRequestType::StatusLog(entries);
}
}
if let Ok(value) = serde_json::from_str::<Value>(log_data) {
return LogRequestType::QueryResult(value);
}
return LogRequestType::RawString(log_data.to_string());
}
if let Some(snapshot) = request.get("snapshot") {
return LogRequestType::Snapshot(snapshot.to_string());
}
if let Some(init_name) = request.get("init") {
return LogRequestType::Init(init_name.to_string());
}
if request.contains_key("health") {
return LogRequestType::Health;
}
if request
.get("action")
.map(|a| a == "features")
.unwrap_or(false)
{
return LogRequestType::Features;
}
if let Some(string_log) = request.get("string") {
return LogRequestType::RawString(string_log.to_string());
}
LogRequestType::RawString(String::new())
}
fn parse_status_entries(&self, log_data: &str) -> Result<Vec<StatusEntry>, String> {
let entries: Vec<Value> = serde_json::from_str(log_data)
.map_err(|e| format!("Failed to parse status log array: {e}"))?;
let mut status_entries = Vec::new();
for entry in entries {
if let Some(obj) = entry.as_object() {
let severity = obj
.get("s")
.and_then(|v| v.as_i64())
.unwrap_or(0)
.try_into()
.unwrap_or(LogSeverity::Info);
let filename = obj
.get("f")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let line = obj.get("i").and_then(|v| v.as_i64()).unwrap_or(0) as u32;
let message = obj
.get("m")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
status_entries.push(StatusEntry {
severity,
filename,
line,
message,
});
}
}
Ok(status_entries)
}
fn handle_log_request(&self, request_type: LogRequestType) -> Result<(), String> {
match request_type {
LogRequestType::StatusLog(entries) => {
for entry in entries {
let status = LogStatus {
severity: entry.severity,
filename: entry.filename,
line: entry.line,
message: entry.message,
};
self.logger.log_status(&status)?;
}
Ok(())
}
LogRequestType::QueryResult(value) => {
let formatted =
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
self.logger.log_string(&formatted)
}
LogRequestType::RawString(s) => self.logger.log_string(&s),
LogRequestType::Snapshot(s) => self.logger.log_snapshot(&s),
LogRequestType::Init(name) => self.logger.init(&name),
LogRequestType::Health => self.logger.health(),
LogRequestType::Features => Ok(()),
}
}
}
impl<L: LoggerPlugin> OsqueryPlugin for LoggerPluginWrapper<L> {
fn name(&self) -> String {
self.logger.name()
}
fn registry(&self) -> crate::plugin::Registry {
crate::plugin::Registry::Logger
}
fn routes(&self) -> ExtensionPluginResponse {
ExtensionPluginResponse::new()
}
fn ping(&self) -> ExtensionStatus {
ExtensionStatus::default()
}
fn handle_call(&self, request: crate::_osquery::ExtensionPluginRequest) -> ExtensionResponse {
let request_type = self.parse_request(&request);
if matches!(request_type, LogRequestType::Features) {
return ExtensionResponseEnum::SuccessWithCode(self.logger.features()).into();
}
match self.handle_log_request(request_type) {
Ok(()) => ExtensionResponseEnum::Success().into(),
Err(e) => ExtensionResponseEnum::Failure(e).into(),
}
}
fn shutdown(&self) {
self.logger.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::OsqueryPlugin;
use std::collections::BTreeMap;
struct TestLogger {
custom_features: Option<i32>,
}
impl TestLogger {
fn new() -> Self {
Self {
custom_features: None,
}
}
fn with_features(features: i32) -> Self {
Self {
custom_features: Some(features),
}
}
}
impl LoggerPlugin for TestLogger {
fn name(&self) -> String {
"test_logger".to_string()
}
fn log_string(&self, _message: &str) -> Result<(), String> {
Ok(())
}
fn features(&self) -> i32 {
self.custom_features.unwrap_or(LoggerFeatures::LOG_STATUS)
}
}
#[test]
fn test_features_request_returns_default_log_status() {
let logger = TestLogger::new();
let wrapper = LoggerPluginWrapper::new(logger);
let mut request: BTreeMap<String, String> = BTreeMap::new();
request.insert("action".to_string(), "features".to_string());
let response = wrapper.handle_call(request);
let status = response.status.as_ref();
assert!(status.is_some(), "response should have status");
assert_eq!(
status.and_then(|s| s.code),
Some(LoggerFeatures::LOG_STATUS)
);
}
#[test]
fn test_features_request_returns_custom_features() {
let features = LoggerFeatures::LOG_STATUS | LoggerFeatures::LOG_EVENT;
let logger = TestLogger::with_features(features);
let wrapper = LoggerPluginWrapper::new(logger);
let mut request: BTreeMap<String, String> = BTreeMap::new();
request.insert("action".to_string(), "features".to_string());
let response = wrapper.handle_call(request);
let status = response.status.as_ref();
assert!(status.is_some(), "response should have status");
assert_eq!(status.and_then(|s| s.code), Some(3));
}
#[test]
fn test_features_request_returns_blank_when_no_features() {
let logger = TestLogger::with_features(LoggerFeatures::BLANK);
let wrapper = LoggerPluginWrapper::new(logger);
let mut request: BTreeMap<String, String> = BTreeMap::new();
request.insert("action".to_string(), "features".to_string());
let response = wrapper.handle_call(request);
let status = response.status.as_ref();
assert!(status.is_some(), "response should have status");
assert_eq!(status.and_then(|s| s.code), Some(LoggerFeatures::BLANK));
}
#[test]
fn test_parse_request_recognizes_features_action() {
let logger = TestLogger::new();
let wrapper = LoggerPluginWrapper::new(logger);
let mut request: BTreeMap<String, String> = BTreeMap::new();
request.insert("action".to_string(), "features".to_string());
let request_type = wrapper.parse_request(&request);
assert!(matches!(request_type, LogRequestType::Features));
}
#[test]
fn test_parse_request_ignores_other_actions() {
let logger = TestLogger::new();
let wrapper = LoggerPluginWrapper::new(logger);
let mut request: BTreeMap<String, String> = BTreeMap::new();
request.insert("action".to_string(), "unknown".to_string());
let request_type = wrapper.parse_request(&request);
assert!(matches!(request_type, LogRequestType::RawString(_)));
}
}