Skip to content

Commit 5b3dac0

Browse files
[FR] Add Ability to Filter Rule Exports from Kibana (#4783)
* Add ability to filter on custom rules and filter exports
1 parent 727a648 commit 5b3dac0

File tree

4 files changed

+34
-15
lines changed

4 files changed

+34
-15
lines changed

detection_rules/kbwrap.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,26 @@ def _process_imported_items(imported_items_list, item_type_description, item_key
206206
"Use same flag for import-rules to prevent warnings and disable its unit test.")
207207
@click.option("--local-creation-date", "-lc", is_flag=True, help="Preserve the local creation date of the rule")
208208
@click.option("--local-updated-date", "-lu", is_flag=True, help="Preserve the local updated date of the rule")
209+
@click.option("--custom-rules-only", "-cro", is_flag=True, help="Only export custom rules")
210+
@click.option(
211+
"--export-query",
212+
"-eq",
213+
type=str,
214+
required=False,
215+
help=(
216+
"Apply a query filter to exporting rules e.g. "
217+
"\"alert.attributes.tags: \\\"test\\\"\" to filter for rules that have the tag \"test\""
218+
)
219+
)
209220
@click.pass_context
210221
def kibana_export_rules(ctx: click.Context, directory: Path, action_connectors_directory: Optional[Path],
211222
exceptions_directory: Optional[Path], default_author: str,
212223
rule_id: Optional[Iterable[str]] = None, rule_name: Optional[str] = None,
213224
export_action_connectors: bool = False,
214225
export_exceptions: bool = False, skip_errors: bool = False, strip_version: bool = False,
215226
no_tactic_filename: bool = False, local_creation_date: bool = False,
216-
local_updated_date: bool = False) -> List[TOMLRule]:
227+
local_updated_date: bool = False, custom_rules_only: bool = False,
228+
export_query: Optional[str] = None) -> List[TOMLRule]:
217229
"""Export custom rules from Kibana."""
218230
kibana = ctx.obj["kibana"]
219231
kibana_include_details = export_exceptions or export_action_connectors
@@ -227,8 +239,21 @@ def kibana_export_rules(ctx: click.Context, directory: Path, action_connectors_d
227239
if rule_name:
228240
found = RuleResource.find(filter=f"alert.attributes.name:{rule_name}")
229241
rule_id = [r["rule_id"] for r in found]
230-
results = RuleResource.export_rules(list(rule_id), exclude_export_details=not kibana_include_details)
242+
query = (
243+
export_query if not custom_rules_only
244+
else (
245+
f"alert.attributes.params.ruleSource.type: \"internal\""
246+
f"{f' and ({export_query})' if export_query else ''}"
247+
)
248+
)
231249

250+
results = (
251+
RuleResource.bulk_export(rule_ids=list(rule_id), query=query)
252+
if query
253+
else RuleResource.export_rules(
254+
list(rule_id), exclude_export_details=not kibana_include_details
255+
)
256+
)
232257
# Handle Exceptions Directory Location
233258
if results and exceptions_directory:
234259
exceptions_directory.mkdir(parents=True, exist_ok=True)

lib/kibana/kibana/resources.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# 2.0.
55

66
import datetime
7-
from typing import Any, List, Optional, Type
7+
from typing import List, Optional, Type
88

99
import json
1010

@@ -138,7 +138,7 @@ def bulk_action(
138138
cls, action: definitions.RuleBulkActions, rule_ids: Optional[List[str]] = None, query: Optional[str] = None,
139139
dry_run: Optional[bool] = False, edit_object: Optional[list[definitions.RuleBulkEditActionTypes]] = None,
140140
include_exceptions: Optional[bool] = False, **kwargs
141-
) -> (dict, List['RuleResource']):
141+
) -> dict | List['RuleResource']:
142142
"""Perform a bulk action on rules using the _bulk_action API."""
143143
assert not (rule_ids and query), 'Cannot provide both rule_ids and query'
144144

@@ -155,17 +155,11 @@ def bulk_action(
155155
data['rule_ids'] = rule_ids
156156
response = Kibana.current().post(cls.BASE_URI + "/_bulk_action", params=params, data=data, **kwargs)
157157

158-
# export returns ndjson, which requires manual parsing since response.json() fails
158+
# export returns ndjson
159159
if action == 'export':
160-
response = [json.loads(r) for r in response.text.splitlines()]
161-
result_ids = [r['rule_id'] for r in response if 'rule_id' in r]
162-
else:
163-
results = response['attributes']['results']
164-
result_ids = [r['rule_id'] for r in results['updated']]
165-
result_ids.extend([r['rule_id'] for r in results['created']])
160+
response = [cls(r) for r in [json.loads(r) for r in response.text.splitlines()]]
166161

167-
rule_resources = cls.export_rules(result_ids)
168-
return response, rule_resources
162+
return response
169163

170164
@classmethod
171165
def bulk_enable(

lib/kibana/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-kibana"
3-
version = "0.4.3"
3+
version = "0.4.4"
44
description = "Kibana API utilities for Elastic Detection Rules"
55
license = {text = "Elastic License v2"}
66
keywords = ["Elastic", "Kibana", "Detection Rules", "Security", "Elasticsearch"]

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.2.12"
3+
version = "1.2.13"
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)