diff --git a/configure.ac b/configure.ac index d58b6e7fcaa6..f4577cd501ee 100644 --- a/configure.ac +++ b/configure.ac @@ -2607,6 +2607,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) AC_CONFIG_FILES(rust/client/Makefile rust/client/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 968f67337606..f34f4e430341 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.toml.in b/rust/Cargo.toml.in index 011eda1294e0..d5846524b6af 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -8,9 +8,14 @@ members = [ ".", "derive", "client", + "suricatactl", + "suricatasc", +] +default-members = [ + ".", + "suricatactl", "suricatasc", ] -default-members = [".", "suricatasc"] [lib] crate-type = ["staticlib", "rlib"] diff --git a/rust/Makefile.am b/rust/Makefile.am index 606c545cac24..84911f8bdb9c 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -1,4 +1,5 @@ SUBDIRS = client \ + suricatactl \ suricatasc EXTRA_DIST = src derive \ @@ -7,6 +8,7 @@ EXTRA_DIST = src derive \ dist/rust-bindings.h \ vendor \ client \ + suricatactl \ suricatasc if !DEBUG @@ -60,6 +62,7 @@ endif 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)" @@ -68,6 +71,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 diff --git a/rust/suricatactl/Cargo.toml.in b/rust/suricatactl/Cargo.toml.in new file mode 100644 index 000000000000..d0bd9859a4c4 --- /dev/null +++ b/rust/suricatactl/Cargo.toml.in @@ -0,0 +1,21 @@ +[package] +name = "suricatactl" +version = "@PACKAGE_VERSION@" +edition = "2021" +license = "GPL-2.0-only" + +[[bin]] +name = "suricatactl" +path = "@e_rustdir@/suricatactl/src/main.rs" + +[dependencies] +regex = "~1.5.5" +tracing = "0.1" +tracing-subscriber = "0.3" + +# 4.0 is the newest version that builds with Rust 1.63.0. +clap = { version = "~4.0.0", features = ["derive"] } + +# This dependency is not used directly, but we have to pin this back +# to 0.3.0 for Rust 1.63.0. +clap_lex = "=0.3.0" \ No newline at end of file diff --git a/rust/suricatactl/Makefile.am b/rust/suricatactl/Makefile.am new file mode 100644 index 000000000000..9b403a7df5c4 --- /dev/null +++ b/rust/suricatactl/Makefile.am @@ -0,0 +1 @@ +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 000000000000..1169eed1d278 --- /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 000000000000..d4289d82867c --- /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 000000000000..54e6f6441375 --- /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), + }, + } +}