Skip to content

Commit 9b682b7

Browse files
frederikb96Mikaayensoneric-forte-elastic
authored
Feature exclude tactic name (#4593)
* Added new cli flag to exclude tactic name in rule file name * added a shortcut for the flag and adjusted CLI readme * Add no tactic flag also to import to prevent warnings * Added info about unit test * version bump * Added no_tactic_filename as config option + fixed linting * pyproject version bump --------- Co-authored-by: Mika Ayenson, PhD <[email protected]> Co-authored-by: Eric Forte <[email protected]>
1 parent 033c828 commit 9b682b7

File tree

7 files changed

+37
-5
lines changed

7 files changed

+37
-5
lines changed

CLI.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ Options:
265265
-e, --overwrite-exceptions Overwrite exceptions in existing rules
266266
-ac, --overwrite-action-connectors
267267
Overwrite action connectors in existing rules
268+
-nt, --no-tactic-filename Allow rule filenames without tactic prefix. Use this if rules have been exported with this flag.
268269
-h, --help Show this message and exit.
269270
```
270271

@@ -520,6 +521,7 @@ Options:
520521
-e, --export-exceptions Include exceptions in export
521522
-s, --skip-errors Skip errors when exporting rules
522523
-sv, --strip-version Strip the version fields from all rules
524+
-nt, --no-tactic-filename Exclude tactic prefix in exported filenames for rules. Use same flag for import-rules to prevent warnings and disable its unit test.
523525
-h, --help Show this message and exit.
524526
525527
```

detection_rules/cli_utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
dict_filter)
2424
from .schemas import definitions
2525
from .utils import clear_caches, rulename_to_filename
26+
from .config import parse_rules_config
27+
28+
RULES_CONFIG = parse_rules_config()
2629

2730

2831
def single_collection(f):
@@ -66,11 +69,15 @@ def multi_collection(f):
6669
@click.option("--directory", "-d", multiple=True, type=click.Path(file_okay=False), required=False,
6770
help="Recursively load rules from a directory")
6871
@click.option("--rule-id", "-id", multiple=True, required=False)
72+
@click.option("--no-tactic-filename", "-nt", is_flag=True, required=False,
73+
help="Allow rule filenames without tactic prefix. "
74+
"Use this if rules have been exported with this flag.")
6975
@functools.wraps(f)
7076
def get_collection(*args, **kwargs):
7177
rule_id: List[str] = kwargs.pop("rule_id", [])
7278
rule_files: List[str] = kwargs.pop("rule_file")
7379
directories: List[str] = kwargs.pop("directory")
80+
no_tactic_filename: bool = kwargs.pop("no_tactic_filename", False)
7481

7582
rules = RuleCollection()
7683

@@ -99,7 +106,10 @@ def get_collection(*args, **kwargs):
99106
for rule in rules:
100107
threat = rule.contents.data.get("threat")
101108
first_tactic = threat[0].tactic.name if threat else ""
102-
rule_name = rulename_to_filename(rule.contents.data.name, tactic_name=first_tactic)
109+
# Check if flag or config is set to not include tactic in the filename
110+
no_tactic_filename = no_tactic_filename or RULES_CONFIG.no_tactic_filename
111+
tactic_name = None if no_tactic_filename else first_tactic
112+
rule_name = rulename_to_filename(rule.contents.data.name, tactic_name=tactic_name)
103113
if rule.path.name != rule_name:
104114
click.secho(
105115
f"WARNING: Rule path does not match required path: {rule.path.name} != {rule_name}", fg="yellow"

detection_rules/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class RulesConfig:
193193
exception_dir: Optional[Path] = None
194194
normalize_kql_keywords: bool = True
195195
bypass_optional_elastic_validation: bool = False
196+
no_tactic_filename: bool = False
196197

197198
def __post_init__(self):
198199
"""Perform post validation on packages.yaml file."""
@@ -311,6 +312,10 @@ def parse_rules_config(path: Optional[Path] = None) -> RulesConfig:
311312
if contents['bypass_optional_elastic_validation']:
312313
set_all_validation_bypass(contents['bypass_optional_elastic_validation'])
313314

315+
# no_tactic_filename
316+
contents['no_tactic_filename'] = loaded.get('no_tactic_filename', False)
317+
318+
# return the config
314319
try:
315320
rules_config = RulesConfig(test_config=test_config, **contents)
316321
except (ValueError, TypeError) as e:

detection_rules/etc/_config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,8 @@ normalize_kql_keywords: False
7272
# If set in this file, the path should be relative to the location of this config. If passed as an environment variable,
7373
# it should be the full path
7474
# Note: Using the `custom-rules setup-config <name>` command will generate a config called `test_config.yaml`
75+
76+
# To prevent the tactic prefix from being added to the rule filename, set the line below to True
77+
# This config line can be used instead of specifying the `--no-tactic-filename` flag in the CLI
78+
# Mind that for unit tests, you also want to disable the filename test in the test_config.yaml
79+
# no_tactic_filename: True

detection_rules/kbwrap.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,18 @@ def _process_imported_items(imported_items_list, item_type_description, item_key
199199
@click.option("--export-exceptions", "-e", is_flag=True, help="Include exceptions in export")
200200
@click.option("--skip-errors", "-s", is_flag=True, help="Skip errors when exporting rules")
201201
@click.option("--strip-version", "-sv", is_flag=True, help="Strip the version fields from all rules")
202+
@click.option("--no-tactic-filename", "-nt", is_flag=True,
203+
help="Exclude tactic prefix in exported filenames for rules. "
204+
"Use same flag for import-rules to prevent warnings and disable its unit test.")
202205
@click.option("--local-creation-date", "-lc", is_flag=True, help="Preserve the local creation date of the rule")
203206
@click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule")
204207
@click.pass_context
205208
def kibana_export_rules(ctx: click.Context, directory: Path, action_connectors_directory: Optional[Path],
206209
exceptions_directory: Optional[Path], default_author: str,
207210
rule_id: Optional[Iterable[str]] = None, export_action_connectors: bool = False,
208211
export_exceptions: bool = False, skip_errors: bool = False, strip_version: bool = False,
209-
local_creation_date: bool = False, local_updated_date: bool = False) -> List[TOMLRule]:
212+
no_tactic_filename: bool = False, local_creation_date: bool = False,
213+
local_updated_date: bool = False) -> List[TOMLRule]:
210214
"""Export custom rules from Kibana."""
211215
kibana = ctx.obj["kibana"]
212216
kibana_include_details = export_exceptions or export_action_connectors
@@ -270,7 +274,11 @@ def kibana_export_rules(ctx: click.Context, directory: Path, action_connectors_d
270274
}
271275
threat = rule_resource.get("threat")
272276
first_tactic = threat[0].get("tactic").get("name") if threat else ""
273-
rule_name = rulename_to_filename(rule_resource.get("name"), tactic_name=first_tactic)
277+
# Check if flag or config is set to not include tactic in the filename
278+
no_tactic_filename = no_tactic_filename or RULES_CONFIG.no_tactic_filename
279+
# Check if the flag is set to not include tactic in the filename
280+
tactic_name = first_tactic if not no_tactic_filename else None
281+
rule_name = rulename_to_filename(rule_resource.get("name"), tactic_name=tactic_name)
274282

275283
save_path = directory / f"{rule_name}"
276284
params.update(

docs-dev/custom-rules-management.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ be set in `_config.yaml` or as the environment variable `DETECTION_RULES_TEST_CO
9494
environment variable if both are set. Having both these options allows for configuring testing on prebuilt Elastic rules
9595
without specifying a rules _config.yaml.
9696

97+
Some notes:
9798

98-
* Note: If set in this file, the path should be relative to the location of this config. If passed as an environment variable, it should be the full path
99+
* If set in this file, the path should be relative to the location of this config. If passed as an environment variable, it should be the full path
100+
* When using the `--no-tactic-filename` flag for kibana imports and exports, be sure to disable the unit test by using the following line `- tests.test_all_rules.TestRuleFiles.test_rule_file_name_tactic` in your test config file.
99101

100102

101103
### How the config is used and it's designed portability

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.0.10"
3+
version = "1.0.11"
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)