mirror of
https://framagit.org/ppom/reaction
synced 2026-03-14 20:55:47 +01:00
WIP configuration setup
This commit is contained in:
parent
b39868b228
commit
582ba571dc
8 changed files with 505 additions and 6 deletions
33
rust/Cargo.lock
generated
33
rust/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
1
rust/TODO
Normal file
|
|
@ -0,0 +1 @@
|
|||
cargo clippy
|
||||
122
rust/example.json
Normal file
122
rust/example.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
29
rust/src/parse_duration.rs
Normal file
29
rust/src/parse_duration.rs
Normal 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
|
||||
)),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue