17
17
18
18
import eql
19
19
import kql
20
+ from kql .ast import FieldComparison
20
21
from marko .block import Document as MarkoDocument
21
22
from marko .ext .gfm import gfm
22
23
from marshmallow import ValidationError , validates_schema
23
24
24
25
from . import beats , ecs , utils
26
+ from .integrations import (find_least_compatible_version ,
27
+ load_integrations_manifests )
25
28
from .misc import load_current_package_version
26
29
from .mixins import MarshmallowDataclassMixin , StackCompatMixin
27
30
from .rule_formatter import nested_normalize , toml_write
@@ -165,6 +168,12 @@ class RequiredFields:
165
168
type : definitions .NonEmptyStr
166
169
ecs : bool
167
170
171
+ @dataclass
172
+ class RelatedIntegrations :
173
+ package : definitions .NonEmptyStr
174
+ version : definitions .NonEmptyStr
175
+ integration : Optional [definitions .NonEmptyStr ]
176
+
168
177
actions : Optional [list ]
169
178
author : List [str ]
170
179
building_block_type : Optional [str ]
@@ -186,7 +195,7 @@ class RequiredFields:
186
195
# explicitly NOT allowed!
187
196
# output_index: Optional[str]
188
197
references : Optional [List [str ]]
189
- related_integrations : Optional [List [str ]] = field (metadata = dict (metadata = dict (min_compat = "8.3" )))
198
+ related_integrations : Optional [List [RelatedIntegrations ]] = field (metadata = dict (metadata = dict (min_compat = "8.3" )))
190
199
required_fields : Optional [List [RequiredFields ]] = field (metadata = dict (metadata = dict (min_compat = "8.3" )))
191
200
risk_score : definitions .RiskScore
192
201
risk_score_mapping : Optional [List [RiskScoreMapping ]]
@@ -665,7 +674,7 @@ def _post_dict_transform(self, obj: dict) -> dict:
665
674
"""Transform the converted API in place before sending to Kibana."""
666
675
super ()._post_dict_transform (obj )
667
676
668
- self .add_related_integrations (obj )
677
+ self ._add_related_integrations (obj )
669
678
self ._add_required_fields (obj )
670
679
self ._add_setup (obj )
671
680
@@ -675,10 +684,37 @@ def _post_dict_transform(self, obj: dict) -> dict:
675
684
subclass .from_dict (obj )
676
685
return obj
677
686
678
- def add_related_integrations (self , obj : dict ) -> None :
687
+ def _add_related_integrations (self , obj : dict ) -> None :
679
688
"""Add restricted field related_integrations to the obj."""
680
- # field_name = "related_integrations"
681
- ...
689
+ field_name = "related_integrations"
690
+ package_integrations = obj .get (field_name , [])
691
+
692
+ if not package_integrations and self .metadata .integration :
693
+ packages_manifest = load_integrations_manifests ()
694
+ current_stack_version = load_current_package_version ()
695
+
696
+ if self .check_restricted_field_version (field_name ):
697
+ if isinstance (self .data , QueryRuleData ) and self .data .language != 'lucene' :
698
+ package_integrations = self ._get_packaged_integrations (packages_manifest )
699
+
700
+ if not package_integrations :
701
+ return
702
+
703
+ for package in package_integrations :
704
+ package ["version" ] = find_least_compatible_version (
705
+ package = package ["package" ],
706
+ integration = package ["integration" ],
707
+ current_stack_version = current_stack_version ,
708
+ packages_manifest = packages_manifest )
709
+
710
+ # if integration is not a policy template remove
711
+ if package ["version" ]:
712
+ policy_templates = packages_manifest [
713
+ package ["package" ]][package ["version" ]]["policy_templates" ]
714
+ if package ["integration" ] not in policy_templates :
715
+ del package ["integration" ]
716
+
717
+ obj .setdefault ("related_integrations" , package_integrations )
682
718
683
719
def _add_required_fields (self , obj : dict ) -> None :
684
720
"""Add restricted field required_fields to the obj, derived from the query AST."""
@@ -689,7 +725,7 @@ def _add_required_fields(self, obj: dict) -> None:
689
725
required_fields = []
690
726
691
727
field_name = "required_fields"
692
- if self .check_restricted_field_version (field_name = field_name ):
728
+ if required_fields and self .check_restricted_field_version (field_name = field_name ):
693
729
obj .setdefault (field_name , required_fields )
694
730
695
731
def _add_setup (self , obj : dict ) -> None :
@@ -759,6 +795,31 @@ def compare_field_versions(min_stack: Version, max_stack: Version) -> bool:
759
795
max_stack = max_stack or current_version
760
796
return Version (min_stack ) <= current_version >= Version (max_stack )
761
797
798
+ def _get_packaged_integrations (self , package_manifest : dict ) -> Optional [List [dict ]]:
799
+ packaged_integrations = []
800
+ datasets = set ()
801
+
802
+ for node in self .data .get ('ast' , []):
803
+ if isinstance (node , eql .ast .Comparison ) and str (node .left ) == 'event.dataset' :
804
+ datasets .update (set (n .value for n in node if isinstance (n , eql .ast .Literal )))
805
+ elif isinstance (node , FieldComparison ) and str (node .field ) == 'event.dataset' :
806
+ datasets .update (set (str (n ) for n in node if isinstance (n , kql .ast .Value )))
807
+
808
+ if not datasets :
809
+ return
810
+
811
+ for value in sorted (datasets ):
812
+ integration = 'Unknown'
813
+ if '.' in value :
814
+ package , integration = value .split ('.' , 1 )
815
+ else :
816
+ package = value
817
+
818
+ if package in list (package_manifest ):
819
+ packaged_integrations .append ({"package" : package , "integration" : integration })
820
+
821
+ return packaged_integrations
822
+
762
823
@validates_schema
763
824
def post_validation (self , value : dict , ** kwargs ):
764
825
"""Additional validations beyond base marshmallow schemas."""
0 commit comments