Skip to content

Commit 2943a4e

Browse files
author
Patrick
committed
Add suport for Data Source Objects and Event Source Objects
1 parent 98d6f6a commit 2943a4e

File tree

8 files changed

+104
-30
lines changed

8 files changed

+104
-30
lines changed

contentctl/actions/validate.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,47 @@
66
from typing import Union
77

88
from contentctl.objects.enums import SecurityContentProduct
9-
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract
10-
from contentctl.input.director import (
11-
Director,
12-
DirectorOutputDto
9+
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
10+
SecurityContentObject_Abstract,
1311
)
12+
from contentctl.input.director import Director, DirectorOutputDto
1413

1514
from contentctl.objects.config import validate
1615
from contentctl.enrichments.attack_enrichment import AttackEnrichment
1716
from contentctl.enrichments.cve_enrichment import CveEnrichment
1817
from contentctl.objects.atomic import AtomicTest
1918

19+
2020
class Validate:
2121
def execute(self, input_dto: validate) -> DirectorOutputDto:
22-
23-
director_output_dto = DirectorOutputDto(AtomicTest.getAtomicTestsFromArtRepo(repo_path=input_dto.getAtomicRedTeamRepoPath(),
24-
enabled=input_dto.enrichments),
25-
AttackEnrichment.getAttackEnrichment(input_dto),
26-
CveEnrichment.getCveEnrichment(input_dto),
27-
[],[],[],[],[],[],[],[],[],[])
28-
29-
22+
23+
director_output_dto = DirectorOutputDto(
24+
AtomicTest.getAtomicTestsFromArtRepo(
25+
repo_path=input_dto.getAtomicRedTeamRepoPath(),
26+
enabled=input_dto.enrichments,
27+
),
28+
AttackEnrichment.getAttackEnrichment(input_dto),
29+
CveEnrichment.getCveEnrichment(input_dto),
30+
[],
31+
[],
32+
[],
33+
[],
34+
[],
35+
[],
36+
[],
37+
[],
38+
[],
39+
[],
40+
[],
41+
)
42+
3043
director = Director(director_output_dto)
3144
director.execute(input_dto)
32-
3345
return director_output_dto
3446

35-
def validate_duplicate_uuids(self, security_content_objects:list[SecurityContentObject_Abstract]):
47+
def validate_duplicate_uuids(
48+
self, security_content_objects: list[SecurityContentObject_Abstract]
49+
):
3650
all_uuids = set()
3751
duplicate_uuids = set()
3852
for elem in security_content_objects:
@@ -45,14 +59,20 @@ def validate_duplicate_uuids(self, security_content_objects:list[SecurityContent
4559

4660
if len(duplicate_uuids) == 0:
4761
return
48-
62+
4963
# At least once duplicate uuid has been found. Enumerate all
5064
# the pieces of content that use duplicate uuids
5165
duplicate_messages = []
5266
for uuid in duplicate_uuids:
53-
duplicate_uuid_content = [str(content.file_path) for content in security_content_objects if content.id in duplicate_uuids]
54-
duplicate_messages.append(f"Duplicate UUID [{uuid}] in {duplicate_uuid_content}")
55-
67+
duplicate_uuid_content = [
68+
str(content.file_path)
69+
for content in security_content_objects
70+
if content.id in duplicate_uuids
71+
]
72+
duplicate_messages.append(
73+
f"Duplicate UUID [{uuid}] in {duplicate_uuid_content}"
74+
)
75+
5676
raise ValueError(
5777
"ERROR: Duplicate ID(s) found in objects:\n"
5878
+ "\n - ".join(duplicate_messages)

contentctl/helper/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class Utils:
2525
@staticmethod
2626
def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
2727
listOfFiles:list[pathlib.Path] = []
28+
base_path = pathlib.Path(path)
29+
if not base_path.exists():
30+
return listOfFiles
2831
for (dirpath, dirnames, filenames) in os.walk(path):
2932
for file in filenames:
3033
if file.endswith(".yml"):
@@ -36,6 +39,8 @@ def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
3639
def get_all_yml_files_from_directory_one_layer_deep(path: str) -> list[pathlib.Path]:
3740
listOfFiles: list[pathlib.Path] = []
3841
base_path = pathlib.Path(path)
42+
if not base_path.exists():
43+
return listOfFiles
3944
# Check the base directory
4045
for item in base_path.iterdir():
4146
if item.is_file() and item.suffix == '.yml':

contentctl/input/director.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from contentctl.objects.atomic import AtomicTest
2323
from contentctl.objects.security_content_object import SecurityContentObject
2424
from contentctl.objects.data_source import DataSource
25+
from contentctl.objects.event_source import EventSource
2526

2627
from contentctl.enrichments.attack_enrichment import AttackEnrichment
2728
from contentctl.enrichments.cve_enrichment import CveEnrichment
@@ -57,6 +58,7 @@ class DirectorOutputDto:
5758
deployments: list[Deployment]
5859
ssa_detections: list[SSADetection]
5960
data_sources: list[DataSource]
61+
event_sources: list[EventSource]
6062
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
6163
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
6264

@@ -122,6 +124,7 @@ def execute(self, input_dto: validate) -> None:
122124
self.createSecurityContent(SecurityContentType.stories)
123125
self.createSecurityContent(SecurityContentType.baselines)
124126
self.createSecurityContent(SecurityContentType.investigations)
127+
self.createSecurityContent(SecurityContentType.event_sources)
125128
self.createSecurityContent(SecurityContentType.data_sources)
126129
self.createSecurityContent(SecurityContentType.playbooks)
127130
self.createSecurityContent(SecurityContentType.detections)
@@ -141,6 +144,21 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
141144
)
142145
)
143146

147+
elif contentType == SecurityContentType.event_sources:
148+
security_content_files = Utils.get_all_yml_files_from_directory(
149+
os.path.join(self.input_dto.path, "data_sources", "cloud", "event_sources")
150+
)
151+
security_content_files.extend(
152+
Utils.get_all_yml_files_from_directory(
153+
os.path.join(self.input_dto.path, "data_sources", "endpoint", "event_sources")
154+
)
155+
)
156+
security_content_files.extend(
157+
Utils.get_all_yml_files_from_directory(
158+
os.path.join(self.input_dto.path, "data_sources", "network", "event_sources")
159+
)
160+
)
161+
144162
elif contentType in [
145163
SecurityContentType.deployments,
146164
SecurityContentType.lookups,
@@ -183,12 +201,6 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
183201
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
184202
self.output_dto.addContentToDictMappings(deployment)
185203

186-
elif contentType == SecurityContentType.data_sources:
187-
data_source = DataSource.model_validate(
188-
modelDict, context={"output_dto": self.output_dto}
189-
)
190-
self.output_dto.data_sources.append(data_source)
191-
192204
elif contentType == SecurityContentType.playbooks:
193205
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
194206
self.output_dto.addContentToDictMappings(playbook)
@@ -214,6 +226,18 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
214226
ssa_detection = self.ssa_detection_builder.getObject()
215227
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
216228
self.output_dto.addContentToDictMappings(ssa_detection)
229+
230+
elif contentType == SecurityContentType.data_sources:
231+
data_source = DataSource.model_validate(
232+
modelDict, context={"output_dto": self.output_dto}
233+
)
234+
self.output_dto.data_sources.append(data_source)
235+
236+
elif contentType == SecurityContentType.event_sources:
237+
event_source = EventSource.model_validate(
238+
modelDict, context={"output_dto": self.output_dto}
239+
)
240+
self.output_dto.event_sources.append(event_source)
217241

218242
else:
219243
raise Exception(f"Unsupported type: [{contentType}]")
@@ -262,7 +286,7 @@ def constructSSADetection(
262286
file_path: str,
263287
) -> None:
264288
builder.reset()
265-
builder.setObject(file_path, self.output_dto)
289+
builder.setObject(file_path)
266290
builder.addMitreAttackEnrichmentNew(directorOutput.attack_enrichment)
267291
builder.addKillChainPhase()
268292
builder.addCIS()

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,6 @@ def model_post_init(self, ctx:dict[str,Any]):
369369
# if not isinstance(director,DirectorOutputDto):
370370
# raise ValueError("DirectorOutputDto was not passed in context of Detection model_post_init")
371371
director: Optional[DirectorOutputDto] = ctx.get("output_dto",None)
372-
for story in self.tags.analytic_story:
373-
story.detections.append(self)
374372

375373
#Ensure that all baselines link to this detection
376374
for baseline in self.baselines:
@@ -399,6 +397,10 @@ def model_post_init(self, ctx:dict[str,Any]):
399397
unique_data_sources[data_source_obj.name] = data_source_obj
400398
self.data_source_objects = list(unique_data_sources.values())
401399

400+
for story in self.tags.analytic_story:
401+
story.detections.append(self)
402+
story.data_sources.extend(self.data_source_objects)
403+
402404
return self
403405

404406

contentctl/objects/data_source.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,17 @@ class DataSource(BaseModel):
1212
configuration: str = None
1313
supported_TA: dict
1414
event_names: list = None
15+
event_sources: list = None
1516
fields: list = None
16-
example_log: str = None
17+
example_log: str = None
18+
19+
def model_post_init(self, ctx:dict[str,Any]):
20+
context = ctx.get("output_dto")
21+
22+
if self.event_names:
23+
self.event_sources = []
24+
for event_source in context.event_sources:
25+
if any(event['event_name'] == event_source.event_name for event in self.event_names):
26+
self.event_sources.append(event_source)
27+
28+
return self

contentctl/objects/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class SecurityContentType(enum.Enum):
5656
unit_tests = 9
5757
ssa_detections = 10
5858
data_sources = 11
59+
event_sources = 12
5960

6061
# Bringing these changes back in line will take some time after
6162
# the initial merge is complete

contentctl/objects/event_source.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
from pydantic import BaseModel
3+
4+
5+
class EventSource(BaseModel):
6+
event_name: str
7+
fields: list[str]
8+
field_mappings: list[dict] = None
9+
convert_to_log_source: list[dict] = None
10+
example_log: str = None

contentctl/objects/story.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from contentctl.objects.detection import Detection
88
from contentctl.objects.investigation import Investigation
99
from contentctl.objects.baseline import Baseline
10-
10+
from contentctl.objects.data_source import DataSource
1111

1212
from contentctl.objects.security_content_object import SecurityContentObject
1313

@@ -33,7 +33,7 @@ class Story(SecurityContentObject):
3333
detections:List[Detection] = []
3434
investigations: List[Investigation] = []
3535
baselines: List[Baseline] = []
36-
36+
data_sources: List[DataSource] = []
3737

3838
def storyAndInvestigationNamesWithApp(self, app_name:str)->List[str]:
3939
return [f"{app_name} - {name} - Rule" for name in self.detection_names] + \

0 commit comments

Comments
 (0)