diff --git a/Cargo.lock b/Cargo.lock index 2bba65e..9e3f0c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,10 +420,12 @@ dependencies = [ "clap", "crypto-auditing", "hex", + "pager", "serde", "serde_cbor", "serde_json", "serde_with", + "toml", ] [[package]] @@ -535,6 +537,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + [[package]] name = "errno" version = "0.3.14" @@ -545,6 +558,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1199,6 +1222,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "pager" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" +dependencies = [ + "errno 0.2.8", + "libc", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1351,7 +1384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", - "errno", + "errno 0.3.14", "libc", "linux-raw-sys", "windows-sys 0.61.2", diff --git a/GNUmakefile b/GNUmakefile index 23316ce..e9dba9e 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -18,13 +18,14 @@ programs = \ ${TARGETDIR}/${PROFILE}/crau-agent \ ${TARGETDIR}/${PROFILE}/crau-client \ ${TARGETDIR}/${PROFILE}/crau-event-broker \ - ${TARGETDIR}/${PROFILE}/crau-log-parser \ + ${TARGETDIR}/${PROFILE}/crau-query \ ${TARGETDIR}/${PROFILE}/crau-monitor conffiles = \ dist/conf/agent.conf \ dist/conf/client.conf \ dist/conf/event-broker.conf \ + dist/conf/query.conf \ dist/conf/monitor.conf .PHONY: all diff --git a/dist/conf/query.conf b/dist/conf/query.conf new file mode 100644 index 0000000..fb4fe91 --- /dev/null +++ b/dist/conf/query.conf @@ -0,0 +1 @@ +# log_file = "/var/log/crypto-auditing/audit.cborseq" diff --git a/log-parser/Cargo.toml b/log-parser/Cargo.toml index f8cf0b3..960c0b1 100644 --- a/log-parser/Cargo.toml +++ b/log-parser/Cargo.toml @@ -15,7 +15,9 @@ serde.workspace = true serde_cbor.workspace = true serde_json.workspace = true serde_with = { workspace = true, features = ["hex"] } +toml.workspace = true +pager = "0.16" [[bin]] -name = "crau-log-parser" -path = "src/log_parser.rs" +name = "crau-query" +path = "src/query.rs" diff --git a/log-parser/src/config.rs b/log-parser/src/config.rs new file mode 100644 index 0000000..5e6ae1a --- /dev/null +++ b/log-parser/src/config.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2022-2023 The crypto-auditing developers. + +use anyhow::{Context as _, Result, anyhow}; +use clap::{ArgMatches, arg, command, parser::ValueSource, value_parser}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use toml::{Table, Value}; + +const CONFIG: &str = "/etc/crypto-auditing/query.conf"; +const LOG: &str = "/var/log/crypto-auditing/audit.cborseq"; + +#[derive(Debug)] +pub struct Config { + /// Path to output log file + pub log_file: PathBuf, +} + +impl Default for Config { + fn default() -> Self { + Self { + log_file: PathBuf::from(LOG), + } + } +} + +impl Config { + pub fn new() -> Result { + let mut config = Config::default(); + + let matches = command!() + .arg( + arg!( + -c --config "Path to configuration file" + ) + .required(false) + .value_parser(value_parser!(PathBuf)), + ) + .arg( + arg!( + --"log-file" "Path to output log file" + ) + .required(false) + .value_parser(value_parser!(PathBuf)) + .default_value("audit.cborseq"), + ) + .get_matches(); + + if let Some(config_file) = matches.get_one::("config") { + config.merge_config_file(config_file)?; + } else if Path::new(CONFIG).exists() { + config.merge_config_file(CONFIG)?; + } + + config.merge_arg_matches(&matches)?; + + Ok(config) + } + + fn merge_config_file(&mut self, file: impl AsRef) -> Result<()> { + let s = fs::read_to_string(file.as_ref()) + .with_context(|| format!("unable to read config file `{}`", file.as_ref().display()))?; + let config = Table::from_str(&s).with_context(|| { + format!("unable to parse config file `{}`", file.as_ref().display()) + })?; + + if let Some(value) = config.get("log_file") { + self.log_file = pathbuf_from_value(value)?; + } + + Ok(()) + } + + fn merge_arg_matches(&mut self, matches: &ArgMatches) -> Result<()> { + if let Some(ValueSource::CommandLine) = matches.value_source("log-file") { + self.log_file = matches.try_get_one::("log-file")?.unwrap().clone(); + } + + Ok(()) + } +} + +fn pathbuf_from_value(value: &Value) -> Result { + value + .as_str() + .ok_or_else(|| anyhow!("value must be string")) + .map(PathBuf::from) +} diff --git a/log-parser/src/log_parser.rs b/log-parser/src/query.rs similarity index 51% rename from log-parser/src/log_parser.rs rename to log-parser/src/query.rs index 3b492df..57f1da0 100644 --- a/log-parser/src/log_parser.rs +++ b/log-parser/src/query.rs @@ -2,28 +2,30 @@ // Copyright (C) 2022-2023 The crypto-auditing developers. use anyhow::{Context as _, Result}; -use clap::Parser; use crypto_auditing::types::{ContextTracker, EventGroup}; +use pager::Pager; use serde_cbor::de::Deserializer; -use std::path::PathBuf; +use std::io::{self, Write}; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -#[command(about = "Primary log parser for crypto-auditing")] -struct Cli { - /// Path to log file to parse - log_path: PathBuf, -} +mod config; fn main() -> Result<(), Box> { - let cli = Cli::parse(); - let log_file = std::fs::File::open(&cli.log_path) - .with_context(|| format!("unable to read file `{}`", cli.log_path.display()))?; + let config = config::Config::new()?; + Pager::new().skip_on_notty().setup(); + + let log_file = std::fs::File::open(&config.log_file) + .with_context(|| format!("unable to read file `{}`", config.log_file.display()))?; + let mut tracker = ContextTracker::new(None); for group in Deserializer::from_reader(&log_file).into_iter::() { tracker.handle_event_group(&group?); } let root_contexts: Vec<_> = tracker.flush(None).into_iter().collect(); - println!("{}", serde_json::to_string_pretty(&root_contexts).unwrap()); + let content = serde_json::to_string_pretty(&root_contexts)?; + if let Err(e) = io::stdout().write_all(content.as_bytes()) { + if e.kind() != io::ErrorKind::BrokenPipe { + return Err(Box::new(e)); + } + } Ok(()) }