Skip to content

Commit 3dc7bc2

Browse files
committed
suricatactl: add config command
Currently this is just an entry point into the app provided by the config crate.
1 parent ae730d5 commit 3dc7bc2

6 files changed

Lines changed: 189 additions & 127 deletions

File tree

rust/Cargo.lock.in

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/config/src/cli.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Open Information Security Foundation
2+
// SPDX-License-Identifier: GPL-2.0-only
3+
4+
use std::path::PathBuf;
5+
6+
use clap::Parser;
7+
use clap::Subcommand;
8+
use clap::ValueEnum;
9+
10+
#[derive(Parser, Debug)]
11+
#[command(about = "Utilities for Suricata configuration files")]
12+
struct Cli {
13+
#[command(subcommand)]
14+
command: Command,
15+
}
16+
17+
#[derive(Subcommand, Debug)]
18+
enum Command {
19+
/// Read and print a Suricata configuration file.
20+
Print(PrintArgs),
21+
22+
/// Validate a Suricata configuration file against a JSON schema.
23+
Validate(ValidateArgs),
24+
25+
/// Print the embedded Suricata YAML JSON schema.
26+
PrintSchema,
27+
}
28+
29+
#[derive(Parser, Debug)]
30+
struct PrintArgs {
31+
/// Path to the Suricata configuration file.
32+
path: PathBuf,
33+
34+
/// Output format.
35+
#[arg(long, value_enum, default_value_t = OutputFormat::Yaml)]
36+
format: OutputFormat,
37+
}
38+
39+
#[derive(Parser, Debug)]
40+
struct ValidateArgs {
41+
/// Path to the Suricata configuration file.
42+
path: PathBuf,
43+
44+
/// Path to a JSON schema file. If omitted, the embedded schema is used.
45+
#[arg(long)]
46+
schema: Option<PathBuf>,
47+
48+
/// Quiet mode. Print nothing when validation succeeds.
49+
#[arg(short, long)]
50+
quiet: bool,
51+
}
52+
53+
#[derive(Clone, Copy, Debug, ValueEnum)]
54+
enum OutputFormat {
55+
Yaml,
56+
Json,
57+
Debug,
58+
Flat,
59+
}
60+
61+
/// Parse CLI arguments from the process environment and dispatch the selected subcommand.
62+
pub fn run_from_env() -> Result<(), Box<dyn std::error::Error>> {
63+
run_from_iter(std::env::args_os())
64+
}
65+
66+
/// Parse CLI arguments from any iterator and dispatch the selected subcommand.
67+
pub fn run_from_iter<I, T>(args: I) -> Result<(), Box<dyn std::error::Error>>
68+
where
69+
I: IntoIterator<Item = T>,
70+
T: Into<std::ffi::OsString> + Clone,
71+
{
72+
let cli = Cli::parse_from(args);
73+
74+
match cli.command {
75+
Command::Print(args) => print_config(args),
76+
Command::Validate(args) => validate_config(args),
77+
Command::PrintSchema => print_schema(),
78+
}
79+
}
80+
81+
// Load a configuration file and print it in the requested format.
82+
fn print_config(args: PrintArgs) -> Result<(), Box<dyn std::error::Error>> {
83+
let config = crate::load_file(&args.path)?;
84+
85+
match args.format {
86+
OutputFormat::Yaml => print!("{}", crate::print_yaml(&config)?),
87+
OutputFormat::Json => {
88+
println!(
89+
"{}",
90+
serde_json::to_string_pretty(&crate::config_to_json(&config))?
91+
);
92+
}
93+
OutputFormat::Debug => println!("{config:#?}"),
94+
OutputFormat::Flat => print!("{}", crate::print_flat_config(&config)),
95+
}
96+
97+
Ok(())
98+
}
99+
100+
// Print the embedded Suricata YAML JSON schema.
101+
fn print_schema() -> Result<(), Box<dyn std::error::Error>> {
102+
print!("{}", crate::SURICATA_YAML_SCHEMA);
103+
Ok(())
104+
}
105+
106+
// Load a configuration file, validate it against a schema, and report all issues.
107+
fn validate_config(args: ValidateArgs) -> Result<(), Box<dyn std::error::Error>> {
108+
let config = crate::load_file(&args.path)?;
109+
let instance = crate::config_to_json(&config);
110+
111+
let (schema, schema_label) = if let Some(schema_path) = args.schema {
112+
let schema_input = std::fs::read_to_string(&schema_path)?;
113+
let schema: serde_json::Value = serde_json::from_str(&schema_input)?;
114+
(schema, schema_path.display().to_string())
115+
} else {
116+
(crate::embedded_schema()?, String::from("embedded schema"))
117+
};
118+
119+
let errors = crate::validate_json_schema(&instance, &schema);
120+
if errors.is_empty() {
121+
if !args.quiet {
122+
println!("OK: {}", args.path.display());
123+
}
124+
return Ok(());
125+
}
126+
127+
eprintln!(
128+
"Validation failed: {} issue(s) in {} against {}",
129+
errors.len(),
130+
args.path.display(),
131+
schema_label
132+
);
133+
for error in errors {
134+
eprintln!("{}", error);
135+
}
136+
137+
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "schema validation failed").into())
138+
}
139+

rust/config/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-FileCopyrightText: Copyright 2026 Open Information Security Foundation
22
// SPDX-License-Identifier: GPL-2.0-only
33

4+
pub mod cli;
45
pub mod ffi;
56
pub mod loader;
67
pub mod validate;

rust/config/src/main.rs

Lines changed: 1 addition & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,6 @@
11
// SPDX-FileCopyrightText: Copyright 2026 Open Information Security Foundation
22
// SPDX-License-Identifier: GPL-2.0-only
33

4-
use std::path::PathBuf;
5-
6-
use clap::Parser;
7-
use clap::Subcommand;
8-
use clap::ValueEnum;
9-
10-
#[derive(Parser, Debug)]
11-
#[command(about = "Utilities for Suricata configuration files")]
12-
struct Cli {
13-
#[command(subcommand)]
14-
command: Command,
15-
}
16-
17-
#[derive(Subcommand, Debug)]
18-
enum Command {
19-
/// Read and print a Suricata configuration file.
20-
Print(PrintArgs),
21-
22-
/// Validate a Suricata configuration file against a JSON schema.
23-
Validate(ValidateArgs),
24-
25-
/// Print the embedded Suricata YAML JSON schema.
26-
PrintSchema,
27-
}
28-
29-
#[derive(Parser, Debug)]
30-
struct PrintArgs {
31-
/// Path to the Suricata configuration file.
32-
path: PathBuf,
33-
34-
/// Output format.
35-
#[arg(long, value_enum, default_value_t = OutputFormat::Yaml)]
36-
format: OutputFormat,
37-
}
38-
39-
#[derive(Parser, Debug)]
40-
struct ValidateArgs {
41-
/// Path to the Suricata configuration file.
42-
path: PathBuf,
43-
44-
/// Path to a JSON schema file. If omitted, the embedded schema is used.
45-
#[arg(long)]
46-
schema: Option<PathBuf>,
47-
48-
/// Quiet mode. Print nothing when validation succeeds.
49-
#[arg(short, long)]
50-
quiet: bool,
51-
}
52-
53-
#[derive(Clone, Copy, Debug, ValueEnum)]
54-
enum OutputFormat {
55-
Yaml,
56-
Json,
57-
Debug,
58-
Flat,
59-
}
60-
61-
// Parse CLI arguments and dispatch the selected subcommand.
624
fn main() -> Result<(), Box<dyn std::error::Error>> {
63-
let cli = Cli::parse();
64-
65-
match cli.command {
66-
Command::Print(args) => print_config(args),
67-
Command::Validate(args) => validate_config(args),
68-
Command::PrintSchema => print_schema(),
69-
}
70-
}
71-
72-
// Load a configuration file and print it in the requested format.
73-
fn print_config(args: PrintArgs) -> Result<(), Box<dyn std::error::Error>> {
74-
let config = suricata_config::load_file(&args.path)?;
75-
76-
match args.format {
77-
OutputFormat::Yaml => print!("{}", suricata_config::print_yaml(&config)?),
78-
OutputFormat::Json => {
79-
println!(
80-
"{}",
81-
serde_json::to_string_pretty(&suricata_config::config_to_json(&config))?
82-
);
83-
}
84-
OutputFormat::Debug => println!("{config:#?}"),
85-
OutputFormat::Flat => print!("{}", suricata_config::print_flat_config(&config)),
86-
}
87-
88-
Ok(())
89-
}
90-
91-
// Print the embedded Suricata YAML JSON schema.
92-
fn print_schema() -> Result<(), Box<dyn std::error::Error>> {
93-
print!("{}", suricata_config::SURICATA_YAML_SCHEMA);
94-
Ok(())
95-
}
96-
97-
// Load a configuration file, validate it against a schema, and report all issues.
98-
fn validate_config(args: ValidateArgs) -> Result<(), Box<dyn std::error::Error>> {
99-
let config = suricata_config::load_file(&args.path)?;
100-
let instance = suricata_config::config_to_json(&config);
101-
102-
let (schema, schema_label) = if let Some(schema_path) = args.schema {
103-
let schema_input = std::fs::read_to_string(&schema_path)?;
104-
let schema: serde_json::Value = serde_json::from_str(&schema_input)?;
105-
(schema, schema_path.display().to_string())
106-
} else {
107-
(
108-
suricata_config::embedded_schema()?,
109-
String::from("embedded schema"),
110-
)
111-
};
112-
113-
let errors = suricata_config::validate_json_schema(&instance, &schema);
114-
if errors.is_empty() {
115-
if !args.quiet {
116-
println!("OK: {}", args.path.display());
117-
}
118-
return Ok(());
119-
}
120-
121-
eprintln!(
122-
"Validation failed: {} issue(s) in {} against {}",
123-
errors.len(),
124-
args.path.display(),
125-
schema_label
126-
);
127-
for error in errors {
128-
eprintln!("{}", error);
129-
}
130-
131-
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "schema validation failed").into())
5+
suricata_config::cli::run_from_env()
1326
}

rust/suricatactl/Cargo.toml.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ tracing = "0.1"
1414
tracing-subscriber = "0.3"
1515
once_cell = { version = "1.21.3" }
1616
clap = { version = "4.5.39", default-features = false, features = ["std", "derive", "help", "usage"] }
17+
suricata-config = { path = "../config", version = "@PACKAGE_VERSION@" }

rust/suricatactl/src/main.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
use clap::Parser;
1010
use clap::Subcommand;
11+
use std::ffi::OsStr;
12+
use std::ffi::OsString;
1113
use tracing::Level;
1214

1315
mod filestore;
@@ -33,6 +35,9 @@ struct Cli {
3335
enum Commands {
3436
/// Filestore management commands
3537
Filestore(FilestoreCommand),
38+
39+
/// Suricata configuration commands
40+
Config,
3641
}
3742

3843
#[derive(Parser, Debug)]
@@ -58,6 +63,10 @@ struct FilestorePruneArgs {
5863
}
5964

6065
fn main() -> Result<(), Box<dyn std::error::Error>> {
66+
if dispatch_config_command_from_argv()? {
67+
return Ok(());
68+
}
69+
6170
let cli = Cli::parse();
6271

6372
let log_level = if cli.quiet {
@@ -73,5 +82,42 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
7382
Commands::Filestore(filestore) => match filestore.command {
7483
FilestoreCommands::Prune(args) => crate::filestore::prune::prune(args),
7584
},
85+
Commands::Config => unreachable!("config dispatch is handled before clap parsing"),
86+
}
87+
}
88+
89+
fn dispatch_config_command_from_argv() -> Result<bool, Box<dyn std::error::Error>> {
90+
let args: Vec<OsString> = std::env::args_os().collect();
91+
if args.len() < 2 {
92+
return Ok(false);
93+
}
94+
95+
let mut index = 1;
96+
while index < args.len() && is_global_passthrough_flag(args[index].as_os_str()) {
97+
index += 1;
7698
}
99+
100+
let Some(command) = args.get(index) else {
101+
return Ok(false);
102+
};
103+
if command != OsStr::new("config") {
104+
return Ok(false);
105+
}
106+
107+
let mut command_name = args[0].clone();
108+
command_name.push(" config");
109+
let forwarded = std::iter::once(command_name)
110+
.chain(args.into_iter().skip(index + 1))
111+
.collect::<Vec<_>>();
112+
suricata_config::cli::run_from_iter(forwarded)?;
113+
Ok(true)
114+
}
115+
116+
fn is_global_passthrough_flag(arg: &OsStr) -> bool {
117+
if arg == OsStr::new("--verbose") || arg == OsStr::new("-q") || arg == OsStr::new("--quiet") {
118+
return true;
119+
}
120+
121+
let value = arg.to_string_lossy();
122+
value.starts_with('-') && value.chars().skip(1).all(|ch| ch == 'v')
77123
}

0 commit comments

Comments
 (0)