contentctl 5.0.0a2__py3-none-any.whl → 5.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentctl/__init__.py +1 -1
- contentctl/actions/build.py +88 -55
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
- contentctl/actions/detection_testing/GitService.py +2 -4
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +3 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +83 -53
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +31 -25
- contentctl/actions/validate.py +54 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +10 -10
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -39
- contentctl/input/director.py +69 -37
- contentctl/input/new_content_questions.py +26 -34
- contentctl/input/yml_reader.py +22 -17
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +255 -323
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +111 -46
- contentctl/objects/alert_action.py +8 -8
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +64 -54
- contentctl/objects/base_test.py +2 -1
- contentctl/objects/base_test_result.py +16 -8
- contentctl/objects/baseline.py +47 -35
- contentctl/objects/baseline_tags.py +29 -22
- contentctl/objects/config.py +1 -1
- contentctl/objects/constants.py +32 -58
- contentctl/objects/correlation_search.py +75 -55
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +13 -13
- contentctl/objects/deployment.py +44 -37
- contentctl/objects/deployment_email.py +1 -1
- contentctl/objects/deployment_notable.py +2 -1
- contentctl/objects/deployment_phantom.py +5 -5
- contentctl/objects/deployment_rba.py +1 -1
- contentctl/objects/deployment_scheduling.py +1 -1
- contentctl/objects/deployment_slack.py +1 -1
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +54 -64
- contentctl/objects/drilldown.py +66 -35
- contentctl/objects/enums.py +61 -43
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +53 -31
- contentctl/objects/investigation_tags.py +29 -17
- contentctl/objects/lookup.py +234 -113
- contentctl/objects/macro.py +55 -38
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +22 -16
- contentctl/objects/rba.py +68 -11
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +27 -20
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -44
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +4 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +22 -22
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +230 -174
- contentctl/output/data_source_writer.py +38 -25
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +20 -8
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +2 -8
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +2 -4
- contentctl/output/yml_writer.py +18 -24
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/METADATA +1 -1
- contentctl-5.0.1.dist-info/RECORD +168 -0
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/observable.py +0 -39
- contentctl-5.0.0a2.dist-info/RECORD +0 -170
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/WHEEL +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/entry_points.txt +0 -0
contentctl/helper/utils.py
CHANGED
|
@@ -12,6 +12,7 @@ import tqdm
|
|
|
12
12
|
from math import ceil
|
|
13
13
|
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
17
18
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
@@ -24,26 +25,29 @@ ALWAYS_PULL = True
|
|
|
24
25
|
class Utils:
|
|
25
26
|
@staticmethod
|
|
26
27
|
def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
|
|
27
|
-
listOfFiles:list[pathlib.Path] = []
|
|
28
|
+
listOfFiles: list[pathlib.Path] = []
|
|
28
29
|
base_path = pathlib.Path(path)
|
|
29
30
|
if not base_path.exists():
|
|
30
31
|
return listOfFiles
|
|
31
|
-
for
|
|
32
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
32
33
|
for file in filenames:
|
|
33
34
|
if file.endswith(".yml"):
|
|
34
35
|
listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
return sorted(listOfFiles)
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
@staticmethod
|
|
39
|
-
def get_security_content_files_from_directory(
|
|
40
|
-
|
|
40
|
+
def get_security_content_files_from_directory(
|
|
41
|
+
path: pathlib.Path,
|
|
42
|
+
allowedFileExtensions: list[str] = [".yml"],
|
|
43
|
+
fileExtensionsToReturn: list[str] = [".yml"],
|
|
44
|
+
) -> list[pathlib.Path]:
|
|
41
45
|
"""
|
|
42
46
|
Get all of the Security Content Object Files rooted in a given directory. These will almost
|
|
43
47
|
certain be YML files, but could be other file types as specified by the user
|
|
44
48
|
|
|
45
49
|
Args:
|
|
46
|
-
path (pathlib.Path): The root path at which to enumerate all Security Content Files. All directories will be traversed.
|
|
50
|
+
path (pathlib.Path): The root path at which to enumerate all Security Content Files. All directories will be traversed.
|
|
47
51
|
allowedFileExtensions (set[str], optional): File extensions which are allowed to be present in this directory. In most cases, we do not want to allow the presence of non-YML files. Defaults to [".yml"].
|
|
48
52
|
fileExtensionsToReturn (set[str], optional): Filenames with extensions that should be returned from this function. For example, the lookups/ directory contains YML, CSV, and MLMODEL directories, but only the YMLs are Security Content Objects for constructing Lookyps. Defaults to[".yml"].
|
|
49
53
|
|
|
@@ -56,14 +60,18 @@ class Utils:
|
|
|
56
60
|
list[pathlib.Path]: list of files with an extension in fileExtensionsToReturn found in path
|
|
57
61
|
"""
|
|
58
62
|
if not set(fileExtensionsToReturn).issubset(set(allowedFileExtensions)):
|
|
59
|
-
raise Exception(
|
|
60
|
-
|
|
63
|
+
raise Exception(
|
|
64
|
+
f"allowedFileExtensions {allowedFileExtensions} MUST be a subset of fileExtensionsToReturn {fileExtensionsToReturn}, but it is not"
|
|
65
|
+
)
|
|
66
|
+
|
|
61
67
|
if not path.exists() or not path.is_dir():
|
|
62
|
-
raise Exception(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
raise Exception(
|
|
69
|
+
f"Unable to get security_content files, required directory '{str(path)}' does not exist or is not a directory"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
allowedFiles: list[pathlib.Path] = []
|
|
73
|
+
erroneousFiles: list[pathlib.Path] = []
|
|
74
|
+
# Get every single file extension
|
|
67
75
|
for filePath in path.glob("**/*.*"):
|
|
68
76
|
if filePath.suffix in allowedFileExtensions:
|
|
69
77
|
# Yes these are allowed
|
|
@@ -73,58 +81,75 @@ class Utils:
|
|
|
73
81
|
erroneousFiles.append(filePath)
|
|
74
82
|
|
|
75
83
|
if len(erroneousFiles):
|
|
76
|
-
raise Exception(
|
|
77
|
-
|
|
84
|
+
raise Exception(
|
|
85
|
+
f"The following files are not allowed in the directory '{path}'. Only files with the extensions {allowedFileExtensions} are allowed:{[str(filePath) for filePath in erroneousFiles]}"
|
|
86
|
+
)
|
|
87
|
+
|
|
78
88
|
# There were no errorneous files, so return the requested files
|
|
79
|
-
return sorted(
|
|
89
|
+
return sorted(
|
|
90
|
+
[
|
|
91
|
+
filePath
|
|
92
|
+
for filePath in allowedFiles
|
|
93
|
+
if filePath.suffix in fileExtensionsToReturn
|
|
94
|
+
]
|
|
95
|
+
)
|
|
80
96
|
|
|
81
97
|
@staticmethod
|
|
82
|
-
def get_all_yml_files_from_directory_one_layer_deep(
|
|
98
|
+
def get_all_yml_files_from_directory_one_layer_deep(
|
|
99
|
+
path: str,
|
|
100
|
+
) -> list[pathlib.Path]:
|
|
83
101
|
listOfFiles: list[pathlib.Path] = []
|
|
84
102
|
base_path = pathlib.Path(path)
|
|
85
103
|
if not base_path.exists():
|
|
86
104
|
return listOfFiles
|
|
87
105
|
# Check the base directory
|
|
88
106
|
for item in base_path.iterdir():
|
|
89
|
-
if item.is_file() and item.suffix ==
|
|
107
|
+
if item.is_file() and item.suffix == ".yml":
|
|
90
108
|
listOfFiles.append(item)
|
|
91
109
|
# Check one subfolder level deep
|
|
92
110
|
for subfolder in base_path.iterdir():
|
|
93
111
|
if subfolder.is_dir() and subfolder.name != "cim":
|
|
94
112
|
for item in subfolder.iterdir():
|
|
95
|
-
if item.is_file() and item.suffix ==
|
|
113
|
+
if item.is_file() and item.suffix == ".yml":
|
|
96
114
|
listOfFiles.append(item)
|
|
97
115
|
return sorted(listOfFiles)
|
|
98
116
|
|
|
99
|
-
|
|
100
117
|
@staticmethod
|
|
101
|
-
def add_id(
|
|
118
|
+
def add_id(
|
|
119
|
+
id_dict: dict[str, list[pathlib.Path]],
|
|
120
|
+
obj: SecurityContentObject,
|
|
121
|
+
path: pathlib.Path,
|
|
122
|
+
) -> None:
|
|
102
123
|
if hasattr(obj, "id"):
|
|
103
124
|
obj_id = obj.id
|
|
104
125
|
if obj_id in id_dict:
|
|
105
126
|
id_dict[obj_id].append(path)
|
|
106
127
|
else:
|
|
107
128
|
id_dict[obj_id] = [path]
|
|
129
|
+
|
|
108
130
|
# Otherwise, no ID so nothing to add....
|
|
109
131
|
|
|
110
132
|
@staticmethod
|
|
111
|
-
def check_ids_for_duplicates(
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
def check_ids_for_duplicates(
|
|
134
|
+
id_dict: dict[str, list[pathlib.Path]],
|
|
135
|
+
) -> list[Tuple[pathlib.Path, ValueError]]:
|
|
136
|
+
validation_errors: list[Tuple[pathlib.Path, ValueError]] = []
|
|
137
|
+
|
|
114
138
|
for key, values in id_dict.items():
|
|
115
139
|
if len(values) > 1:
|
|
116
140
|
error_file_path = pathlib.Path("MULTIPLE")
|
|
117
|
-
all_files =
|
|
118
|
-
exception = ValueError(
|
|
141
|
+
all_files = "\n\t".join(str(pathlib.Path(p)) for p in values)
|
|
142
|
+
exception = ValueError(
|
|
143
|
+
f"Error validating id [{key}] - duplicate ID was used in the following files: \n\t{all_files}"
|
|
144
|
+
)
|
|
119
145
|
validation_errors.append((error_file_path, exception))
|
|
120
|
-
|
|
146
|
+
|
|
121
147
|
return validation_errors
|
|
122
148
|
|
|
123
149
|
@staticmethod
|
|
124
150
|
def validate_git_hash(
|
|
125
151
|
repo_path: str, repo_url: str, commit_hash: str, branch_name: Union[str, None]
|
|
126
152
|
) -> bool:
|
|
127
|
-
|
|
128
153
|
# Get a list of all branches
|
|
129
154
|
repo = git.Repo(repo_path)
|
|
130
155
|
if commit_hash is None:
|
|
@@ -141,14 +166,14 @@ class Utils:
|
|
|
141
166
|
# Note, of course, that a hash can be in 0, 1, more branches!
|
|
142
167
|
for branch_string in all_branches_containing_hash:
|
|
143
168
|
if branch_string.split(" ")[0] == "*" and (
|
|
144
|
-
branch_string.split(" ")[-1] == branch_name or branch_name
|
|
169
|
+
branch_string.split(" ")[-1] == branch_name or branch_name is None
|
|
145
170
|
):
|
|
146
171
|
# Yes, the hash exists in the branch (or branch_name was None and it existed in at least one branch)!
|
|
147
172
|
return True
|
|
148
173
|
# If we get here, it does not exist in the given branch
|
|
149
174
|
raise (Exception("Does not exist in branch"))
|
|
150
175
|
|
|
151
|
-
except Exception
|
|
176
|
+
except Exception:
|
|
152
177
|
if branch_name is None:
|
|
153
178
|
branch_name = "ANY_BRANCH"
|
|
154
179
|
if ALWAYS_PULL:
|
|
@@ -251,7 +276,6 @@ class Utils:
|
|
|
251
276
|
def verify_file_exists(
|
|
252
277
|
file_path: str, verbose_print=False, timeout_seconds: int = 10
|
|
253
278
|
) -> None:
|
|
254
|
-
|
|
255
279
|
try:
|
|
256
280
|
if pathlib.Path(file_path).is_file():
|
|
257
281
|
# This is a file and we know it exists
|
|
@@ -261,18 +285,13 @@ class Utils:
|
|
|
261
285
|
|
|
262
286
|
# Try to make a head request to verify existence of the file
|
|
263
287
|
try:
|
|
264
|
-
|
|
265
288
|
req = requests.head(
|
|
266
289
|
file_path, timeout=timeout_seconds, verify=True, allow_redirects=True
|
|
267
290
|
)
|
|
268
291
|
if req.status_code > 400:
|
|
269
292
|
raise (Exception(f"Return code={req.status_code}"))
|
|
270
293
|
except Exception as e:
|
|
271
|
-
raise (
|
|
272
|
-
Exception(
|
|
273
|
-
f"HTTP Resolution Failed: {str(e)}"
|
|
274
|
-
)
|
|
275
|
-
)
|
|
294
|
+
raise (Exception(f"HTTP Resolution Failed: {str(e)}"))
|
|
276
295
|
|
|
277
296
|
@staticmethod
|
|
278
297
|
def copy_local_file(
|
|
@@ -376,7 +395,7 @@ class Utils:
|
|
|
376
395
|
)
|
|
377
396
|
|
|
378
397
|
try:
|
|
379
|
-
|
|
398
|
+
default_timer()
|
|
380
399
|
bytes_written = 0
|
|
381
400
|
file_to_download = requests.get(file_path, stream=True)
|
|
382
401
|
file_to_download.raise_for_status()
|
contentctl/input/director.py
CHANGED
|
@@ -29,7 +29,7 @@ from contentctl.helper.utils import Utils
|
|
|
29
29
|
|
|
30
30
|
@dataclass
|
|
31
31
|
class DirectorOutputDto:
|
|
32
|
-
# Atomic Tests are first because parsing them
|
|
32
|
+
# Atomic Tests are first because parsing them
|
|
33
33
|
# is far quicker than attack_enrichment
|
|
34
34
|
atomic_enrichment: AtomicEnrichment
|
|
35
35
|
attack_enrichment: AttackEnrichment
|
|
@@ -50,20 +50,20 @@ class DirectorOutputDto:
|
|
|
50
50
|
|
|
51
51
|
def addContentToDictMappings(self, content: SecurityContentObject):
|
|
52
52
|
content_name = content.name
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
|
|
55
54
|
if content_name in self.name_to_content_map:
|
|
56
55
|
raise ValueError(
|
|
57
56
|
f"Duplicate name '{content_name}' with paths:\n"
|
|
58
57
|
f" - {content.file_path}\n"
|
|
59
58
|
f" - {self.name_to_content_map[content_name].file_path}"
|
|
60
59
|
)
|
|
61
|
-
|
|
60
|
+
|
|
62
61
|
if content.id in self.uuid_to_content_map:
|
|
63
62
|
raise ValueError(
|
|
64
63
|
f"Duplicate id '{content.id}' with paths:\n"
|
|
65
64
|
f" - {content.file_path}\n"
|
|
66
|
-
f" - {self.uuid_to_content_map[content.id].file_path}"
|
|
65
|
+
f" - {self.uuid_to_content_map[content.id].file_path}"
|
|
66
|
+
)
|
|
67
67
|
|
|
68
68
|
if isinstance(content, Lookup):
|
|
69
69
|
self.lookups.append(content)
|
|
@@ -82,7 +82,7 @@ class DirectorOutputDto:
|
|
|
82
82
|
elif isinstance(content, Detection):
|
|
83
83
|
self.detections.append(content)
|
|
84
84
|
elif isinstance(content, Dashboard):
|
|
85
|
-
self.dashboards.append(content)
|
|
85
|
+
self.dashboards.append(content)
|
|
86
86
|
|
|
87
87
|
elif isinstance(content, DataSource):
|
|
88
88
|
self.data_sources.append(content)
|
|
@@ -93,7 +93,7 @@ class DirectorOutputDto:
|
|
|
93
93
|
self.uuid_to_content_map[content.id] = content
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
class Director
|
|
96
|
+
class Director:
|
|
97
97
|
input_dto: validate
|
|
98
98
|
output_dto: DirectorOutputDto
|
|
99
99
|
|
|
@@ -112,13 +112,18 @@ class Director():
|
|
|
112
112
|
self.createSecurityContent(SecurityContentType.playbooks)
|
|
113
113
|
self.createSecurityContent(SecurityContentType.detections)
|
|
114
114
|
self.createSecurityContent(SecurityContentType.dashboards)
|
|
115
|
-
|
|
116
|
-
from contentctl.objects.abstract_security_content_objects.detection_abstract import
|
|
115
|
+
|
|
116
|
+
from contentctl.objects.abstract_security_content_objects.detection_abstract import (
|
|
117
|
+
MISSING_SOURCES,
|
|
118
|
+
)
|
|
119
|
+
|
|
117
120
|
if len(MISSING_SOURCES) > 0:
|
|
118
121
|
missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES)))
|
|
119
|
-
print(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
print(
|
|
123
|
+
"WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
|
|
124
|
+
"This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
|
|
125
|
+
f"{missing_sources_string}"
|
|
126
|
+
)
|
|
122
127
|
else:
|
|
123
128
|
print("No missing data_sources!")
|
|
124
129
|
|
|
@@ -133,18 +138,20 @@ class Director():
|
|
|
133
138
|
SecurityContentType.playbooks,
|
|
134
139
|
SecurityContentType.detections,
|
|
135
140
|
SecurityContentType.data_sources,
|
|
136
|
-
SecurityContentType.dashboards
|
|
141
|
+
SecurityContentType.dashboards,
|
|
137
142
|
]:
|
|
138
143
|
files = Utils.get_all_yml_files_from_directory(
|
|
139
144
|
os.path.join(self.input_dto.path, str(contentType.name))
|
|
140
145
|
)
|
|
141
|
-
security_content_files = [
|
|
142
|
-
f for f in files
|
|
143
|
-
]
|
|
146
|
+
security_content_files = [f for f in files]
|
|
144
147
|
else:
|
|
145
|
-
raise (
|
|
148
|
+
raise (
|
|
149
|
+
Exception(
|
|
150
|
+
f"Cannot createSecurityContent for unknown product {contentType}."
|
|
151
|
+
)
|
|
152
|
+
)
|
|
146
153
|
|
|
147
|
-
validation_errors:list[tuple[Path,ValueError]] = []
|
|
154
|
+
validation_errors: list[tuple[Path, ValueError]] = []
|
|
148
155
|
|
|
149
156
|
already_ran = False
|
|
150
157
|
progress_percent = 0
|
|
@@ -156,41 +163,67 @@ class Director():
|
|
|
156
163
|
modelDict = YmlReader.load_file(file)
|
|
157
164
|
|
|
158
165
|
if contentType == SecurityContentType.lookups:
|
|
159
|
-
lookup = LookupAdapter.validate_python(
|
|
160
|
-
|
|
166
|
+
lookup = LookupAdapter.validate_python(
|
|
167
|
+
modelDict,
|
|
168
|
+
context={
|
|
169
|
+
"output_dto": self.output_dto,
|
|
170
|
+
"config": self.input_dto,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
# lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
161
174
|
self.output_dto.addContentToDictMappings(lookup)
|
|
162
|
-
|
|
175
|
+
|
|
163
176
|
elif contentType == SecurityContentType.macros:
|
|
164
|
-
macro = Macro.model_validate(
|
|
177
|
+
macro = Macro.model_validate(
|
|
178
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
179
|
+
)
|
|
165
180
|
self.output_dto.addContentToDictMappings(macro)
|
|
166
|
-
|
|
181
|
+
|
|
167
182
|
elif contentType == SecurityContentType.deployments:
|
|
168
|
-
deployment = Deployment.model_validate(
|
|
183
|
+
deployment = Deployment.model_validate(
|
|
184
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
185
|
+
)
|
|
169
186
|
self.output_dto.addContentToDictMappings(deployment)
|
|
170
187
|
|
|
171
188
|
elif contentType == SecurityContentType.playbooks:
|
|
172
|
-
playbook = Playbook.model_validate(
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
playbook = Playbook.model_validate(
|
|
190
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
191
|
+
)
|
|
192
|
+
self.output_dto.addContentToDictMappings(playbook)
|
|
193
|
+
|
|
175
194
|
elif contentType == SecurityContentType.baselines:
|
|
176
|
-
baseline = Baseline.model_validate(
|
|
195
|
+
baseline = Baseline.model_validate(
|
|
196
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
197
|
+
)
|
|
177
198
|
self.output_dto.addContentToDictMappings(baseline)
|
|
178
|
-
|
|
199
|
+
|
|
179
200
|
elif contentType == SecurityContentType.investigations:
|
|
180
|
-
investigation = Investigation.model_validate(
|
|
201
|
+
investigation = Investigation.model_validate(
|
|
202
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
203
|
+
)
|
|
181
204
|
self.output_dto.addContentToDictMappings(investigation)
|
|
182
205
|
|
|
183
206
|
elif contentType == SecurityContentType.stories:
|
|
184
|
-
story = Story.model_validate(
|
|
207
|
+
story = Story.model_validate(
|
|
208
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
209
|
+
)
|
|
185
210
|
self.output_dto.addContentToDictMappings(story)
|
|
186
|
-
|
|
211
|
+
|
|
187
212
|
elif contentType == SecurityContentType.detections:
|
|
188
|
-
detection = Detection.model_validate(
|
|
213
|
+
detection = Detection.model_validate(
|
|
214
|
+
modelDict,
|
|
215
|
+
context={
|
|
216
|
+
"output_dto": self.output_dto,
|
|
217
|
+
"app": self.input_dto.app,
|
|
218
|
+
},
|
|
219
|
+
)
|
|
189
220
|
self.output_dto.addContentToDictMappings(detection)
|
|
190
|
-
|
|
221
|
+
|
|
191
222
|
elif contentType == SecurityContentType.dashboards:
|
|
192
|
-
|
|
193
|
-
self.output_dto
|
|
223
|
+
dashboard = Dashboard.model_validate(
|
|
224
|
+
modelDict, context={"output_dto": self.output_dto}
|
|
225
|
+
)
|
|
226
|
+
self.output_dto.addContentToDictMappings(dashboard)
|
|
194
227
|
|
|
195
228
|
elif contentType == SecurityContentType.data_sources:
|
|
196
229
|
data_source = DataSource.model_validate(
|
|
@@ -237,4 +270,3 @@ class Director():
|
|
|
237
270
|
raise Exception(
|
|
238
271
|
f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED"
|
|
239
272
|
)
|
|
240
|
-
|
|
@@ -3,9 +3,8 @@ from contentctl.objects.enums import DataSource
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class NewContentQuestions:
|
|
6
|
-
|
|
7
6
|
@classmethod
|
|
8
|
-
def get_questions_detection(cls) -> list[dict[str,Any]]:
|
|
7
|
+
def get_questions_detection(cls) -> list[dict[str, Any]]:
|
|
9
8
|
questions = [
|
|
10
9
|
{
|
|
11
10
|
"type": "text",
|
|
@@ -14,22 +13,16 @@ class NewContentQuestions:
|
|
|
14
13
|
"default": "Powershell Encoded Command",
|
|
15
14
|
},
|
|
16
15
|
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'cloud',
|
|
23
|
-
'application',
|
|
24
|
-
'network',
|
|
25
|
-
'web'
|
|
26
|
-
],
|
|
27
|
-
'default': 'endpoint'
|
|
16
|
+
"type": "select",
|
|
17
|
+
"message": "what kind of detection is this",
|
|
18
|
+
"name": "detection_kind",
|
|
19
|
+
"choices": ["endpoint", "cloud", "application", "network", "web"],
|
|
20
|
+
"default": "endpoint",
|
|
28
21
|
},
|
|
29
22
|
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
"type": "text",
|
|
24
|
+
"message": "enter author name",
|
|
25
|
+
"name": "detection_author",
|
|
33
26
|
},
|
|
34
27
|
{
|
|
35
28
|
"type": "select",
|
|
@@ -46,12 +39,11 @@ class NewContentQuestions:
|
|
|
46
39
|
"default": "TTP",
|
|
47
40
|
},
|
|
48
41
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
"type": "checkbox",
|
|
43
|
+
"message": "Your data source",
|
|
44
|
+
"name": "data_sources",
|
|
45
|
+
# In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
|
|
46
|
+
"choices": sorted(DataSource._value2member_map_),
|
|
55
47
|
},
|
|
56
48
|
{
|
|
57
49
|
"type": "text",
|
|
@@ -66,24 +58,24 @@ class NewContentQuestions:
|
|
|
66
58
|
"default": "T1003.002",
|
|
67
59
|
},
|
|
68
60
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
61
|
+
"type": "select",
|
|
62
|
+
"message": "security_domain for detection",
|
|
63
|
+
"name": "security_domain",
|
|
64
|
+
"choices": [
|
|
65
|
+
"access",
|
|
66
|
+
"endpoint",
|
|
67
|
+
"network",
|
|
68
|
+
"threat",
|
|
69
|
+
"identity",
|
|
70
|
+
"audit",
|
|
79
71
|
],
|
|
80
|
-
|
|
72
|
+
"default": "endpoint",
|
|
81
73
|
},
|
|
82
74
|
]
|
|
83
75
|
return questions
|
|
84
76
|
|
|
85
77
|
@classmethod
|
|
86
|
-
def get_questions_story(cls)-> list[dict[str,Any]]:
|
|
78
|
+
def get_questions_story(cls) -> list[dict[str, Any]]:
|
|
87
79
|
questions = [
|
|
88
80
|
{
|
|
89
81
|
"type": "text",
|
contentctl/input/yml_reader.py
CHANGED
|
@@ -3,36 +3,43 @@ import yaml
|
|
|
3
3
|
import sys
|
|
4
4
|
import pathlib
|
|
5
5
|
|
|
6
|
-
class YmlReader():
|
|
7
6
|
|
|
7
|
+
class YmlReader:
|
|
8
8
|
@staticmethod
|
|
9
|
-
def load_file(
|
|
9
|
+
def load_file(
|
|
10
|
+
file_path: pathlib.Path,
|
|
11
|
+
add_fields: bool = True,
|
|
12
|
+
STRICT_YML_CHECKING: bool = False,
|
|
13
|
+
) -> Dict[str, Any]:
|
|
10
14
|
try:
|
|
11
|
-
file_handler = open(file_path,
|
|
12
|
-
|
|
13
|
-
# The following code can help diagnose issues with duplicate keys or
|
|
15
|
+
file_handler = open(file_path, "r", encoding="utf-8")
|
|
16
|
+
|
|
17
|
+
# The following code can help diagnose issues with duplicate keys or
|
|
14
18
|
# poorly-formatted but still "compliant" YML. This code should be
|
|
15
|
-
# enabled manually for debugging purposes. As such, strictyaml
|
|
19
|
+
# enabled manually for debugging purposes. As such, strictyaml
|
|
16
20
|
# library is intentionally excluded from the contentctl requirements
|
|
17
21
|
|
|
18
22
|
if STRICT_YML_CHECKING:
|
|
19
23
|
import strictyaml
|
|
24
|
+
|
|
20
25
|
try:
|
|
21
|
-
strictyaml.dirty_load(file_handler.read(), allow_flow_style
|
|
26
|
+
strictyaml.dirty_load(file_handler.read(), allow_flow_style=True)
|
|
22
27
|
file_handler.seek(0)
|
|
23
28
|
except Exception as e:
|
|
24
29
|
print(f"Error loading YML file {file_path}: {str(e)}")
|
|
25
30
|
sys.exit(1)
|
|
26
31
|
try:
|
|
27
|
-
#Ideally we should use
|
|
28
|
-
# from contentctl.actions.new_content import NewContent
|
|
29
|
-
# and use NewContent.UPDATE_PREFIX,
|
|
32
|
+
# Ideally we should use
|
|
33
|
+
# from contentctl.actions.new_content import NewContent
|
|
34
|
+
# and use NewContent.UPDATE_PREFIX,
|
|
30
35
|
# but there is a circular dependency right now which makes that difficult.
|
|
31
36
|
# We have instead hardcoded UPDATE_PREFIX
|
|
32
37
|
UPDATE_PREFIX = "__UPDATE__"
|
|
33
38
|
data = file_handler.read()
|
|
34
39
|
if UPDATE_PREFIX in data:
|
|
35
|
-
raise Exception(
|
|
40
|
+
raise Exception(
|
|
41
|
+
f"The file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required."
|
|
42
|
+
)
|
|
36
43
|
yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
|
|
37
44
|
except yaml.YAMLError as exc:
|
|
38
45
|
print(exc)
|
|
@@ -41,12 +48,10 @@ class YmlReader():
|
|
|
41
48
|
except OSError as exc:
|
|
42
49
|
print(exc)
|
|
43
50
|
sys.exit(1)
|
|
44
|
-
|
|
45
|
-
if add_fields
|
|
51
|
+
|
|
52
|
+
if add_fields is False:
|
|
46
53
|
return yml_obj
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
yml_obj['file_path'] = str(file_path)
|
|
50
|
-
|
|
54
|
+
|
|
55
|
+
yml_obj["file_path"] = str(file_path)
|
|
51
56
|
|
|
52
57
|
return yml_obj
|