Skip to content

Commit a5b2817

Browse files
authored
Merge pull request #390 from splunk/json_api_updates
update detections.json output with new rba structure
2 parents d89553e + 7583d2c commit a5b2817

File tree

4 files changed

+60
-48
lines changed

4 files changed

+60
-48
lines changed

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@
4343
DetectionStatus,
4444
NistCategory,
4545
ProvidingTechnology,
46+
RiskSeverity,
4647
)
4748
from contentctl.objects.integration_test import IntegrationTest
4849
from contentctl.objects.manual_test import ManualTest
49-
from contentctl.objects.rba import RBAObject
50+
from contentctl.objects.rba import RBAObject, RiskScoreValue_Type
5051
from contentctl.objects.security_content_object import SecurityContentObject
5152
from contentctl.objects.test_group import TestGroup
5253
from 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]]] = []

contentctl/objects/rba.py

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
from enum import Enum
55
from typing import Annotated, Set
66

7-
from pydantic import BaseModel, Field, computed_field, model_serializer
8-
9-
from contentctl.objects.enums import RiskSeverity
7+
from pydantic import BaseModel, Field, model_serializer
108

119
RiskScoreValue_Type = Annotated[int, Field(ge=1, le=100)]
1210

@@ -108,36 +106,6 @@ class RBAObject(BaseModel, ABC):
108106
risk_objects: Annotated[Set[RiskObject], Field(min_length=1)]
109107
threat_objects: Set[ThreatObject]
110108

111-
@computed_field
112-
@property
113-
def risk_score(self) -> RiskScoreValue_Type:
114-
# First get the maximum score associated with
115-
# a risk object. If there are no objects, then
116-
# we should throw an exception.
117-
if len(self.risk_objects) == 0:
118-
raise Exception(
119-
"There must be at least one Risk Object present to get Severity."
120-
)
121-
return max([risk_object.score for risk_object in self.risk_objects])
122-
123-
@computed_field
124-
@property
125-
def severity(self) -> RiskSeverity:
126-
if 0 <= self.risk_score <= 20:
127-
return RiskSeverity.INFORMATIONAL
128-
elif 20 < self.risk_score <= 40:
129-
return RiskSeverity.LOW
130-
elif 40 < self.risk_score <= 60:
131-
return RiskSeverity.MEDIUM
132-
elif 60 < self.risk_score <= 80:
133-
return RiskSeverity.HIGH
134-
elif 80 < self.risk_score <= 100:
135-
return RiskSeverity.CRITICAL
136-
else:
137-
raise Exception(
138-
f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}"
139-
)
140-
141109
@model_serializer
142110
def serialize_rba(self) -> dict[str, str | list[dict[str, str | int]]]:
143111
return {

contentctl/output/api_json_output.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from __future__ import annotations
2+
23
from typing import TYPE_CHECKING
34

45
if TYPE_CHECKING:
6+
from contentctl.objects.baseline import Baseline
7+
from contentctl.objects.deployment import Deployment
58
from contentctl.objects.detection import Detection
9+
from contentctl.objects.investigation import Investigation
610
from contentctl.objects.lookup import Lookup
711
from contentctl.objects.macro import Macro
812
from contentctl.objects.story import Story
9-
from contentctl.objects.baseline import Baseline
10-
from contentctl.objects.investigation import Investigation
11-
from contentctl.objects.deployment import Deployment
1213

1314
import os
1415
import pathlib
@@ -42,6 +43,7 @@ def writeDetections(
4243
"search",
4344
"how_to_implement",
4445
"known_false_positives",
46+
"rba",
4547
"references",
4648
"datamodel",
4749
"macros",

contentctl/output/templates/savedsearches_detections.j2

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,7 @@ action.notable.param.nes_fields = {{ detection.nes_fields }}
6565
action.notable.param.rule_description = {{ detection.deployment.alert_action.notable.rule_description | custom_jinja2_enrichment_filter(detection) | escapeNewlines()}}
6666
action.notable.param.rule_title = {% if detection.type | lower == "correlation" %}RBA: {{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% else %}{{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% endif +%}
6767
action.notable.param.security_domain = {{ detection.tags.security_domain }}
68-
{% if detection.rba %}
69-
action.notable.param.severity = {{ detection.rba.severity }}
70-
{% else %}
71-
{# Correlations do not have detection.rba defined, but should get a default severity #}
72-
action.notable.param.severity = high
73-
{% endif %}
68+
action.notable.param.severity = {{ detection.severity }}
7469
{% endif %}
7570
{% if detection.deployment.alert_action.email %}
7671
action.email = 1

0 commit comments

Comments
 (0)