Skip to content

Commit de2ab17

Browse files
authored
Merge pull request #165 from splunk/improve_filter_macro_checking
Improve filter macro checking
2 parents 05f4db9 + abdf2a5 commit de2ab17

File tree

3 files changed

+134
-82
lines changed

3 files changed

+134
-82
lines changed

contentctl/input/director.py

Lines changed: 72 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
from pydantic import ValidationError
66
from uuid import UUID
77
from contentctl.input.yml_reader import YmlReader
8-
9-
10-
8+
9+
1110
from contentctl.objects.detection import Detection
1211
from contentctl.objects.story import Story
1312

@@ -28,30 +27,69 @@
2827
from contentctl.objects.config import validate
2928

3029

31-
32-
@dataclass()
30+
@dataclass
3331
class DirectorOutputDto:
34-
# Atomic Tests are first because parsing them
35-
# is far quicker than attack_enrichment
36-
atomic_tests: Union[list[AtomicTest],None]
37-
attack_enrichment: AttackEnrichment
38-
cve_enrichment: CveEnrichment
39-
detections: list[Detection]
40-
stories: list[Story]
41-
baselines: list[Baseline]
42-
investigations: list[Investigation]
43-
playbooks: list[Playbook]
44-
macros: list[Macro]
45-
lookups: list[Lookup]
46-
deployments: list[Deployment]
47-
ssa_detections: list[SSADetection]
48-
49-
50-
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
51-
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
52-
32+
# Atomic Tests are first because parsing them
33+
# is far quicker than attack_enrichment
34+
atomic_tests: Union[list[AtomicTest],None]
35+
attack_enrichment: AttackEnrichment
36+
cve_enrichment: CveEnrichment
37+
detections: list[Detection]
38+
stories: list[Story]
39+
baselines: list[Baseline]
40+
investigations: list[Investigation]
41+
playbooks: list[Playbook]
42+
macros: list[Macro]
43+
lookups: list[Lookup]
44+
deployments: list[Deployment]
45+
ssa_detections: list[SSADetection]
46+
47+
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
48+
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
49+
50+
def addContentToDictMappings(self, content: SecurityContentObject):
51+
content_name = content.name
52+
if isinstance(content, SSADetection):
53+
# Since SSA detections may have the same name as ESCU detection,
54+
# for this function we prepend 'SSA ' to the name.
55+
content_name = f"SSA {content_name}"
56+
if content_name in self.name_to_content_map:
57+
raise ValueError(
58+
f"Duplicate name '{content_name}' with paths:\n"
59+
f" - {content.file_path}\n"
60+
f" - {self.name_to_content_map[content_name].file_path}"
61+
)
62+
elif content.id in self.uuid_to_content_map:
63+
raise ValueError(
64+
f"Duplicate id '{content.id}' with paths:\n"
65+
f" - {content.file_path}\n"
66+
f" - {self.name_to_content_map[content_name].file_path}"
67+
)
68+
69+
if isinstance(content, Lookup):
70+
self.lookups.append(content)
71+
elif isinstance(content, Macro):
72+
self.macros.append(content)
73+
elif isinstance(content, Deployment):
74+
self.deployments.append(content)
75+
elif isinstance(content, Playbook):
76+
self.playbooks.append(content)
77+
elif isinstance(content, Baseline):
78+
self.baselines.append(content)
79+
elif isinstance(content, Investigation):
80+
self.investigations.append(content)
81+
elif isinstance(content, Story):
82+
self.stories.append(content)
83+
elif isinstance(content, Detection):
84+
self.detections.append(content)
85+
elif isinstance(content, SSADetection):
86+
self.ssa_detections.append(content)
87+
else:
88+
raise Exception(f"Unknown security content type: {type(content)}")
5389

5490

91+
self.name_to_content_map[content_name] = content
92+
self.uuid_to_content_map[content.id] = content
5593

5694

5795
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
@@ -61,13 +99,6 @@ class DirectorOutputDto:
6199
from contentctl.helper.utils import Utils
62100

63101

64-
65-
66-
67-
68-
69-
70-
71102
class Director():
72103
input_dto: validate
73104
output_dto: DirectorOutputDto
@@ -78,27 +109,7 @@ class Director():
78109
def __init__(self, output_dto: DirectorOutputDto) -> None:
79110
self.output_dto = output_dto
80111
self.ssa_detection_builder = SSADetectionBuilder()
81-
82-
def addContentToDictMappings(self, content:SecurityContentObject):
83-
content_name = content.name
84-
if isinstance(content,SSADetection):
85-
# Since SSA detections may have the same name as ESCU detection,
86-
# for this function we prepend 'SSA ' to the name.
87-
content_name = f"SSA {content_name}"
88-
if content_name in self.output_dto.name_to_content_map:
89-
raise ValueError(f"Duplicate name '{content_name}' with paths:\n"
90-
f" - {content.file_path}\n"
91-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
92-
elif content.id in self.output_dto.uuid_to_content_map:
93-
raise ValueError(f"Duplicate id '{content.id}' with paths:\n"
94-
f" - {content.file_path}\n"
95-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
96-
97-
self.output_dto.name_to_content_map[content_name] = content
98-
self.output_dto.uuid_to_content_map[content.id] = content
99-
100112

101-
102113
def execute(self, input_dto: validate) -> None:
103114
self.input_dto = input_dto
104115

@@ -147,50 +158,41 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
147158

148159
if contentType == SecurityContentType.lookups:
149160
lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
150-
self.output_dto.lookups.append(lookup)
151-
self.addContentToDictMappings(lookup)
161+
self.output_dto.addContentToDictMappings(lookup)
152162

153163
elif contentType == SecurityContentType.macros:
154164
macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
155-
self.output_dto.macros.append(macro)
156-
self.addContentToDictMappings(macro)
165+
self.output_dto.addContentToDictMappings(macro)
157166

158167
elif contentType == SecurityContentType.deployments:
159168
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
160-
self.output_dto.deployments.append(deployment)
161-
self.addContentToDictMappings(deployment)
169+
self.output_dto.addContentToDictMappings(deployment)
162170

163171
elif contentType == SecurityContentType.playbooks:
164172
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
165-
self.output_dto.playbooks.append(playbook)
166-
self.addContentToDictMappings(playbook)
173+
self.output_dto.addContentToDictMappings(playbook)
167174

168175
elif contentType == SecurityContentType.baselines:
169176
baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
170-
self.output_dto.baselines.append(baseline)
171-
self.addContentToDictMappings(baseline)
177+
self.output_dto.addContentToDictMappings(baseline)
172178

173179
elif contentType == SecurityContentType.investigations:
174180
investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
175-
self.output_dto.investigations.append(investigation)
176-
self.addContentToDictMappings(investigation)
181+
self.output_dto.addContentToDictMappings(investigation)
177182

178183
elif contentType == SecurityContentType.stories:
179184
story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
180-
self.output_dto.stories.append(story)
181-
self.addContentToDictMappings(story)
185+
self.output_dto.addContentToDictMappings(story)
182186

183187
elif contentType == SecurityContentType.detections:
184-
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto})
185-
self.output_dto.detections.append(detection)
186-
self.addContentToDictMappings(detection)
188+
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
189+
self.output_dto.addContentToDictMappings(detection)
187190

188191
elif contentType == SecurityContentType.ssa_detections:
189192
self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
190193
ssa_detection = self.ssa_detection_builder.getObject()
191194
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
192-
self.output_dto.ssa_detections.append(ssa_detection)
193-
self.addContentToDictMappings(ssa_detection)
195+
self.output_dto.addContentToDictMappings(ssa_detection)
194196

195197
else:
196198
raise Exception(f"Unsupported type: [{contentType}]")
@@ -229,6 +231,3 @@ def constructSSADetection(self, builder: SSADetectionBuilder, directorOutput:Dir
229231
builder.addMappings()
230232
builder.addUnitTest()
231233
builder.addRBA()
232-
233-
234-

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,58 @@ class Detection_Abstract(SecurityContentObject):
5353
# A list of groups of tests, relying on the same data
5454
test_groups: Union[list[TestGroup], None] = Field(None,validate_default=True)
5555

56+
57+
@field_validator("search", mode="before")
58+
@classmethod
59+
def validate_presence_of_filter_macro(cls, value:Union[str, dict[str,Any]], info:ValidationInfo)->Union[str, dict[str,Any]]:
60+
"""
61+
Validates that, if required to be present, the filter macro is present with the proper name.
62+
The filter macro MUST be derived from the name of the detection
63+
64+
65+
Args:
66+
value (Union[str, dict[str,Any]]): The search. It can either be a string (and should be SPL)
67+
or a dict, in which case it is Sigma-formatted.
68+
info (ValidationInfo): The validation info can contain a number of different objects. Today it only contains the director.
69+
70+
Returns:
71+
Union[str, dict[str,Any]]: The search, either in sigma or SPL format.
72+
"""
73+
74+
if isinstance(value,dict):
75+
#If the search is a dict, then it is in Sigma format so return it
76+
return value
77+
78+
# Otherwise, the search is SPL.
79+
80+
81+
# In the future, we will may add support that makes the inclusion of the
82+
# filter macro optional or automatically generates it for searches that
83+
# do not have it. For now, continue to require that all searches have a filter macro.
84+
FORCE_FILTER_MACRO = True
85+
if not FORCE_FILTER_MACRO:
86+
return value
87+
88+
# Get the required macro name, which is derived from the search name.
89+
# Note that a separate validation ensures that the file name matches the content name
90+
name:Union[str,None] = info.data.get("name",None)
91+
if name is None:
92+
#The search was sigma formatted (or failed other validation and was None), so we will not validate macros in it
93+
raise ValueError("Cannot validate filter macro, field 'name' (which is required to validate the macro) was missing from the detection YML.")
94+
95+
#Get the file name without the extension. Note this is not a full path!
96+
file_name = pathlib.Path(cls.contentNameToFileName(name)).stem
97+
file_name_with_filter = f"`{file_name}_filter`"
98+
99+
if file_name_with_filter not in value:
100+
raise ValueError(f"Detection does not contain the EXACT filter macro {file_name_with_filter}. "
101+
"This filter macro MUST be present in the search. It usually placed at the end "
102+
"of the search and is useful for environment-specific filtering of False Positive or noisy results.")
103+
104+
return value
105+
106+
107+
56108
@field_validator("test_groups")
57109
@classmethod
58110
def validate_test_groups(cls, value:Union[None, List[TestGroup]], info:ValidationInfo) -> Union[List[TestGroup], None]:
@@ -394,11 +446,11 @@ def getDetectionMacros(cls, v:list[str], info:ValidationInfo)->list[Macro]:
394446
filter_macro = Macro.model_validate({"name":filter_macro_name,
395447
"definition":'search *',
396448
"description":'Update this macro to limit the output results to filter out false positives.'})
397-
director.macros.append(filter_macro)
449+
director.addContentToDictMappings(filter_macro)
398450

399451
macros_from_search = Macro.get_macros(search, director)
400452

401-
return macros_from_search + [filter_macro]
453+
return macros_from_search
402454

403455
def get_content_dependencies(self)->list[SecurityContentObject]:
404456
#Do this separately to satisfy type checker

contentctl/objects/macro.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from contentctl.objects.security_content_object import SecurityContentObject
1010

1111

12-
13-
MACROS_TO_IGNORE = set(["_filter", "drop_dm_object_name"])
14-
#Should all of the following be included as well?
15-
MACROS_TO_IGNORE.add("get_asset" )
16-
MACROS_TO_IGNORE.add("get_risk_severity")
17-
MACROS_TO_IGNORE.add("cim_corporate_web_domain_search")
18-
MACROS_TO_IGNORE.add("prohibited_processes")
12+
#The following macros are included in commonly-installed apps.
13+
#As such, we will ignore if they are missing from our app.
14+
#Included in
15+
MACROS_TO_IGNORE = set(["drop_dm_object_name"]) # Part of CIM/Splunk_SA_CIM
16+
MACROS_TO_IGNORE.add("get_asset") #SA-IdentityManagement, part of Enterprise Security
17+
MACROS_TO_IGNORE.add("get_risk_severity") #SA-ThreatIntelligence, part of Enterprise Security
18+
MACROS_TO_IGNORE.add("cim_corporate_web_domain_search") #Part of CIM/Splunk_SA_CIM
19+
#MACROS_TO_IGNORE.add("prohibited_processes")
1920

2021

2122
class Macro(SecurityContentObject):

0 commit comments

Comments
 (0)