mirror of https://github.com/OISF/suricata
suricatactl: rust version of suricatactl
parent
7e4de3d1b9
commit
8fa347410e
@ -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"] }
|
@ -0,0 +1,3 @@
|
|||||||
|
EXTRA_DIST = Cargo.toml
|
||||||
|
|
||||||
|
all-local: Cargo.toml
|
@ -0,0 +1,4 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2023 Open Information Security Foundation
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
pub(crate) mod prune;
|
@ -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<dyn std::error::Error>> {
|
||||||
|
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<Self, Box<dyn std::error::Error>> {
|
||||||
|
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<u64, String> {
|
||||||
|
// 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::<u64>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<dyn std::error::Error>> {
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue