contentctl 5.2.0__py3-none-any.whl → 5.3.0__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 (42) hide show
  1. contentctl/actions/build.py +5 -43
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +64 -24
  3. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +146 -42
  4. contentctl/actions/detection_testing/views/DetectionTestingView.py +5 -6
  5. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +2 -0
  6. contentctl/actions/initialize.py +35 -9
  7. contentctl/actions/release_notes.py +14 -12
  8. contentctl/actions/test.py +16 -20
  9. contentctl/actions/validate.py +8 -15
  10. contentctl/helper/utils.py +69 -20
  11. contentctl/input/director.py +147 -119
  12. contentctl/input/yml_reader.py +39 -27
  13. contentctl/objects/abstract_security_content_objects/detection_abstract.py +94 -20
  14. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +548 -8
  15. contentctl/objects/baseline.py +24 -6
  16. contentctl/objects/config.py +32 -8
  17. contentctl/objects/content_versioning_service.py +508 -0
  18. contentctl/objects/correlation_search.py +53 -63
  19. contentctl/objects/dashboard.py +15 -1
  20. contentctl/objects/data_source.py +13 -1
  21. contentctl/objects/deployment.py +23 -9
  22. contentctl/objects/detection.py +2 -0
  23. contentctl/objects/enums.py +28 -18
  24. contentctl/objects/investigation.py +40 -20
  25. contentctl/objects/lookup.py +61 -5
  26. contentctl/objects/macro.py +19 -4
  27. contentctl/objects/playbook.py +16 -2
  28. contentctl/objects/rba.py +1 -33
  29. contentctl/objects/removed_security_content_object.py +50 -0
  30. contentctl/objects/security_content_object.py +1 -0
  31. contentctl/objects/story.py +37 -5
  32. contentctl/output/api_json_output.py +5 -3
  33. contentctl/output/conf_output.py +9 -1
  34. contentctl/output/runtime_csv_writer.py +111 -0
  35. contentctl/output/svg_output.py +4 -5
  36. contentctl/output/templates/savedsearches_detections.j2 +2 -6
  37. {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/METADATA +4 -3
  38. {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/RECORD +41 -39
  39. {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/WHEEL +1 -1
  40. contentctl/output/data_source_writer.py +0 -52
  41. {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/LICENSE.md +0 -0
  42. {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,44 +1,36 @@
1
+ import pathlib
1
2
  from dataclasses import dataclass
2
3
  from typing import List
3
4
 
4
- from contentctl.objects.config import test_common, Selected, Changes
5
- from contentctl.objects.detection import Detection
6
-
7
-
8
5
  from contentctl.actions.detection_testing.DetectionTestingManager import (
9
6
  DetectionTestingManager,
10
7
  DetectionTestingManagerInputDto,
11
8
  )
12
-
13
-
14
9
  from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
15
10
  DetectionTestingManagerOutputDto,
16
11
  )
17
-
18
-
19
- from contentctl.actions.detection_testing.views.DetectionTestingViewWeb import (
20
- DetectionTestingViewWeb,
21
- )
22
-
23
12
  from contentctl.actions.detection_testing.views.DetectionTestingViewCLI import (
24
13
  DetectionTestingViewCLI,
25
14
  )
26
-
27
15
  from contentctl.actions.detection_testing.views.DetectionTestingViewFile import (
28
16
  DetectionTestingViewFile,
29
17
  )
30
-
18
+ from contentctl.actions.detection_testing.views.DetectionTestingViewWeb import (
19
+ DetectionTestingViewWeb,
20
+ )
21
+ from contentctl.objects.config import Changes, Selected
22
+ from contentctl.objects.config import test as test_
23
+ from contentctl.objects.config import test_servers
24
+ from contentctl.objects.detection import Detection
31
25
  from contentctl.objects.integration_test import IntegrationTest
32
26
 
33
- import pathlib
34
-
35
27
  MAXIMUM_CONFIGURATION_TIME_SECONDS = 600
36
28
 
37
29
 
38
30
  @dataclass(frozen=True)
39
31
  class TestInputDto:
40
32
  detections: List[Detection]
41
- config: test_common
33
+ config: test_ | test_servers
42
34
 
43
35
 
44
36
  class Test:
@@ -77,8 +69,8 @@ class Test:
77
69
 
78
70
  if len(input_dto.detections) == 0:
79
71
  print(
80
- f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were [0] detections found to test."
81
- "\nAs such, we will quit immediately."
72
+ f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were "
73
+ "[0] detections found to test.\nAs such, we will quit immediately."
82
74
  )
83
75
  # Directly call stop so that the summary.yml will be generated. Of course it will not
84
76
  # have any test results, but we still want it to contain a summary showing that now
@@ -109,6 +101,10 @@ class Test:
109
101
  try:
110
102
  summary_results = file.getSummaryObject()
111
103
  summary = summary_results.get("summary", {})
104
+ if not isinstance(summary, dict):
105
+ raise ValueError(
106
+ f"Summary in results was an unexpected type ({type(summary)}): {summary}"
107
+ )
112
108
 
113
109
  print(f"Test Summary (mode: {summary.get('mode', 'Error')})")
114
110
  print(f"\tSuccess : {summary.get('success', False)}")
@@ -152,7 +148,7 @@ class Test:
152
148
  "detection types (e.g. Correlation), but there may be overlap between these\n"
153
149
  "categories."
154
150
  )
155
- return summary_results.get("summary", {}).get("success", False)
151
+ return summary.get("success", False)
156
152
 
157
153
  except Exception as e:
158
154
  print(f"Error determining if whole test was successful: {str(e)}")
@@ -1,14 +1,14 @@
1
1
  import pathlib
2
2
 
3
- from contentctl.input.director import Director, DirectorOutputDto
4
- from contentctl.objects.config import validate
5
3
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
6
4
  from contentctl.enrichments.cve_enrichment import CveEnrichment
7
- from contentctl.objects.atomic import AtomicEnrichment
8
- from contentctl.objects.lookup import FileBackedLookup
5
+ from contentctl.helper.splunk_app import SplunkApp
9
6
  from contentctl.helper.utils import Utils
7
+ from contentctl.input.director import Director, DirectorOutputDto
8
+ from contentctl.objects.atomic import AtomicEnrichment
9
+ from contentctl.objects.config import validate
10
10
  from contentctl.objects.data_source import DataSource
11
- from contentctl.helper.splunk_app import SplunkApp
11
+ from contentctl.objects.lookup import FileBackedLookup, RuntimeCSV
12
12
 
13
13
 
14
14
  class Validate:
@@ -17,16 +17,6 @@ class Validate:
17
17
  AtomicEnrichment.getAtomicEnrichment(input_dto),
18
18
  AttackEnrichment.getAttackEnrichment(input_dto),
19
19
  CveEnrichment.getCveEnrichment(input_dto),
20
- [],
21
- [],
22
- [],
23
- [],
24
- [],
25
- [],
26
- [],
27
- [],
28
- [],
29
- [],
30
20
  )
31
21
 
32
22
  director = Director(director_output_dto)
@@ -68,7 +58,10 @@ class Validate:
68
58
  usedLookupFiles: list[pathlib.Path] = [
69
59
  lookup.filename
70
60
  for lookup in director_output_dto.lookups
61
+ # Of course Runtime CSVs do not have underlying CSV files, so make
62
+ # sure that we do not check for that existence.
71
63
  if isinstance(lookup, FileBackedLookup)
64
+ and not isinstance(lookup, RuntimeCSV)
72
65
  ] + [
73
66
  lookup.file_path
74
67
  for lookup in director_output_dto.lookups
@@ -1,40 +1,34 @@
1
- import os
2
- import git
3
- import shutil
4
- import requests
1
+ import logging
2
+ import pathlib
5
3
  import random
4
+ import shutil
6
5
  import string
6
+ from math import ceil
7
7
  from timeit import default_timer
8
- import pathlib
8
+ from typing import TYPE_CHECKING, Tuple, Union
9
9
 
10
- from typing import Union, Tuple
10
+ import git
11
+ import requests
11
12
  import tqdm
12
- from math import ceil
13
-
14
- from typing import TYPE_CHECKING
15
13
 
16
14
  if TYPE_CHECKING:
17
15
  from contentctl.objects.security_content_object import SecurityContentObject
18
16
  from contentctl.objects.security_content_object import SecurityContentObject
19
17
 
20
-
21
18
  TOTAL_BYTES = 0
22
19
  ALWAYS_PULL = True
23
20
 
24
21
 
25
22
  class Utils:
26
23
  @staticmethod
27
- def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
28
- listOfFiles: list[pathlib.Path] = []
29
- base_path = pathlib.Path(path)
30
- if not base_path.exists():
31
- return listOfFiles
32
- for dirpath, dirnames, filenames in os.walk(path):
33
- for file in filenames:
34
- if file.endswith(".yml"):
35
- listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
24
+ def get_all_yml_files_from_directory(path: pathlib.Path) -> list[pathlib.Path]:
25
+ if not path.exists():
26
+ raise FileNotFoundError(
27
+ f"Trying to find files in the directory '{path.absolute()}', but it does not exist.\n"
28
+ "It is not mandatory to have content/YMLs in this directory, but it must exist. Please create it."
29
+ )
36
30
 
37
- return sorted(listOfFiles)
31
+ return sorted(pathlib.Path(yml_path) for yml_path in path.glob("**/*.yml"))
38
32
 
39
33
  @staticmethod
40
34
  def get_security_content_files_from_directory(
@@ -490,3 +484,58 @@ class Utils:
490
484
  ratio = numerator / denominator
491
485
  percent = ratio * 100
492
486
  return Utils.getFixedWidth(percent, decimal_places) + "%"
487
+
488
+ @staticmethod
489
+ def get_logger(
490
+ name: str, log_level: int, log_path: str, enable_logging: bool
491
+ ) -> logging.Logger:
492
+ """
493
+ Gets a logger instance for the given name; logger is configured if not already configured.
494
+ The NullHandler is used to suppress loggging when running in production so as not to
495
+ conflict w/ contentctl's larger pbar-based logging. The StreamHandler is enabled by setting
496
+ enable_logging to True (useful for debugging/testing locally)
497
+
498
+ :param name: the logger name
499
+ :type name: str
500
+ :param log_level: the logging level (e.g. `logging.Debug`)
501
+ :type log_level: int
502
+ :param log_path: the path for the log file
503
+ :type log_path: str
504
+ :param enable_logging: a flag indicating whether logging should be redirected from null to
505
+ the stream handler
506
+ :type enable_logging: bool
507
+
508
+ :return: a logger
509
+ :rtype: :class:`logging.Logger`
510
+ """
511
+ # get logger for module
512
+ logger = logging.getLogger(name)
513
+
514
+ # set propagate to False if not already set as such (needed to that we do not flow up to any
515
+ # root loggers)
516
+ if logger.propagate:
517
+ logger.propagate = False
518
+
519
+ # if logger has no handlers, it needs to be configured for the first time
520
+ if not logger.hasHandlers():
521
+ # set level
522
+ logger.setLevel(log_level)
523
+
524
+ # if logging enabled, use a StreamHandler; else, use the NullHandler to suppress logging
525
+ handler: logging.Handler
526
+ if enable_logging:
527
+ handler = logging.FileHandler(log_path)
528
+ else:
529
+ handler = logging.NullHandler()
530
+
531
+ # Format our output
532
+ formatter = logging.Formatter(
533
+ "%(asctime)s - %(levelname)s:%(name)s - %(message)s"
534
+ )
535
+ handler.setFormatter(formatter)
536
+
537
+ # Set handler level and add to logger
538
+ handler.setLevel(log_level)
539
+ logger.addHandler(handler)
540
+
541
+ return logger
@@ -1,29 +1,42 @@
1
- import os
2
1
  import sys
3
2
  from dataclasses import dataclass, field
4
3
  from pathlib import Path
5
4
  from uuid import UUID
6
5
 
7
- from pydantic import ValidationError
6
+ from pydantic import TypeAdapter, ValidationError
8
7
 
9
8
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
10
9
  from contentctl.enrichments.cve_enrichment import CveEnrichment
11
10
  from contentctl.helper.utils import Utils
12
11
  from contentctl.input.yml_reader import YmlReader
12
+ from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
13
+ DeprecationDocumentationFile,
14
+ )
13
15
  from contentctl.objects.atomic import AtomicEnrichment
14
16
  from contentctl.objects.baseline import Baseline
15
- from contentctl.objects.config import validate
17
+ from contentctl.objects.config import CustomApp, validate
16
18
  from contentctl.objects.dashboard import Dashboard
17
19
  from contentctl.objects.data_source import DataSource
18
20
  from contentctl.objects.deployment import Deployment
19
21
  from contentctl.objects.detection import Detection
20
- from contentctl.objects.enums import SecurityContentType
21
22
  from contentctl.objects.investigation import Investigation
22
- from contentctl.objects.lookup import Lookup, LookupAdapter
23
+ from contentctl.objects.lookup import (
24
+ CSVLookup,
25
+ KVStoreLookup,
26
+ Lookup,
27
+ Lookup_Type,
28
+ LookupAdapter,
29
+ MlModel,
30
+ RuntimeCSV,
31
+ )
23
32
  from contentctl.objects.macro import Macro
24
33
  from contentctl.objects.playbook import Playbook
34
+ from contentctl.objects.removed_security_content_object import (
35
+ RemovedSecurityContentObject,
36
+ )
25
37
  from contentctl.objects.security_content_object import SecurityContentObject
26
38
  from contentctl.objects.story import Story
39
+ from contentctl.output.runtime_csv_writer import RuntimeCsvWriter
27
40
 
28
41
 
29
42
  @dataclass
@@ -33,17 +46,20 @@ class DirectorOutputDto:
33
46
  atomic_enrichment: AtomicEnrichment
34
47
  attack_enrichment: AttackEnrichment
35
48
  cve_enrichment: CveEnrichment
36
- detections: list[Detection]
37
- stories: list[Story]
38
- baselines: list[Baseline]
39
- investigations: list[Investigation]
40
- playbooks: list[Playbook]
41
- macros: list[Macro]
42
- lookups: list[Lookup]
43
- deployments: list[Deployment]
44
- dashboards: list[Dashboard]
45
-
46
- data_sources: list[DataSource]
49
+ detections: list[Detection] = field(default_factory=list)
50
+ stories: list[Story] = field(default_factory=list)
51
+ baselines: list[Baseline] = field(default_factory=list)
52
+ investigations: list[Investigation] = field(default_factory=list)
53
+ playbooks: list[Playbook] = field(default_factory=list)
54
+ macros: list[Macro] = field(default_factory=list)
55
+ lookups: list[Lookup] = field(default_factory=list)
56
+ deployments: list[Deployment] = field(default_factory=list)
57
+ dashboards: list[Dashboard] = field(default_factory=list)
58
+ deprecated: list[RemovedSecurityContentObject] = field(default_factory=list)
59
+ data_sources: list[DataSource] = field(default_factory=list)
60
+ deprecation_documentation: DeprecationDocumentationFile = field(
61
+ default_factory=DeprecationDocumentationFile
62
+ )
47
63
  name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
48
64
  uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
49
65
 
@@ -82,9 +98,10 @@ class DirectorOutputDto:
82
98
  self.detections.append(content)
83
99
  elif isinstance(content, Dashboard):
84
100
  self.dashboards.append(content)
85
-
86
101
  elif isinstance(content, DataSource):
87
102
  self.data_sources.append(content)
103
+ elif isinstance(content, RemovedSecurityContentObject):
104
+ self.deprecated.append(content)
88
105
  else:
89
106
  raise Exception(f"Unknown security content type: {type(content)}")
90
107
 
@@ -101,123 +118,134 @@ class Director:
101
118
 
102
119
  def execute(self, input_dto: validate) -> None:
103
120
  self.input_dto = input_dto
104
- self.createSecurityContent(SecurityContentType.deployments)
105
- self.createSecurityContent(SecurityContentType.lookups)
106
- self.createSecurityContent(SecurityContentType.macros)
107
- self.createSecurityContent(SecurityContentType.stories)
108
- self.createSecurityContent(SecurityContentType.baselines)
109
- self.createSecurityContent(SecurityContentType.investigations)
110
- self.createSecurityContent(SecurityContentType.data_sources)
111
- self.createSecurityContent(SecurityContentType.playbooks)
112
- self.createSecurityContent(SecurityContentType.detections)
113
- self.createSecurityContent(SecurityContentType.dashboards)
114
-
115
- def createSecurityContent(self, contentType: SecurityContentType) -> None:
116
- if contentType in [
117
- SecurityContentType.deployments,
118
- SecurityContentType.lookups,
119
- SecurityContentType.macros,
120
- SecurityContentType.stories,
121
- SecurityContentType.baselines,
122
- SecurityContentType.investigations,
123
- SecurityContentType.playbooks,
124
- SecurityContentType.detections,
125
- SecurityContentType.data_sources,
126
- SecurityContentType.dashboards,
121
+
122
+ for content in [
123
+ Deployment,
124
+ LookupAdapter,
125
+ Macro,
126
+ Story,
127
+ Baseline,
128
+ DataSource,
129
+ Playbook,
130
+ Detection,
131
+ Dashboard,
132
+ RemovedSecurityContentObject,
127
133
  ]:
128
- files = Utils.get_all_yml_files_from_directory(
129
- os.path.join(self.input_dto.path, str(contentType.name))
130
- )
131
- security_content_files = [f for f in files]
132
- else:
133
- raise (
134
- Exception(
135
- f"Cannot createSecurityContent for unknown product {contentType}."
134
+ self.createSecurityContent(content)
135
+
136
+ self.loadDeprecationInfo(input_dto.app)
137
+ self.buildRuntimeCsvs()
138
+
139
+ def buildRuntimeCsvs(self):
140
+ self.buildDataSourceCsv()
141
+ self.buildDeprecationRemovalCsv()
142
+
143
+ def buildDeprecationRemovalCsv(self):
144
+ if self.input_dto.enforce_deprecation_mapping_requirement is False:
145
+ # Do not build the CSV, it would be wasteful to include it if it
146
+ # is not even used
147
+ return
148
+ deprecation_lookup = RuntimeCSV(
149
+ name="deprecation_info",
150
+ id=UUID("99262bf2-9606-4b52-b377-c96713527b35"),
151
+ version=1,
152
+ author=self.input_dto.app.author_name,
153
+ description="A lookup file that contains information about content that has been deprecated or removed from the app.",
154
+ lookup_type=Lookup_Type.csv,
155
+ contents=RuntimeCsvWriter.generateDeprecationCSVContent(
156
+ self.output_dto, self.input_dto.app
157
+ ),
158
+ )
159
+ self.output_dto.addContentToDictMappings(deprecation_lookup)
160
+
161
+ def buildDataSourceCsv(self):
162
+ datasource_lookup = RuntimeCSV(
163
+ name="data_sources",
164
+ id=UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
165
+ version=1,
166
+ author=self.input_dto.app.author_name,
167
+ description="A lookup file that contains the data source objects for detections.",
168
+ lookup_type=Lookup_Type.csv,
169
+ contents=RuntimeCsvWriter.generateDatasourceCSVContent(
170
+ self.output_dto.data_sources
171
+ ),
172
+ )
173
+ self.output_dto.addContentToDictMappings(datasource_lookup)
174
+
175
+ def loadDeprecationInfo(self, app: CustomApp):
176
+ mapping_file_paths = list(
177
+ (self.input_dto.path / "removed").glob("deprecation_mapping*.YML")
178
+ )
179
+
180
+ if self.input_dto.enforce_deprecation_mapping_requirement is False:
181
+ # If we are not required to enforce deprecation mapping, then do nothing at all (even if the files exist)
182
+ if len(mapping_file_paths) > 0:
183
+ file_paths = "\n - " + "\n - ".join(
184
+ str(name) for name in mapping_file_paths
136
185
  )
137
- )
186
+ print(
187
+ "The following deprecation_mapping*.YML files were found, but will not be parsed because "
188
+ f"[enforce_deprecation_mapping_requirement = {self.input_dto.enforce_deprecation_mapping_requirement}]:",
189
+ file_paths,
190
+ )
191
+ # Otherwise, no need to output extra information
192
+ return
193
+
194
+ # If there are no mapping files, that's okay. We will other throw exceptions later on if
195
+ # there are 1 or more detections marked as deprecated or removed.
196
+ for mapping_file_path in mapping_file_paths:
197
+ print(f"Parsing mapping file {mapping_file_path.name}")
198
+ data = YmlReader.load_file(mapping_file_path)
199
+ mapping = DeprecationDocumentationFile.model_validate(data)
200
+ self.output_dto.deprecation_documentation += mapping
201
+
202
+ self.output_dto.deprecation_documentation.mapAllContent(self.output_dto, app)
203
+
204
+ def createSecurityContent(
205
+ self,
206
+ contentType: type[SecurityContentObject]
207
+ | TypeAdapter[CSVLookup | KVStoreLookup | MlModel],
208
+ ) -> None:
209
+ files = Utils.get_all_yml_files_from_directory(
210
+ self.input_dto.path / contentType.containing_folder() # type: ignore
211
+ )
212
+
213
+ # convert this generator to a list so that we can
214
+ # calculate progress as we iterate over the files
215
+ security_content_files = [f for f in files]
138
216
 
139
217
  validation_errors: list[tuple[Path, ValueError]] = []
140
218
 
141
219
  already_ran = False
142
220
  progress_percent = 0
221
+ context: dict[str, validate | DirectorOutputDto] = {
222
+ "output_dto": self.output_dto,
223
+ "config": self.input_dto,
224
+ }
225
+ contentCartegoryName: str = contentType.__name__.upper() # type: ignore
143
226
 
144
227
  for index, file in enumerate(security_content_files):
145
228
  progress_percent = ((index + 1) / len(security_content_files)) * 100
146
229
  try:
147
- type_string = contentType.name.upper()
230
+ type_string = contentType.__name__.upper() # type: ignore
148
231
  modelDict = YmlReader.load_file(file)
149
232
 
150
- if contentType == SecurityContentType.lookups:
151
- lookup = LookupAdapter.validate_python(
152
- modelDict,
153
- context={
154
- "output_dto": self.output_dto,
155
- "config": self.input_dto,
156
- },
157
- )
158
- # lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
159
- self.output_dto.addContentToDictMappings(lookup)
160
-
161
- elif contentType == SecurityContentType.macros:
162
- macro = Macro.model_validate(
163
- modelDict, context={"output_dto": self.output_dto}
233
+ if isinstance(contentType, type(SecurityContentObject)):
234
+ content: SecurityContentObject = contentType.model_validate(
235
+ modelDict, context=context
164
236
  )
165
- self.output_dto.addContentToDictMappings(macro)
166
-
167
- elif contentType == SecurityContentType.deployments:
168
- deployment = Deployment.model_validate(
169
- modelDict, context={"output_dto": self.output_dto}
237
+ elif contentType == LookupAdapter:
238
+ content: SecurityContentObject = ( # type: ignore
239
+ contentType.validate_python(modelDict, context=context) # type:ignore
170
240
  )
171
- self.output_dto.addContentToDictMappings(deployment)
172
-
173
- elif contentType == SecurityContentType.playbooks:
174
- playbook = Playbook.model_validate(
175
- modelDict, context={"output_dto": self.output_dto}
176
- )
177
- self.output_dto.addContentToDictMappings(playbook)
178
-
179
- elif contentType == SecurityContentType.baselines:
180
- baseline = Baseline.model_validate(
181
- modelDict, context={"output_dto": self.output_dto}
182
- )
183
- self.output_dto.addContentToDictMappings(baseline)
184
-
185
- elif contentType == SecurityContentType.investigations:
186
- investigation = Investigation.model_validate(
187
- modelDict, context={"output_dto": self.output_dto}
188
- )
189
- self.output_dto.addContentToDictMappings(investigation)
190
-
191
- elif contentType == SecurityContentType.stories:
192
- story = Story.model_validate(
193
- modelDict, context={"output_dto": self.output_dto}
194
- )
195
- self.output_dto.addContentToDictMappings(story)
196
-
197
- elif contentType == SecurityContentType.detections:
198
- detection = Detection.model_validate(
199
- modelDict,
200
- context={
201
- "output_dto": self.output_dto,
202
- "app": self.input_dto.app,
203
- },
204
- )
205
- self.output_dto.addContentToDictMappings(detection)
206
-
207
- elif contentType == SecurityContentType.dashboards:
208
- dashboard = Dashboard.model_validate(
209
- modelDict, context={"output_dto": self.output_dto}
210
- )
211
- self.output_dto.addContentToDictMappings(dashboard)
212
-
213
- elif contentType == SecurityContentType.data_sources:
214
- data_source = DataSource.model_validate(
215
- modelDict, context={"output_dto": self.output_dto}
216
- )
217
- self.output_dto.addContentToDictMappings(data_source)
218
-
241
+ if not isinstance(content, SecurityContentObject):
242
+ raise Exception(
243
+ f"Expected lookup to be a SecurityContentObject (CSVLookup, KVStoreLookup, or MLModel), but it was actually: {type(content)}" # type: ignore
244
+ )
219
245
  else:
220
- raise Exception(f"Unsupported type: [{contentType}]")
246
+ raise Exception(f"Unknown contentType in Director: {contentType}")
247
+
248
+ self.output_dto.addContentToDictMappings(content)
221
249
 
222
250
  if (
223
251
  sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()
@@ -236,7 +264,7 @@ class Director:
236
264
  validation_errors.append((relative_path, e))
237
265
 
238
266
  print(
239
- f"\r{f'{contentType.name.upper()} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...",
267
+ f"\r{f'{contentCartegoryName} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...",
240
268
  end="",
241
269
  flush=True,
242
270
  )
@@ -1,7 +1,8 @@
1
- from typing import Dict, Any
2
- import yaml
3
- import sys
4
1
  import pathlib
2
+ import sys
3
+ from typing import Any, Dict
4
+
5
+ import yaml
5
6
 
6
7
 
7
8
  class YmlReader:
@@ -13,40 +14,51 @@ class YmlReader:
13
14
  ) -> Dict[str, Any]:
14
15
  try:
15
16
  file_handler = open(file_path, "r", encoding="utf-8")
17
+ except OSError as exc:
18
+ print(
19
+ f"\nThere was an unrecoverable error when opening the file '{file_path}' - we will exit immediately:\n{str(exc)}"
20
+ )
21
+ sys.exit(1)
16
22
 
17
23
  # The following code can help diagnose issues with duplicate keys or
18
24
  # poorly-formatted but still "compliant" YML. This code should be
19
25
  # enabled manually for debugging purposes. As such, strictyaml
20
26
  # library is intentionally excluded from the contentctl requirements
21
27
 
28
+ try:
22
29
  if STRICT_YML_CHECKING:
30
+ # This is an extra level of verbose parsing that can be
31
+ # enabled for debugging purpose. It is intentionally done in
32
+ # addition to the regular yml parsing
23
33
  import strictyaml
24
34
 
25
- try:
26
- strictyaml.dirty_load(file_handler.read(), allow_flow_style=True)
27
- file_handler.seek(0)
28
- except Exception as e:
29
- print(f"Error loading YML file {file_path}: {str(e)}")
30
- sys.exit(1)
31
- try:
32
- # Ideally we should use
33
- # from contentctl.actions.new_content import NewContent
34
- # and use NewContent.UPDATE_PREFIX,
35
- # but there is a circular dependency right now which makes that difficult.
36
- # We have instead hardcoded UPDATE_PREFIX
37
- UPDATE_PREFIX = "__UPDATE__"
38
- data = file_handler.read()
39
- if UPDATE_PREFIX in data:
40
- raise Exception(
41
- f"The file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required."
42
- )
43
- yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
44
- except yaml.YAMLError as exc:
45
- print(exc)
46
- sys.exit(1)
35
+ strictyaml.dirty_load(file_handler.read(), allow_flow_style=True)
36
+ file_handler.seek(0)
47
37
 
48
- except OSError as exc:
49
- print(exc)
38
+ # Ideally we should use
39
+ # from contentctl.actions.new_content import NewContent
40
+ # and use NewContent.UPDATE_PREFIX,
41
+ # but there is a circular dependency right now which makes that difficult.
42
+ # We have instead hardcoded UPDATE_PREFIX
43
+ UPDATE_PREFIX = "__UPDATE__"
44
+ data = file_handler.read()
45
+ if UPDATE_PREFIX in data:
46
+ raise Exception(
47
+ f"\nThe file {file_path} contains the value '{UPDATE_PREFIX}'. Please fill out any unpopulated fields as required."
48
+ )
49
+ yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
50
+ if yml_obj is None:
51
+ raise yaml.YAMLError(
52
+ f"The YML file's value was parsed as [{None}]. "
53
+ "This probably means that the file was entirely "
54
+ "empty or contains only comments, which is not "
55
+ "supported. Please ensure this file is NOT empty "
56
+ "or remove the file."
57
+ )
58
+ except yaml.YAMLError as exc:
59
+ print(
60
+ f"\nThere was an unrecoverable YML Parsing error when reading or parsing the file '{file_path}' - we will exit immediately:\n{str(exc)}"
61
+ )
50
62
  sys.exit(1)
51
63
 
52
64
  if add_fields is False: