From 8fa347410e21c63c2af9403a14cd4f1158e15f70 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Tue, 27 Jun 2023 00:28:07 -0600 Subject: [PATCH] suricatactl: rust version of suricatactl --- configure.ac | 1 + python/Makefile.am | 9 -- rust/Cargo.lock.in | 114 +++++++++++++++++++++++ rust/Cargo.toml.in | 2 + rust/Makefile.am | 8 +- rust/suricatactl/Cargo.toml.in | 16 ++++ rust/suricatactl/Makefile.am | 3 + rust/suricatactl/src/filestore/mod.rs | 4 + rust/suricatactl/src/filestore/prune.rs | 116 ++++++++++++++++++++++++ rust/suricatactl/src/main.rs | 72 +++++++++++++++ 10 files changed, 334 insertions(+), 11 deletions(-) create mode 100644 rust/suricatactl/Cargo.toml.in create mode 100644 rust/suricatactl/Makefile.am create mode 100644 rust/suricatactl/src/filestore/mod.rs create mode 100644 rust/suricatactl/src/filestore/prune.rs create mode 100644 rust/suricatactl/src/main.rs diff --git a/configure.ac b/configure.ac index 3fe429afe1..b5d0e00c56 100644 --- a/configure.ac +++ b/configure.ac @@ -2537,6 +2537,7 @@ AM_CONDITIONAL([BUILD_SHARED_LIBRARY], [test "x$enable_shared" = "xyes"] && [tes AC_CONFIG_FILES(Makefile src/Makefile rust/Makefile rust/Cargo.lock rust/Cargo.toml rust/derive/Cargo.toml rust/.cargo/config.toml) AC_CONFIG_FILES(rust/sys/Makefile rust/sys/Cargo.toml) +AC_CONFIG_FILES(rust/suricatactl/Makefile rust/suricatactl/Cargo.toml) AC_CONFIG_FILES(rust/suricatasc/Makefile rust/suricatasc/Cargo.toml) AC_CONFIG_FILES(qa/Makefile qa/coccinelle/Makefile) AC_CONFIG_FILES(rules/Makefile doc/Makefile doc/userguide/Makefile) diff --git a/python/Makefile.am b/python/Makefile.am index 968f673376..f34f4e4303 100644 --- a/python/Makefile.am +++ b/python/Makefile.am @@ -11,8 +11,6 @@ LIBS = \ suricata/sc/suricatasc.py \ suricatasc/__init__.py -BINS = suricatactl - EXTRA_DIST = $(LIBS) bin suricata/config/defaults.py if HAVE_PYTHON @@ -22,20 +20,13 @@ install-exec-local: install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/ctl" install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/sc" install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricatasc" - install -d -m 0755 "$(DESTDIR)$(prefix)/bin" for src in $(LIBS); do \ install -m 0644 $(srcdir)/$$src "$(DESTDIR)$(prefix)/lib/suricata/python/$$src"; \ done install suricata/config/defaults.py \ "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/config/defaults.py" - for bin in $(BINS); do \ - cat "$(srcdir)/bin/$$bin" | \ - sed -e "1 s,.*,#"'!'" ${HAVE_PYTHON}," > "${DESTDIR}$(bindir)/$$bin"; \ - chmod 0755 "$(DESTDIR)$(bindir)/$$bin"; \ - done uninstall-local: - rm -f $(DESTDIR)$(bindir)/suricatactl rm -rf $(DESTDIR)$(prefix)/lib/suricata/python clean-local: diff --git a/rust/Cargo.lock.in b/rust/Cargo.lock.in index a867f3966e..484a92f8ce 100644 --- a/rust/Cargo.lock.in +++ b/rust/Cargo.lock.in @@ -731,6 +731,16 @@ dependencies = [ "nom-derive", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.2.1" @@ -885,6 +895,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "phf" version = "0.10.1" @@ -923,6 +939,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "polyval" version = "0.5.3" @@ -1217,6 +1239,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1325,6 +1356,16 @@ dependencies = [ name = "suricata-sys" version = "8.0.0-dev" +[[package]] +name = "suricatactl" +version = "8.0.0-dev" +dependencies = [ + "clap", + "regex", + "tracing", + "tracing-subscriber", +] + [[package]] name = "suricatasc" version = "8.0.0-dev" @@ -1455,6 +1496,16 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -1509,6 +1560,63 @@ dependencies = [ "serde", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "typenum" version = "1.18.0" @@ -1561,6 +1669,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index fd4613b9ea..54f7924484 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -10,12 +10,14 @@ rust-version = "1.67.1" members = [ ".", "derive", + "suricatactl", "suricatasc", "sys", ] default-members = [ ".", + "suricatactl", "suricatasc", ] diff --git a/rust/Makefile.am b/rust/Makefile.am index 3630fdab89..f6a8a364e2 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -1,5 +1,6 @@ SUBDIRS = sys \ - suricatasc + suricatasc \ + suricatactl EXTRA_DIST = src derive \ .cargo/config.toml.in \ @@ -10,7 +11,8 @@ EXTRA_DIST = src derive \ derive/Cargo.toml \ sys \ sys/Cargo.toml \ - suricatasc + suricatasc \ + suricatactl if !DEBUG RELEASE = --release @@ -73,6 +75,7 @@ all-local: Cargo.toml install-exec-local: install -d -m 0755 "$(DESTDIR)$(bindir)" install -m 0755 $(RUST_SURICATA_LIBDIR)/suricatasc "$(DESTDIR)$(bindir)/suricatasc" + install -m 0755 $(RUST_SURICATA_LIBDIR)/suricatactl "$(DESTDIR)$(bindir)/suricatactl" install-library: $(MKDIR_P) "$(DESTDIR)$(libdir)" @@ -81,6 +84,7 @@ install-library: uninstall-local: rm -f "$(DESTDIR)$(libdir)/$(RUST_SURICATA_LIBNAME)" rm -f "$(DESTDIR)$(bindir)/suricatasc" + rm -f "$(DESTDIR)$(bindir)/suricatactl" clean-local: rm -rf target gen diff --git a/rust/suricatactl/Cargo.toml.in b/rust/suricatactl/Cargo.toml.in new file mode 100644 index 0000000000..d60eab1d88 --- /dev/null +++ b/rust/suricatactl/Cargo.toml.in @@ -0,0 +1,16 @@ +[package] +name = "suricatactl" +version = "@PACKAGE_VERSION@" +edition = "2021" +license = "GPL-2.0-only" + +[[bin]] +name = "suricatactl" + +[dependencies] +regex = "~1.5.5" +tracing = "0.1" +tracing-subscriber = "0.3" + +# 4.0 is the newest version that builds with Rust 1.67.1. +clap = { version = "=4.2.0", default-features = false, features = ["std", "derive", "help", "usage"] } diff --git a/rust/suricatactl/Makefile.am b/rust/suricatactl/Makefile.am new file mode 100644 index 0000000000..acf00aeb68 --- /dev/null +++ b/rust/suricatactl/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = Cargo.toml + +all-local: Cargo.toml diff --git a/rust/suricatactl/src/filestore/mod.rs b/rust/suricatactl/src/filestore/mod.rs new file mode 100644 index 0000000000..1169eed1d2 --- /dev/null +++ b/rust/suricatactl/src/filestore/mod.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Information Security Foundation +// SPDX-License-Identifier: GPL-2.0-only + +pub(crate) mod prune; diff --git a/rust/suricatactl/src/filestore/prune.rs b/rust/suricatactl/src/filestore/prune.rs new file mode 100644 index 0000000000..d4289d8286 --- /dev/null +++ b/rust/suricatactl/src/filestore/prune.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Information Security Foundation +// SPDX-License-Identifier: GPL-2.0-only + +use std::path::{Path, PathBuf}; +use tracing::{debug, error, info}; + +use crate::FilestorePruneArgs; + +pub(crate) fn prune(args: FilestorePruneArgs) -> Result<(), Box> { + let age = parse_age(&args.age)?; + info!("Pruning files older than {} seconds", age); + + let mut total_bytes = 0; + let mut file_count = 0; + + let mut stack = vec![PathBuf::from(&args.directory)]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + if path.is_dir() { + stack.push(path); + } else { + match FileInfo::from_path(&path) { + Ok(info) => { + if info.age > age { + debug!("Deleting {:?}", path); + file_count += 1; + total_bytes += info.size; + if !args.dry_run { + if let Err(err) = std::fs::remove_file(&path) { + error!("Failed to delete {}: {}", path.display(), err); + } + } + } + } + Err(err) => { + error!( + "Failed to get last modified time of file {}: {}", + path.display(), + err + ); + } + } + } + } + } + + info!("Removed {} files; {} bytes", file_count, total_bytes); + + Ok(()) +} + +struct FileInfo { + age: u64, + size: u64, +} + +impl FileInfo { + fn from_path(path: &Path) -> Result> { + let metadata = path.metadata()?; + let age = metadata.modified()?.elapsed()?.as_secs(); + Ok(Self { + age, + size: metadata.len(), + }) + } +} + +/// Given input like "1s", "1m", "1h" or "1d" return the number of +/// seconds +fn parse_age(age: &str) -> Result { + // Use a regex to separate the value from the unit. + let re = regex::Regex::new(r"^(\d+)([smhd])$").unwrap(); + let caps = re.captures(age).ok_or_else(|| { + format!( + "Invalid age: {}. Must be a number followed by one of s, m, h, d", + age + ) + })?; + let value = caps + .get(1) + .unwrap() + .as_str() + .parse::() + .map_err(|e| format!("Invalid age: {}: {}", age, e))?; + let unit = caps.get(2).unwrap().as_str(); + + match unit { + "s" => Ok(value), + "m" => Ok(value * 60), + "h" => Ok(value * 60 * 60), + "d" => Ok(value * 60 * 60 * 24), + _ => unreachable!(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_age() { + assert!(parse_age("1").is_err()); + assert!(parse_age("s").is_err()); + assert!(parse_age("1a").is_err()); + + // Valid tests + assert_eq!(parse_age("1s").unwrap(), 1); + assert_eq!(parse_age("3s").unwrap(), 3); + assert_eq!(parse_age("1m").unwrap(), 60); + assert_eq!(parse_age("3m").unwrap(), 180); + assert_eq!(parse_age("3h").unwrap(), 10800); + assert_eq!(parse_age("1d").unwrap(), 86400); + assert_eq!(parse_age("3d").unwrap(), 86400 * 3); + } +} diff --git a/rust/suricatactl/src/main.rs b/rust/suricatactl/src/main.rs new file mode 100644 index 0000000000..54e6f64413 --- /dev/null +++ b/rust/suricatactl/src/main.rs @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright 2023 Open Information Security Foundation +// SPDX-License-Identifier: GPL-2.0-only + +use clap::Parser; +use clap::Subcommand; +use tracing::Level; + +mod filestore; + +#[derive(Parser, Debug)] +struct Cli { + #[arg(long, short, global = true, action = clap::ArgAction::Count)] + verbose: u8, + + #[arg( + long, + short, + global = true, + help = "Quiet mode, only warnings and errors will be logged" + )] + quiet: bool, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Filestore management commands + Filestore(FilestoreCommand), +} + +#[derive(Parser, Debug)] +struct FilestoreCommand { + #[command(subcommand)] + command: FilestoreCommands, +} + +#[derive(Subcommand, Debug)] +enum FilestoreCommands { + /// Remove files by age + Prune(FilestorePruneArgs), +} + +#[derive(Parser, Debug)] +struct FilestorePruneArgs { + #[arg(long, short = 'n', help = "only print what would happen")] + dry_run: bool, + #[arg(long, short, help = "file-store directory")] + directory: String, + #[arg(long, help = "prune files older than age, units: s, m, h, d")] + age: String, +} + +fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + let log_level = if cli.quiet { + Level::WARN + } else if cli.verbose > 0 { + Level::DEBUG + } else { + Level::INFO + }; + tracing_subscriber::fmt().with_max_level(log_level).init(); + + match cli.command { + Commands::Filestore(filestore) => match filestore.command { + FilestoreCommands::Prune(args) => crate::filestore::prune::prune(args), + }, + } +}