By providing Rust bindings for Osquery this crate facilitates the implementation of Osquery extensions.
- âś… Table plugins - Create custom tables to query system information
- âś… Logger plugins - Implement custom logging backends for osquery
- âś… Config plugins - Provide custom configuration sources for osquery
- âś… Writable tables - Support for INSERT, UPDATE, and DELETE operations
- 🦀 Pure Rust - No C/C++ dependencies, just safe Rust code
- 🚀 High performance - Minimal overhead for extensions
- 📦 Easy to use - Simple API with examples to get started quickly
Clone the repository and build the workspace:
git clone https://github.com/withzombies/osquery-rust.git
cd osquery-rust
cargo build --workspaceRun tests:
cargo test --workspaceThe project uses a workspace structure with the main library and several examples. All examples are built automatically when you build the workspace.
Here's a simple example of creating a read-only table plugin:
use osquery_rust_ng::prelude::*;
use osquery_rust_ng::plugin::{ColumnDef, ColumnType, ColumnOptions, Plugin, ReadOnlyTable};
use std::collections::BTreeMap;
struct MyTable;
impl ReadOnlyTable for MyTable {
fn name(&self) -> String {
"my_table".to_string()
}
fn columns(&self) -> Vec<ColumnDef> {
vec![
ColumnDef::new("greeting", ColumnType::Text, ColumnOptions::DEFAULT),
ColumnDef::new("count", ColumnType::Integer, ColumnOptions::DEFAULT),
]
}
fn generate(&self, _req: ExtensionPluginRequest) -> ExtensionResponse {
let row = BTreeMap::from([
("greeting".to_string(), "Hello, osquery!".to_string()),
("count".to_string(), "42".to_string()),
]);
ExtensionResponse::new(ExtensionStatus::default(), vec![row])
}
fn shutdown(&self) {
// Called when the extension is shutting down
}
}
fn main() -> std::io::Result<()> {
let mut server = Server::new(None, "/path/to/osquery/socket")?;
server.register_plugin(Plugin::readonly_table(MyTable));
server.run().map_err(std::io::Error::other)
}Version 2.0 simplifies the shutdown API by removing ShutdownReason. If upgrading from 1.x:
Before (1.x):
fn shutdown(&self, reason: ShutdownReason) {
println!("Shutting down: {reason}");
}After (2.0):
fn shutdown(&self) {
println!("Shutting down");
}This affects all plugin traits: ReadOnlyTable, Table, LoggerPlugin, and ConfigPlugin.
The Server::stop() and ServerStopHandle::stop() methods also no longer take a reason parameter.
Table plugins allow you to expose data as SQL tables in osquery. There are two types:
- Read-only tables - Implement the
ReadOnlyTabletrait - Writable tables - Implement the
Tabletrait for full CRUD operations
Logger plugins receive log data from osquery and can forward it to various backends:
use osquery_rust_ng::plugin::{LoggerPlugin, LogStatus, LoggerFeatures};
struct MyLogger;
impl LoggerPlugin for MyLogger {
fn name(&self) -> String {
"my_logger".to_string()
}
fn log_string(&self, message: &str) -> Result<(), String> {
println!("Log: {}", message);
Ok(())
}
fn log_status(&self, status: &LogStatus) -> Result<(), String> {
println!("[{}] {}:{} - {}",
status.severity, status.filename, status.line, status.message);
Ok(())
}
// Advertise support for status logs (enabled by default)
fn features(&self) -> i32 {
LoggerFeatures::LOG_STATUS
}
}The features() method tells osquery what log types your plugin supports. By default, loggers receive status logs (INFO/WARNING/ERROR from osquery internals). Available features:
LoggerFeatures::BLANK- Query results onlyLoggerFeatures::LOG_STATUS- Status logs (default)LoggerFeatures::LOG_EVENT- Event logs
Config plugins provide configuration data to osquery, allowing dynamic configuration management:
use osquery_rust_ng::plugin::ConfigPlugin;
use std::collections::HashMap;
struct MyConfig;
impl ConfigPlugin for MyConfig {
fn name(&self) -> String {
"my_config".to_string()
}
fn gen_config(&self) -> Result<HashMap<String, String>, String> {
let mut config_map = HashMap::new();
// Provide JSON configuration
let config = r#"{
"options": {
"host_identifier": "hostname",
"schedule_splay_percent": 10
},
"schedule": {
"heartbeat": {
"query": "SELECT version FROM osquery_info;",
"interval": 3600
}
}
}"#;
config_map.insert("main".to_string(), config.to_string());
Ok(config_map)
}
fn gen_pack(&self, name: &str, _value: &str) -> Result<String, String> {
// Optionally provide query packs
Err(format!("Pack '{}' not found", name))
}
}There are three ways to run your extension:
- Direct loading:
osqueryi --extension /path/to/extension - Socket connection: Run extension separately with
--socket /path/to/osquery.sock - Auto-loading: Place extension in osquery's autoload directory
See the examples directory for complete implementations.
Extensions support graceful shutdown through multiple mechanisms:
Signal Handling (recommended for production)
Use run_with_signal_handling() to automatically handle SIGTERM and SIGINT:
fn main() -> std::io::Result<()> {
let mut server = Server::new(None, "/path/to/socket")?;
server.register_plugin(Plugin::readonly_table(MyTable));
// Handles SIGTERM (systemd) and SIGINT (Ctrl+C)
server.run_with_signal_handling().map_err(std::io::Error::other)
}Programmatic Shutdown
Use ServerStopHandle to stop the server from another thread:
let mut server = Server::new(None, "/path/to/socket")?;
let handle = server.get_stop_handle();
// In another thread or signal handler:
std::thread::spawn(move || {
// ... wait for condition ...
handle.stop();
});
server.run()?;Shutdown Lifecycle
When shutdown is triggered (via signal, osquery RPC, or stop()):
- The server deregisters from osquery
- All plugins receive a
shutdown()callback - The socket file is cleaned up
run()returns
The repository includes several complete examples:
- table-proc-meminfo - Exposes
/proc/meminfoas a queryable table - writeable-table - Demonstrates INSERT, UPDATE, DELETE operations
- two-tables - Shows how to register multiple tables in one extension
- logger-file - Logger plugin that writes to files
- logger-syslog - Logger plugin that sends logs to syslog
- config-file - Config plugin that loads configuration from JSON files
- config-static - Config plugin that provides static configuration
Each example includes its own README with specific build and usage instructions.
We welcome contributions! Here's how to get started:
- Fork and clone the repository
- Install the pre-commit hook:
cp .hooks/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit
This project maintains high code quality standards:
- All code must pass
cargo fmt - No clippy warnings allowed (enforced by CI)
- All tests must pass
- Unsafe code must be documented
The pre-commit hook automatically runs these checks.
Run the full test suite:
cargo test --workspace- Create a feature branch from
main - Write tests for new functionality
- Ensure all checks pass
- Submit a PR with a clear description
- Address review feedback
Please report issues on GitHub with:
- osquery version
- Rust version
- Operating system
- Steps to reproduce
- Expected vs actual behavior
The project is organized as a Cargo workspace:
- osquery-rust/ - The main library crate with Thrift bindings and plugin framework
- examples/ - Working examples demonstrating different plugin types:
table-proc-meminfo/- Read-only table examplewriteable-table/- Full CRUD table exampletwo-tables/- Multiple tables in one extensionlogger-file/- File logger pluginlogger-syslog/- Syslog logger pluginconfig-file/- An example that loads a config from a json fileconfig-static/- An example that provides a static config
- Examples: osquery-rust by example
- API Documentation: docs.rs/osquery-rust-ng
This project contributed the support for Unix Domain Sockets to Apache Thrift's Rust crate.
This project was initially forked from polarlab's osquery-rust project.