use getopts::Options;
use std::collections::HashSet;
use std::env;
use std::error::Error;
use std::fs;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::ops::AddAssign;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::io::FromRawFd;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use nix::unistd::{chown, setresgid, setresuid, Uid, User};
use caps::securebits::set_keepcaps;
use caps::{CapSet, Capability};
use serde::Serialize;
use laurel::coalesce::Coalesce;
use laurel::config::{Config, Logfile};
use laurel::rotate::FileRotate;
mod syslog {
use libc::{openlog, syslog, LOG_CRIT, LOG_DAEMON, LOG_ERR, LOG_INFO, LOG_PERROR, LOG_WARNING};
use std::ffi::CString;
use std::mem::forget;
pub fn init(progname: &str) {
let ident = CString::new(progname).unwrap();
unsafe { openlog(ident.as_ptr(), LOG_PERROR, LOG_DAEMON) };
forget(ident);
}
pub fn log_info(message: &str) {
let fs = CString::new("%s").unwrap();
let s = CString::new(message).unwrap();
unsafe { syslog(LOG_INFO | LOG_DAEMON, fs.as_ptr(), s.as_ptr()) };
}
pub fn log_warn(message: &str) {
let fs = CString::new("%s").unwrap();
let s = CString::new(message).unwrap();
unsafe { syslog(LOG_WARNING | LOG_DAEMON, fs.as_ptr(), s.as_ptr()) };
}
pub fn log_err(message: &str) {
let fs = CString::new("%s").unwrap();
let s = CString::new(message).unwrap();
unsafe { syslog(LOG_ERR | LOG_DAEMON, fs.as_ptr(), s.as_ptr()) };
}
pub fn log_crit(message: &str) {
let fs = CString::new("%s").unwrap();
let s = CString::new(message).unwrap();
unsafe { syslog(LOG_CRIT | LOG_DAEMON, fs.as_ptr(), s.as_ptr()) };
}
}
use syslog::{log_crit, log_err, log_info, log_warn};
#[derive(Default, Serialize)]
struct Stats {
lines: u64,
events: u64,
errors: u64,
}
impl AddAssign for Stats {
fn add_assign(&mut self, other: Self) {
*self = Self {
lines: self.lines + other.lines,
events: self.events + other.events,
errors: self.errors + other.errors,
};
}
}
fn drop_privileges(runas_user: &User) -> Result<(), Box<dyn Error>> {
set_keepcaps(true)?;
let uid = runas_user.uid;
let gid = runas_user.gid;
setresgid(gid, gid, gid).map_err(|e| format!("setresgid({}): {}", uid, e))?;
setresuid(uid, uid, uid).map_err(|e| format!("setresuid({}): {}", gid, e))?;
let mut capabilities = HashSet::new();
capabilities.insert(Capability::CAP_SYS_PTRACE);
capabilities.insert(Capability::CAP_DAC_READ_SEARCH);
caps::set(None, CapSet::Permitted, &capabilities)
.map_err(|e| format!("set effective capabilities: {}", e))?;
caps::set(None, CapSet::Effective, &capabilities)
.map_err(|e| format!("set effective capabilities: {}", e))?;
caps::set(None, CapSet::Inheritable, &HashSet::new())
.map_err(|e| format!("set inherited capabilities: {}", e))?;
set_keepcaps(false)?;
Ok(())
}
struct Logger {
prefix: Option<String>,
output: BufWriter<Box<dyn Write>>,
}
impl Logger {
fn log<S: Serialize>(&mut self, message: S) {
if let Some(prefix) = &self.prefix {
self.output.write_all(prefix.as_bytes()).unwrap();
}
serde_json::to_writer(&mut self.output, &message).unwrap();
self.output.write_all(b"\n").unwrap();
self.output.flush().unwrap();
}
fn new(def: &Logfile, dir: &Path) -> Result<Self, Box<dyn Error>> {
match &def.file {
p if p.as_os_str() == "-" => Ok(Logger {
prefix: def.line_prefix.clone(),
output: BufWriter::new(Box::new(io::stdout())),
}),
p if p.has_root() && p.parent() != None => Err(format!(
"invalid file directory={} file={}",
dir.to_string_lossy(),
p.to_string_lossy()
)
.into()),
p => {
let mut filename = dir.to_path_buf();
filename.push(&p);
let mut rot = FileRotate::new(filename);
for user in &def.clone().users.unwrap_or_default() {
rot = rot.with_uid(
User::from_name(user)?
.ok_or_else(|| format!("user {} not found", &user))?
.uid,
);
}
if let Some(generations) = &def.generations {
rot = rot.with_generations(*generations);
}
if let Some(filesize) = &def.size {
rot = rot.with_filesize(*filesize);
}
Ok(Logger {
prefix: def.line_prefix.clone(),
output: BufWriter::new(Box::new(rot)),
})
}
}
}
}
const LAUREL_VERSION: &str = env!("CARGO_PKG_VERSION");
fn run_app() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = env::args().collect();
let mut opts = Options::new();
opts.optopt("c", "config", "Configuration file", "FILE");
opts.optflag("d", "dry-run", "Only parse configuration and exit");
opts.optflag("h", "help", "Print short help text and exit");
opts.optflag("v", "version", "Print version and exit");
let matches = opts.parse(&args[1..])?;
if matches.opt_present("h") {
println!("{}", opts.usage(&args[0]));
return Ok(());
}
if matches.opt_present("v") {
println!("{}", LAUREL_VERSION);
return Ok(());
}
let config: Config = match matches.opt_str("c") {
Some(f_name) => {
if fs::metadata(&f_name)?.permissions().mode() & 0o002 != 0 {
return Err(format!("Config file {} must not be world-writable", f_name).into());
}
let lines = fs::read(&f_name).map_err(|e| format!("read {}: {}", f_name, e))?;
toml::from_str(&String::from_utf8(lines)?)
.map_err(|e| format!("parse {}: {}", f_name, e))?
}
None => Config::default(),
};
let runas_user = match config.user {
Some(ref username) => {
User::from_name(username)?.ok_or_else(|| format!("user {} not found", username))?
}
None => {
let uid = Uid::effective();
User::from_uid(uid)?.ok_or_else(|| format!("uid {} not found", uid))?
}
};
if matches.opt_present("d") {
println!("Laurel {}: Config ok.", LAUREL_VERSION);
return Ok(());
}
let dir = config
.directory
.clone()
.unwrap_or_else(|| Path::new(".").to_path_buf());
if dir.exists() {
if !dir.is_dir() {
return Err(format!("{} is not a directory", dir.to_string_lossy()).into());
}
if dir.metadata()?.permissions().mode() & 0o002 != 0 {
log_warn(&format!(
"Base directory {} must not be world-wirtable",
dir.to_string_lossy()
));
}
} else {
fs::create_dir_all(&dir)
.map_err(|e| format!("create_dir: {}: {}", dir.to_string_lossy(), e))?;
}
chown(&dir, Some(runas_user.uid), Some(runas_user.gid))
.map_err(|e| format!("chown: {}: {}", dir.to_string_lossy(), e))?;
fs::set_permissions(&dir, PermissionsExt::from_mode(0o755))
.map_err(|e| format!("chmod: {}: {}", dir.to_string_lossy(), e))?;
let logger = std::cell::RefCell::new(Logger::new(&config.auditlog, &dir)?);
let mut debug_logger = if let Some(l) = &config.debug.log {
Some(Logger::new(l, &dir)?)
} else {
None
};
let mut error_logger = if let Some(def) = &config.debug.parse_error_log {
let mut filename = dir;
filename.push(&def.file);
let mut rot = FileRotate::new(filename);
for user in &def.clone().users.unwrap_or_default() {
rot = rot.with_uid(
User::from_name(user)?
.ok_or_else(|| format!("user {} not found", &user))?
.uid,
);
}
if let Some(generations) = &def.generations {
rot = rot.with_generations(*generations);
}
if let Some(filesize) = &def.size {
rot = rot.with_filesize(*filesize);
}
Some(rot)
} else {
None
};
if !Uid::effective().is_root() {
log_warn("Not dropping privileges -- not running as root");
} else if runas_user.uid.is_root() {
log_warn("Not dropping privileges -- no user configured");
} else if let Err(e) = drop_privileges(&runas_user) {
return Err(e);
}
log_info(&format!(
"Started {} running version {}",
&args[0], LAUREL_VERSION
));
log_info(&format!(
"Running with EUID {} using config {}",
Uid::effective().as_raw(),
&config
));
let mut coalesce = Coalesce::new(|e| logger.borrow_mut().log(e));
coalesce.settings = config.make_coalesce_settings();
coalesce.initialize()?;
let mut line: Vec<u8> = Vec::new();
let mut stats = Stats::default();
let mut overall_stats = Stats::default();
let mut input = BufReader::with_capacity(1 << 20, unsafe { std::fs::File::from_raw_fd(0) });
let statusreport_period = config.statusreport_period.map(Duration::from_secs);
let mut statusreport_last_t = SystemTime::now();
let dump_state_period = config.debug.dump_state_period.map(Duration::from_secs);
let mut dump_state_last_t = SystemTime::now();
loop {
line.clear();
if input.read_until(b'\n', &mut line)? == 0 {
break;
}
stats.lines += 1;
match coalesce.process_line(line.clone()) {
Ok(()) => (),
Err(e) => {
stats.errors += 1;
if let Some(ref mut l) = error_logger {
l.write_all(&line)?;
l.flush()?;
}
let line = String::from_utf8_lossy(&line).replace('\n', "");
log_err(&format!("Error {} processing msg: {}", e, &line));
continue;
}
};
if let Some(statusreport_period_t) = statusreport_period {
if statusreport_period_t.as_secs() > 0
&& statusreport_last_t.elapsed()? >= statusreport_period_t
{
log_info(&format!("Laurel version {}", LAUREL_VERSION));
log_info( &format!(
"Parsing stats (until now): processed {} lines {} events with {} errors in total",
&stats.lines, &stats.events, &stats.errors ) );
log_info(&format!(
"Running with EUID {} using config {}",
Uid::effective().as_raw(),
&config
));
overall_stats += stats;
stats = Stats::default();
statusreport_last_t = SystemTime::now();
}
}
if let (Some(dl), Some(p)) = (&mut debug_logger, &dump_state_period) {
if dump_state_last_t.elapsed()? >= *p {
coalesce.dump_state(&mut dl.output)?;
dump_state_last_t = SystemTime::now();
}
}
}
if let Some(statusreport_period_t) = statusreport_period {
if statusreport_period_t.as_secs() > 0 {
stats = overall_stats;
}
}
log_info(&format!(
"Stopped {} processed {} lines {} events with {} errors in total",
&args[0], &stats.lines, &stats.events, &stats.errors
));
Ok(())
}
pub fn main() {
let progname = Arc::new(env::args().next().unwrap_or_else(|| "laurel".to_string()));
syslog::init(&progname);
{
std::panic::set_hook(Box::new(move |panic_info| {
let payload = panic_info.payload();
let message = if let Some(s) = payload.downcast_ref::<&str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s
} else {
"(unknown error)"
};
let location = match panic_info.location() {
Some(l) => format!("{}:{},{}", l.file(), l.line(), l.column()),
None => "(unknown)".to_string(),
};
let e = format!("fatal error '{}' at {}", &message, &location);
eprintln!("{}: {}", &progname, &e);
log_crit(&e);
}));
}
match run_app() {
Ok(_) => (),
Err(e) => {
let e = e.to_string();
log_crit(&e);
std::process::abort();
}
};
}