18
18
format_pbar_string , # type: ignore
19
19
)
20
20
from contentctl .helper .utils import Utils
21
+ from contentctl .objects .base_security_event import BaseSecurityEvent
21
22
from contentctl .objects .base_test_result import TestResultStatus
22
23
from contentctl .objects .detection import Detection
23
24
from contentctl .objects .errors import (
@@ -222,6 +223,9 @@ class CorrelationSearch(BaseModel):
222
223
# The list of risk events found
223
224
_risk_events : list [RiskEvent ] | None = PrivateAttr (default = None )
224
225
226
+ # The list of risk data model events found
227
+ _risk_dm_events : list [BaseSecurityEvent ] | None = PrivateAttr (default = None )
228
+
225
229
# The list of notable events found
226
230
_notable_events : list [NotableEvent ] | None = PrivateAttr (default = None )
227
231
@@ -554,6 +558,13 @@ def get_risk_events(self, force_update: bool = False) -> list[RiskEvent]:
554
558
raise
555
559
events .append (event )
556
560
self .logger .debug (f"Found risk event for '{ self .name } ': { event } " )
561
+ else :
562
+ msg = (
563
+ f"Found event for unexpected index ({ result ['index' ]} ) in our query "
564
+ f"results (expected { Indexes .RISK_INDEX } )"
565
+ )
566
+ self .logger .error (msg )
567
+ raise ValueError (msg )
557
568
except ServerError as e :
558
569
self .logger .error (f"Error returned from Splunk instance: { e } " )
559
570
raise e
@@ -623,6 +634,13 @@ def get_notable_events(self, force_update: bool = False) -> list[NotableEvent]:
623
634
raise
624
635
events .append (event )
625
636
self .logger .debug (f"Found notable event for '{ self .name } ': { event } " )
637
+ else :
638
+ msg = (
639
+ f"Found event for unexpected index ({ result ['index' ]} ) in our query "
640
+ f"results (expected { Indexes .NOTABLE_INDEX } )"
641
+ )
642
+ self .logger .error (msg )
643
+ raise ValueError (msg )
626
644
except ServerError as e :
627
645
self .logger .error (f"Error returned from Splunk instance: { e } " )
628
646
raise e
@@ -637,15 +655,119 @@ def get_notable_events(self, force_update: bool = False) -> list[NotableEvent]:
637
655
638
656
return events
639
657
658
+ def risk_dm_event_exists (self ) -> bool :
659
+ """Whether at least one matching risk data model event exists
660
+
661
+ Queries the `risk` data model and returns True if at least one matching event (could come
662
+ from risk or notable index) exists for this search
663
+ :return: a bool indicating whether a risk data model event for this search exists in the
664
+ risk data model
665
+ """
666
+ # We always force an update on the cache when checking if events exist
667
+ events = self .get_risk_dm_events (force_update = True )
668
+ return len (events ) > 0
669
+
670
+ def get_risk_dm_events (self , force_update : bool = False ) -> list [BaseSecurityEvent ]:
671
+ """Get risk data model events from the Splunk instance
672
+
673
+ Queries the `risk` data model and returns any matching events (could come from risk or
674
+ notable index)
675
+ :param force_update: whether the cached _risk_events should be forcibly updated if already
676
+ set
677
+ :return: a list of risk events
678
+ """
679
+ # Reset the list of risk data model events if we're forcing an update
680
+ if force_update :
681
+ self .logger .debug ("Resetting risk data model event cache." )
682
+ self ._risk_dm_events = None
683
+
684
+ # Use the cached risk_dm_events unless we're forcing an update
685
+ if self ._risk_dm_events is not None :
686
+ self .logger .debug (
687
+ f"Using cached risk data model events ({ len (self ._risk_dm_events )} total)."
688
+ )
689
+ return self ._risk_dm_events
690
+
691
+ # TODO (#248): Refactor risk/notable querying to pin to a single savedsearch ID
692
+ # Search for all risk data model events from a single scheduled search (indicated by
693
+ # orig_sid)
694
+ query = (
695
+ f'datamodel Risk All_Risk flat | search search_name="{ self .name } " [datamodel Risk '
696
+ f'All_Risk flat | search search_name="{ self .name } " | tail 1 | fields orig_sid] '
697
+ "| tojson"
698
+ )
699
+ result_iterator = self ._search (query )
700
+
701
+ # Iterate over the events, storing them in a list and checking for any errors
702
+ events : list [BaseSecurityEvent ] = []
703
+ risk_count = 0
704
+ notable_count = 0
705
+ try :
706
+ for result in result_iterator :
707
+ # sanity check that this result from the iterator is a risk event and not some
708
+ # other metadata
709
+ if result ["index" ] == Indexes .RISK_INDEX :
710
+ try :
711
+ parsed_raw = json .loads (result ["_raw" ])
712
+ event = RiskEvent .model_validate (parsed_raw )
713
+ except Exception :
714
+ self .logger .error (
715
+ f"Failed to parse RiskEvent from search result: { result } "
716
+ )
717
+ raise
718
+ events .append (event )
719
+ risk_count += 1
720
+ self .logger .debug (
721
+ f"Found risk event in risk data model for '{ self .name } ': { event } "
722
+ )
723
+ elif result ["index" ] == Indexes .NOTABLE_INDEX :
724
+ try :
725
+ parsed_raw = json .loads (result ["_raw" ])
726
+ event = NotableEvent .model_validate (parsed_raw )
727
+ except Exception :
728
+ self .logger .error (
729
+ f"Failed to parse NotableEvent from search result: { result } "
730
+ )
731
+ raise
732
+ events .append (event )
733
+ notable_count += 1
734
+ self .logger .debug (
735
+ f"Found notable event in risk data model for '{ self .name } ': { event } "
736
+ )
737
+ else :
738
+ msg = (
739
+ f"Found event for unexpected index ({ result ['index' ]} ) in our query "
740
+ f"results (expected { Indexes .NOTABLE_INDEX } or { Indexes .RISK_INDEX } )"
741
+ )
742
+ self .logger .error (msg )
743
+ raise ValueError (msg )
744
+ except ServerError as e :
745
+ self .logger .error (f"Error returned from Splunk instance: { e } " )
746
+ raise e
747
+
748
+ # Log if no events were found
749
+ if len (events ) < 1 :
750
+ self .logger .debug (f"No events found in risk data model for '{ self .name } '" )
751
+ else :
752
+ # Set the cache if we found events
753
+ self ._risk_dm_events = events
754
+ self .logger .debug (
755
+ f"Caching { len (self ._risk_dm_events )} risk data model events."
756
+ )
757
+
758
+ # Log counts of risk and notable events found
759
+ self .logger .debug (
760
+ f"Found { risk_count } risk events and { notable_count } notable events in the risk data "
761
+ "model"
762
+ )
763
+
764
+ return events
765
+
640
766
def validate_risk_events (self ) -> None :
641
767
"""Validates the existence of any expected risk events
642
768
643
769
First ensure the risk event exists, and if it does validate its risk message and make sure
644
- any events align with the specified risk object. Also adds the risk index to the purge list
645
- if risk events existed
646
- :param elapsed_sleep_time: an int representing the amount of time slept thus far waiting to
647
- check the risks/notables
648
- :returns: an IntegrationTestResult on failure; None on success
770
+ any events align with the specified risk object.
649
771
"""
650
772
# Ensure the rba object is defined
651
773
if self .detection .rba is None :
@@ -735,13 +857,29 @@ def validate_risk_events(self) -> None:
735
857
def validate_notable_events (self ) -> None :
736
858
"""Validates the existence of any expected notables
737
859
738
- Ensures the notable exists. Also adds the notable index to the purge list if notables
739
- existed
740
- :param elapsed_sleep_time: an int representing the amount of time slept thus far waiting to
741
- check the risks/notables
742
- :returns: an IntegrationTestResult on failure; None on success
860
+ Check various fields within the notable to ensure alignment with the detection definition.
861
+ Additionally, ensure that the notable does not appear in the risk data model, as this is
862
+ currently undesired behavior for ESCU detections.
863
+ """
864
+ if self .notable_in_risk_dm ():
865
+ raise ValidationFailed (
866
+ "One or more notables appeared in the risk data model. This could lead to risk "
867
+ "score doubling, and/or notable multiplexing, depending on the detection type "
868
+ "(e.g. TTP), or the number of risk modifiers."
869
+ )
870
+
871
+ def notable_in_risk_dm (self ) -> bool :
872
+ """Check if notables are in the risk data model
873
+
874
+ Returns a bool indicating whether notables are in the risk data model or not.
875
+
876
+ :returns: a bool, True if notables are in the risk data model results; False if not
743
877
"""
744
- raise NotImplementedError ()
878
+ if self .risk_dm_event_exists ():
879
+ for event in self .get_risk_dm_events ():
880
+ if isinstance (event , NotableEvent ):
881
+ return True
882
+ return False
745
883
746
884
# NOTE: it would be more ideal to switch this to a system which gets the handle of the saved search job and polls
747
885
# it for completion, but that seems more tricky
@@ -828,8 +966,8 @@ def test(
828
966
829
967
try :
830
968
# Validate risk events
831
- self .logger .debug ("Checking for matching risk events" )
832
969
if self .has_risk_analysis_action :
970
+ self .logger .debug ("Checking for matching risk events" )
833
971
if self .risk_event_exists ():
834
972
# TODO (PEX-435): should this in the retry loop? or outside it?
835
973
# -> I've observed there being a missing risk event (15/16) on
@@ -846,22 +984,28 @@ def test(
846
984
raise ValidationFailed (
847
985
f"TEST FAILED: No matching risk event created for: { self .name } "
848
986
)
987
+ else :
988
+ self .logger .debug (
989
+ f"No risk action defined for '{ self .name } '"
990
+ )
849
991
850
992
# Validate notable events
851
- self .logger .debug ("Checking for matching notable events" )
852
993
if self .has_notable_action :
994
+ self .logger .debug ("Checking for matching notable events" )
853
995
# NOTE: because we check this last, if both fail, the error message about notables will
854
996
# always be the last to be added and thus the one surfaced to the user
855
997
if self .notable_event_exists ():
856
998
# TODO (PEX-435): should this in the retry loop? or outside it?
857
- # TODO (PEX-434): implement deeper notable validation (the method
858
- # commented out below is unimplemented)
859
- # self.validate_notable_events(elapsed_sleep_time)
999
+ self .validate_notable_events ()
860
1000
pass
861
1001
else :
862
1002
raise ValidationFailed (
863
1003
f"TEST FAILED: No matching notable event created for: { self .name } "
864
1004
)
1005
+ else :
1006
+ self .logger .debug (
1007
+ f"No notable action defined for '{ self .name } '"
1008
+ )
865
1009
except ValidationFailed as e :
866
1010
self .logger .error (f"Risk/notable validation failed: { e } " )
867
1011
result = IntegrationTestResult (
@@ -1015,6 +1159,7 @@ def cleanup(self, delete_test_index: bool = False) -> None:
1015
1159
# reset caches
1016
1160
self ._risk_events = None
1017
1161
self ._notable_events = None
1162
+ self ._risk_dm_events = None
1018
1163
1019
1164
def update_pbar (self , state : str ) -> str :
1020
1165
"""
0 commit comments