Skip to content

Commit 5ca8ade

Browse files
committed
Fix serialization issue with drilldowns.
Format of multiple drilldowns in savedsearches.conf is now correct. We are still populating the default drilldowns, this feature will eventually be removed.
1 parent c0cff81 commit 5ca8ade

File tree

3 files changed

+36
-11
lines changed

3 files changed

+36
-11
lines changed

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,11 +570,21 @@ def model_post_init(self, __context: Any) -> None:
570570
# Update the search fields with the original search, if required
571571
for drilldown in self.drilldown_searches:
572572
drilldown.perform_search_substitutions(self)
573-
print("adding default drilldown?")
573+
574+
#For experimental purposes, add the default drilldowns
574575
self.drilldown_searches.extend(Drilldown.constructDrilldownsFromDetection(self))
575576

576577
@property
577-
def drilldownsInJSON(self) -> list[dict[str,str]]:
578+
def drilldowns_in_JSON(self) -> list[dict[str,str]]:
579+
"""This function is required for proper JSON
580+
serializiation of drilldowns to occur in savedsearches.conf.
581+
It returns the list[Drilldown] as a list[dict].
582+
Without this function, the jinja template is unable
583+
to convert list[Drilldown] to JSON
584+
585+
Returns:
586+
list[dict[str,str]]: List of Drilldowns dumped to dict format
587+
"""
578588
return [drilldown.model_dump() for drilldown in self.drilldown_searches]
579589

580590
@field_validator('lookups', mode="before")

contentctl/objects/drilldown.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
from __future__ import annotations
2-
from pydantic import BaseModel, Field
2+
from pydantic import BaseModel, Field, model_serializer
33
from typing import TYPE_CHECKING
44
if TYPE_CHECKING:
55
from contentctl.objects.detection import Detection
66
from contentctl.objects.enums import AnalyticsType
77
SEARCH_PLACEHOLDER = "%original_detection_search%"
88
EARLIEST_OFFSET = "$info_min_time$"
99
LATEST_OFFSET = "$info_max_time$"
10-
RISK_SEARCH = "index = risk | stats count values(search_name) values(risk_message) values(analyticstories) values(annotations._all) values(annotations.mitre_attack.mitre_tactic) by risk_object"
10+
RISK_SEARCH = "index = risk starthoursago = 168 endhoursago = 0 | stats count values(search_name) values(risk_message) values(analyticstories) values(annotations._all) values(annotations.mitre_attack.mitre_tactic) "
1111

1212
class Drilldown(BaseModel):
1313
name: str = Field(..., description="The name of the drilldown search", min_length=5)
1414
search: str = Field(..., description="The text of a drilldown search. This must be valid SPL.", min_length=1)
15-
earliest_offset:str = Field(...,
15+
earliest_offset:None | str = Field(...,
1616
description="Earliest offset time for the drilldown search. "
1717
f"The most common value for this field is '{EARLIEST_OFFSET}', "
1818
"but it is NOT the default value and must be supplied explicitly.",
1919
min_length= 1)
20-
latest_offset:str = Field(...,
20+
latest_offset:None | str = Field(...,
2121
description="Latest offset time for the driolldown search. "
2222
f"The most common value for this field is '{LATEST_OFFSET}', "
2323
"but it is NOT the default value and must be supplied explicitly.",
@@ -29,7 +29,7 @@ def constructDrilldownsFromDetection(cls, detection: Detection) -> list[Drilldow
2929
if len(victim_observables) == 0 or detection.type == AnalyticsType.Hunting:
3030
# No victims, so no drilldowns
3131
return []
32-
32+
print("Adding default drilldowns. REMOVE THIS BEFORE MERGING")
3333
variableNamesString = ' and '.join([f"${o.name}$" for o in victim_observables])
3434
nameField = f"View the detection results for {variableNamesString}"
3535
appendedSearch = " | search " + ' '.join([f"{o.name} = ${o.name}$" for o in victim_observables])
@@ -38,8 +38,10 @@ def constructDrilldownsFromDetection(cls, detection: Detection) -> list[Drilldow
3838

3939

4040
nameField = f"View risk events for the last 7 days for {variableNamesString}"
41-
search_field = f"{RISK_SEARCH}{appendedSearch}"
42-
risk_events_last_7_days = cls(name=nameField, earliest_offset=EARLIEST_OFFSET, latest_offset=LATEST_OFFSET, search=search_field)
41+
fieldNamesListString = ', '.join([o.name for o in victim_observables])
42+
search_field = f"{RISK_SEARCH}by {fieldNamesListString} {appendedSearch}"
43+
#risk_events_last_7_days = cls(name=nameField, earliest_offset=EARLIEST_OFFSET, latest_offset=LATEST_OFFSET, search=search_field)
44+
risk_events_last_7_days = cls(name=nameField, earliest_offset=None, latest_offset=None, search=search_field)
4345

4446
return [detection_results,risk_events_last_7_days]
4547

@@ -50,4 +52,17 @@ def perform_search_substitutions(self, detection:Detection)->None:
5052
f"drilldown search '{self.search}' for Detection {detection.file_path}.\n"
5153
"If this was intentional, then please ignore this warning.\n")
5254
self.search = self.search.replace(SEARCH_PLACEHOLDER, detection.search)
53-
55+
56+
57+
@model_serializer
58+
def serialize_model(self) -> dict[str,str]:
59+
#Call serializer for parent
60+
model:dict[str,str] = {}
61+
62+
model['name'] = self.name
63+
model['search'] = self.search
64+
if self.earliest_offset is not None:
65+
model['earliest_offset'] = self.earliest_offset
66+
if self.latest_offset is not None:
67+
model['latest_offset'] = self.latest_offset
68+
return model

contentctl/output/templates/savedsearches_detections.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ alert.suppress.fields = {{ detection.tags.throttling.conf_formatted_fields() }}
112112
alert.suppress.period = {{ detection.tags.throttling.period }}
113113
{% endif %}
114114
search = {{ detection.search | escapeNewlines() }}
115-
action.notable.param.drilldown_searches = {{ detection.drilldownsInJSON | tojson | escapeNewlines() }}
115+
action.notable.param.drilldown_searches = {{ detection.drilldowns_in_JSON | tojson | escapeNewlines() }}
116116
{% endif %}
117117

118118
{% endfor %}

0 commit comments

Comments
 (0)