Skip to content

Commit b3e7d09

Browse files
committed
Fix error where duplicate data_sources
were added to an analytic story if multiple detections referenced the same data_source. This was done by making data_sources a computed_field for Story rather than building at while deteciton objects are built. Additionally added eq, lt, and hash methods to SecurityContentObject_Abstract so that set operations and sorts can happen easily for all objects.
1 parent 0eebfd9 commit b3e7d09

File tree

4 files changed

+72
-11
lines changed

4 files changed

+72
-11
lines changed

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class Detection_Abstract(SecurityContentObject):
3737
#contentType: SecurityContentType = SecurityContentType.detections
3838
type: AnalyticsType = Field(...)
3939
status: DetectionStatus = Field(...)
40-
data_source: Optional[List[str]] = None
40+
data_source: list[str] = []
4141
tags: DetectionTags = Field(...)
4242
search: Union[str, dict[str,Any]] = Field(...)
4343
how_to_implement: str = Field(..., min_length=4)
@@ -54,7 +54,7 @@ class Detection_Abstract(SecurityContentObject):
5454
# A list of groups of tests, relying on the same data
5555
test_groups: Union[list[TestGroup], None] = Field(None,validate_default=True)
5656

57-
data_source_objects: Optional[List[DataSource]] = None
57+
data_source_objects: list[DataSource] = []
5858

5959

6060
@field_validator("search", mode="before")
@@ -420,9 +420,7 @@ def model_post_init(self, ctx:dict[str,Any]):
420420
self.data_source_objects = matched_data_sources
421421

422422
for story in self.tags.analytic_story:
423-
story.detections.append(self)
424-
story.data_sources.extend(self.data_source_objects)
425-
423+
story.detections.append(self)
426424
return self
427425

428426

@@ -446,14 +444,16 @@ def mapDetectionNamesToBaselineObjects(cls, v:list[str], info:ValidationInfo)->L
446444
raise ValueError("Error, baselines are constructed automatically at runtime. Please do not include this field.")
447445

448446

449-
name:Union[str,dict] = info.data.get("name",None)
447+
name:Union[str,None] = info.data.get("name",None)
450448
if name is None:
451449
raise ValueError("Error, cannot get Baselines because the Detection does not have a 'name' defined.")
452-
450+
453451
director:DirectorOutputDto = info.context.get("output_dto",None)
454452
baselines:List[Baseline] = []
455453
for baseline in director.baselines:
456-
if name in baseline.tags.detections:
454+
# This matching is a bit strange, because baseline.tags.detections starts as a list of strings, but
455+
# is eventually updated to a list of Detections as we construct all of the detection objects.
456+
if name in [detection_name for detection_name in baseline.tags.detections if isinstance(detection_name,str)]:
457457
baselines.append(baseline)
458458

459459
return baselines

contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,33 @@ def __repr__(self)->str:
194194

195195
def __str__(self)->str:
196196
return(self.__repr__())
197+
198+
def __lt__(self, other:object)->bool:
199+
if not isinstance(other,SecurityContentObject_Abstract):
200+
raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
201+
return self.name < other.name
202+
203+
def __eq__(self, other:object)->bool:
204+
if not isinstance(other,SecurityContentObject_Abstract):
205+
raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
206+
207+
if id(self) == id(other) and self.name == other.name and self.id == other.id:
208+
# Yes, this is the same object
209+
return True
210+
211+
elif id(self) == id(other) or self.name == other.name or self.id == other.id:
212+
raise Exception("Attempted to compare two SecurityContentObjects, but their fields indicate they were not globally unique:"
213+
f"\n\tid(obj1) : {id(self)}"
214+
f"\n\tid(obj2) : {id(other)}"
215+
f"\n\tobj1.name : {self.name}"
216+
f"\n\tobj2.name : {other.name}"
217+
f"\n\tobj1.id : {self.id}"
218+
f"\n\tobj2.id : {other.id}")
219+
else:
220+
return False
221+
222+
def __hash__(self) -> NonNegativeInt:
223+
return id(self)
197224

198225

199226

contentctl/objects/data_source.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
2-
from typing import Union, Optional, List
3-
from pydantic import model_validator, Field, FilePath
2+
from typing import Optional, Any
3+
from pydantic import Field, FilePath, model_serializer
44
from contentctl.objects.security_content_object import SecurityContentObject
55
from contentctl.objects.event_source import EventSource
66

@@ -16,3 +16,27 @@ class DataSource(SecurityContentObject):
1616
example_log: Optional[str] = None
1717

1818

19+
@model_serializer
20+
def serialize_model(self):
21+
#Call serializer for parent
22+
super_fields = super().serialize_model()
23+
24+
#All fields custom to this model
25+
model:dict[str,Any] = {
26+
"source": self.source,
27+
"sourcetype": self.sourcetype,
28+
"separator": self.separator,
29+
"configuration": self.configuration,
30+
"supported_TA": self.supported_TA,
31+
"fields": self.fields,
32+
"field_mappings": self.field_mappings,
33+
"convert_to_log_source": self.convert_to_log_source,
34+
"example_log":self.example_log
35+
}
36+
37+
38+
#Combine fields from this model with fields from parent
39+
super_fields.update(model)
40+
41+
#return the model
42+
return super_fields

contentctl/objects/story.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@ class Story(SecurityContentObject):
3333
detections:List[Detection] = []
3434
investigations: List[Investigation] = []
3535
baselines: List[Baseline] = []
36-
data_sources: List[DataSource] = []
36+
37+
38+
@computed_field
39+
@property
40+
def data_sources(self)-> list[DataSource]:
41+
# Only add a data_source if it does not already exist in the story
42+
data_source_objects:set[DataSource] = set()
43+
for detection in self.detections:
44+
data_source_objects.update(set(detection.data_source_objects))
45+
46+
return sorted(list(data_source_objects))
3747

3848

3949
def storyAndInvestigationNamesWithApp(self, app_name:str)->List[str]:

0 commit comments

Comments
 (0)