Skip to content

update detections.json output with new rba structure #390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
DetectionStatus,
NistCategory,
ProvidingTechnology,
RiskSeverity,
)
from contentctl.objects.integration_test import IntegrationTest
from contentctl.objects.manual_test import ManualTest
from contentctl.objects.rba import RBAObject
from contentctl.objects.rba import RBAObject, RiskScoreValue_Type
from contentctl.objects.security_content_object import SecurityContentObject
from contentctl.objects.test_group import TestGroup
from contentctl.objects.unit_test import UnitTest
Expand All @@ -66,6 +67,54 @@ class Detection_Abstract(SecurityContentObject):
how_to_implement: str = Field(..., min_length=4)
known_false_positives: str = Field(..., min_length=4)
rba: Optional[RBAObject] = Field(default=None)

@computed_field
@property
def risk_score(self) -> RiskScoreValue_Type:
# First get the maximum score associated with
# a risk object. If there are no objects, then
# we should throw an exception.
if self.rba is None or len(self.rba.risk_objects) == 0:
raise Exception(
"There must be at least one Risk Object present to get Severity."
)
return max([risk_object.score for risk_object in self.rba.risk_objects])

@computed_field
@property
def severity(self) -> RiskSeverity:
"""
Severity is required for notables (but not risk objects).
In the contentctl codebase, instead of requiring an additional
field to be added to the YMLs, we derive the severity from the
HIGHEST risk score of any risk object that is part of this detection.
However, if a detection does not have a risk object but still has a notable,
we will use a default value of high. This only impact Correlation searches. As
TTP searches, which also generate notables, must also have risk object(s)
"""
try:
risk_score = self.risk_score
except Exception:
# This object does not have any RBA objects,
# hence no disk score is returned. So we will
# return the defualt value of high
return RiskSeverity.HIGH

if 0 <= risk_score <= 20:
return RiskSeverity.INFORMATIONAL
elif 20 < risk_score <= 40:
return RiskSeverity.LOW
elif 40 < risk_score <= 60:
return RiskSeverity.MEDIUM
elif 60 < risk_score <= 80:
return RiskSeverity.HIGH
elif 80 < risk_score <= 100:
return RiskSeverity.CRITICAL
else:
raise Exception(
f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}"
)

explanation: None | str = Field(
default=None,
exclude=True, # Don't serialize this value when dumping the object
Expand Down Expand Up @@ -435,12 +484,10 @@ def serialize_model(self):
"datamodel": self.datamodel,
"source": self.source,
"nes_fields": self.nes_fields,
"rba": self.rba or {},
}
if self.rba is not None:
model["risk_severity"] = self.rba.severity
model["tags"]["risk_score"] = self.rba.risk_score
else:
model["tags"]["risk_score"] = 0
if self.deployment.alert_action.notable:
model["risk_severity"] = self.severity

# Only a subset of macro fields are required:
all_macros: list[dict[str, str | list[str]]] = []
Expand Down
34 changes: 1 addition & 33 deletions contentctl/objects/rba.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
from enum import Enum
from typing import Annotated, Set

from pydantic import BaseModel, Field, computed_field, model_serializer

from contentctl.objects.enums import RiskSeverity
from pydantic import BaseModel, Field, model_serializer

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

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

@computed_field
@property
def risk_score(self) -> RiskScoreValue_Type:
# First get the maximum score associated with
# a risk object. If there are no objects, then
# we should throw an exception.
if len(self.risk_objects) == 0:
raise Exception(
"There must be at least one Risk Object present to get Severity."
)
return max([risk_object.score for risk_object in self.risk_objects])

@computed_field
@property
def severity(self) -> RiskSeverity:
if 0 <= self.risk_score <= 20:
return RiskSeverity.INFORMATIONAL
elif 20 < self.risk_score <= 40:
return RiskSeverity.LOW
elif 40 < self.risk_score <= 60:
return RiskSeverity.MEDIUM
elif 60 < self.risk_score <= 80:
return RiskSeverity.HIGH
elif 80 < self.risk_score <= 100:
return RiskSeverity.CRITICAL
else:
raise Exception(
f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}"
)

@model_serializer
def serialize_rba(self) -> dict[str, str | list[dict[str, str | int]]]:
return {
Expand Down
8 changes: 5 additions & 3 deletions contentctl/output/api_json_output.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from contentctl.objects.baseline import Baseline
from contentctl.objects.deployment import Deployment
from contentctl.objects.detection import Detection
from contentctl.objects.investigation import Investigation
from contentctl.objects.lookup import Lookup
from contentctl.objects.macro import Macro
from contentctl.objects.story import Story
from contentctl.objects.baseline import Baseline
from contentctl.objects.investigation import Investigation
from contentctl.objects.deployment import Deployment

import os
import pathlib
Expand Down Expand Up @@ -42,6 +43,7 @@ def writeDetections(
"search",
"how_to_implement",
"known_false_positives",
"rba",
"references",
"datamodel",
"macros",
Expand Down
7 changes: 1 addition & 6 deletions contentctl/output/templates/savedsearches_detections.j2
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,7 @@ action.notable.param.nes_fields = {{ detection.nes_fields }}
action.notable.param.rule_description = {{ detection.deployment.alert_action.notable.rule_description | custom_jinja2_enrichment_filter(detection) | escapeNewlines()}}
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 +%}
action.notable.param.security_domain = {{ detection.tags.security_domain }}
{% if detection.rba %}
action.notable.param.severity = {{ detection.rba.severity }}
{% else %}
{# Correlations do not have detection.rba defined, but should get a default severity #}
action.notable.param.severity = high
{% endif %}
action.notable.param.severity = {{ detection.severity }}
{% endif %}
{% if detection.deployment.alert_action.email %}
action.email = 1
Expand Down
Loading