pub mod clean;
pub mod cli;
pub mod config;
pub mod dir_cache;
pub mod fid_parser;
pub mod filters;
pub mod help;
pub mod metrics;
pub mod monitor;
pub mod monitored;
pub mod proc_cache;
pub mod query;
pub mod socket;
pub mod utils;
pub mod watchdog;
pub use cli::{AddArgs, ChangesArgs, CleanArgs, QueryArgs};
pub use utils::{
SizeFilter, SizeOp, TimeFilter, TimeOp, format_datetime, parse_size, parse_size_filter,
parse_time, parse_time_filter,
};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fs;
use std::path::PathBuf;
pub struct DaemonLock {
#[allow(dead_code)]
listener: std::os::unix::net::UnixListener,
path: PathBuf,
}
impl DaemonLock {
pub fn acquire(uid: u32) -> Result<Self> {
let path = PathBuf::from(format!("/tmp/fsmon-{}.lock.sock", uid));
let listener = match std::os::unix::net::UnixListener::bind(&path) {
Ok(l) => l,
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
if std::os::unix::net::UnixStream::connect(&path).is_err() {
let _ = fs::remove_file(&path);
std::os::unix::net::UnixListener::bind(&path).map_err(|e| {
anyhow::anyhow!("Another fsmon daemon is already running: {}", e)
})?
} else {
return Err(anyhow::anyhow!("Another fsmon daemon is already running"));
}
}
Err(e) => return Err(anyhow::anyhow!("Failed to bind lock socket: {}", e)),
};
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o666));
crate::config::chown_to_original_user(&path);
Ok(DaemonLock { listener, path })
}
}
impl Drop for DaemonLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
use std::str::FromStr;
pub const DEFAULT_KEEP_DAYS: u32 = 30;
pub const DEFAULT_MAX_SIZE: &str = ">=1GB";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EventType {
Access,
Modify,
CloseWrite,
CloseNowrite,
Open,
OpenExec,
Attrib,
Create,
Delete,
DeleteSelf,
MovedFrom,
MovedTo,
MoveSelf,
FsError,
}
impl EventType {
pub const ALL: &'static [EventType] = &[
EventType::Access,
EventType::Modify,
EventType::CloseWrite,
EventType::CloseNowrite,
EventType::Open,
EventType::OpenExec,
EventType::Attrib,
EventType::Create,
EventType::Delete,
EventType::DeleteSelf,
EventType::MovedFrom,
EventType::MovedTo,
EventType::MoveSelf,
EventType::FsError,
];
}
impl fmt::Display for EventType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
EventType::Access => "ACCESS",
EventType::Modify => "MODIFY",
EventType::CloseWrite => "CLOSE_WRITE",
EventType::CloseNowrite => "CLOSE_NOWRITE",
EventType::Open => "OPEN",
EventType::OpenExec => "OPEN_EXEC",
EventType::Attrib => "ATTRIB",
EventType::Create => "CREATE",
EventType::Delete => "DELETE",
EventType::DeleteSelf => "DELETE_SELF",
EventType::MovedFrom => "MOVED_FROM",
EventType::MovedTo => "MOVED_TO",
EventType::MoveSelf => "MOVE_SELF",
EventType::FsError => "FS_ERROR",
};
write!(f, "{}", s)
}
}
impl FromStr for EventType {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"ACCESS" => Ok(EventType::Access),
"MODIFY" => Ok(EventType::Modify),
"CLOSE_WRITE" => Ok(EventType::CloseWrite),
"CLOSE_NOWRITE" => Ok(EventType::CloseNowrite),
"OPEN" => Ok(EventType::Open),
"OPEN_EXEC" => Ok(EventType::OpenExec),
"ATTRIB" => Ok(EventType::Attrib),
"CREATE" => Ok(EventType::Create),
"DELETE" => Ok(EventType::Delete),
"DELETE_SELF" => Ok(EventType::DeleteSelf),
"MOVED_FROM" => Ok(EventType::MovedFrom),
"MOVED_TO" => Ok(EventType::MovedTo),
"MOVE_SELF" => Ok(EventType::MoveSelf),
"FS_ERROR" => Ok(EventType::FsError),
_ => Err(format!("Unknown event type: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEvent {
pub time: DateTime<Utc>,
pub event_type: EventType,
pub path: PathBuf,
pub pid: u32,
pub cmd: String,
pub user: String,
pub file_size: u64,
#[serde(default)]
pub ppid: u32,
#[serde(default)]
pub tgid: u32,
pub chain: String,
}
impl FileEvent {
pub fn to_jsonl_string(&self) -> String {
serde_json::to_string(self).expect("FileEvent serialization should not fail")
}
pub fn to_jsonl_string_local(&self) -> String {
use chrono::TimeZone;
let local_time = chrono::Local
.from_utc_datetime(&self.time.naive_utc())
.to_rfc3339();
let json = serde_json::to_string(self).expect("FileEvent serialization");
if let Some(start) = json.find("\"time\":\"") {
let val_start = start + 8; if let Some(end) = json[val_start..].find('"') {
let val_end = val_start + end;
let mut out = String::with_capacity(json.len() + 10);
out.push_str(&json[..val_start]);
out.push_str(&local_time);
out.push_str(&json[val_end..]);
return out;
}
}
json
}
pub fn from_jsonl_str(s: &str) -> Option<Self> {
serde_json::from_str(s).ok()
}
}
pub fn parse_log_line_jsonl(line: &str) -> Option<FileEvent> {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
FileEvent::from_jsonl_str(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_to_jsonl_string_field_order() {
let ev = FileEvent {
time: Utc::now(),
event_type: EventType::Create,
path: std::path::PathBuf::from("/tmp/test.txt"),
pid: 1234,
cmd: "touch".into(),
user: "pilot".into(),
file_size: 0,
ppid: 100,
tgid: 1234,
chain: "1234|touch|pilot;100|bash|pilot".into(),
};
let normal = ev.to_jsonl_string();
let local = ev.to_jsonl_string_local();
fn field_names(s: &str) -> Vec<String> {
s[1..s.len() - 1]
.split(',')
.map(|p| p.split(':').next().unwrap().trim_matches('"').to_string())
.collect()
}
assert_eq!(
field_names(&normal),
field_names(&local),
"field order must be identical"
);
assert!(
normal.contains("\"time\":\"") && normal.contains("Z\""),
"normal uses UTC Z"
);
assert!(
local.contains("+"),
"local time should have +HH:MM offset, got: {}",
&local[..local.len().min(120)]
);
}
}