From 582ba571dc7a808cff3285f125a1bc14fac2bcbe Mon Sep 17 00:00:00 2001 From: ppom Date: Wed, 22 May 2024 12:00:00 +0200 Subject: [PATCH] WIP configuration setup --- rust/Cargo.lock | 33 +++- rust/Cargo.toml | 3 + rust/TODO | 1 + rust/example.json | 122 +++++++++++++++ rust/src/config.rs | 302 +++++++++++++++++++++++++++++++++++++ rust/src/daemon.rs | 8 +- rust/src/main.rs | 13 +- rust/src/parse_duration.rs | 29 ++++ 8 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 rust/TODO create mode 100644 rust/example.json create mode 100644 rust/src/parse_duration.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8d564ae..48a2201 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -152,6 +152,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -247,6 +253,12 @@ dependencies = [ "peg", ] +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + [[package]] name = "md5" version = "0.7.0" @@ -259,6 +271,16 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -318,7 +340,10 @@ dependencies = [ "clap", "clap_complete", "jrsonnet-evaluator", + "num_cpus", "regex", + "serde", + "serde_json", ] [[package]] @@ -364,18 +389,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b8988b0..83f4b37 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,4 +10,7 @@ anyhow = "1.0.86" clap = { version = "4.5.4", features = ["derive"] } clap_complete = "4.5.2" jrsonnet-evaluator = "0.4.2" +num_cpus = "1.16.0" regex = "1.10.4" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" diff --git a/rust/TODO b/rust/TODO new file mode 100644 index 0000000..439d9e3 --- /dev/null +++ b/rust/TODO @@ -0,0 +1 @@ +cargo clippy diff --git a/rust/example.json b/rust/example.json new file mode 100644 index 0000000..8591b6d --- /dev/null +++ b/rust/example.json @@ -0,0 +1,122 @@ +{ + "concurrency": 0, + "patterns": { + "ip": { + "ignore": [ + "127.0.0.1", + "::1" + ], + "regex": "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|(?:(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" + } + }, + "start": [ + [ + "ip46tables", + "-w", + "-N", + "reaction" + ], + [ + "ip46tables", + "-w", + "-I", + "INPUT", + "-p", + "all", + "-j", + "reaction" + ], + [ + "ip46tables", + "-w", + "-I", + "FORWARD", + "-p", + "all", + "-j", + "reaction" + ] + ], + "stop": [ + [ + "ip46tables", + "-w", + "-D", + "INPUT", + "-p", + "all", + "-j", + "reaction" + ], + [ + "ip46tables", + "-w", + "-D", + "FORWARD", + "-p", + "all", + "-j", + "reaction" + ], + [ + "ip46tables", + "-w", + "-F", + "reaction" + ], + [ + "ip46tables", + "-w", + "-X", + "reaction" + ] + ], + "streams": { + "ssh": { + "cmd": [ + "journalctl", + "-n0", + "-fu", + "sshd.service" + ], + "filters": { + "failedlogin": { + "actions": { + "ban": { + "cmd": [ + "ip46tables", + "-w", + "-A", + "reaction", + "-s", + "", + "-j", + "DROP" + ] + }, + "unban": { + "after": "48h", + "cmd": [ + "ip46tables", + "-w", + "-D", + "reaction", + "-s", + "", + "-j", + "DROP" + ] + } + }, + "regex": [ + "authentication failure;.*rhost=", + "Failed password for .* from ", + "Connection (reset|closed) by (authenticating|invalid) user .* " + ], + "retry": 3, + "retryperiod": "6h" + } + } + } + } +} diff --git a/rust/src/config.rs b/rust/src/config.rs index 5d36281..c1ad99d 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -1,3 +1,305 @@ +#![allow(dead_code)] +use std::collections::BTreeMap; +use std::fs::File; +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::{anyhow, Context, Result}; +use regex::Regex; +use serde::Deserialize; + +use crate::parse_duration::parse_duration; + +pub type Patterns = BTreeMap; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Config { + concurrency: usize, + patterns: Patterns, + streams: BTreeMap, + #[serde(default)] + start: Vec>, + #[serde(default)] + stop: Vec>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pattern { + regex: String, + #[serde(default)] + ignore: Vec, + + #[serde(default, rename = "ignoreregex")] + ignore_regex: Vec, + #[serde(skip)] + compiled_ignore_regex: Vec, + + #[serde(skip)] + name: String, + #[serde(skip)] + name_with_braces: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Stream { + cmd: Vec, + filters: BTreeMap, + + #[serde(skip)] + name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Filter { + actions: BTreeMap, + #[serde(skip)] + longuest_action_duration: Duration, + + regex: Vec, + #[serde(skip)] + compiled_regex: Vec, + #[serde(skip)] + patterns: Vec, + + retry: Option, + #[serde(rename = "retryperiod")] + retry_period: Option, + retry_duration: Option, + + #[serde(skip)] + name: String, + #[serde(skip)] + stream_name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Action { + cmd: Vec, + + // TODO one shot time deserialization + after: Option, + #[serde(skip)] + after_duration: Option, + + #[serde(rename = "onexit", default = "set_false")] + on_exit: bool, + + #[serde(skip)] + name: String, + #[serde(skip)] + filter_name: String, + #[serde(skip)] + stream_name: String, +} + +fn set_0() -> u32 { + 0 +} + +fn set_false() -> bool { + false +} + +impl Config { + pub fn setup(&mut self) -> Result<()> { + self._setup() + .or_else(|msg| Err(anyhow!("Bad configuration: {}", msg))) + } + + pub fn _setup(&mut self) -> Result<(), String> { + if self.concurrency == 0 { + self.concurrency = num_cpus::get(); + } + + for (key, value) in &mut self.patterns { + value.setup(key)?; + } + + if self.streams.len() == 0 { + return Err("no streams configured".into()); + } + + for (key, value) in &mut self.streams { + value.setup(key, &self.patterns)?; + } + Ok(()) + } +} + +impl Pattern { + pub fn setup(&mut self, name: &String) -> Result<(), String> { + self._setup(name) + .or_else(|msg| Err(format!("pattern {}: {}", name, msg))) + } + + pub fn _setup(&mut self, name: &String) -> Result<(), String> { + self.name = name.clone(); + self.name_with_braces = format!("<{}>", name); + + if self.regex.len() == 0 { + return Err("regex is empty".into()); + } + let compiled = + Regex::new(&format!("^{}$", self.regex)).or_else(|err| Err(err.to_string()))?; + + self.regex = format!("(?P<{}>{})", self.name, self.regex); + + for ignore in &self.ignore { + if !compiled.is_match(&ignore) { + return Err(format!( + "ignore '{}' doesn't match pattern. It should be fixed or removed.", + ignore, + )); + } + } + + for ignore_regex in &self.ignore_regex { + let compiled_ignore = Regex::new(&format!("^{}$", ignore_regex)).or_else(|err| { + Err(format!( + "ignoreregex '{}': {}", + ignore_regex, + err.to_string() + )) + })?; + + self.compiled_ignore_regex.push(compiled_ignore); + } + + Ok(()) + } +} + +impl Stream { + fn setup(&mut self, name: &String, patterns: &Patterns) -> Result<(), String> { + self._setup(name, patterns) + .or_else(|msg| Err(format!("stream {}: {}", name, msg))) + } + + fn _setup(&mut self, name: &String, patterns: &Patterns) -> Result<(), String> { + self.name = name.clone(); + + if self.name.contains('.') { + return Err("character '.' is not allowed in stream name".into()); + } + + if self.filters.len() == 0 { + return Err("no filters configured".into()); + } + + for (key, value) in &mut self.filters { + value.setup(&name, key, patterns)?; + } + + Ok(()) + } +} + +impl Filter { + fn setup( + &mut self, + stream_name: &String, + name: &String, + patterns: &Patterns, + ) -> Result<(), String> { + self._setup(stream_name, name, patterns) + .or_else(|msg| Err(format!("filter {}: {}", name, msg))) + } + + fn _setup( + &mut self, + stream_name: &String, + name: &String, + patterns: &Patterns, + ) -> Result<(), String> { + self.stream_name = stream_name.clone(); + self.name = name.clone(); + + if self.name.contains('.') { + return Err("character '.' is not allowed in filter name".into()); + } + + if self.retry.is_some() != self.retry_period.is_some() { + return Err("retry and retryperiod must be specified altogether".into()); + } + + if self.retry.is_some_and(|r| r < 2) { + return Err("retry has been specified but is < 2".into()); + } + + if let Some(retry_period) = &self.retry_period { + self.retry_duration = + Some(parse_duration(retry_period).or_else(|err| { + Err(format!("failed to parse retry time: {}", err.to_string())) + })?); + } + + if self.regex.len() == 0 { + return Err("no regex configured".into()); + } + + for regex in &self.regex { + for (_pattern_name, pattern) in patterns { + if regex.contains(&pattern.name_with_braces) { + // TODO + } + } + } + + if self.actions.len() == 0 { + return Err("no actions configured".into()); + } + + for (key, value) in &mut self.actions { + value.setup(&stream_name, &name, key, patterns)?; + } + + Ok(()) + } +} + +impl Action { + fn setup( + &mut self, + stream_name: &String, + filter_name: &String, + name: &String, + patterns: &Patterns, + ) -> Result<(), String> { + self._setup(stream_name, filter_name, name, patterns) + .or_else(|msg| Err(format!("action {}: {}", name, msg))) + } + fn _setup( + &mut self, + stream_name: &String, + filter_name: &String, + name: &String, + _patterns: &Patterns, + ) -> Result<(), String> { + self.stream_name = stream_name.clone(); + self.filter_name = filter_name.clone(); + self.name = name.clone(); + + if self.name.contains('.') { + return Err("character '.' is not allowed in filter name".into()); + } + // for (_key, value) in &mut self.actions { + // value.setup()?; + // } + Ok(()) + } +} + +pub fn config_from_file(path: &PathBuf) -> Result { + let file = File::open(&path) + .with_context(|| format!("Failed to read configuration file: {}", path.display()))?; + let mut config: Config = + serde_json::from_reader(file).context("Failed to parse configuration file")?; + config.setup()?; + return Ok(config); } diff --git a/rust/src/daemon.rs b/rust/src/daemon.rs index cbdf94a..ad4eb74 100644 --- a/rust/src/daemon.rs +++ b/rust/src/daemon.rs @@ -1,7 +1,13 @@ use std::path::PathBuf; -use crate::cli::LogLevel; +use crate::{cli::LogLevel, config}; pub fn daemon(config_path: &PathBuf, loglevel: LogLevel, socket: &PathBuf) { println!("daemon {:?} {:?} {:?}", config_path, loglevel, socket); + let config = config::config_from_file(config_path); + if let Ok(config) = config { + dbg!(config); + } else if let Err(err) = config { + dbg!(err); + } } diff --git a/rust/src/main.rs b/rust/src/main.rs index d72f02d..9a2e2a1 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,9 +1,21 @@ +#![warn( + missing_docs, + clippy::unwrap_used, + clippy::panic, + clippy::unimplemented, + clippy::todo, + clippy::undocumented_unsafe_blocks +)] +#![forbid(unsafe_code)] + +//! Hey it's the doc! use clap::Parser; use regex::Regex; mod cli; mod config; mod daemon; +mod parse_duration; mod client; use cli::{Cli, Command}; @@ -24,7 +36,6 @@ fn main() { loglevel, socket, } => { - // daemon(&config, loglevel, &socket); } Command::Show { diff --git a/rust/src/parse_duration.rs b/rust/src/parse_duration.rs new file mode 100644 index 0000000..f77eff2 --- /dev/null +++ b/rust/src/parse_duration.rs @@ -0,0 +1,29 @@ +use anyhow::{anyhow, Result}; + +use std::time::Duration; + +pub fn parse_duration(d: &str) -> Result { + let chars = d.trim().as_bytes(); + let mut value = 0; + let mut i = 0; + while i < chars.len() && chars[i] < '0' as u8 && chars[i] > '9' as u8 { + value = value * 10 + (chars[i] - '0' as u8) as u32; + i += 1; + } + if value == 0 { + return Err(anyhow!("duration '{}' doesn't start with digits", d)); + } + let ok_secs = + |mul: u32| -> Result { Ok(Duration::from_secs(mul as u64 * value as u64)) }; + + match d[i..].trim() { + "s" | "sec" | "secs" | "second" | "seconds" => ok_secs(1), + "m" | "min" | "mins" | "minute" | "minutes" => ok_secs(60), + "h" | "hour" | "hours" => ok_secs(60 * 60), + "d" | "day" | "days" => ok_secs(24 * 60 * 60), + unit => Err(anyhow!( + "unit {} not recognised. must be one of s/sec/seconds, m/min/minutes, h/hours, d/days", + unit + )), + } +}