diff --git a/Cargo.lock b/Cargo.lock index c5371b0..bc377f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -325,6 +325,22 @@ dependencies = [ "toml 0.6.0", ] +[[package]] +name = "crypto-auditing-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.0.22", + "crypto-auditing", + "futures", + "serde_cbor 0.10.2", + "serde_json", + "tokio", + "toml 0.6.0", + "tracing", + "tracing-subscriber", +] + [[package]] name = "crypto-auditing-event-broker" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 10df3c8..1083647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] -members = ["agent", "crypto-auditing", "event-broker", "log-parser"] +members = ["agent", "client", "crypto-auditing", "event-broker", "log-parser"] resolver = "2" diff --git a/GNUmakefile b/GNUmakefile index be7b44a..e427be5 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -16,11 +16,13 @@ systemdsystemunitdir := $(shell pkg-config systemd --variable=systemdsystemunitd programs = \ ${TARGETDIR}/${PROFILE}/crypto-auditing-agent \ + ${TARGETDIR}/${PROFILE}/crypto-auditing-client \ ${TARGETDIR}/${PROFILE}/crypto-auditing-event-broker \ ${TARGETDIR}/${PROFILE}/crypto-auditing-log-parser conffiles = \ dist/conf/agent.conf \ + dist/conf/client.conf \ dist/conf/event-broker.conf .PHONY: all @@ -32,21 +34,22 @@ agent/src/bpf/vmlinux.h: $(programs): agent/src/bpf/vmlinux.h cargo build --target-dir="${TARGETDIR}" ${CARGO_ARGS} +.PHONY: install-programs +install-programs: all + for f in $(programs); do \ + install -D -t ${DESTDIR}/usr/bin "$$f"; \ + done + .PHONY: install -install: all +install: install-programs for f in $(conffiles); do \ install -D -m 644 -S .orig -t /etc/crypto-auditing "$$f"; \ done - for f in $(programs); do \ - install -D -t ${DESTDIR}/usr/bin "$$f"; \ - done install -D -m 644 -t ${DESTDIR}$(systemdsystemunitdir) dist/systemd/system/crypto-auditing-agent.service install -D -m 644 -t ${DESTDIR}$(systemdsystemunitdir) dist/systemd/system/crypto-auditing-event-broker.service install -d ${DESTDIR}/var/lib/crypto-auditing install -d ${DESTDIR}/var/log/crypto-auditing -# This only runs tests without TPM access. See tests/run.sh for -# running full testsuite with swtpm. .PHONY: check check: all cargo test --target-dir="${TARGETDIR}" diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..0e4b8fb --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "crypto-auditing-client" +description = "Event broker client for crypto-auditing project" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-or-later" +authors = ["The crypto-auditing developers"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +clap = { version = "4", features=["derive"] } +crypto-auditing = { path = "../crypto-auditing" } +futures = "0.3" +serde_cbor = "0.10" +serde_json = "1.0" +tokio = "1.23" +toml = "0.6" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features=["env-filter"] } diff --git a/client/src/config.rs b/client/src/config.rs new file mode 100644 index 0000000..22778a9 --- /dev/null +++ b/client/src/config.rs @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2022-2023 The crypto-auditing developers. + +use anyhow::{anyhow, Context as _, Result}; +use clap::{arg, command, parser::ValueSource, value_parser, ArgAction, ArgMatches, ValueEnum}; +use crypto_auditing::event_broker::SOCKET_PATH; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use toml::{Table, Value}; + +const CONFIG: &'static str = "/etc/crypto-auditing/event-broker.conf"; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum Format { + Json, + Cbor, +} + +#[derive(Debug)] +pub struct Config { + /// Path to Unix socket + pub socket_path: PathBuf, + + /// Scope to match + pub scope: Vec, + + /// Output format + pub format: Format, + + /// Path to output file + pub output: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + socket_path: PathBuf::from(SOCKET_PATH), + scope: Vec::default(), + format: Format::Json, + output: None, + } + } +} + +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!( + --"socket-path" "Path to Unix socket" + ) + .required(false) + .value_parser(value_parser!(PathBuf)) + .default_value(SOCKET_PATH), + ) + .arg( + arg!( + --scope "Scope to restrict matches" + ) + .required(false) + .value_parser(value_parser!(PathBuf)) + .action(ArgAction::Append), + ) + .arg( + arg!( + --format "Output format" + ) + .required(false) + .value_parser(value_parser!(Format)), + ) + .arg( + arg!( + --output "Path to output file" + ) + .required(false) + .value_parser(value_parser!(PathBuf)), + ) + .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("socket_path") { + self.socket_path = pathbuf_from_value(value)?; + } + + if let Some(value) = config.get("scope") { + self.scope = string_array_from_value(value)?; + } + + if let Some(value) = config.get("format") { + self.format = format_from_value(value)?; + } + + if let Some(value) = config.get("output") { + self.output = Some(pathbuf_from_value(value)?); + } + + Ok(()) + } + + fn merge_arg_matches(&mut self, matches: &ArgMatches) -> Result<()> { + if let Some(ValueSource::CommandLine) = matches.value_source("socket-path") { + self.socket_path = matches + .try_get_one::("socket-path")? + .unwrap() + .clone(); + } + + if let Some(ValueSource::CommandLine) = matches.value_source("scope") { + self.scope = matches.try_get_many("scope")?.unwrap().cloned().collect(); + } + + if let Some(ValueSource::CommandLine) = matches.value_source("format") { + self.format = *matches.try_get_one::("format")?.unwrap(); + } + + if let Some(ValueSource::CommandLine) = matches.value_source("output") { + self.output = Some(matches.try_get_one::("output")?.unwrap().clone()); + } + + Ok(()) + } +} + +fn string_array_from_value(value: &Value) -> Result> { + value + .as_array() + .ok_or_else(|| anyhow!("value must be array")) + .and_then(|array| { + array + .iter() + .map(|v| string_from_value(v)) + .collect::>>() + }) +} + +fn string_from_value(value: &Value) -> Result { + value + .as_str() + .ok_or_else(|| anyhow!("value must be string")) + .and_then(|v| Ok(v.to_string())) +} + +fn pathbuf_from_value(value: &Value) -> Result { + value + .as_str() + .ok_or_else(|| anyhow!("value must be string")) + .and_then(|v| Ok(PathBuf::from(v))) +} + +fn format_from_value(value: &Value) -> Result { + value + .as_str() + .ok_or_else(|| anyhow!("value must be format")) + .and_then(|v| Format::from_str(v, false).map_err(|e| anyhow!("{}", e))) +} diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..5d40b5b --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2022-2023 The crypto-auditing developers. + +use anyhow::{Context as _, Result}; +use crypto_auditing::event_broker::Client; +use futures::StreamExt; +use std::fs::File; +use std::io::{stdout, Write}; +use tokio::signal; +use tracing::info; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +mod config; + +fn get_writer(c: &config::Config) -> Result> { + if let Some(path) = &c.output { + Ok(File::create(path) + .map(|f| Box::new(f)) + .with_context(|| format!("unable to create file {}", path.display()))?) + } else { + Ok(Box::new(stdout())) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let config = config::Config::new()?; + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .try_init()?; + + let client = Client::new() + .address(&config.socket_path) + .scopes(&config.scope); + + let mut writer = get_writer(&config)?; + + let (_handle, mut reader) = client.start().await?; + + tokio::spawn(async move { + while let Some(group) = reader.next().await { + match config.format { + config::Format::Json => { + if let Err(e) = serde_json::to_writer_pretty(&mut writer, &group) { + info!(error = %e, + "unable to write group"); + } + } + config::Format::Cbor => { + if let Err(e) = serde_cbor::ser::to_writer(&mut writer, &group) { + info!(error = %e, + "unable to write group"); + } + } + } + } + }); + + signal::ctrl_c().await?; + + Ok(()) +} diff --git a/dist/conf/client.conf b/dist/conf/client.conf new file mode 100644 index 0000000..a43e2c6 --- /dev/null +++ b/dist/conf/client.conf @@ -0,0 +1,2 @@ +# socket_path = "/var/lib/crypto-auditing/audit.sock" +# scope = ["tls"] diff --git a/event-broker/Cargo.toml b/event-broker/Cargo.toml index 5c9e919..56bbe7e 100644 --- a/event-broker/Cargo.toml +++ b/event-broker/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "crypto-auditing-event-broker" +description = "Event broker for crypto-auditing project" version = "0.1.0" edition = "2021" license = "GPL-3.0-or-later"