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 <Mikaayenson@users.noreply.github.com> Co-authored-by: Eric Forte <119343520+eric-forte-elastic@users.noreply.github.com>
This commit is contained in:
@@ -265,6 +265,7 @@ Options:
|
||||
-e, --overwrite-exceptions Overwrite exceptions in existing rules
|
||||
-ac, --overwrite-action-connectors
|
||||
Overwrite action connectors in existing rules
|
||||
-nt, --no-tactic-filename Allow rule filenames without tactic prefix. Use this if rules have been exported with this flag.
|
||||
-h, --help Show this message and exit.
|
||||
```
|
||||
|
||||
@@ -520,6 +521,7 @@ Options:
|
||||
-e, --export-exceptions Include exceptions in export
|
||||
-s, --skip-errors Skip errors when exporting rules
|
||||
-sv, --strip-version Strip the version fields from all rules
|
||||
-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.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
```
|
||||
|
||||
@@ -23,6 +23,9 @@ from .rule_loader import (DEFAULT_PREBUILT_BBR_DIRS,
|
||||
dict_filter)
|
||||
from .schemas import definitions
|
||||
from .utils import clear_caches, rulename_to_filename
|
||||
from .config import parse_rules_config
|
||||
|
||||
RULES_CONFIG = parse_rules_config()
|
||||
|
||||
|
||||
def single_collection(f):
|
||||
@@ -66,11 +69,15 @@ def multi_collection(f):
|
||||
@click.option("--directory", "-d", multiple=True, type=click.Path(file_okay=False), required=False,
|
||||
help="Recursively load rules from a directory")
|
||||
@click.option("--rule-id", "-id", multiple=True, required=False)
|
||||
@click.option("--no-tactic-filename", "-nt", is_flag=True, required=False,
|
||||
help="Allow rule filenames without tactic prefix. "
|
||||
"Use this if rules have been exported with this flag.")
|
||||
@functools.wraps(f)
|
||||
def get_collection(*args, **kwargs):
|
||||
rule_id: List[str] = kwargs.pop("rule_id", [])
|
||||
rule_files: List[str] = kwargs.pop("rule_file")
|
||||
directories: List[str] = kwargs.pop("directory")
|
||||
no_tactic_filename: bool = kwargs.pop("no_tactic_filename", False)
|
||||
|
||||
rules = RuleCollection()
|
||||
|
||||
@@ -99,7 +106,10 @@ def multi_collection(f):
|
||||
for rule in rules:
|
||||
threat = rule.contents.data.get("threat")
|
||||
first_tactic = threat[0].tactic.name if threat else ""
|
||||
rule_name = rulename_to_filename(rule.contents.data.name, tactic_name=first_tactic)
|
||||
# Check if flag or config is set to not include tactic in the filename
|
||||
no_tactic_filename = no_tactic_filename or RULES_CONFIG.no_tactic_filename
|
||||
tactic_name = None if no_tactic_filename else first_tactic
|
||||
rule_name = rulename_to_filename(rule.contents.data.name, tactic_name=tactic_name)
|
||||
if rule.path.name != rule_name:
|
||||
click.secho(
|
||||
f"WARNING: Rule path does not match required path: {rule.path.name} != {rule_name}", fg="yellow"
|
||||
|
||||
@@ -193,6 +193,7 @@ class RulesConfig:
|
||||
exception_dir: Optional[Path] = None
|
||||
normalize_kql_keywords: bool = True
|
||||
bypass_optional_elastic_validation: bool = False
|
||||
no_tactic_filename: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""Perform post validation on packages.yaml file."""
|
||||
@@ -311,6 +312,10 @@ def parse_rules_config(path: Optional[Path] = None) -> RulesConfig:
|
||||
if contents['bypass_optional_elastic_validation']:
|
||||
set_all_validation_bypass(contents['bypass_optional_elastic_validation'])
|
||||
|
||||
# no_tactic_filename
|
||||
contents['no_tactic_filename'] = loaded.get('no_tactic_filename', False)
|
||||
|
||||
# return the config
|
||||
try:
|
||||
rules_config = RulesConfig(test_config=test_config, **contents)
|
||||
except (ValueError, TypeError) as e:
|
||||
|
||||
@@ -72,3 +72,8 @@ normalize_kql_keywords: False
|
||||
# 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
|
||||
# Note: Using the `custom-rules setup-config <name>` command will generate a config called `test_config.yaml`
|
||||
|
||||
# To prevent the tactic prefix from being added to the rule filename, set the line below to True
|
||||
# This config line can be used instead of specifying the `--no-tactic-filename` flag in the CLI
|
||||
# Mind that for unit tests, you also want to disable the filename test in the test_config.yaml
|
||||
# no_tactic_filename: True
|
||||
@@ -199,6 +199,9 @@ def kibana_import_rules(ctx: click.Context, rules: RuleCollection, overwrite: Op
|
||||
@click.option("--export-exceptions", "-e", is_flag=True, help="Include exceptions in export")
|
||||
@click.option("--skip-errors", "-s", is_flag=True, help="Skip errors when exporting rules")
|
||||
@click.option("--strip-version", "-sv", is_flag=True, help="Strip the version fields from all rules")
|
||||
@click.option("--no-tactic-filename", "-nt", is_flag=True,
|
||||
help="Exclude tactic prefix in exported filenames for rules. "
|
||||
"Use same flag for import-rules to prevent warnings and disable its unit test.")
|
||||
@click.option("--local-creation-date", "-lc", is_flag=True, help="Preserve the local creation date of the rule")
|
||||
@click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule")
|
||||
@click.pass_context
|
||||
@@ -206,7 +209,8 @@ def kibana_export_rules(ctx: click.Context, directory: Path, action_connectors_d
|
||||
exceptions_directory: Optional[Path], default_author: str,
|
||||
rule_id: Optional[Iterable[str]] = None, export_action_connectors: bool = False,
|
||||
export_exceptions: bool = False, skip_errors: bool = False, strip_version: bool = False,
|
||||
local_creation_date: bool = False, local_updated_date: bool = False) -> List[TOMLRule]:
|
||||
no_tactic_filename: bool = False, local_creation_date: bool = False,
|
||||
local_updated_date: bool = False) -> List[TOMLRule]:
|
||||
"""Export custom rules from Kibana."""
|
||||
kibana = ctx.obj["kibana"]
|
||||
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
|
||||
}
|
||||
threat = rule_resource.get("threat")
|
||||
first_tactic = threat[0].get("tactic").get("name") if threat else ""
|
||||
rule_name = rulename_to_filename(rule_resource.get("name"), tactic_name=first_tactic)
|
||||
# Check if flag or config is set to not include tactic in the filename
|
||||
no_tactic_filename = no_tactic_filename or RULES_CONFIG.no_tactic_filename
|
||||
# Check if the flag is set to not include tactic in the filename
|
||||
tactic_name = first_tactic if not no_tactic_filename else None
|
||||
rule_name = rulename_to_filename(rule_resource.get("name"), tactic_name=tactic_name)
|
||||
|
||||
save_path = directory / f"{rule_name}"
|
||||
params.update(
|
||||
|
||||
@@ -94,8 +94,10 @@ be set in `_config.yaml` or as the environment variable `DETECTION_RULES_TEST_CO
|
||||
environment variable if both are set. Having both these options allows for configuring testing on prebuilt Elastic rules
|
||||
without specifying a rules _config.yaml.
|
||||
|
||||
Some notes:
|
||||
|
||||
* 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
|
||||
* 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
|
||||
* 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.
|
||||
|
||||
|
||||
### How the config is used and it's designed portability
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "detection_rules"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user