Skip to content

Commit 4385277

Browse files
committed
Add new tool "crau-monitor"
It is a common use-case to monitor cryptographic usage on the system at real time. This adds a dedicated CLI tool, crau-monitor, by combining the functionalities of event-broker and client. Unlike those, crau-monitor doesn't require any system service but directly reads the log file. Signed-off-by: Daiki Ueno <[email protected]>
1 parent 41f298a commit 4385277

File tree

6 files changed

+469
-2
lines changed

6 files changed

+469
-2
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ members = [
55
"client",
66
"crypto-auditing",
77
"event-broker",
8-
"log-parser"
8+
"log-parser",
9+
"monitor"
910
]
1011
resolver = "2"
1112

crypto-auditing/src/types.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
// Copyright (C) 2022-2023 The crypto-auditing developers.
33

4-
use serde::{Deserialize, Serialize, ser::{SerializeSeq, Serializer}};
4+
use serde::{
5+
Deserialize, Serialize,
6+
ser::{SerializeSeq, Serializer},
7+
};
58
use serde_with::{hex::Hex, serde_as};
69
use std::cell::RefCell;
710
use std::collections::BTreeMap;

monitor/Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "crypto-auditing-event-monitor"
3+
description = "Event monitor for crypto-auditing project"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
authors.workspace = true
8+
9+
[dependencies]
10+
anyhow.workspace = true
11+
clap = { workspace = true, features = ["cargo", "derive"] }
12+
crypto-auditing.workspace = true
13+
futures.workspace = true
14+
hex.workspace = true
15+
inotify.workspace = true
16+
serde = { workspace = true, features = ["rc"] }
17+
serde_cbor.workspace = true
18+
serde_json.workspace = true
19+
serde_with = { workspace = true, features = ["hex"] }
20+
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
21+
tokio-serde.workspace = true
22+
tokio-stream.workspace = true
23+
tokio-util.workspace = true
24+
toml.workspace = true
25+
tracing-subscriber = { workspace = true, features = ["env-filter"] }
26+
tracing.workspace = true
27+
28+
[dev-dependencies]
29+
tempfile.workspace = true
30+
31+
[[bin]]
32+
name = "crau-monitor"
33+
path = "src/monitor.rs"

monitor/src/config.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
// Copyright (C) 2022-2025 The crypto-auditing developers.
3+
4+
use anyhow::{Context as _, Result, anyhow};
5+
use clap::{ArgAction, ArgMatches, arg, command, parser::ValueSource, value_parser};
6+
use std::fs;
7+
use std::path::{Path, PathBuf};
8+
use std::str::FromStr;
9+
use std::time::Duration;
10+
use toml::{Table, Value};
11+
12+
const CONFIG: &str = "/etc/crypto-auditing/monitor.conf";
13+
const LOG: &str = "/var/log/crypto-auditing/audit.cborseq";
14+
15+
#[derive(Debug)]
16+
pub struct Config {
17+
/// Path to output log file
18+
pub log_file: PathBuf,
19+
20+
/// Scope to match
21+
pub scope: Vec<String>,
22+
23+
/// Event window
24+
pub event_window: Duration,
25+
}
26+
27+
impl Default for Config {
28+
fn default() -> Self {
29+
Self {
30+
log_file: PathBuf::from(LOG),
31+
scope: Vec::default(),
32+
event_window: Duration::from_secs(3),
33+
}
34+
}
35+
}
36+
37+
fn parse_duration(arg: &str) -> Result<Duration, std::num::ParseIntError> {
38+
let milliseconds = arg.parse()?;
39+
Ok(Duration::from_millis(milliseconds))
40+
}
41+
42+
impl Config {
43+
pub fn new() -> Result<Self> {
44+
let mut config = Config::default();
45+
46+
let matches = command!()
47+
.arg(
48+
arg!(
49+
-c --config <FILE> "Path to configuration file"
50+
)
51+
.required(false)
52+
.value_parser(value_parser!(PathBuf)),
53+
)
54+
.arg(
55+
arg!(
56+
--"log-file" <FILE> "Path to output log file"
57+
)
58+
.required(false)
59+
.value_parser(value_parser!(PathBuf))
60+
.default_value("audit.cborseq"),
61+
)
62+
.arg(
63+
arg!(
64+
--scope <SCOPE> "Scope to restrict matches"
65+
)
66+
.required(false)
67+
.value_parser(value_parser!(String))
68+
.action(ArgAction::Append),
69+
)
70+
.arg(
71+
arg!(
72+
--"event-window" <WINDOW> "Event window"
73+
)
74+
.required(false)
75+
.value_parser(parse_duration),
76+
)
77+
.get_matches();
78+
79+
if let Some(config_file) = matches.get_one::<PathBuf>("config") {
80+
config.merge_config_file(config_file)?;
81+
} else if Path::new(CONFIG).exists() {
82+
config.merge_config_file(CONFIG)?;
83+
}
84+
85+
config.merge_arg_matches(&matches)?;
86+
87+
Ok(config)
88+
}
89+
90+
fn merge_config_file(&mut self, file: impl AsRef<Path>) -> Result<()> {
91+
let s = fs::read_to_string(file.as_ref())
92+
.with_context(|| format!("unable to read config file `{}`", file.as_ref().display()))?;
93+
let config = Table::from_str(&s).with_context(|| {
94+
format!("unable to parse config file `{}`", file.as_ref().display())
95+
})?;
96+
97+
if let Some(value) = config.get("log_file") {
98+
self.log_file = pathbuf_from_value(value)?;
99+
}
100+
101+
if let Some(value) = config.get("scope") {
102+
self.scope = string_array_from_value(value)?;
103+
}
104+
105+
if let Some(value) = config.get("event_window") {
106+
self.event_window = duration_millis_from_value(value)?;
107+
}
108+
109+
Ok(())
110+
}
111+
112+
fn merge_arg_matches(&mut self, matches: &ArgMatches) -> Result<()> {
113+
if let Some(ValueSource::CommandLine) = matches.value_source("log-file") {
114+
self.log_file = matches.try_get_one::<PathBuf>("log-file")?.unwrap().clone();
115+
}
116+
117+
if let Some(ValueSource::CommandLine) = matches.value_source("event-window") {
118+
self.event_window = *matches.try_get_one::<Duration>("event-window")?.unwrap();
119+
}
120+
121+
if let Some(ValueSource::CommandLine) = matches.value_source("scope") {
122+
self.scope = matches.try_get_many("scope")?.unwrap().cloned().collect();
123+
}
124+
125+
Ok(())
126+
}
127+
}
128+
129+
fn string_array_from_value(value: &Value) -> Result<Vec<String>> {
130+
value
131+
.as_array()
132+
.ok_or_else(|| anyhow!("value must be array"))
133+
.and_then(|array| {
134+
array
135+
.iter()
136+
.map(string_from_value)
137+
.collect::<Result<Vec<String>>>()
138+
})
139+
}
140+
141+
fn string_from_value(value: &Value) -> Result<String> {
142+
value
143+
.as_str()
144+
.ok_or_else(|| anyhow!("value must be string"))
145+
.map(|v| v.to_string())
146+
}
147+
148+
fn pathbuf_from_value(value: &Value) -> Result<PathBuf> {
149+
value
150+
.as_str()
151+
.ok_or_else(|| anyhow!("value must be string"))
152+
.map(PathBuf::from)
153+
}
154+
155+
fn duration_millis_from_value(value: &Value) -> Result<Duration> {
156+
value
157+
.as_integer()
158+
.ok_or_else(|| anyhow!("value must be duration in milliseconds"))
159+
.map(|v| Duration::from_millis(v as u64))
160+
}

0 commit comments

Comments
 (0)