use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
pub(crate) const SIGMA_CLI_ENV: &str = "RSIGMA_SIGMA_CLI";
const DEFAULT_PROGRAM: &str = "sigma";
pub(crate) struct SigmaCli {
program: PathBuf,
is_override: bool,
}
impl SigmaCli {
pub(crate) fn configured() -> Self {
let (program, is_override) = resolve_program(std::env::var_os(SIGMA_CLI_ENV));
Self {
program,
is_override,
}
}
pub(crate) fn program(&self) -> &Path {
&self.program
}
pub(crate) fn is_override(&self) -> bool {
self.is_override
}
pub(crate) fn run<I, S>(&self, args: I) -> std::io::Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
Command::new(&self.program).args(args).output()
}
}
fn resolve_program(env_value: Option<OsString>) -> (PathBuf, bool) {
match env_value {
Some(value) if !value.is_empty() => (PathBuf::from(value), true),
_ => (PathBuf::from(DEFAULT_PROGRAM), false),
}
}
pub(crate) fn build_convert_args(
target: &str,
format: &str,
pipelines: &[PathBuf],
without_pipeline: bool,
skip_unsupported: bool,
backend_options: &[String],
rules: &[PathBuf],
) -> Vec<OsString> {
fn os(value: impl AsRef<OsStr>) -> OsString {
value.as_ref().to_os_string()
}
let mut argv: Vec<OsString> = vec![os("convert"), os("-t"), os(target), os("-f"), os(format)];
for pipeline in pipelines {
argv.push(os("-p"));
argv.push(os(pipeline));
}
if without_pipeline {
argv.push(os("--without-pipeline"));
}
if skip_unsupported {
argv.push(os("-s"));
}
for option in backend_options {
match option.split_once('=') {
Some(("correlation_method", value)) => {
argv.push(os("-c"));
argv.push(os(value));
}
_ => {
argv.push(os("-O"));
argv.push(os(option));
}
}
}
for rule in rules {
argv.push(os(rule));
}
argv
}
pub(crate) fn install_hint(
target: &str,
program: &Path,
is_override: bool,
native_targets: &[&str],
) -> String {
let program = program.display();
let native = native_targets.join(", ");
if is_override {
format!(
"No native rsigma backend for target '{target}' (native targets: {native}), \
and the sigma-cli override {SIGMA_CLI_ENV}='{program}' could not be executed.\n\
Point {SIGMA_CLI_ENV} at a working sigma executable, or unset it to use one on PATH."
)
} else {
format!(
"No native rsigma backend for target '{target}' (native targets: {native}), \
and sigma-cli was not found on PATH.\n\
Install it to convert to '{target}':\n\
\x20\x20pipx install sigma-cli\n\
\x20\x20sigma plugin install {target}\n\
Or set {SIGMA_CLI_ENV} to the path of an existing sigma executable."
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn to_strings(argv: &[OsString]) -> Vec<String> {
argv.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect()
}
#[test]
fn resolve_program_prefers_override() {
let (program, is_override) = resolve_program(Some(OsString::from("/opt/sigma/bin/sigma")));
assert_eq!(program, PathBuf::from("/opt/sigma/bin/sigma"));
assert!(is_override);
}
#[test]
fn resolve_program_falls_back_to_path() {
let (program, is_override) = resolve_program(None);
assert_eq!(program, PathBuf::from("sigma"));
assert!(!is_override);
}
#[test]
fn resolve_program_treats_empty_override_as_unset() {
let (program, is_override) = resolve_program(Some(OsString::new()));
assert_eq!(program, PathBuf::from("sigma"));
assert!(!is_override);
}
#[test]
fn build_args_maps_flags_one_to_one() {
let argv = build_convert_args(
"splunk",
"default",
&[PathBuf::from("ecs.yml"), PathBuf::from("custom.yml")],
false,
true,
&["index=main".to_string()],
&[PathBuf::from("rule.yml")],
);
assert_eq!(
to_strings(&argv),
vec![
"convert",
"-t",
"splunk",
"-f",
"default",
"-p",
"ecs.yml",
"-p",
"custom.yml",
"-s",
"-O",
"index=main",
"rule.yml",
]
);
}
#[test]
fn build_args_special_cases_correlation_method() {
let argv = build_convert_args(
"loki",
"default",
&[],
false,
false,
&["correlation_method=stats".to_string()],
&[PathBuf::from("rule.yml")],
);
assert_eq!(
to_strings(&argv),
vec![
"convert", "-t", "loki", "-f", "default", "-c", "stats", "rule.yml",
]
);
}
#[test]
fn build_args_adds_without_pipeline_flag() {
let argv = build_convert_args(
"loki",
"ruler",
&[],
true,
false,
&[],
&[PathBuf::from("a.yml"), PathBuf::from("b.yml")],
);
assert_eq!(
to_strings(&argv),
vec![
"convert",
"-t",
"loki",
"-f",
"ruler",
"--without-pipeline",
"a.yml",
"b.yml",
]
);
}
#[test]
fn install_hint_mentions_plugin_install_when_not_override() {
let hint = install_hint("splunk", Path::new("sigma"), false, &["postgres", "lynxdb"]);
assert!(hint.contains("sigma-cli was not found"));
assert!(hint.contains("sigma plugin install splunk"));
assert!(hint.contains("RSIGMA_SIGMA_CLI"));
assert!(hint.contains("native targets: postgres, lynxdb"));
}
#[test]
fn install_hint_mentions_override_path_when_override() {
let hint = install_hint("splunk", Path::new("/bad/sigma"), true, &["postgres"]);
assert!(hint.contains("/bad/sigma"));
assert!(hint.contains("could not be executed"));
}
}