Skip to content

Commit a9ad669

Browse files
[FR] [DAC] Add Arbitrary File location Support for Local Creation Date (#4915)
* Add support for local file contents * Update Rule Params * Update CLI docs * Update to Pathlib * Format updating * Delete duplicate * Update logic to handle just local_contents path * Update to Glob Based Approach * Updated to use RawRuleCollection * Fix Logging Typo * New utils functions no longer needed * Update naming for convention
1 parent bf3071d commit a9ad669

File tree

5 files changed

+53
-13
lines changed

5 files changed

+53
-13
lines changed

CLI.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Options:
106106
-snv, --strip-none-values Strip None values from the rule
107107
-lc, --local-creation-date Preserve the local creation date of the rule
108108
-lu, --local-updated-date Preserve the local updated date of the rule
109+
-lr, --load-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!)
109110
-h, --help Show this message and exit.
110111
```
111112

@@ -507,6 +508,7 @@ Options:
507508
-lu, --local-updated-date Preserve the local updated date of the rule
508509
-cro, --custom-rules-only Only export custom rules
509510
-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"
511+
-lr, --load-rule-loading Enable arbitrary rule loading from the rules directories (Can be very slow!)
510512
-h, --help Show this message and exit.
511513
512514
```

detection_rules/kbwrap.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
import sys
1010
from pathlib import Path
11-
from typing import Any
11+
from typing import Any, cast
1212

1313
import click
1414
import kql # type: ignore[reportMissingTypeStubs]
@@ -27,7 +27,8 @@
2727
from .main import root
2828
from .misc import add_params, get_kibana_client, kibana_options, nested_set, raise_client_error
2929
from .rule import TOMLRule, TOMLRuleContents, downgrade_contents_from_rule
30-
from .rule_loader import RuleCollection, update_metadata_from_file
30+
from .rule_loader import RawRuleCollection, RuleCollection, update_metadata_from_file
31+
from .schemas import definitions # noqa: TC001
3132
from .utils import format_command_options, rulename_to_filename
3233

3334
RULES_CONFIG = parse_rules_config()
@@ -250,6 +251,12 @@ def _process_imported_items(
250251
'"alert.attributes.tags: \\"test\\"" to filter for rules that have the tag "test"'
251252
),
252253
)
254+
@click.option(
255+
"--load-rule-loading",
256+
"-lr",
257+
is_flag=True,
258+
help="Enable arbitrary rule loading from the rules directories (Can be very slow!)",
259+
)
253260
@click.pass_context
254261
def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915
255262
ctx: click.Context,
@@ -268,15 +275,20 @@ def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915
268275
local_updated_date: bool = False,
269276
custom_rules_only: bool = False,
270277
export_query: str | None = None,
278+
load_rule_loading: bool = False,
271279
) -> list[TOMLRule]:
272280
"""Export custom rules from Kibana."""
273281
kibana = ctx.obj["kibana"]
274-
kibana_include_details = export_exceptions or export_action_connectors
282+
kibana_include_details = export_exceptions or export_action_connectors or custom_rules_only or export_query
275283

276284
# Only allow one of rule_id or rule_name
277285
if rule_name and rule_id:
278286
raise click.UsageError("Cannot use --rule-id and --rule-name together. Please choose one.")
279287

288+
raw_rule_collection = RawRuleCollection()
289+
if load_rule_loading:
290+
raw_rule_collection = raw_rule_collection.default()
291+
280292
with kibana:
281293
# Look up rule IDs by name if --rule-name was provided
282294
if rule_name:
@@ -365,6 +377,16 @@ def kibana_export_rules( # noqa: PLR0912, PLR0913, PLR0915
365377
rule_name = rulename_to_filename(rule_resource.get("name"), tactic_name=tactic_name) # type: ignore[reportUnknownMemberType]
366378

367379
save_path = directory / f"{rule_name}"
380+
381+
# Get local rule data if load_rule_loading is enabled. If not enabled rules variable will be None.
382+
local_rule: dict[str, Any] = params.get("rule", {})
383+
input_rule_id: str | None = None
384+
385+
if local_rule:
386+
input_rule_id = cast("definitions.UUIDString", local_rule.get("rule_id"))
387+
388+
if input_rule_id and input_rule_id in raw_rule_collection.id_map:
389+
save_path = raw_rule_collection.id_map[input_rule_id].path or save_path
368390
params.update(
369391
update_metadata_from_file(
370392
save_path, {"creation_date": local_creation_date, "updated_date": local_updated_date}

detection_rules/main.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .misc import add_client, nested_set, parse_user_config, raise_client_error
3434
from .rule import DeprecatedRule, QueryRuleData, TOMLRule, TOMLRuleContents
3535
from .rule_formatter import toml_write
36-
from .rule_loader import RuleCollection, update_metadata_from_file
36+
from .rule_loader import RawRuleCollection, RuleCollection, update_metadata_from_file
3737
from .schemas import all_versions, definitions, get_incompatible_fields, get_schema_file
3838
from .utils import (
3939
Ndjson,
@@ -157,6 +157,12 @@ def generate_rules_index(
157157
@click.option("--strip-none-values", "-snv", is_flag=True, help="Strip None values from the rule")
158158
@click.option("--local-creation-date", "-lc", is_flag=True, help="Preserve the local creation date of the rule")
159159
@click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule")
160+
@click.option(
161+
"--load-rule-loading",
162+
"-lr",
163+
is_flag=True,
164+
help="Enable arbitrary rule loading from the rules directories (Can be very slow!)",
165+
)
160166
def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915
161167
input_file: tuple[Path, ...] | None,
162168
required_only: bool,
@@ -171,6 +177,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915
171177
strip_none_values: bool,
172178
local_creation_date: bool,
173179
local_updated_date: bool,
180+
load_rule_loading: bool,
174181
) -> None:
175182
"""Import rules from json, toml, or yaml files containing Kibana exported rule(s)."""
176183
errors: list[str] = []
@@ -189,6 +196,10 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915
189196
if not file_contents:
190197
click.echo("Must specify at least one file!")
191198

199+
raw_rule_collection = RawRuleCollection()
200+
if load_rule_loading:
201+
raw_rule_collection = raw_rule_collection.default()
202+
192203
exceptions_containers = {}
193204
exceptions_items = {}
194205

@@ -210,7 +221,12 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915
210221
base_path = rulename_to_filename(base_path) if base_path else base_path
211222
if base_path is None:
212223
raise ValueError(f"Invalid rule file, please ensure the rule has a name field: {contents}")
213-
rule_path = Path(os.path.join(str(save_directory) if save_directory else RULES_DIRS[0], base_path)) # noqa: PTH118
224+
225+
rule_base_path = Path(save_directory or RULES_DIRS[0])
226+
rule_path = rule_base_path / base_path
227+
rule_id = contents.get("rule_id")
228+
if rule_id in raw_rule_collection.id_map:
229+
rule_path = raw_rule_collection.id_map[rule_id].path or rule_path
214230

215231
# handle both rule json formats loaded from kibana and toml
216232
data_view_id = contents.get("data_view_id") or contents.get("rule", {}).get("data_view_id")
@@ -226,7 +242,7 @@ def import_rules_into_repo( # noqa: PLR0912, PLR0913, PLR0915
226242

227243
contents.update(
228244
update_metadata_from_file(
229-
Path(rule_path), {"creation_date": local_creation_date, "updated_date": local_updated_date}
245+
rule_path, {"creation_date": local_creation_date, "updated_date": local_updated_date}
230246
)
231247
)
232248

detection_rules/rule.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,18 @@ def metadata(self) -> dict[str, Any]:
8282

8383
@property
8484
def data(self) -> dict[str, Any]:
85-
"""Rule portion of TOML file rule."""
86-
return self.contents.get("data") or self.contents
85+
"""Rule portion of TOML file rule. Supports nested and flattened rule dictionaries"""
86+
return self.contents.get("data", {}) or self.contents or self.contents.get("rule", {})
8787

8888
@property
8989
def id(self) -> str:
90-
"""Get the rule ID."""
91-
return self.data["rule_id"] # type: ignore[reportUnknownMemberType]
90+
"""Get the rule ID. Supports nested and flattened rule dictionaries."""
91+
return self.data.get("rule_id") or self.data.get("rule", {}).get("rule_id")
9292

9393
@property
9494
def name(self) -> str:
95-
"""Get the rule name."""
96-
return self.data["name"] # type: ignore[reportUnknownMemberType]
95+
"""Get the rule name. Supports nested and flattened rule dictionaries"""
96+
return self.data.get("name") or self.data.get("rule", {}).get("name")
9797

9898
def __hash__(self) -> int:
9999
"""Get the hash of the rule."""

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "detection_rules"
3-
version = "1.3.17"
3+
version = "1.3.18"
44
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."
55
readme = "README.md"
66
requires-python = ">=3.12"

0 commit comments

Comments
 (0)