From 1f45eebca88135b1ea1fe0a7c9a60c9fa7438dcb Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Thu, 18 Jun 2026 16:54:08 -0400 Subject: [PATCH 1/8] bugfix separate deprecated rules from non-deprecated rules in raw rule loader --- detection_rules/rule_loader.py | 54 ++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/detection_rules/rule_loader.py b/detection_rules/rule_loader.py index e2dfb936c48..f7e2d986a45 100644 --- a/detection_rules/rule_loader.py +++ b/detection_rules/rule_loader.py @@ -198,6 +198,20 @@ def filter(self, cb: Callable[[DeprecatedRule], bool]) -> "RuleCollection": return filtered_collection +@dataclass +class RawDeprecatedCollection(BaseCollection[DictRule]): + """Collection of loaded deprecated rules in raw dict form.""" + + id_map: dict[str, DictRule] = field(default_factory=dict) # type: ignore[reportUnknownVariableType] + file_map: dict[Path, DictRule] = field(default_factory=dict) # type: ignore[reportUnknownVariableType] + name_map: dict[str, DictRule] = field(default_factory=dict) # type: ignore[reportUnknownVariableType] + rules: list[DictRule] = field(default_factory=list) # type: ignore[reportUnknownVariableType] + + def __contains__(self, rule: DictRule) -> bool: + """Check if a rule is in the map by comparing IDs.""" + return rule.id in self.id_map + + class RawRuleCollection(BaseCollection[DictRule]): """Collection of rules in raw dict form.""" @@ -213,12 +227,16 @@ def __init__(self, rules: list[DictRule] | None = None, ext_patterns: list[str] self.file_map: dict[Path, DictRule] = {} self.name_map: dict[definitions.RuleName, DictRule] = {} self.rules: list[DictRule] = [] + self.deprecated: RawDeprecatedCollection = RawDeprecatedCollection() self.errors: dict[Path, Exception] = {} self.frozen = False self._raw_load_cache: dict[Path, dict[str, Any]] = {} for rule in rules or []: - self.add_rule(rule) + if self.is_deprecated_rule(rule): + self.add_deprecated_rule(rule) + else: + self.add_rule(rule) def __contains__(self, rule: DictRule) -> bool: """Check if a rule is in the map by comparing IDs.""" @@ -260,11 +278,21 @@ def _get_paths(self, directory: Path, recursive: bool = True) -> list[Path]: paths.extend(sorted(directory.rglob(pattern) if recursive else directory.glob(pattern))) return paths - def _assert_new(self, rule: DictRule) -> None: + @staticmethod + def is_deprecated_rule(rule: DictRule) -> bool: + """Return whether a raw rule dict represents a deprecated rule.""" + return rule.metadata.get("maturity") == "deprecated" + + def _assert_new(self, rule: DictRule, is_deprecated: bool = False) -> None: """Assert that a rule is new and can be added to the collection.""" - id_map = self.id_map - file_map = self.file_map - name_map = self.name_map + if is_deprecated: + id_map = self.deprecated.id_map + file_map = self.deprecated.file_map + name_map = self.deprecated.name_map + else: + id_map = self.id_map + file_map = self.file_map + name_map = self.name_map if self.frozen: raise ValueError(f"Unable to add rule {rule.name} {rule.id} to a frozen collection") @@ -288,10 +316,20 @@ def add_rule(self, rule: DictRule) -> None: self.name_map[rule.name] = rule self.rules.append(rule) + def add_deprecated_rule(self, rule: DictRule) -> None: + """Add a deprecated rule to the collection.""" + self._assert_new(rule, is_deprecated=True) + self.deprecated.id_map[rule.id] = rule + self.deprecated.name_map[rule.name] = rule + self.deprecated.rules.append(rule) + def load_dict(self, obj: dict[str, Any], path: Path | None = None) -> DictRule: """Load a rule from a dictionary.""" rule = DictRule(contents=obj, path=path) - self.add_rule(rule) + if self.is_deprecated_rule(rule): + self.add_deprecated_rule(rule) + else: + self.add_rule(rule) return rule def load_file(self, path: Path) -> DictRule: @@ -304,6 +342,10 @@ def load_file(self, path: Path) -> DictRule: rule = self.__default.file_map[path] self.add_rule(rule) return rule + if self.__default and self is not self.__default and path in self.__default.deprecated.file_map: + rule = self.__default.deprecated.file_map[path] + self.add_deprecated_rule(rule) + return rule obj = self._load_rule_file(path) return self.load_dict(obj, path=path) diff --git a/pyproject.toml b/pyproject.toml index c44dc992e13..fb142af962b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "detection_rules" -version = "1.6.53" +version = "1.6.54" description = "Detection Rules is the home for rules used by Elastic Security. This repository is used for the development, maintenance, testing, validation, and release of rules for Elastic Security’s Detection Engine." readme = "README.md" requires-python = ">=3.12" From d5a9a86121c7da3db8cba1763b4c5358f997ec79 Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 12:49:54 -0400 Subject: [PATCH 2/8] Update name to local-rule-loading --- CLI.md | 4 ++-- detection_rules/kbwrap.py | 2 +- detection_rules/main.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CLI.md b/CLI.md index 2621da5ef95..aae9a7b199c 100644 --- a/CLI.md +++ b/CLI.md @@ -116,7 +116,7 @@ Options: -snv, --strip-none-values Strip None values from the rule -lc, --local-creation-date Preserve the local creation date of the rule -lu, --local-updated-date Preserve the local updated date of the rule - -lr, --load-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) + -lr, --local-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) -h, --help Show this message and exit. ``` @@ -523,7 +523,7 @@ Options: -lu, --local-updated-date Preserve the local updated date of the rule -cro, --custom-rules-only Only export custom rules -eq, --export-query TEXT Apply a query filter to exporting rules e.g. "alert.attributes.tags: \"test\"" to filter for rules that have the tag "test" - -lr, --load-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) + -lr, --local-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) -h, --help Show this message and exit. ``` diff --git a/detection_rules/kbwrap.py b/detection_rules/kbwrap.py index 3c1d58164bd..735d76ed914 100644 --- a/detection_rules/kbwrap.py +++ b/detection_rules/kbwrap.py @@ -253,7 +253,7 @@ def _process_imported_items( ), ) @click.option( - "--load-rule-loading", + "--local-rule-loading", "-lr", is_flag=True, help="Enable arbitrary rule loading from the rules directories (Can be very slow!)", diff --git a/detection_rules/main.py b/detection_rules/main.py index baf1d1de800..56157e58516 100644 --- a/detection_rules/main.py +++ b/detection_rules/main.py @@ -172,7 +172,7 @@ def generate_rules_index( @click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule") @click.option("--dates-import", "-di", is_flag=True, help="Parse created_at and updated_at from the rule content") @click.option( - "--load-rule-loading", + "--local-rule-loading", "-lr", is_flag=True, help="Enable arbitrary rule loading from the rules directories (Can be very slow!)", From e4b251e0fa7b4d45007478f04d0b1f1990a1e841 Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 12:54:17 -0400 Subject: [PATCH 3/8] fix rename --- detection_rules/kbwrap.py | 6 +++--- detection_rules/main.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/detection_rules/kbwrap.py b/detection_rules/kbwrap.py index 735d76ed914..d3c2a3a1671 100644 --- a/detection_rules/kbwrap.py +++ b/detection_rules/kbwrap.py @@ -277,7 +277,7 @@ def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915 local_updated_date: bool = False, custom_rules_only: bool = False, export_query: str | None = None, - load_rule_loading: bool = False, + local_rule_loading: bool = False, ) -> list[TOMLRule]: """Export rules from Kibana.""" @@ -292,7 +292,7 @@ def _raise_missing_path(message: str) -> None: raise click.UsageError("Cannot use --rule-id and --rule-name together. Please choose one.") raw_rule_collection = RawRuleCollection() - if load_rule_loading: + if local_rule_loading: raw_rule_collection = raw_rule_collection.default() with kibana: @@ -384,7 +384,7 @@ def _raise_missing_path(message: str) -> None: save_path = directory / f"{rule_name}" - # Get local rule data if load_rule_loading is enabled. If not enabled rules variable will be None. + # Get local rule data if local_rule_loading is enabled. If not enabled rules variable will be None. local_rule: dict[str, Any] = params.get("rule", {}) input_rule_id: str | None = None diff --git a/detection_rules/main.py b/detection_rules/main.py index 56157e58516..733cdc69436 100644 --- a/detection_rules/main.py +++ b/detection_rules/main.py @@ -192,7 +192,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915 local_creation_date: bool, local_updated_date: bool, dates_import: bool, - load_rule_loading: bool, + local_rule_loading: bool, ) -> None: """Import rules from json, toml, or yaml files containing Kibana exported rule(s).""" errors: list[str] = [] @@ -216,7 +216,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915 click.echo("Must specify at least one file!") raw_rule_collection = RawRuleCollection() - if load_rule_loading: + if local_rule_loading: raw_rule_collection = raw_rule_collection.default() exceptions_containers = {} From eeef9a18d7f0f011f73b6ad6ebe4db7eaeb7d9cb Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 13:02:32 -0400 Subject: [PATCH 4/8] clarify rename --- CLI.md | 4 ++-- detection_rules/kbwrap.py | 12 ++++++------ detection_rules/main.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CLI.md b/CLI.md index aae9a7b199c..ef293feb2a8 100644 --- a/CLI.md +++ b/CLI.md @@ -116,7 +116,7 @@ Options: -snv, --strip-none-values Strip None values from the rule -lc, --local-creation-date Preserve the local creation date of the rule -lu, --local-updated-date Preserve the local updated date of the rule - -lr, --local-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) + -lr, --use-existing-rule-dirs Enable arbitrary rule loading from the rules directories (Can be very slow!) -h, --help Show this message and exit. ``` @@ -523,7 +523,7 @@ Options: -lu, --local-updated-date Preserve the local updated date of the rule -cro, --custom-rules-only Only export custom rules -eq, --export-query TEXT Apply a query filter to exporting rules e.g. "alert.attributes.tags: \"test\"" to filter for rules that have the tag "test" - -lr, --local-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!) + -lr, --use-existing-rule-dirs Enable arbitrary rule loading from the rules directories (Can be very slow!) -h, --help Show this message and exit. ``` diff --git a/detection_rules/kbwrap.py b/detection_rules/kbwrap.py index d3c2a3a1671..c8383aee360 100644 --- a/detection_rules/kbwrap.py +++ b/detection_rules/kbwrap.py @@ -253,10 +253,10 @@ def _process_imported_items( ), ) @click.option( - "--local-rule-loading", - "-lr", + "--use-existing-rule-dirs", + "-ud", is_flag=True, - help="Enable arbitrary rule loading from the rules directories (Can be very slow!)", + help="Enable arbitrary local rule path usage from config rules directories (Can be very slow!)", ) @click.pass_context def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915 @@ -277,7 +277,7 @@ def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915 local_updated_date: bool = False, custom_rules_only: bool = False, export_query: str | None = None, - local_rule_loading: bool = False, + use_existing_rule_dirs: bool = False, ) -> list[TOMLRule]: """Export rules from Kibana.""" @@ -292,7 +292,7 @@ def _raise_missing_path(message: str) -> None: raise click.UsageError("Cannot use --rule-id and --rule-name together. Please choose one.") raw_rule_collection = RawRuleCollection() - if local_rule_loading: + if use_existing_rule_dirs: raw_rule_collection = raw_rule_collection.default() with kibana: @@ -384,7 +384,7 @@ def _raise_missing_path(message: str) -> None: save_path = directory / f"{rule_name}" - # Get local rule data if local_rule_loading is enabled. If not enabled rules variable will be None. + # Get local rule data if use_existing_rule_dirs is enabled. If not enabled rules variable will be None. local_rule: dict[str, Any] = params.get("rule", {}) input_rule_id: str | None = None diff --git a/detection_rules/main.py b/detection_rules/main.py index 733cdc69436..60803c4a333 100644 --- a/detection_rules/main.py +++ b/detection_rules/main.py @@ -172,7 +172,7 @@ def generate_rules_index( @click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule") @click.option("--dates-import", "-di", is_flag=True, help="Parse created_at and updated_at from the rule content") @click.option( - "--local-rule-loading", + "--use-existing-rule-dirs", "-lr", is_flag=True, help="Enable arbitrary rule loading from the rules directories (Can be very slow!)", @@ -192,7 +192,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915 local_creation_date: bool, local_updated_date: bool, dates_import: bool, - local_rule_loading: bool, + use_existing_rule_dirs: bool, ) -> None: """Import rules from json, toml, or yaml files containing Kibana exported rule(s).""" errors: list[str] = [] @@ -216,7 +216,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915 click.echo("Must specify at least one file!") raw_rule_collection = RawRuleCollection() - if local_rule_loading: + if use_existing_rule_dirs: raw_rule_collection = raw_rule_collection.default() exceptions_containers = {} From 345500e2242be9fd5c13d93c45e3c1b8759fcb86 Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 15:30:13 -0400 Subject: [PATCH 5/8] Update help note --- detection_rules/kbwrap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/detection_rules/kbwrap.py b/detection_rules/kbwrap.py index c8383aee360..e7a5bec75bd 100644 --- a/detection_rules/kbwrap.py +++ b/detection_rules/kbwrap.py @@ -256,7 +256,10 @@ def _process_imported_items( "--use-existing-rule-dirs", "-ud", is_flag=True, - help="Enable arbitrary local rule path usage from config rules directories (Can be very slow!)", + help=( + "Enable arbitrary local rule path usage from config rules directories (Can be very slow!). " + "This option used to be referred to as --local-rule-loading. It is now renamed to be more descriptive." + ), ) @click.pass_context def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915 From dfca1d51cc2cb9057f805959630666a9bc45c930 Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 16:03:34 -0400 Subject: [PATCH 6/8] add backwards compatibility --- CLI.md | 7 +++++-- detection_rules/kbwrap.py | 6 +++++- detection_rules/main.py | 8 +++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CLI.md b/CLI.md index ef293feb2a8..5f287b29e29 100644 --- a/CLI.md +++ b/CLI.md @@ -116,7 +116,9 @@ Options: -snv, --strip-none-values Strip None values from the rule -lc, --local-creation-date Preserve the local creation date of the rule -lu, --local-updated-date Preserve the local updated date of the rule - -lr, --use-existing-rule-dirs Enable arbitrary rule loading from the rules directories (Can be very slow!) + -di, --dates-import Parse created_at and updated_at from the rule content + -lr, --use-existing-rule-dirs, --load-rule-loading + Enable arbitrary rule loading from the rules directories (Can be very slow!). `--load-rule-loading` is a deprecated alias kept for backwards compatibility. -h, --help Show this message and exit. ``` @@ -523,7 +525,8 @@ Options: -lu, --local-updated-date Preserve the local updated date of the rule -cro, --custom-rules-only Only export custom rules -eq, --export-query TEXT Apply a query filter to exporting rules e.g. "alert.attributes.tags: \"test\"" to filter for rules that have the tag "test" - -lr, --use-existing-rule-dirs Enable arbitrary rule loading from the rules directories (Can be very slow!) + -ud, -lr, --use-existing-rule-dirs, --load-rule-loading + Enable arbitrary local rule path usage from config rules directories (Can be very slow!). `--load-rule-loading` and `-lr` are deprecated aliases kept for backwards compatibility. -h, --help Show this message and exit. ``` diff --git a/detection_rules/kbwrap.py b/detection_rules/kbwrap.py index e7a5bec75bd..a5aea397cb3 100644 --- a/detection_rules/kbwrap.py +++ b/detection_rules/kbwrap.py @@ -254,11 +254,15 @@ def _process_imported_items( ) @click.option( "--use-existing-rule-dirs", + "--load-rule-loading", "-ud", + "-lr", + "use_existing_rule_dirs", is_flag=True, help=( "Enable arbitrary local rule path usage from config rules directories (Can be very slow!). " - "This option used to be referred to as --local-rule-loading. It is now renamed to be more descriptive." + "This option was previously named --load-rule-loading; that name is kept as an alias " + "for backwards compatibility." ), ) @click.pass_context diff --git a/detection_rules/main.py b/detection_rules/main.py index 60803c4a333..0158b262ea6 100644 --- a/detection_rules/main.py +++ b/detection_rules/main.py @@ -173,9 +173,15 @@ def generate_rules_index( @click.option("--dates-import", "-di", is_flag=True, help="Parse created_at and updated_at from the rule content") @click.option( "--use-existing-rule-dirs", + "--load-rule-loading", "-lr", + "use_existing_rule_dirs", is_flag=True, - help="Enable arbitrary rule loading from the rules directories (Can be very slow!)", + help=( + "Enable arbitrary rule loading from the rules directories (Can be very slow!). " + "This option was previously named --load-rule-loading; that name is kept as an alias " + "for backwards compatibility." + ), ) def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915 input_file: tuple[Path, ...] | None, From ff6e1bf9a7e4438ac5978402344455a4192ef019 Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Tue, 30 Jun 2026 16:04:35 -0400 Subject: [PATCH 7/8] bump patch version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a28da764163..1404a2d4cae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "detection_rules" -version = "1.7.4" +version = "1.7.5" description = "Detection Rules is the home for rules used by Elastic Security. This repository is used for the development, maintenance, testing, validation, and release of rules for Elastic Security’s Detection Engine." readme = "README.md" requires-python = ">=3.12" From 68d2ad99190bf05e18094c4333540fea06e9c1cd Mon Sep 17 00:00:00 2001 From: eric-forte-elastic Date: Wed, 1 Jul 2026 10:53:33 -0400 Subject: [PATCH 8/8] add unit tests --- tests/test_rule_loader.py | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/test_rule_loader.py diff --git a/tests/test_rule_loader.py b/tests/test_rule_loader.py new file mode 100644 index 00000000000..bb60bde2369 --- /dev/null +++ b/tests/test_rule_loader.py @@ -0,0 +1,109 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. + +"""Test RawRuleCollection loading and CLI flag backwards compatibility.""" + +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + +from detection_rules.kbwrap import kibana_export_rules +from detection_rules.main import import_rules_into_repo +from detection_rules.rule_loader import RawRuleCollection + +DUPLICATE_NAME = "Duplicate Name Rule" +ACTIVE_RULE_ID = "11111111-1111-4111-8111-111111111111" +DEPRECATED_RULE_ID = "22222222-2222-4222-8222-222222222222" + + +def build_rule_dict(rule_id: str, name: str, maturity: str) -> dict[str, Any]: + """Build a minimal raw rule dict for RawRuleCollection tests.""" + metadata: dict[str, Any] = {"creation_date": "2020/01/01", "updated_date": "2020/01/01", "maturity": maturity} + if maturity == "deprecated": + metadata["deprecation_date"] = "2024/01/01" + + return { + "metadata": metadata, + "rule": {"rule_id": rule_id, "name": name, "description": "Test rule"}, + } + + +class TestRawRuleCollectionDeprecatedSplit(unittest.TestCase): + """RawRuleCollection should allow an active and deprecated rule to share a name.""" + + def test_active_and_deprecated_rule_can_share_a_name(self) -> None: + """A deprecated rule should not collide with an active rule of the same name.""" + collection = RawRuleCollection() + active = collection.load_dict(build_rule_dict(ACTIVE_RULE_ID, DUPLICATE_NAME, "production")) + deprecated = collection.load_dict(build_rule_dict(DEPRECATED_RULE_ID, DUPLICATE_NAME, "deprecated")) + + self.assertIn(active, collection.rules) + self.assertIn(deprecated, collection.deprecated.rules) + self.assertEqual(collection.name_map[DUPLICATE_NAME].id, ACTIVE_RULE_ID) + self.assertEqual(collection.deprecated.name_map[DUPLICATE_NAME].id, DEPRECATED_RULE_ID) + + def test_two_active_rules_with_same_name_still_collide(self) -> None: + """Name collision detection between two active rules should be unaffected.""" + collection = RawRuleCollection() + _ = collection.load_dict(build_rule_dict(ACTIVE_RULE_ID, DUPLICATE_NAME, "production")) + + with self.assertRaises(ValueError): + _ = collection.load_dict(build_rule_dict(DEPRECATED_RULE_ID, DUPLICATE_NAME, "production")) + + def test_two_deprecated_rules_with_same_name_still_collide(self) -> None: + """Name collision detection between two deprecated rules should be unaffected.""" + collection = RawRuleCollection() + _ = collection.load_dict(build_rule_dict(ACTIVE_RULE_ID, DUPLICATE_NAME, "deprecated")) + + with self.assertRaises(ValueError): + _ = collection.load_dict(build_rule_dict(DEPRECATED_RULE_ID, DUPLICATE_NAME, "deprecated")) + + def test_load_file_from_directory_with_shared_name(self) -> None: + """Loading two files from disk with a shared name should not raise, mirroring the DaC bug report.""" + with TemporaryDirectory() as tmp_dir: + tmp = Path(tmp_dir) + active_path = tmp / "active_rule.toml" + deprecated_path = tmp / "deprecated_rule.toml" + + _ = active_path.write_text( + '[metadata]\ncreation_date = "2020/01/01"\nupdated_date = "2020/01/01"\n' + 'maturity = "production"\n\n[rule]\n' + f'rule_id = "{ACTIVE_RULE_ID}"\nname = "{DUPLICATE_NAME}"\ndescription = "Active version"\n' + ) + _ = deprecated_path.write_text( + '[metadata]\ncreation_date = "2020/01/01"\nupdated_date = "2024/01/01"\n' + 'deprecation_date = "2024/01/01"\nmaturity = "deprecated"\n\n[rule]\n' + f'rule_id = "{DEPRECATED_RULE_ID}"\nname = "{DUPLICATE_NAME}"\ndescription = "Deprecated version"\n' + ) + + collection = RawRuleCollection() + collection.load_directory(tmp) + + self.assertEqual(len(collection.rules), 1) + self.assertEqual(len(collection.deprecated.rules), 1) + + +class TestLoadRuleLoadingFlagBackwardsCompatibility(unittest.TestCase): + """--load-rule-loading / -lr must keep working as deprecated aliases for --use-existing-rule-dirs.""" + + def test_import_rules_into_repo_accepts_old_flag(self) -> None: + """The old --load-rule-loading and -lr flags must still enable use_existing_rule_dirs.""" + for args in (["--load-rule-loading"], ["-lr"], ["--use-existing-rule-dirs"]): + ctx = import_rules_into_repo.make_context("import-rules-into-repo", list(args)) + self.assertTrue(ctx.params["use_existing_rule_dirs"], f"failed for args: {args}") + + ctx = import_rules_into_repo.make_context("import-rules-into-repo", []) + self.assertFalse(ctx.params["use_existing_rule_dirs"]) + + def test_kibana_export_rules_accepts_old_flag(self) -> None: + """The old --load-rule-loading and -lr flags must still enable use_existing_rule_dirs.""" + base_args = ["--directory", "export-rules-test"] + for flag in (["--load-rule-loading"], ["-lr"], ["--use-existing-rule-dirs"], ["-ud"]): + ctx = kibana_export_rules.make_context("export-rules", [*base_args, *flag]) + self.assertTrue(ctx.params["use_existing_rule_dirs"], f"failed for flag: {flag}") + + ctx = kibana_export_rules.make_context("export-rules", base_args) + self.assertFalse(ctx.params["use_existing_rule_dirs"])