contentctl 5.0.0a2__py3-none-any.whl → 5.0.0a3__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.
Files changed (106) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +2 -4
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +78 -50
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +10 -10
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +41 -30
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +29 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +41 -26
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +22 -16
  74. contentctl/objects/rba.py +14 -8
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +45 -44
  81. contentctl/objects/story_tags.py +56 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/transforms.j2 +2 -2
  98. contentctl/output/yml_writer.py +18 -24
  99. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  100. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  101. contentctl/actions/initialize_old.py +0 -245
  102. contentctl/objects/observable.py +0 -39
  103. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  104. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  105. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  106. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -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 (dirpath, dirnames, filenames) in os.walk(path):
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(path: pathlib.Path, allowedFileExtensions:list[str]=[".yml"], fileExtensionsToReturn:list[str]=[".yml"]) -> list[pathlib.Path]:
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(f"allowedFileExtensions {allowedFileExtensions} MUST be a subset of fileExtensionsToReturn {fileExtensionsToReturn}, but it is not")
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(f"Unable to get security_content files, required directory '{str(path)}' does not exist or is not a directory")
63
-
64
- allowedFiles:list[pathlib.Path] = []
65
- erroneousFiles:list[pathlib.Path] = []
66
- #Get every single file extension
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(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]}")
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([filePath for filePath in allowedFiles if filePath.suffix in fileExtensionsToReturn])
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(path: str) -> list[pathlib.Path]:
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 == '.yml':
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 == '.yml':
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(id_dict:dict[str, list[pathlib.Path]], obj:SecurityContentObject, path:pathlib.Path) -> None:
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(id_dict:dict[str, list[pathlib.Path]])->list[Tuple[pathlib.Path, ValueError]]:
112
- validation_errors:list[Tuple[pathlib.Path, ValueError]] = []
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 = '\n\t'.join(str(pathlib.Path(p)) for p in values)
118
- exception = ValueError(f"Error validating id [{key}] - duplicate ID was used in the following files: \n\t{all_files}")
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 == None
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 as e:
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
- download_start_time = default_timer()
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()
@@ -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 MISSING_SOURCES
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("WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
120
- "This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
121
- f"{missing_sources_string}")
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 (Exception(f"Cannot createSecurityContent for unknown product {contentType}."))
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(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
160
- #lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
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(modelDict, context={"output_dto":self.output_dto})
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(modelDict, context={"output_dto":self.output_dto})
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(modelDict, context={"output_dto":self.output_dto})
173
- self.output_dto.addContentToDictMappings(playbook)
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(modelDict, context={"output_dto":self.output_dto})
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(modelDict, context={"output_dto":self.output_dto})
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(modelDict, context={"output_dto":self.output_dto})
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(modelDict, context={"output_dto":self.output_dto, "app":self.input_dto.app})
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
- dashboard = Dashboard.model_validate(modelDict,context={"output_dto":self.output_dto})
193
- self.output_dto.addContentToDictMappings(dashboard)
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
- 'type': 'select',
18
- 'message': 'what kind of detection is this',
19
- 'name': 'detection_kind',
20
- 'choices': [
21
- 'endpoint',
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
- 'type': 'text',
31
- 'message': 'enter author name',
32
- 'name': 'detection_author',
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
- 'type': 'checkbox',
50
- 'message': 'Your data source',
51
- 'name': 'data_sources',
52
- #In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
53
- 'choices': sorted(DataSource._value2member_map_ )
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
- 'type': 'select',
70
- 'message': 'security_domain for detection',
71
- 'name': 'security_domain',
72
- 'choices': [
73
- 'access',
74
- 'endpoint',
75
- 'network',
76
- 'threat',
77
- 'identity',
78
- 'audit'
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
- 'default': 'endpoint'
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",
@@ -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(file_path: pathlib.Path, add_fields:bool=True, STRICT_YML_CHECKING:bool=False) -> Dict[str,Any]:
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, 'r', encoding="utf-8")
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 = True)
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(f"The file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required.")
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 == False:
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