Skip to content

Commit 81dac1e

Browse files
committed
suricatactl: add config command
Currently this is just an entry point into the app provided by the config crate.
1 parent 654b20d commit 81dac1e

6 files changed

Lines changed: 292 additions & 228 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: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// SPDX-FileCopyrightText: Copyright 2026 Open Information Security Foundation
2+
// SPDX-License-Identifier: GPL-2.0-only
3+
4+
use std::path::Path;
5+
use std::path::PathBuf;
6+
7+
use clap::Parser;
8+
use clap::Subcommand;
9+
use clap::ValueEnum;
10+
use serde_json::Value;
11+
12+
const SCHEMA_FILENAME: &str = "suricata-yaml.schema.json";
13+
14+
#[derive(Parser, Debug)]
15+
#[command(about = "Utilities for Suricata configuration files")]
16+
struct Cli {
17+
#[command(subcommand)]
18+
command: Command,
19+
}
20+
21+
#[derive(Subcommand, Debug)]
22+
enum Command {
23+
/// Read and print a Suricata configuration file.
24+
Print(PrintArgs),
25+
26+
/// Validate a Suricata configuration file against a JSON schema.
27+
Validate(ValidateArgs),
28+
29+
/// Print the embedded Suricata YAML JSON schema.
30+
PrintSchema,
31+
32+
/// Merge a loaded Suricata configuration layout into suricata-yaml.schema.json.
33+
UpdateSchema(UpdateSchemaArgs),
34+
}
35+
36+
#[derive(Parser, Debug)]
37+
struct PrintArgs {
38+
/// Path to the Suricata configuration file.
39+
path: PathBuf,
40+
41+
/// Output format.
42+
#[arg(long, value_enum, default_value_t = OutputFormat::Yaml)]
43+
format: OutputFormat,
44+
}
45+
46+
#[derive(Parser, Debug)]
47+
struct ValidateArgs {
48+
/// Path to the Suricata configuration file.
49+
path: PathBuf,
50+
51+
/// Path to a JSON schema file. If omitted, the embedded schema is used.
52+
#[arg(long)]
53+
schema: Option<PathBuf>,
54+
55+
/// Quiet mode. Print nothing when validation succeeds.
56+
#[arg(short, long)]
57+
quiet: bool,
58+
}
59+
60+
#[derive(Parser, Debug)]
61+
struct UpdateSchemaArgs {
62+
/// Path to the Suricata configuration file whose layout should be merged.
63+
path: PathBuf,
64+
65+
/// Dot-path to force-regenerate (can be specified multiple times).
66+
///
67+
/// Example: --force stats.exception-policy
68+
#[arg(long = "force", value_name = "PATH")]
69+
force: Vec<String>,
70+
}
71+
72+
#[derive(Clone, Copy, Debug, ValueEnum)]
73+
enum OutputFormat {
74+
Yaml,
75+
Json,
76+
Debug,
77+
Flat,
78+
}
79+
80+
/// Parse CLI arguments from the process environment and dispatch the selected subcommand.
81+
pub fn run_from_env() -> Result<(), Box<dyn std::error::Error>> {
82+
run_from_iter(std::env::args_os())
83+
}
84+
85+
/// Parse CLI arguments from any iterator and dispatch the selected subcommand.
86+
pub fn run_from_iter<I, T>(args: I) -> Result<(), Box<dyn std::error::Error>>
87+
where
88+
I: IntoIterator<Item = T>,
89+
T: Into<std::ffi::OsString> + Clone,
90+
{
91+
let cli = Cli::parse_from(args);
92+
93+
match cli.command {
94+
Command::Print(args) => print_config(args),
95+
Command::Validate(args) => validate_config(args),
96+
Command::PrintSchema => print_schema(),
97+
Command::UpdateSchema(args) => update_schema(args),
98+
}
99+
}
100+
101+
// Load a configuration file and print it in the requested format.
102+
fn print_config(args: PrintArgs) -> Result<(), Box<dyn std::error::Error>> {
103+
let config = crate::load_file(&args.path)?;
104+
105+
match args.format {
106+
OutputFormat::Yaml => print!("{}", crate::print_yaml(&config)?),
107+
OutputFormat::Json => {
108+
println!(
109+
"{}",
110+
serde_json::to_string_pretty(&crate::config_to_json(&config))?
111+
);
112+
}
113+
OutputFormat::Debug => println!("{config:#?}"),
114+
OutputFormat::Flat => print!("{}", crate::print_flat_config(&config)),
115+
}
116+
117+
Ok(())
118+
}
119+
120+
// Print the embedded Suricata YAML JSON schema.
121+
fn print_schema() -> Result<(), Box<dyn std::error::Error>> {
122+
print!("{}", crate::SURICATA_YAML_SCHEMA);
123+
Ok(())
124+
}
125+
126+
// Load a configuration file, validate it against a schema, and report all issues.
127+
fn validate_config(args: ValidateArgs) -> Result<(), Box<dyn std::error::Error>> {
128+
let config = crate::load_file(&args.path)?;
129+
let instance = crate::config_to_json(&config);
130+
131+
let (schema, schema_label) = if let Some(schema_path) = args.schema {
132+
let schema_input = std::fs::read_to_string(&schema_path)?;
133+
let schema: serde_json::Value = serde_json::from_str(&schema_input)?;
134+
(schema, schema_path.display().to_string())
135+
} else {
136+
(crate::embedded_schema()?, String::from("embedded schema"))
137+
};
138+
139+
let errors = crate::validate_json_schema(&instance, &schema);
140+
if errors.is_empty() {
141+
if !args.quiet {
142+
println!("OK: {}", args.path.display());
143+
}
144+
return Ok(());
145+
}
146+
147+
eprintln!(
148+
"Validation failed: {} issue(s) in {} against {}",
149+
errors.len(),
150+
args.path.display(),
151+
schema_label
152+
);
153+
for error in errors {
154+
eprintln!("{}", error);
155+
}
156+
157+
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "schema validation failed").into())
158+
}
159+
160+
// Merge one loaded config layout into rust/config/suricata-yaml.schema.json.
161+
fn update_schema(args: UpdateSchemaArgs) -> Result<(), Box<dyn std::error::Error>> {
162+
let config = crate::load_file(&args.path)?;
163+
let instance = crate::config_to_json(&config);
164+
165+
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(SCHEMA_FILENAME);
166+
let schema_input = std::fs::read_to_string(&schema_path)?;
167+
let mut schema: serde_json::Value = serde_json::from_str(&schema_input)?;
168+
let mut original_schema = schema.clone();
169+
crate::sort_schema_properties(&mut original_schema);
170+
171+
for force_path in &args.force {
172+
let force_segments = parse_dot_path(force_path)?;
173+
if !json_object_path_exists(&instance, &force_segments) {
174+
return Err(std::io::Error::new(
175+
std::io::ErrorKind::InvalidInput,
176+
format!(
177+
"forced path '{}' does not exist in {}",
178+
force_path,
179+
args.path.display()
180+
),
181+
)
182+
.into());
183+
}
184+
185+
let removed =
186+
crate::remove_schema_property_path(&mut schema, &force_segments).map_err(|error| {
187+
std::io::Error::new(
188+
std::io::ErrorKind::InvalidInput,
189+
format!("invalid forced path '{}': {}", force_path, error),
190+
)
191+
})?;
192+
if removed {
193+
println!("Force-cleared schema path '{}'", force_path);
194+
}
195+
}
196+
197+
crate::merge_instance_layout_additive(&mut schema, &instance);
198+
crate::sort_schema_properties(&mut schema);
199+
if schema == original_schema {
200+
println!("No schema updates required for {}", args.path.display());
201+
return Ok(());
202+
}
203+
204+
let output = serde_json::to_string_pretty(&schema)?;
205+
std::fs::write(&schema_path, format!("{output}\n"))?;
206+
207+
println!(
208+
"Updated {} using {}",
209+
schema_path.display(),
210+
args.path.display()
211+
);
212+
213+
Ok(())
214+
}
215+
216+
fn parse_dot_path(path: &str) -> Result<Vec<&str>, std::io::Error> {
217+
let segments = path.split('.').collect::<Vec<_>>();
218+
if segments.is_empty() || segments.iter().any(|segment| segment.is_empty()) {
219+
return Err(std::io::Error::new(
220+
std::io::ErrorKind::InvalidInput,
221+
format!("invalid path '{path}': use non-empty dot-separated segments"),
222+
));
223+
}
224+
Ok(segments)
225+
}
226+
227+
fn json_object_path_exists(value: &Value, path: &[&str]) -> bool {
228+
let mut current = value;
229+
for segment in path {
230+
let Some(object) = current.as_object() else {
231+
return false;
232+
};
233+
let Some(next) = object.get(*segment) else {
234+
return false;
235+
};
236+
current = next;
237+
}
238+
true
239+
}

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 schema;

0 commit comments

Comments
 (0)