Skip to content

Commit 7e459dd

Browse files
[FR] Add support for New Terms Fields and Window Start History (elastic#2360)
* adding support new_terms_fields and window_start_history * adjusted rule.py to address flake errors * added assertion error if history_window_start does not exist * removed sample rule * removed self.rule_id from DataValidator * added new_terms to RuleType * changed new terms to its own class in rule.py * removed nonexisting function call in DataValidator class * adjusted new_terms field value in dataclass * changed literal type for history_window_start; view-rule working * removing test TOML rule * addressed flake errors for missing newlines * added validation option and adjusted object referencing * adjusted validation method call in post_validation * addressed flake errors for multiple spaces * added transform method to NewTermsRuleData class * added validation for min stack version and new terms array length restraints * added validation for unique new terms array * Update detection_rules/rule.py Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com> * removed historywindowstart definition and adjusted subclass * removed test rule from commit * adjusted if/else for data transform method check * adjusted stack-schema-map; validation method name * Update detection_rules/rule.py Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com> * added assertion for history_window_start field value * added variables for feature min stack and extended field min stack * Update detection_rules/rule.py Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com> * Update detection_rules/rule.py Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com> * addressed flake errors for continuation line with same indent Co-authored-by: Mika Ayenson <Mikaayenson@users.noreply.github.com>
1 parent c6f5d47 commit 7e459dd

File tree

3 files changed

+79
-5
lines changed

3 files changed

+79
-5
lines changed

detection_rules/etc/stack-schema-map.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
"8.4.0":
5353
beats: "main"
54-
ecs: "8.3.1"
54+
ecs: "8.4.0"
5555
endgame: "8.4.0"
5656

5757
"8.5.0":

detection_rules/rule.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ class RelatedIntegrations:
186186
filters: Optional[List[dict]]
187187
# trailing `_` required since `from` is a reserved word in python
188188
from_: Optional[str] = field(metadata=dict(data_key="from"))
189-
190189
interval: Optional[definitions.Interval]
191190
max_signals: Optional[definitions.MaxSignals]
192191
meta: Optional[Dict[str, Any]]
@@ -277,7 +276,6 @@ def __init__(self,
277276
self.is_elastic_rule = is_elastic_rule
278277
self.note = note
279278
self.setup = setup
280-
281279
self._setup_in_note = False
282280

283281
@cached_property
@@ -462,6 +460,74 @@ class ThresholdCardinality:
462460
threshold: ThresholdMapping
463461

464462

463+
@dataclass(frozen=True)
464+
class NewTermsRuleData(QueryRuleData):
465+
"""Specific fields for new terms field rule."""
466+
467+
@dataclass(frozen=True)
468+
class NewTermsMapping(MarshmallowDataclassMixin):
469+
@dataclass(frozen=True)
470+
class HistoryWindowStart:
471+
field: definitions.NonEmptyStr
472+
value: definitions.NonEmptyStr
473+
474+
field: definitions.NonEmptyStr
475+
value: definitions.NewTermsFields
476+
history_window_start: List[HistoryWindowStart]
477+
478+
type: Literal["new_terms"]
479+
new_terms: NewTermsMapping
480+
481+
def validate(self, meta: RuleMeta) -> None:
482+
"""Validates terms in new_terms_fields are valid ECS schema."""
483+
484+
super(NewTermsRuleData, self).validate_query(meta)
485+
feature_min_stack = '8.4.0'
486+
feature_min_stack_extended_fields = '8.6.0'
487+
488+
# validate history window start field exists and is correct
489+
assert self.new_terms.history_window_start, \
490+
"new terms field found with no history_window_start field defined"
491+
assert self.new_terms.history_window_start[0].field == "history_window_start", \
492+
f"{self.new_terms.history_window_start} should be 'history_window_start'"
493+
494+
# validate new terms and history window start fields is correct
495+
assert self.new_terms.field == "new_terms_fields", \
496+
f"{self.new_terms.field} should be 'new_terms_fields' for new_terms rule type"
497+
498+
# ecs validation
499+
min_stack_version = meta.get("min_stack_version")
500+
if min_stack_version is None:
501+
min_stack_version = str(Version(Version(load_current_package_version()) + (0,)))
502+
503+
stack_version = Version(feature_min_stack)
504+
assert stack_version >= Version(feature_min_stack), \
505+
f"New Terms rule types only compatible with {feature_min_stack}+"
506+
ecs_version = get_stack_schemas()[str(stack_version)]['ecs']
507+
ecs_schema = ecs.get_schema(ecs_version)
508+
for new_terms_field in self.new_terms.value:
509+
assert new_terms_field in ecs_schema.keys(), \
510+
f"{new_terms_field} not found in ECS schema (version {ecs_version})"
511+
512+
# validates length of new_terms to stack version - https://github.com/elastic/kibana/issues/142862
513+
if stack_version >= Version(feature_min_stack) and \
514+
stack_version < Version(feature_min_stack_extended_fields):
515+
assert len(self.new_terms.value) == 1, \
516+
f"new terms have a max limit of 1 for stack versions below {feature_min_stack_extended_fields}"
517+
518+
# validate fields are unique
519+
assert len(set(self.new_terms.value)) == len(self.new_terms.value), \
520+
f"new terms fields values are not unique - {self.new_terms.value}"
521+
522+
def transform(self, obj: dict) -> dict:
523+
"""Transforms new terms data to API format for Kibana."""
524+
525+
obj[obj["new_terms"].get("field")] = obj["new_terms"].get("value")
526+
obj["history_window_start"] = obj["new_terms"]["history_window_start"][0].get("value")
527+
del obj["new_terms"]
528+
return obj
529+
530+
465531
@dataclass(frozen=True)
466532
class EQLRuleData(QueryRuleData):
467533
"""EQL rules are a special case of query rules."""
@@ -565,7 +631,8 @@ def validate_query(self, meta: RuleMeta) -> None:
565631

566632
# All of the possible rule types
567633
# Sort inverse of any inheritance - see comment in TOMLRuleContents.to_dict
568-
AnyRuleData = Union[EQLRuleData, ThresholdQueryRuleData, ThreatMatchRuleData, MachineLearningRuleData, QueryRuleData]
634+
AnyRuleData = Union[EQLRuleData, ThresholdQueryRuleData, ThreatMatchRuleData,
635+
MachineLearningRuleData, QueryRuleData, NewTermsRuleData]
569636

570637

571638
class BaseRuleContents(ABC):
@@ -748,6 +815,7 @@ def _post_dict_transform(self, obj: dict) -> dict:
748815
"""Transform the converted API in place before sending to Kibana."""
749816
super()._post_dict_transform(obj)
750817

818+
# build time fields
751819
self._add_related_integrations(obj)
752820
self._add_required_fields(obj)
753821
self._add_setup(obj)
@@ -756,6 +824,10 @@ def _post_dict_transform(self, obj: dict) -> dict:
756824
rule_type = obj['type']
757825
subclass = self.get_data_subclass(rule_type)
758826
subclass.from_dict(obj)
827+
828+
# rule type transforms
829+
self.data.transform(obj) if hasattr(self.data, 'transform') else False
830+
759831
return obj
760832

761833
def _add_related_integrations(self, obj: dict) -> None:
@@ -902,6 +974,7 @@ def post_validation(self, value: dict, **kwargs):
902974

903975
data.validate_query(metadata)
904976
data.data_validator.validate_note()
977+
data.validate(metadata) if hasattr(data, 'validate') else False
905978

906979
def to_dict(self, strip_none_values=True) -> dict:
907980
# Load schemas directly from the data and metadata classes to avoid schema ambiguity which can

detection_rules/schemas/definitions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,13 @@
6565
Markdown = NewType("MarkdownField", CodeString)
6666
Maturity = Literal['development', 'experimental', 'beta', 'production', 'deprecated']
6767
MaxSignals = NewType("MaxSignals", int, validate=validate.Range(min=1))
68+
NewTermsFields = NewType('NewTermsFields', List[NonEmptyStr], validate=validate.Length(min=1, max=3))
6869
Operator = Literal['equals']
6970
OSType = Literal['windows', 'linux', 'macos']
7071
PositiveInteger = NewType('PositiveInteger', int, validate=validate.Range(min=1))
7172
RiskScore = NewType("MaxSignals", int, validate=validate.Range(min=1, max=100))
7273
RuleName = NewType('RuleName', str, validate=validate.Regexp(NAME_PATTERN))
73-
RuleType = Literal['query', 'saved_query', 'machine_learning', 'eql', 'threshold', 'threat_match']
74+
RuleType = Literal['query', 'saved_query', 'machine_learning', 'eql', 'threshold', 'threat_match', 'new_terms']
7475
SemVer = NewType('SemVer', str, validate=validate.Regexp(VERSION_PATTERN))
7576
SemVerMinorOnly = NewType('SemVerFullStrict', str, validate=validate.Regexp(MINOR_SEMVER))
7677
Severity = Literal['low', 'medium', 'high', 'critical']

0 commit comments

Comments
 (0)