43
43
DetectionStatus ,
44
44
NistCategory ,
45
45
ProvidingTechnology ,
46
+ RiskSeverity ,
46
47
)
47
48
from contentctl .objects .integration_test import IntegrationTest
48
49
from contentctl .objects .manual_test import ManualTest
49
- from contentctl .objects .rba import RBAObject
50
+ from contentctl .objects .rba import RBAObject , RiskScoreValue_Type
50
51
from contentctl .objects .security_content_object import SecurityContentObject
51
52
from contentctl .objects .test_group import TestGroup
52
53
from contentctl .objects .unit_test import UnitTest
@@ -66,6 +67,54 @@ class Detection_Abstract(SecurityContentObject):
66
67
how_to_implement : str = Field (..., min_length = 4 )
67
68
known_false_positives : str = Field (..., min_length = 4 )
68
69
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
+
69
118
explanation : None | str = Field (
70
119
default = None ,
71
120
exclude = True , # Don't serialize this value when dumping the object
@@ -435,12 +484,10 @@ def serialize_model(self):
435
484
"datamodel" : self .datamodel ,
436
485
"source" : self .source ,
437
486
"nes_fields" : self .nes_fields ,
487
+ "rba" : self .rba or {},
438
488
}
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
444
491
445
492
# Only a subset of macro fields are required:
446
493
all_macros : list [dict [str , str | list [str ]]] = []
0 commit comments