Skip to content

Commit bccb33e

Browse files
authored
Merge pull request #180 from splunk/add_support_for_data_source_objects
Add support for data source objects
2 parents d670f3e + 58f08fb commit bccb33e

File tree

10 files changed

+232
-97
lines changed

10 files changed

+232
-97
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"python.envFile": "${workspaceFolder}/.env",
44
"python.testing.cwd": "${workspaceFolder}",
55
"python.languageServer": "Pylance",
6-
"python.analysis.typeCheckingMode": "strict"
6+
"python.analysis.typeCheckingMode": "strict",
7+
"editor.defaultFormatter": "ms-python.black-formatter"
78

89

910
}

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,34 @@ 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"):
3134
listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
3235

3336
return sorted(listOfFiles)
3437

38+
@staticmethod
39+
def get_all_yml_files_from_directory_one_layer_deep(path: str) -> list[pathlib.Path]:
40+
listOfFiles: list[pathlib.Path] = []
41+
base_path = pathlib.Path(path)
42+
if not base_path.exists():
43+
return listOfFiles
44+
# Check the base directory
45+
for item in base_path.iterdir():
46+
if item.is_file() and item.suffix == '.yml':
47+
listOfFiles.append(item)
48+
# Check one subfolder level deep
49+
for subfolder in base_path.iterdir():
50+
if subfolder.is_dir() and subfolder.name != "cim":
51+
for item in subfolder.iterdir():
52+
if item.is_file() and item.suffix == '.yml':
53+
listOfFiles.append(item)
54+
return sorted(listOfFiles)
55+
3556

3657
@staticmethod
3758
def add_id(id_dict:dict[str, list[pathlib.Path]], obj:SecurityContentObject, path:pathlib.Path) -> None:

contentctl/input/director.py

Lines changed: 119 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import pathlib
34
from typing import Union
45
from dataclasses import dataclass, field
56
from pydantic import ValidationError
@@ -20,11 +21,24 @@
2021
from contentctl.objects.ssa_detection import SSADetection
2122
from contentctl.objects.atomic import AtomicTest
2223
from contentctl.objects.security_content_object import SecurityContentObject
24+
from contentctl.objects.data_source import DataSource
25+
from contentctl.objects.event_source import EventSource
2326

2427
from contentctl.enrichments.attack_enrichment import AttackEnrichment
2528
from contentctl.enrichments.cve_enrichment import CveEnrichment
2629

2730
from contentctl.objects.config import validate
31+
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
32+
from contentctl.objects.enums import SecurityContentType
33+
34+
from contentctl.objects.enums import DetectionStatus
35+
from contentctl.helper.utils import Utils
36+
37+
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
38+
from contentctl.objects.enums import SecurityContentType
39+
40+
from contentctl.objects.enums import DetectionStatus
41+
from contentctl.helper.utils import Utils
2842

2943

3044
@dataclass
@@ -43,7 +57,8 @@ class DirectorOutputDto:
4357
lookups: list[Lookup]
4458
deployments: list[Deployment]
4559
ssa_detections: list[SSADetection]
46-
60+
data_sources: list[DataSource]
61+
event_sources: list[EventSource]
4762
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
4863
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
4964

@@ -92,66 +107,84 @@ def addContentToDictMappings(self, content: SecurityContentObject):
92107
self.uuid_to_content_map[content.id] = content
93108

94109

95-
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
96-
from contentctl.objects.enums import SecurityContentType
97-
98-
from contentctl.objects.enums import DetectionStatus
99-
from contentctl.helper.utils import Utils
100-
101-
102110
class Director():
103111
input_dto: validate
104112
output_dto: DirectorOutputDto
105113
ssa_detection_builder: SSADetectionBuilder
106-
107-
108114

109115
def __init__(self, output_dto: DirectorOutputDto) -> None:
110116
self.output_dto = output_dto
111117
self.ssa_detection_builder = SSADetectionBuilder()
112-
118+
113119
def execute(self, input_dto: validate) -> None:
114120
self.input_dto = input_dto
115-
116-
117121
self.createSecurityContent(SecurityContentType.deployments)
118122
self.createSecurityContent(SecurityContentType.lookups)
119123
self.createSecurityContent(SecurityContentType.macros)
120124
self.createSecurityContent(SecurityContentType.stories)
121125
self.createSecurityContent(SecurityContentType.baselines)
122126
self.createSecurityContent(SecurityContentType.investigations)
127+
self.createSecurityContent(SecurityContentType.event_sources)
128+
self.createSecurityContent(SecurityContentType.data_sources)
123129
self.createSecurityContent(SecurityContentType.playbooks)
124130
self.createSecurityContent(SecurityContentType.detections)
125-
126-
127131
self.createSecurityContent(SecurityContentType.ssa_detections)
128-
129132

130133
def createSecurityContent(self, contentType: SecurityContentType) -> None:
131134
if contentType == SecurityContentType.ssa_detections:
132-
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.path, 'ssa_detections'))
133-
security_content_files = [f for f in files if f.name.startswith('ssa___')]
134-
135-
elif contentType in [SecurityContentType.deployments,
136-
SecurityContentType.lookups,
137-
SecurityContentType.macros,
138-
SecurityContentType.stories,
139-
SecurityContentType.baselines,
140-
SecurityContentType.investigations,
141-
SecurityContentType.playbooks,
142-
SecurityContentType.detections]:
143-
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.path, str(contentType.name)))
144-
security_content_files = [f for f in files if not f.name.startswith('ssa___')]
135+
files = Utils.get_all_yml_files_from_directory(
136+
os.path.join(self.input_dto.path, "ssa_detections")
137+
)
138+
security_content_files = [f for f in files if f.name.startswith("ssa___")]
139+
140+
elif contentType == SecurityContentType.data_sources:
141+
security_content_files = (
142+
Utils.get_all_yml_files_from_directory_one_layer_deep(
143+
os.path.join(self.input_dto.path, "data_sources")
144+
)
145+
)
146+
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+
162+
elif contentType in [
163+
SecurityContentType.deployments,
164+
SecurityContentType.lookups,
165+
SecurityContentType.macros,
166+
SecurityContentType.stories,
167+
SecurityContentType.baselines,
168+
SecurityContentType.investigations,
169+
SecurityContentType.playbooks,
170+
SecurityContentType.detections,
171+
]:
172+
files = Utils.get_all_yml_files_from_directory(
173+
os.path.join(self.input_dto.path, str(contentType.name))
174+
)
175+
security_content_files = [
176+
f for f in files if not f.name.startswith("ssa___")
177+
]
145178
else:
146-
raise(Exception(f"Cannot createSecurityContent for unknown product."))
179+
raise (Exception(f"Cannot createSecurityContent for unknown product."))
147180

148181
validation_errors = []
149-
182+
150183
already_ran = False
151184
progress_percent = 0
152-
153-
for index,file in enumerate(security_content_files):
154-
progress_percent = ((index+1)/len(security_content_files)) * 100
185+
186+
for index, file in enumerate(security_content_files):
187+
progress_percent = ((index + 1) / len(security_content_files)) * 100
155188
try:
156189
type_string = contentType.name.upper()
157190
modelDict = YmlReader.load_file(file)
@@ -167,7 +200,7 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
167200
elif contentType == SecurityContentType.deployments:
168201
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
169202
self.output_dto.addContentToDictMappings(deployment)
170-
203+
171204
elif contentType == SecurityContentType.playbooks:
172205
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
173206
self.output_dto.addContentToDictMappings(playbook)
@@ -193,36 +226,67 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
193226
ssa_detection = self.ssa_detection_builder.getObject()
194227
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
195228
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)
196241

197242
else:
198-
raise Exception(f"Unsupported type: [{contentType}]")
199-
200-
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()) or not already_ran:
201-
already_ran = True
202-
print(f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
203-
204-
except (ValidationError, ValueError) as e:
205-
relative_path = file.absolute().relative_to(self.input_dto.path.absolute())
206-
validation_errors.append((relative_path,e))
207-
243+
raise Exception(f"Unsupported type: [{contentType}]")
244+
245+
if (
246+
sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()
247+
) or not already_ran:
248+
already_ran = True
249+
print(
250+
f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...",
251+
end="",
252+
flush=True,
253+
)
208254

209-
print(f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
255+
except (ValidationError, ValueError) as e:
256+
relative_path = file.absolute().relative_to(
257+
self.input_dto.path.absolute()
258+
)
259+
validation_errors.append((relative_path, e))
260+
261+
print(
262+
f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...",
263+
end="",
264+
flush=True,
265+
)
210266
print("Done!")
211267

212268
if len(validation_errors) > 0:
213-
errors_string = '\n\n'.join([f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}" for e_tuple in validation_errors])
214-
#print(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED")
269+
errors_string = "\n\n".join(
270+
[
271+
f"File: {e_tuple[0]}\nError: {str(e_tuple[1])}"
272+
for e_tuple in validation_errors
273+
]
274+
)
275+
# print(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED")
215276
# We quit after validation a single type/group of content because it can cause significant cascading errors in subsequent
216277
# types of content (since they may import or otherwise use it)
217-
raise Exception(f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED")
218-
219-
220-
221-
278+
raise Exception(
279+
f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED"
280+
)
222281

223-
def constructSSADetection(self, builder: SSADetectionBuilder, directorOutput:DirectorOutputDto, file_path: str) -> None:
282+
def constructSSADetection(
283+
self,
284+
builder: SSADetectionBuilder,
285+
directorOutput: DirectorOutputDto,
286+
file_path: str,
287+
) -> None:
224288
builder.reset()
225-
builder.setObject(file_path,self.output_dto)
289+
builder.setObject(file_path)
226290
builder.addMitreAttackEnrichmentNew(directorOutput.attack_enrichment)
227291
builder.addKillChainPhase()
228292
builder.addCIS()

0 commit comments

Comments
 (0)