4343 DetectionStatus ,
4444 NistCategory ,
4545 ProvidingTechnology ,
46+ RiskSeverity ,
4647)
4748from contentctl .objects .integration_test import IntegrationTest
4849from contentctl .objects .manual_test import ManualTest
49- from contentctl .objects .rba import RBAObject
50+ from contentctl .objects .rba import RBAObject , RiskScoreValue_Type
5051from contentctl .objects .security_content_object import SecurityContentObject
5152from contentctl .objects .test_group import TestGroup
5253from contentctl .objects .unit_test import UnitTest
@@ -66,6 +67,54 @@ class Detection_Abstract(SecurityContentObject):
6667 how_to_implement : str = Field (..., min_length = 4 )
6768 known_false_positives : str = Field (..., min_length = 4 )
6869 rba : Optional [RBAObject ] = Field (default = None )
70+
71+ @computed_field
72+ @property
73+ def risk_score (self ) -> RiskScoreValue_Type :
74+ # First get the maximum score associated with
75+ # a risk object. If there are no objects, then
76+ # we should throw an exception.
77+ if self .rba is None or len (self .rba .risk_objects ) == 0 :
78+ raise Exception (
79+ "There must be at least one Risk Object present to get Severity."
80+ )
81+ return max ([risk_object .score for risk_object in self .rba .risk_objects ])
82+
83+ @computed_field
84+ @property
85+ def severity (self ) -> RiskSeverity :
86+ """
87+ Severity is required for notables (but not risk objects).
88+ In the contentctl codebase, instead of requiring an additional
89+ field to be added to the YMLs, we derive the severity from the
90+ HIGHEST risk score of any risk object that is part of this detection.
91+ However, if a detection does not have a risk object but still has a notable,
92+ we will use a default value of high. This only impact Correlation searches. As
93+ TTP searches, which also generate notables, must also have risk object(s)
94+ """
95+ try :
96+ risk_score = self .risk_score
97+ except Exception :
98+ # This object does not have any RBA objects,
99+ # hence no disk score is returned. So we will
100+ # return the defualt value of high
101+ return RiskSeverity .HIGH
102+
103+ if 0 <= risk_score <= 20 :
104+ return RiskSeverity .INFORMATIONAL
105+ elif 20 < risk_score <= 40 :
106+ return RiskSeverity .LOW
107+ elif 40 < risk_score <= 60 :
108+ return RiskSeverity .MEDIUM
109+ elif 60 < risk_score <= 80 :
110+ return RiskSeverity .HIGH
111+ elif 80 < risk_score <= 100 :
112+ return RiskSeverity .CRITICAL
113+ else :
114+ raise Exception (
115+ f"Error getting severity - risk_score must be between 0-100, but was actually { self .risk_score } "
116+ )
117+
69118 explanation : None | str = Field (
70119 default = None ,
71120 exclude = True , # Don't serialize this value when dumping the object
@@ -435,12 +484,10 @@ def serialize_model(self):
435484 "datamodel" : self .datamodel ,
436485 "source" : self .source ,
437486 "nes_fields" : self .nes_fields ,
487+ "rba" : self .rba or {},
438488 }
439- if self .rba is not None :
440- model ["risk_severity" ] = self .rba .severity
441- model ["tags" ]["risk_score" ] = self .rba .risk_score
442- else :
443- model ["tags" ]["risk_score" ] = 0
489+ if self .deployment .alert_action .notable :
490+ model ["risk_severity" ] = self .severity
444491
445492 # Only a subset of macro fields are required:
446493 all_macros : list [dict [str , str | list [str ]]] = []
0 commit comments