mod commands;
#[cfg(feature = "daemon")]
mod daemon;
pub(crate) mod exit_code;
mod fix;
use std::path::PathBuf;
use std::process;
use clap::{Parser, Subcommand};
use commands::{
ConditionArgs, ConvertArgs, EvalArgs, FieldsArgs, LintArgs, LintCounts, ListFormatsArgs,
ParseArgs, ResolveArgs, StdinArgs, ValidateArgs,
};
#[cfg(feature = "daemon")]
use commands::{DaemonArgs, cmd_daemon};
use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val};
use rsigma_eval::{
CorrelationAction, CorrelationConfig, CorrelationEventMode, Pipeline, parse_pipeline_file,
resolve_builtin_pipeline,
};
use rsigma_parser::{SigmaCollection, parse_sigma_directory, parse_sigma_file};
use serde_json_path::JsonPath;
#[derive(Parser)]
#[command(name = "rsigma")]
#[command(about = "Parse, validate, and evaluate Sigma detection rules")]
#[command(version)]
struct Cli {
#[arg(long = "log-format", value_enum, global = true)]
log_format: Option<LogFormat>,
#[command(subcommand)]
command: Commands,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum LogFormat {
Json,
Text,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Commands {
Engine {
#[command(subcommand)]
cmd: EngineCommands,
},
Rule {
#[command(subcommand)]
cmd: RuleCommands,
},
Backend {
#[command(subcommand)]
cmd: BackendCommands,
},
Pipeline {
#[command(subcommand)]
cmd: PipelineCommands,
},
Attack {
#[command(subcommand)]
cmd: AttackCommands,
},
Eval(EvalArgs),
#[cfg(feature = "daemon")]
Daemon(DaemonArgs),
Parse(ParseArgs),
Validate(ValidateArgs),
Lint(LintArgs),
Fields(FieldsArgs),
Condition(ConditionArgs),
Stdin(StdinArgs),
Convert(ConvertArgs),
#[command(name = "list-targets")]
ListTargets,
#[command(name = "list-formats")]
ListFormats(ListFormatsArgs),
Resolve(ResolveArgs),
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum EngineCommands {
Eval(EvalArgs),
#[cfg(feature = "daemon")]
Daemon(DaemonArgs),
}
#[derive(Subcommand)]
enum RuleCommands {
Parse(ParseArgs),
Validate(ValidateArgs),
Lint(LintArgs),
Fields(FieldsArgs),
Condition(ConditionArgs),
Stdin(StdinArgs),
}
#[derive(Subcommand)]
enum BackendCommands {
Convert(ConvertArgs),
Targets,
Formats(ListFormatsArgs),
}
#[derive(Subcommand)]
enum PipelineCommands {
Resolve(ResolveArgs),
}
#[derive(Subcommand)]
enum AttackCommands {}
fn main() {
let cli = Cli::parse();
#[cfg(feature = "daemon")]
let is_daemon = matches!(
cli.command,
Commands::Engine {
cmd: EngineCommands::Daemon(_),
..
} | Commands::Daemon(_)
);
#[cfg(not(feature = "daemon"))]
let is_daemon = false;
if !is_daemon && let Some(format) = cli.log_format {
init_cli_log_subscriber(format);
}
dispatch(cli.command);
}
fn deprecation_warn(old: &str, new: &str) {
eprintln!(
"warning: `rsigma {old}` is deprecated; use `rsigma {new}` instead. \
This alias will be hidden in the next release and removed in v1.0."
);
}
fn dispatch(command: Commands) {
match command {
Commands::Engine { cmd } => dispatch_engine(cmd),
Commands::Rule { cmd } => dispatch_rule(cmd),
Commands::Backend { cmd } => dispatch_backend(cmd),
Commands::Pipeline { cmd } => dispatch_pipeline(cmd),
Commands::Attack { cmd } => dispatch_attack(cmd),
Commands::Eval(args) => {
deprecation_warn("eval", "engine eval");
run_eval(args);
}
#[cfg(feature = "daemon")]
Commands::Daemon(args) => {
deprecation_warn("daemon", "engine daemon");
cmd_daemon(args);
}
Commands::Parse(args) => {
deprecation_warn("parse", "rule parse");
commands::cmd_parse(args);
}
Commands::Validate(args) => {
deprecation_warn("validate", "rule validate");
commands::cmd_validate(args);
}
Commands::Lint(args) => {
deprecation_warn("lint", "rule lint");
run_lint(args);
}
Commands::Fields(args) => {
deprecation_warn("fields", "rule fields");
commands::cmd_fields(args);
}
Commands::Condition(args) => {
deprecation_warn("condition", "rule condition");
commands::cmd_condition(args);
}
Commands::Stdin(args) => {
deprecation_warn("stdin", "rule stdin");
commands::cmd_stdin(args);
}
Commands::Convert(args) => {
deprecation_warn("convert", "backend convert");
commands::cmd_convert(args);
}
Commands::ListTargets => {
deprecation_warn("list-targets", "backend targets");
commands::cmd_list_targets();
}
Commands::ListFormats(ListFormatsArgs { target }) => {
deprecation_warn("list-formats", "backend formats");
commands::cmd_list_formats(target);
}
Commands::Resolve(args) => {
deprecation_warn("resolve", "pipeline resolve");
commands::cmd_resolve(args);
}
}
}
fn dispatch_engine(cmd: EngineCommands) {
match cmd {
EngineCommands::Eval(args) => run_eval(args),
#[cfg(feature = "daemon")]
EngineCommands::Daemon(args) => cmd_daemon(args),
}
}
fn dispatch_rule(cmd: RuleCommands) {
match cmd {
RuleCommands::Parse(args) => commands::cmd_parse(args),
RuleCommands::Validate(args) => commands::cmd_validate(args),
RuleCommands::Lint(args) => run_lint(args),
RuleCommands::Fields(args) => commands::cmd_fields(args),
RuleCommands::Condition(args) => commands::cmd_condition(args),
RuleCommands::Stdin(args) => commands::cmd_stdin(args),
}
}
fn dispatch_backend(cmd: BackendCommands) {
match cmd {
BackendCommands::Convert(args) => commands::cmd_convert(args),
BackendCommands::Targets => commands::cmd_list_targets(),
BackendCommands::Formats(ListFormatsArgs { target }) => commands::cmd_list_formats(target),
}
}
fn dispatch_pipeline(cmd: PipelineCommands) {
match cmd {
PipelineCommands::Resolve(args) => commands::cmd_resolve(args),
}
}
fn dispatch_attack(cmd: AttackCommands) {
match cmd {}
}
fn run_eval(args: EvalArgs) {
let fail_on_detection = args.fail_on_detection;
let had_matches = commands::cmd_eval(args);
if fail_on_detection && had_matches {
process::exit(exit_code::FINDINGS);
}
}
fn run_lint(args: LintArgs) {
let fail_level = args.fail_level.clone();
let LintCounts {
errors,
warnings,
infos,
} = commands::cmd_lint(args);
let should_fail = match fail_level.as_str() {
"info" => errors > 0 || warnings > 0 || infos > 0,
"warning" => errors > 0 || warnings > 0,
_ => errors > 0,
};
if should_fail {
process::exit(exit_code::FINDINGS);
}
}
fn init_cli_log_subscriber(format: LogFormat) {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let builder = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr);
match format {
LogFormat::Json => {
let _ = builder.json().try_init();
}
LogFormat::Text => {
let _ = builder.try_init();
}
}
}
pub(crate) fn load_pipelines(paths: &[PathBuf]) -> Vec<Pipeline> {
let mut pipelines = Vec::new();
for path in paths {
let name = path.to_str().unwrap_or("");
if let Some(result) = resolve_builtin_pipeline(name) {
match result {
Ok(p) => {
eprintln!(
"Loaded builtin pipeline: {} (priority {})",
p.name, p.priority
);
pipelines.push(p);
}
Err(e) => {
eprintln!("Error parsing builtin pipeline '{name}': {e}");
process::exit(exit_code::CONFIG_ERROR);
}
}
} else {
match parse_pipeline_file(path) {
Ok(p) => {
eprintln!("Loaded pipeline: {} (priority {})", p.name, p.priority);
if p.is_dynamic() {
let source_ids: Vec<&str> =
p.sources.iter().map(|s| s.id.as_str()).collect();
eprintln!(" dynamic source(s): {}", source_ids.join(", "));
}
pipelines.push(p);
}
Err(e) => {
eprintln!("Error loading pipeline {}: {e}", path.display());
process::exit(exit_code::CONFIG_ERROR);
}
}
}
}
pipelines.sort_by_key(|p| p.priority);
pipelines
}
pub(crate) fn load_collection(path: &std::path::Path) -> SigmaCollection {
let collection = if path.is_dir() {
match parse_sigma_directory(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading rules from {}: {e}", path.display());
process::exit(exit_code::RULE_ERROR);
}
}
} else {
match parse_sigma_file(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading rule {}: {e}", path.display());
process::exit(exit_code::RULE_ERROR);
}
}
};
if !collection.errors.is_empty() {
eprintln!(
"Warning: {} parse errors while loading rules",
collection.errors.len()
);
}
collection
}
pub(crate) fn print_warnings(errors: &[String]) {
if !errors.is_empty() {
eprintln!("Warnings:");
for err in errors {
eprintln!(" - {err}");
}
}
}
pub(crate) fn print_json(value: &impl serde::Serialize, pretty: bool) {
let json = if pretty {
serde_json::to_string_pretty(value)
} else {
serde_json::to_string(value)
};
match json {
Ok(j) => println!("{j}"),
Err(e) => {
eprintln!("JSON serialization error: {e}");
process::exit(exit_code::CONFIG_ERROR);
}
}
}
pub(crate) enum EventFilter {
None,
Jq(jaq_interpret::Filter),
JsonPath(JsonPath),
}
pub(crate) fn build_event_filter(jq: Option<String>, jsonpath: Option<String>) -> EventFilter {
if let Some(jq_expr) = jq {
eprintln!("Event filter: jq '{jq_expr}'");
let mut defs = ParseCtx::new(Vec::new());
let (parsed, errs) = jaq_parse::parse(&jq_expr, jaq_parse::main());
if !errs.is_empty() {
eprintln!("Invalid jq filter: {:?}", errs);
process::exit(exit_code::CONFIG_ERROR);
}
let Some(parsed) = parsed else {
eprintln!("Invalid jq filter: failed to parse '{jq_expr}'");
process::exit(exit_code::CONFIG_ERROR);
};
let filter = defs.compile(parsed);
if !defs.errs.is_empty() {
eprintln!("jq compilation errors ({} error(s))", defs.errs.len());
process::exit(exit_code::CONFIG_ERROR);
}
EventFilter::Jq(filter)
} else if let Some(jp_expr) = jsonpath {
eprintln!("Event filter: jsonpath '{jp_expr}'");
match JsonPath::parse(&jp_expr) {
Ok(path) => EventFilter::JsonPath(path),
Err(e) => {
eprintln!("Invalid JSONPath: {e}");
process::exit(exit_code::CONFIG_ERROR);
}
}
} else {
EventFilter::None
}
}
pub(crate) fn build_correlation_config(
suppress: Option<String>,
action: Option<String>,
no_detections: bool,
correlation_event_mode: String,
max_correlation_events: usize,
extra_timestamp_fields: Vec<String>,
timestamp_fallback: &str,
) -> CorrelationConfig {
let suppress_secs = suppress.map(|s| match rsigma_parser::Timespan::parse(&s) {
Ok(ts) => ts.seconds,
Err(e) => {
eprintln!("Invalid suppress duration '{s}': {e}");
process::exit(exit_code::CONFIG_ERROR);
}
});
let action_on_match = action
.map(|s| {
s.parse::<CorrelationAction>().unwrap_or_else(|e| {
eprintln!("{e}");
process::exit(exit_code::CONFIG_ERROR);
})
})
.unwrap_or_default();
let event_mode = correlation_event_mode
.parse::<CorrelationEventMode>()
.unwrap_or_else(|e| {
eprintln!("{e}");
process::exit(exit_code::CONFIG_ERROR);
});
let ts_fallback = match timestamp_fallback {
"skip" => rsigma_eval::TimestampFallback::Skip,
_ => rsigma_eval::TimestampFallback::WallClock,
};
let mut config = CorrelationConfig {
suppress: suppress_secs,
action_on_match,
emit_detections: !no_detections,
correlation_event_mode: event_mode,
max_correlation_events,
timestamp_fallback: ts_fallback,
..Default::default()
};
if !extra_timestamp_fields.is_empty() {
let mut fields = extra_timestamp_fields;
fields.extend(config.timestamp_fields);
config.timestamp_fields = fields;
}
config
}
pub(crate) fn apply_event_filter(
value: &serde_json::Value,
filter: &EventFilter,
) -> Vec<serde_json::Value> {
match filter {
EventFilter::None => vec![value.clone()],
EventFilter::Jq(f) => {
let inputs = RcIter::new(core::iter::empty());
let out = f.run((Ctx::new([], &inputs), Val::from(value.clone())));
out.filter_map(|r| match r {
Ok(val) => val_to_json(val),
Err(e) => {
eprintln!("jq runtime error: {e}");
None
}
})
.collect()
}
EventFilter::JsonPath(path) => {
let nodes = path.query(value);
nodes.all().into_iter().cloned().collect()
}
}
}
fn val_to_json(val: Val) -> Option<serde_json::Value> {
match val {
Val::Null => Some(serde_json::Value::Null),
Val::Bool(b) => Some(serde_json::Value::Bool(b)),
Val::Int(n) => Some(serde_json::Value::Number(n.into())),
Val::Float(f) => serde_json::Number::from_f64(f).map(serde_json::Value::Number),
Val::Num(n) => {
if let Ok(i) = n.parse::<i64>() {
Some(serde_json::Value::Number(i.into()))
} else if let Ok(f) = n.parse::<f64>() {
serde_json::Number::from_f64(f).map(serde_json::Value::Number)
} else {
Some(serde_json::Value::String(n.to_string()))
}
}
Val::Str(s) => Some(serde_json::Value::String(s.to_string())),
Val::Arr(arr) => {
let items: Vec<serde_json::Value> =
arr.iter().filter_map(|v| val_to_json(v.clone())).collect();
Some(serde_json::Value::Array(items))
}
Val::Obj(obj) => {
let map: serde_json::Map<String, serde_json::Value> = obj
.iter()
.filter_map(|(k, v)| val_to_json(v.clone()).map(|jv| (k.to_string(), jv)))
.collect();
Some(serde_json::Value::Object(map))
}
}
}