Skip to content

Commit 1bb05ed

Browse files
[FR] Updates to KQL Lib Parsing and Install (#3605)
* Bump Version * updated * Bump patch version * Optimization should only occur on single values * Wildcard semantically equivalent to query_string* * Add unit test for optimization * Move code-checks to yml * Add tests path to code-checks * Add lib path for code-checks * Install deps from local * Update DSL optimization unit test --------- Co-authored-by: Terrance DeJesus <[email protected]> (cherry picked from commit 03f9772)
1 parent a54eff2 commit 1bb05ed

File tree

8 files changed

+37
-7
lines changed

8 files changed

+37
-7
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ on:
88
paths:
99
- 'detection_rules/**/*.py'
1010
- 'hunting/**/*.py'
11+
- 'tests/**/*.py'
12+
- 'lib/**/*.py'
1113

1214
jobs:
1315
code-checks:

.github/workflows/pythonpackage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
python -m pip install --upgrade pip
3131
pip cache purge
3232
pip install .[dev]
33+
pip install lib/kibana
34+
pip install lib/kql
3335
3436
- name: Unit tests
3537
env:

lib/kql/kql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .kql2eql import KqlToEQL
1414
from .parser import lark_parse, KqlParser
1515

16-
__version__ = '0.1.7'
16+
__version__ = '0.1.8'
1717
__all__ = (
1818
"ast",
1919
"from_eql",

lib/kql/kql/dsl.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,18 @@ def boolean(**kwargs):
4747

4848
elif boolean_type == "must_not" and len(children) == 1:
4949
# must_not: [{bool: {must: x}}] -> {must_not: x}
50+
# optimize can only occur with one term
51+
# e.g. the following would not be valid
52+
# must_not: [{bool: {must: x} and {bool: {must: y} }] -> {must_not: x} {must_not: y}
5053
child = children[0]
51-
if list(child) == ["bool"] and list(child["bool"]) in (["filter"], ["must"]):
52-
negated, = child["bool"].values()
54+
is_bool = list(child) == ["bool"]
55+
bool_keys = list(child.get("bool", {}))
56+
has_valid_keys = bool_keys in (["filter"], ["must"])
57+
has_single_filter = len(child.get("bool", {}).get("filter", [])) == 1
58+
has_single_must = len(child.get("bool", {}).get("must", [])) == 1
59+
60+
if is_bool and has_valid_keys and (has_single_filter or has_single_must):
61+
(negated,) = child["bool"].values()
5362
dsl = {"must_not": negated}
5463
else:
5564
dsl = {"must_not": children}
@@ -65,7 +74,6 @@ def boolean(**kwargs):
6574

6675

6776
class ToDsl(Walker):
68-
6977
def _walk_default(self, node, *args, **kwargs):
7078
raise KqlCompileError("Unable to convert {}".format(node))
7179

lib/kql/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-kql"
3-
version = "0.1.7"
3+
version = "0.1.8"
44
description = "Kibana Query Language parser for Elastic Detection Rules"
55
license = {text = "Elastic License v2"}
66
keywords = ["Elastic", "sour", "Detection Rules", "Security", "Elasticsearch", "kql"]

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.6"
3+
version = "1.3.7"
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"

tests/kuery/test_dsl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def test_not_query(self):
7676

7777
self.validate(
7878
"not (field:value and field2:value2)",
79-
{"must_not": [{"match": {"field": "value"}}, {"match": {"field2": "value2"}}]},
79+
{"must_not": [{"bool": {"filter": [{"match": {"field": "value"}}, {"match": {"field2": "value2"}}]}}]},
8080
)
8181

8282
def test_optimizations(self):

tests/kuery/test_parser.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,21 @@ def test_date(self):
8585

8686
with self.assertRaises(kql.KqlParseError):
8787
kql.parse("@time > 5", schema=schema)
88+
89+
def test_optimization(self):
90+
query = 'host.name: test-* and not (destination.ip : "127.0.0.53" and destination.ip : "169.254.169.254")'
91+
dsl_str = str(kql.to_dsl(query))
92+
93+
bad_case = (
94+
"{'bool': {'filter': [{'query_string': {'fields': ['host.name'], 'query': 'test-*'}}], "
95+
"'must_not': [{'match': {'destination.ip': '127.0.0.53'}}, "
96+
"{'match': {'destination.ip': '169.254.169.254'}}]}}"
97+
)
98+
self.assertNotEqual(dsl_str, bad_case, "DSL string matches the bad case, optimization failed.")
99+
100+
good_case = (
101+
"{'bool': {'filter': [{'query_string': {'fields': ['host.name'], 'query': 'test-*'}}], "
102+
"'must_not': [{'bool': {'filter': [{'match': {'destination.ip': '127.0.0.53'}}, "
103+
"{'match': {'destination.ip': '169.254.169.254'}}]}}]}}"
104+
)
105+
self.assertEqual(dsl_str, good_case, "DSL string does not match the good case, optimization failed.")

0 commit comments

Comments
 (0)