Skip to content

Commit ae19112

Browse files
committed
rust/config: add a config schema and validation tool
1 parent 6693b69 commit ae19112

8 files changed

Lines changed: 4014 additions & 1 deletion

File tree

rust/Cargo.lock.in

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

rust/config/Cargo.toml.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ name = "suricata-config"
1111

1212
[dependencies]
1313
clap = { version = "4.5.39", default-features = false, features = ["std", "derive", "help", "usage"] }
14+
jsonschema = { version = "0.14.0", default-features = false, features = ["draft202012"] }
1415
saphyr = { version = "0.0.6" }
1516
saphyr-parser = { version = "0.0.6" }
17+
serde_json = "1.0.143"
1618
thiserror = "2.0.12"

rust/config/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
EXTRA_DIST = Cargo.toml
1+
EXTRA_DIST = Cargo.toml \
2+
suricata-yaml.schema.json
23

34
all-local: Cargo.toml

rust/config/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
pub mod ffi;
55
pub mod loader;
6+
pub mod validate;
67

78
pub use loader::{load_file, load_string, LoadError};
9+
pub use validate::{config_to_json, validate_json_schema, ValidationError};
810

911
use saphyr::MappingOwned;
1012
use saphyr::Yaml;
@@ -22,6 +24,14 @@ pub type Config = YamlOwned;
2224
/// Limit for nesting depth. Fuzzing can easily reach this.
2325
const MAX_YAML_NESTING_DEPTH: usize = 255;
2426

27+
/// Embedded default JSON Schema for Suricata YAML configuration.
28+
pub const SURICATA_YAML_SCHEMA: &str = include_str!("../suricata-yaml.schema.json");
29+
30+
/// Parse and return the embedded default JSON Schema.
31+
pub fn embedded_schema() -> Result<serde_json::Value, serde_json::Error> {
32+
serde_json::from_str(SURICATA_YAML_SCHEMA)
33+
}
34+
2535
/// Errors returned while parsing a configuration document.
2636
#[derive(Debug, Error)]
2737
pub enum ParseError {
@@ -509,6 +519,12 @@ tagged.path: !leaf final
509519
assert_eq!(config["tagged-key"].as_str(), Some("tagged-value"));
510520
}
511521

522+
#[test]
523+
fn test_embedded_schema_parses() {
524+
let schema = embedded_schema().expect("embedded schema should parse as JSON");
525+
assert!(schema["properties"].is_object());
526+
}
527+
512528
#[test]
513529
fn test_null() {
514530
// Standard YAML null forms.

rust/config/src/main.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ struct Cli {
1818
enum Command {
1919
/// Read and print a Suricata configuration file.
2020
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,
2127
}
2228

2329
#[derive(Parser, Debug)]
@@ -30,9 +36,24 @@ struct PrintArgs {
3036
format: OutputFormat,
3137
}
3238

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+
3353
#[derive(Clone, Copy, Debug, ValueEnum)]
3454
enum OutputFormat {
3555
Yaml,
56+
Json,
3657
Debug,
3758
Flat,
3859
}
@@ -43,6 +64,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4364

4465
match cli.command {
4566
Command::Print(args) => print_config(args),
67+
Command::Validate(args) => validate_config(args),
68+
Command::PrintSchema => print_schema(),
4669
}
4770
}
4871

@@ -52,9 +75,58 @@ fn print_config(args: PrintArgs) -> Result<(), Box<dyn std::error::Error>> {
5275

5376
match args.format {
5477
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+
}
5584
OutputFormat::Debug => println!("{config:#?}"),
5685
OutputFormat::Flat => print!("{}", suricata_config::print_flat_config(&config)),
5786
}
5887

5988
Ok(())
6089
}
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())
132+
}

0 commit comments

Comments
 (0)