From e6d29d6031a729c7fdf3c46a1ce3a90ae1ebc39e Mon Sep 17 00:00:00 2001 From: elhoim Date: Thu, 24 Sep 2026 10:58:40 +0000 Subject: [PATCH 1/3] Exit non-zero and keep converting when a rule fails with --output-dir write_separate_files() wrapped the whole backend.convert() call in 'except Exception' and only printed a warning, so a single unconvertible rule dropped every rule after it and the command still exited 0. Rules are now converted one by one (like Backend.convert does), failures are reported per rule, the remaining rules are still written and the command exits with status 1. Co-Authored-By: Claude Opus 5.5 (1M context) --- sigma/cli/convert.py | 28 ++++++++++++++++++----- tests/test_convert.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/sigma/cli/convert.py b/sigma/cli/convert.py index 0e0172c..58d8fcd 100644 --- a/sigma/cli/convert.py +++ b/sigma/cli/convert.py @@ -188,12 +188,23 @@ def write_callback(rule, output_format, index, cond, result): return result - # Convert the entire collection with the callback - try: - backend.convert(rule_collection, format, correlation_method, callback=write_callback) - except Exception as e: - click.echo(f"Warning: Failed to convert rules: {e}", err=True) - + # Convert rule by rule (mirroring Backend.convert) so that one failing rule neither + # aborts the conversion of the remaining rules nor gets silently ignored. + failed_rules = [] + backend.init_processing_pipeline(format) + rule_collection.resolve_rule_references() + for rule in rule_collection.rules: + try: + if isinstance(rule, SigmaCorrelationRule): + backend.convert_correlation_rule( + rule, format, correlation_method, callback=write_callback + ) + else: + backend.convert_rule(rule, format, callback=write_callback) + except (SigmaError, NotImplementedError) as e: + failed_rules.append((rule, e)) + click.echo(f"Error: Failed to convert rule {rule.source or rule.title}: {e}", err=True) + # Now write the collected results to files for rule_id, results in rule_results.items(): if not results: @@ -250,6 +261,11 @@ def write_callback(rule, output_format, index, cond, result): click.echo(f"Wrote {files_written} file(s) to {output_dir}", err=True) + if failed_rules: + raise click.ClickException( + f"{len(failed_rules)} rule(s) failed to convert, see errors above." + ) + @click.command() @click.option( diff --git a/tests/test_convert.py b/tests/test_convert.py index a39794e..ea217b8 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -1,3 +1,5 @@ +import pathlib + from click.testing import CliRunner import pytest from sigma.cli.convert import convert @@ -461,3 +463,54 @@ def test_convert_output_dir_with_filter(tmp_path): content = (output_dir / "sigma_rule.txt").read_text() assert 'not User startswith "ADM_"' in content + +UNCONVERTIBLE_RULE = """title: Unconvertible +id: 9f1b8f4a-0000-4000-8000-000000000001 +logsource: + category: test +detection: + sel: + fieldA|expand: "%var%" + condition: sel +""" + + +def _write_rules_with_unconvertible(tmp_path): + input_dir = tmp_path / "rules" + input_dir.mkdir() + (input_dir / "a_unconvertible.yml").write_text(UNCONVERTIBLE_RULE) + (input_dir / "b_rule.yml").write_text( + pathlib.Path("tests/files/valid/sigma_rule.yml").read_text() + ) + return input_dir + + +def test_convert_output_dir_conversion_error_fails_and_continues(tmp_path): + """A rule that fails to convert makes --output-dir exit non-zero, but later rules are still written.""" + input_dir = _write_rules_with_unconvertible(tmp_path) + output_dir = tmp_path / "output" + cli = CliRunner() + result = cli.invoke( + convert, + ["-t", "text_query_test", "--output-dir", str(output_dir), str(input_dir)], + ) + assert result.exit_code == 1 + assert "a_unconvertible.yml" in result.output + assert "1 rule(s) failed to convert" in result.output + assert not (output_dir / "a_unconvertible.txt").exists() + assert (output_dir / "b_rule.txt").exists() + + +def test_convert_output_dir_conversion_error_skip_unsupported(tmp_path): + """With --skip-unsupported the failing rule is only reported as ignored error.""" + input_dir = _write_rules_with_unconvertible(tmp_path) + output_dir = tmp_path / "output" + cli = CliRunner() + result = cli.invoke( + convert, + ["-t", "text_query_test", "-s", "--output-dir", str(output_dir), str(input_dir)], + ) + assert result.exit_code == 0 + assert "Ignored errors" in result.output + assert (output_dir / "b_rule.txt").exists() + From 7e85405da20aa42ec385afa339bc012d48fbf65e Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Sun, 27 Sep 2026 12:58:30 +0200 Subject: [PATCH 2/3] Change output assertions to stderr in test_convert Update assertions to check stderr instead of output. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index ea217b8..83869e2 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -494,9 +494,9 @@ def test_convert_output_dir_conversion_error_fails_and_continues(tmp_path): convert, ["-t", "text_query_test", "--output-dir", str(output_dir), str(input_dir)], ) - assert result.exit_code == 1 - assert "a_unconvertible.yml" in result.output - assert "1 rule(s) failed to convert" in result.output +assert result.exit_code == 1 +assert "a_unconvertible.yml" in result.stderr +assert "1 rule(s) failed to convert" in result.stderr assert not (output_dir / "a_unconvertible.txt").exists() assert (output_dir / "b_rule.txt").exists() From 97ad32b42b9bc941422d56f644b51a4e9dd9a688 Mon Sep 17 00:00:00 2001 From: Thomas Patzke Date: Sun, 27 Sep 2026 13:01:01 +0200 Subject: [PATCH 3/3] Fix indentation in test assertions for convert function --- tests/test_convert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index 83869e2..34f5f9e 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -494,9 +494,9 @@ def test_convert_output_dir_conversion_error_fails_and_continues(tmp_path): convert, ["-t", "text_query_test", "--output-dir", str(output_dir), str(input_dir)], ) -assert result.exit_code == 1 -assert "a_unconvertible.yml" in result.stderr -assert "1 rule(s) failed to convert" in result.stderr + assert result.exit_code == 1 + assert "a_unconvertible.yml" in result.stderr + assert "1 rule(s) failed to convert" in result.stderr assert not (output_dir / "a_unconvertible.txt").exists() assert (output_dir / "b_rule.txt").exists()