WIP configuration setup

This commit is contained in:
ppom 2024-05-22 12:00:00 +02:00
commit 582ba571dc
8 changed files with 505 additions and 6 deletions

33
rust/Cargo.lock generated
View file

@ -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",

View file

@ -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"

1
rust/TODO Normal file
View file

@ -0,0 +1 @@
cargo clippy

122
rust/example.json Normal file
View file

@ -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",
"<ip>",
"-j",
"DROP"
]
},
"unban": {
"after": "48h",
"cmd": [
"ip46tables",
"-w",
"-D",
"reaction",
"-s",
"<ip>",
"-j",
"DROP"
]
}
},
"regex": [
"authentication failure;.*rhost=<ip>",
"Failed password for .* from <ip>",
"Connection (reset|closed) by (authenticating|invalid) user .* <ip>"
],
"retry": 3,
"retryperiod": "6h"
}
}
}
}
}

View file

@ -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<String, Pattern>;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
concurrency: usize,
patterns: Patterns,
streams: BTreeMap<String, Stream>,
#[serde(default)]
start: Vec<Vec<String>>,
#[serde(default)]
stop: Vec<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Pattern {
regex: String,
#[serde(default)]
ignore: Vec<String>,
#[serde(default, rename = "ignoreregex")]
ignore_regex: Vec<String>,
#[serde(skip)]
compiled_ignore_regex: Vec<Regex>,
#[serde(skip)]
name: String,
#[serde(skip)]
name_with_braces: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Stream {
cmd: Vec<String>,
filters: BTreeMap<String, Filter>,
#[serde(skip)]
name: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Filter {
actions: BTreeMap<String, Action>,
#[serde(skip)]
longuest_action_duration: Duration,
regex: Vec<String>,
#[serde(skip)]
compiled_regex: Vec<Regex>,
#[serde(skip)]
patterns: Vec<Pattern>,
retry: Option<u32>,
#[serde(rename = "retryperiod")]
retry_period: Option<String>,
retry_duration: Option<Duration>,
#[serde(skip)]
name: String,
#[serde(skip)]
stream_name: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Action {
cmd: Vec<String>,
// TODO one shot time deserialization
after: Option<String>,
#[serde(skip)]
after_duration: Option<Duration>,
#[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<Config> {
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);
}

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -0,0 +1,29 @@
use anyhow::{anyhow, Result};
use std::time::Duration;
pub fn parse_duration(d: &str) -> Result<Duration> {
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<Duration> { 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
)),
}
}