contentctl 4.4.7__py3-none-any.whl → 5.0.0a0__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 (69) hide show
  1. contentctl/actions/build.py +39 -27
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +0 -1
  3. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +32 -26
  4. contentctl/actions/detection_testing/progress_bar.py +6 -6
  5. contentctl/actions/detection_testing/views/DetectionTestingView.py +4 -4
  6. contentctl/actions/new_content.py +98 -81
  7. contentctl/actions/test.py +4 -5
  8. contentctl/actions/validate.py +2 -1
  9. contentctl/contentctl.py +114 -79
  10. contentctl/helper/utils.py +0 -14
  11. contentctl/input/director.py +5 -5
  12. contentctl/input/new_content_questions.py +2 -2
  13. contentctl/input/yml_reader.py +11 -6
  14. contentctl/objects/abstract_security_content_objects/detection_abstract.py +228 -120
  15. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +5 -7
  16. contentctl/objects/alert_action.py +2 -1
  17. contentctl/objects/atomic.py +1 -0
  18. contentctl/objects/base_test.py +4 -3
  19. contentctl/objects/base_test_result.py +3 -3
  20. contentctl/objects/baseline.py +26 -6
  21. contentctl/objects/baseline_tags.py +2 -3
  22. contentctl/objects/config.py +26 -45
  23. contentctl/objects/constants.py +4 -1
  24. contentctl/objects/correlation_search.py +89 -95
  25. contentctl/objects/data_source.py +5 -6
  26. contentctl/objects/deployment.py +2 -10
  27. contentctl/objects/deployment_email.py +2 -1
  28. contentctl/objects/deployment_notable.py +2 -1
  29. contentctl/objects/deployment_phantom.py +2 -1
  30. contentctl/objects/deployment_rba.py +2 -1
  31. contentctl/objects/deployment_scheduling.py +2 -1
  32. contentctl/objects/deployment_slack.py +2 -1
  33. contentctl/objects/detection_tags.py +7 -42
  34. contentctl/objects/drilldown.py +1 -0
  35. contentctl/objects/enums.py +21 -58
  36. contentctl/objects/investigation.py +6 -5
  37. contentctl/objects/investigation_tags.py +2 -3
  38. contentctl/objects/lookup.py +145 -63
  39. contentctl/objects/macro.py +2 -3
  40. contentctl/objects/mitre_attack_enrichment.py +2 -2
  41. contentctl/objects/observable.py +3 -1
  42. contentctl/objects/playbook_tags.py +5 -1
  43. contentctl/objects/rba.py +90 -0
  44. contentctl/objects/risk_event.py +87 -144
  45. contentctl/objects/story_tags.py +1 -2
  46. contentctl/objects/test_attack_data.py +2 -1
  47. contentctl/objects/unit_test_baseline.py +2 -1
  48. contentctl/output/api_json_output.py +233 -220
  49. contentctl/output/conf_output.py +51 -44
  50. contentctl/output/conf_writer.py +201 -125
  51. contentctl/output/data_source_writer.py +0 -1
  52. contentctl/output/json_writer.py +2 -4
  53. contentctl/output/svg_output.py +1 -1
  54. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  55. contentctl/output/templates/collections.j2 +1 -1
  56. contentctl/output/templates/doc_detections.j2 +0 -5
  57. contentctl/output/templates/savedsearches_detections.j2 +8 -3
  58. contentctl/output/templates/transforms.j2 +4 -4
  59. contentctl/output/yml_writer.py +15 -0
  60. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  61. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/METADATA +5 -4
  62. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/RECORD +65 -68
  63. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/WHEEL +1 -1
  64. contentctl/objects/event_source.py +0 -11
  65. contentctl/output/detection_writer.py +0 -28
  66. contentctl/output/new_content_yml_output.py +0 -56
  67. contentctl/output/yml_output.py +0 -66
  68. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/LICENSE.md +0 -0
  69. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import List
3
3
 
4
- from contentctl.objects.config import test_common
4
+ from contentctl.objects.config import test_common, Selected, Changes
5
5
  from contentctl.objects.enums import DetectionTestingMode, DetectionStatus, AnalyticsType
6
6
  from contentctl.objects.detection import Detection
7
7
 
@@ -78,10 +78,9 @@ class Test:
78
78
  input_dto=manager_input_dto, output_dto=output_dto
79
79
  )
80
80
 
81
- mode = input_dto.config.getModeName()
82
81
  if len(input_dto.detections) == 0:
83
82
  print(
84
- f"With Detection Testing Mode '{mode}', there were [0] detections found to test."
83
+ f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were [0] detections found to test."
85
84
  "\nAs such, we will quit immediately."
86
85
  )
87
86
  # Directly call stop so that the summary.yml will be generated. Of course it will not
@@ -89,8 +88,8 @@ class Test:
89
88
  # detections were tested.
90
89
  file.stop()
91
90
  else:
92
- print(f"MODE: [{mode}] - Test [{len(input_dto.detections)}] detections")
93
- if mode in [DetectionTestingMode.changes.value, DetectionTestingMode.selected.value]:
91
+ print(f"MODE: [{input_dto.config.mode.mode_name}] - Test [{len(input_dto.detections)}] detections")
92
+ if isinstance(input_dto.config.mode, Selected) or isinstance(input_dto.config.mode, Changes):
94
93
  files_string = '\n- '.join(
95
94
  [str(pathlib.Path(detection.file_path).relative_to(input_dto.config.path)) for detection in input_dto.detections]
96
95
  )
@@ -6,6 +6,7 @@ from contentctl.objects.config import validate
6
6
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
7
7
  from contentctl.enrichments.cve_enrichment import CveEnrichment
8
8
  from contentctl.objects.atomic import AtomicEnrichment
9
+ from contentctl.objects.lookup import FileBackedLookup
9
10
  from contentctl.helper.utils import Utils
10
11
  from contentctl.objects.data_source import DataSource
11
12
  from contentctl.helper.splunk_app import SplunkApp
@@ -64,7 +65,7 @@ class Validate:
64
65
  lookupsDirectory = repo_path/"lookups"
65
66
 
66
67
  # Get all of the files referneced by Lookups
67
- usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if lookup.filename is not None] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
68
+ usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if isinstance(lookup, FileBackedLookup)] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
68
69
 
69
70
  # Get all of the mlmodel and csv files in the lookups directory
70
71
  csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(lookupsDirectory, allowedFileExtensions=[".yml",".csv",".mlmodel"], fileExtensionsToReturn=[".csv",".mlmodel"])
contentctl/contentctl.py CHANGED
@@ -1,31 +1,39 @@
1
- import traceback
1
+ import pathlib
2
2
  import sys
3
+ import traceback
3
4
  import warnings
4
- import pathlib
5
+
5
6
  import tyro
6
7
 
7
- from contentctl.actions.initialize import Initialize
8
- from contentctl.objects.config import init, validate, build, new, deploy_acs, test, test_servers, inspect, report, test_common, release_notes
9
- from contentctl.actions.validate import Validate
10
- from contentctl.actions.new_content import NewContent
8
+ from contentctl.actions.build import Build, BuildInputDto, DirectorOutputDto
9
+ from contentctl.actions.deploy_acs import Deploy
11
10
  from contentctl.actions.detection_testing.GitService import GitService
12
- from contentctl.actions.build import (
13
- BuildInputDto,
14
- DirectorOutputDto,
15
- Build,
16
- )
17
- from contentctl.actions.test import Test
18
- from contentctl.actions.test import TestInputDto
19
- from contentctl.actions.reporting import ReportingInputDto, Reporting
11
+ from contentctl.actions.initialize import Initialize
20
12
  from contentctl.actions.inspect import Inspect
21
- from contentctl.input.yml_reader import YmlReader
22
- from contentctl.actions.deploy_acs import Deploy
13
+ from contentctl.actions.new_content import NewContent
23
14
  from contentctl.actions.release_notes import ReleaseNotes
15
+ from contentctl.actions.reporting import Reporting, ReportingInputDto
16
+ from contentctl.actions.test import Test, TestInputDto
17
+ from contentctl.actions.validate import Validate
18
+ from contentctl.input.yml_reader import YmlReader
19
+ from contentctl.objects.config import (
20
+ build,
21
+ deploy_acs,
22
+ init,
23
+ inspect,
24
+ new,
25
+ release_notes,
26
+ report,
27
+ test,
28
+ test_common,
29
+ test_servers,
30
+ validate,
31
+ )
24
32
 
25
33
  # def print_ascii_art():
26
34
  # print(
27
35
  # """
28
- # Running Splunk Security Content Control Tool (contentctl)
36
+ # Running Splunk Security Content Control Tool (contentctl)
29
37
  # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
30
38
  # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⠛⡇⠀⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
31
39
  # ⠀⠀⠀⠀⠀⠀⠀⠀⣀⠼⠖⠛⠋⠉⠉⠓⠢⣴⡻⣾⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
@@ -53,114 +61,137 @@ from contentctl.actions.release_notes import ReleaseNotes
53
61
  # )
54
62
 
55
63
 
56
-
57
-
58
- def init_func(config:test):
64
+ def init_func(config: test):
59
65
  Initialize().execute(config)
60
66
 
61
67
 
62
- def validate_func(config:validate)->DirectorOutputDto:
68
+ def validate_func(config: validate) -> DirectorOutputDto:
63
69
  validate = Validate()
64
70
  return validate.execute(config)
65
71
 
66
- def report_func(config:report)->None:
72
+
73
+ def report_func(config: report) -> None:
67
74
  # First, perform validation. Remember that the validate
68
75
  # configuration is actually a subset of the build configuration
69
76
  director_output_dto = validate_func(config)
70
-
71
- r = Reporting()
72
- return r.execute(ReportingInputDto(director_output_dto=director_output_dto,
73
- config=config))
74
-
75
77
 
76
- def build_func(config:build)->DirectorOutputDto:
78
+ r = Reporting()
79
+ return r.execute(
80
+ ReportingInputDto(director_output_dto=director_output_dto, config=config)
81
+ )
82
+
83
+
84
+ def build_func(config: build) -> DirectorOutputDto:
77
85
  # First, perform validation. Remember that the validate
78
86
  # configuration is actually a subset of the build configuration
79
87
  director_output_dto = validate_func(config)
80
88
  builder = Build()
81
89
  return builder.execute(BuildInputDto(director_output_dto, config))
82
90
 
83
- def inspect_func(config:inspect)->str:
84
- #Make sure that we have built the most recent version of the app
91
+
92
+ def inspect_func(config: inspect) -> str:
93
+ # Make sure that we have built the most recent version of the app
85
94
  _ = build_func(config)
86
95
  inspect_token = Inspect().execute(config)
87
96
  return inspect_token
88
-
89
97
 
90
- def release_notes_func(config:release_notes)->None:
98
+
99
+ def release_notes_func(config: release_notes) -> None:
91
100
  ReleaseNotes().release_notes(config)
92
101
 
93
- def new_func(config:new):
94
- NewContent().execute(config)
95
102
 
103
+ def new_func(config: new):
104
+ NewContent().execute(config)
96
105
 
97
106
 
98
- def deploy_acs_func(config:deploy_acs):
107
+ def deploy_acs_func(config: deploy_acs):
99
108
  print("Building and inspecting app...")
100
109
  token = inspect_func(config)
101
110
  print("App successfully built and inspected.")
102
111
  print("Deploying app...")
103
112
  Deploy().execute(config, token)
104
113
 
105
- def test_common_func(config:test_common):
114
+
115
+ def test_common_func(config: test_common):
106
116
  if type(config) == test:
107
- #construct the container Infrastructure objects
117
+ # construct the container Infrastructure objects
108
118
  config.getContainerInfrastructureObjects()
109
- #otherwise, they have already been passed as servers
119
+ # otherwise, they have already been passed as servers
110
120
 
111
121
  director_output_dto = build_func(config)
112
- gitServer = GitService(director=director_output_dto,config=config)
122
+ gitServer = GitService(director=director_output_dto, config=config)
113
123
  detections_to_test = gitServer.getContent()
114
124
 
115
-
116
-
117
125
  test_input_dto = TestInputDto(detections_to_test, config)
118
-
126
+
119
127
  t = Test()
120
128
  t.filter_tests(test_input_dto)
121
-
129
+
122
130
  if config.plan_only:
123
- #Emit the test plan and quit. Do not actually run the test
124
- config.dumpCICDPlanAndQuit(gitServer.getHash(),test_input_dto.detections)
125
- return
126
-
131
+ # Emit the test plan and quit. Do not actually run the test
132
+ config.dumpCICDPlanAndQuit(gitServer.getHash(), test_input_dto.detections)
133
+ return
134
+
127
135
  success = t.execute(test_input_dto)
128
-
136
+
129
137
  if success:
130
- #Everything passed!
138
+ # Everything passed!
131
139
  print("All tests have run successfully or been marked as 'skipped'")
132
140
  return
133
141
  raise Exception("There was at least one unsuccessful test")
134
142
 
143
+
144
+ CONTENTCTL_5_WARNING = """
145
+ *****************************************************************************
146
+ WARNING - THIS IS AN ALPHA BUILD OF CONTENTCTL 5.
147
+ THERE HAVE BEEN NUMEROUS CHANGES IN CONTENTCTL (ESPECIALLY TO YML FORMATS).
148
+ YOU ALMOST CERTAINLY DO NOT WANT TO USE THIS BUILD.
149
+ IF YOU ENCOUNTER ERRORS, PLEASE USE THE LATEST CURRENTYLY SUPPORTED RELEASE:
150
+
151
+ CONTENTCTL==4.4.7
152
+
153
+ YOU HAVE BEEN WARNED!
154
+ *****************************************************************************
155
+ """
156
+
157
+
135
158
  def main():
159
+ print(CONTENTCTL_5_WARNING)
136
160
  try:
137
161
  configFile = pathlib.Path("contentctl.yml")
138
-
162
+
139
163
  # We MUST load a config (with testing info) object so that we can
140
164
  # properly construct the command line, including 'contentctl test' parameters.
141
165
  if not configFile.is_file():
142
- if "init" not in sys.argv and "--help" not in sys.argv and "-h" not in sys.argv:
143
- raise Exception(f"'{configFile}' not found in the current directory.\n"
144
- "Please ensure you are in the correct directory or run 'contentctl init' to create a new content pack.")
145
-
166
+ if (
167
+ "init" not in sys.argv
168
+ and "--help" not in sys.argv
169
+ and "-h" not in sys.argv
170
+ ):
171
+ raise Exception(
172
+ f"'{configFile}' not found in the current directory.\n"
173
+ "Please ensure you are in the correct directory or run 'contentctl init' to create a new content pack."
174
+ )
175
+
146
176
  if "--help" in sys.argv or "-h" in sys.argv:
147
- print("Warning - contentctl.yml is missing from this directory. The configuration values showed at the default and are informational only.\n"
148
- "Please ensure that contentctl.yml exists by manually creating it or running 'contentctl init'")
177
+ print(
178
+ "Warning - contentctl.yml is missing from this directory. The configuration values showed at the default and are informational only.\n"
179
+ "Please ensure that contentctl.yml exists by manually creating it or running 'contentctl init'"
180
+ )
149
181
  # Otherwise generate a stub config file.
150
182
  # It will be used during init workflow
151
183
 
152
184
  t = test()
153
185
  config_obj = t.model_dump()
154
-
186
+
155
187
  else:
156
- #The file exists, so load it up!
157
- config_obj = YmlReader().load_file(configFile)
188
+ # The file exists, so load it up!
189
+ config_obj = YmlReader().load_file(configFile, add_fields=False)
158
190
  t = test.model_validate(config_obj)
159
191
  except Exception as e:
160
192
  print(f"Error validating 'contentctl.yml':\n{str(e)}")
161
193
  sys.exit(1)
162
-
163
-
194
+
164
195
  # For ease of generating the constructor, we want to allow construction
165
196
  # of an object from default values WITHOUT requiring all fields to be declared
166
197
  # with defaults OR in the config file. As such, we construct the model rather
@@ -169,22 +200,19 @@ def main():
169
200
 
170
201
  models = tyro.extras.subcommand_type_from_defaults(
171
202
  {
172
- "init":init.model_validate(config_obj),
203
+ "init": init.model_validate(config_obj),
173
204
  "validate": validate.model_validate(config_obj),
174
205
  "report": report.model_validate(config_obj),
175
- "build":build.model_validate(config_obj),
206
+ "build": build.model_validate(config_obj),
176
207
  "inspect": inspect.model_construct(**t.__dict__),
177
- "new":new.model_validate(config_obj),
178
- "test":test.model_validate(config_obj),
179
- "test_servers":test_servers.model_construct(**t.__dict__),
208
+ "new": new.model_validate(config_obj),
209
+ "test": test.model_validate(config_obj),
210
+ "test_servers": test_servers.model_construct(**t.__dict__),
180
211
  "release_notes": release_notes.model_construct(**config_obj),
181
- "deploy_acs": deploy_acs.model_construct(**t.__dict__)
212
+ "deploy_acs": deploy_acs.model_construct(**t.__dict__),
182
213
  }
183
214
  )
184
-
185
-
186
215
 
187
-
188
216
  config = None
189
217
  try:
190
218
  # Since some model(s) were constructed and not model_validated, we have to catch
@@ -192,7 +220,6 @@ def main():
192
220
  with warnings.catch_warnings(action="ignore"):
193
221
  config = tyro.cli(models)
194
222
 
195
-
196
223
  if type(config) == init:
197
224
  t.__dict__.update(config.__dict__)
198
225
  init_func(t)
@@ -219,21 +246,29 @@ def main():
219
246
  print(e)
220
247
  sys.exit(1)
221
248
  except Exception as e:
249
+ print(CONTENTCTL_5_WARNING)
250
+
222
251
  if config is None:
223
- print("There was a serious issue where the config file could not be created.\n"
224
- "The entire stack trace is provided below (please include it if filing a bug report).\n")
252
+ print(
253
+ "There was a serious issue where the config file could not be created.\n"
254
+ "The entire stack trace is provided below (please include it if filing a bug report).\n"
255
+ )
225
256
  traceback.print_exc()
226
257
  elif config.verbose:
227
- print("Verbose error logging is ENABLED.\n"
228
- "The entire stack trace has been provided below (please include it if filing a bug report):\n")
258
+ print(
259
+ "Verbose error logging is ENABLED.\n"
260
+ "The entire stack trace has been provided below (please include it if filing a bug report):\n"
261
+ )
229
262
  traceback.print_exc()
230
263
  else:
231
- print("Verbose error logging is DISABLED.\n"
232
- "Please use the --verbose command line argument if you need more context for your error or file a bug report.")
264
+ print(
265
+ "Verbose error logging is DISABLED.\n"
266
+ "Please use the --verbose command line argument if you need more context for your error or file a bug report."
267
+ )
233
268
  print(e)
234
-
269
+
235
270
  sys.exit(1)
236
271
 
237
272
 
238
273
  if __name__ == "__main__":
239
- main()
274
+ main()
@@ -247,20 +247,6 @@ class Utils:
247
247
 
248
248
  return hash
249
249
 
250
- # @staticmethod
251
- # def check_required_fields(
252
- # thisField: str, definedFields: dict, requiredFields: list[str]
253
- # ):
254
- # missing_fields = [
255
- # field for field in requiredFields if field not in definedFields
256
- # ]
257
- # if len(missing_fields) > 0:
258
- # raise (
259
- # ValueError(
260
- # f"Could not validate - please resolve other errors resulting in missing fields {missing_fields}"
261
- # )
262
- # )
263
-
264
250
  @staticmethod
265
251
  def verify_file_exists(
266
252
  file_path: str, verbose_print=False, timeout_seconds: int = 10
@@ -14,7 +14,7 @@ from contentctl.objects.investigation import Investigation
14
14
  from contentctl.objects.playbook import Playbook
15
15
  from contentctl.objects.deployment import Deployment
16
16
  from contentctl.objects.macro import Macro
17
- from contentctl.objects.lookup import Lookup
17
+ from contentctl.objects.lookup import LookupAdapter, Lookup
18
18
  from contentctl.objects.atomic import AtomicEnrichment
19
19
  from contentctl.objects.security_content_object import SecurityContentObject
20
20
  from contentctl.objects.data_source import DataSource
@@ -58,13 +58,12 @@ class DirectorOutputDto:
58
58
  f" - {content.file_path}\n"
59
59
  f" - {self.name_to_content_map[content_name].file_path}"
60
60
  )
61
-
61
+
62
62
  if content.id in self.uuid_to_content_map:
63
63
  raise ValueError(
64
64
  f"Duplicate id '{content.id}' with paths:\n"
65
65
  f" - {content.file_path}\n"
66
- f" - {self.uuid_to_content_map[content.id].file_path}"
67
- )
66
+ f" - {self.uuid_to_content_map[content.id].file_path}")
68
67
 
69
68
  if isinstance(content, Lookup):
70
69
  self.lookups.append(content)
@@ -157,7 +156,8 @@ class Director():
157
156
  modelDict = YmlReader.load_file(file)
158
157
 
159
158
  if contentType == SecurityContentType.lookups:
160
- lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
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})
161
161
  self.output_dto.addContentToDictMappings(lookup)
162
162
 
163
163
  elif contentType == SecurityContentType.macros:
@@ -48,7 +48,7 @@ class NewContentQuestions:
48
48
  {
49
49
  'type': 'checkbox',
50
50
  'message': 'Your data source',
51
- 'name': 'data_source',
51
+ 'name': 'data_sources',
52
52
  #In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
53
53
  'choices': sorted(DataSource._value2member_map_ )
54
54
 
@@ -57,7 +57,7 @@ class NewContentQuestions:
57
57
  "type": "text",
58
58
  "message": "enter search (spl)",
59
59
  "name": "detection_search",
60
- "default": "| UPDATE_SPL",
60
+ "default": "| __UPDATE__ SPL",
61
61
  },
62
62
  {
63
63
  "type": "text",
@@ -1,15 +1,12 @@
1
1
  from typing import Dict, Any
2
-
3
2
  import yaml
4
-
5
-
6
3
  import sys
7
4
  import pathlib
8
5
 
9
6
  class YmlReader():
10
7
 
11
8
  @staticmethod
12
- def load_file(file_path: pathlib.Path, add_fields=True, STRICT_YML_CHECKING=False) -> Dict[str,Any]:
9
+ def load_file(file_path: pathlib.Path, add_fields:bool=True, STRICT_YML_CHECKING:bool=False) -> Dict[str,Any]:
13
10
  try:
14
11
  file_handler = open(file_path, 'r', encoding="utf-8")
15
12
 
@@ -27,8 +24,16 @@ class YmlReader():
27
24
  print(f"Error loading YML file {file_path}: {str(e)}")
28
25
  sys.exit(1)
29
26
  try:
30
- #yml_obj = list(yaml.safe_load_all(file_handler))[0]
31
- yml_obj = yaml.load(file_handler, Loader=yaml.CSafeLoader)
27
+ #Ideally we should use
28
+ # from contentctl.actions.new_content import NewContent
29
+ # and use NewContent.UPDATE_PREFIX,
30
+ # but there is a circular dependency right now which makes that difficult.
31
+ # We have instead hardcoded UPDATE_PREFIX
32
+ UPDATE_PREFIX = "__UPDATE__"
33
+ data = file_handler.read()
34
+ 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.")
36
+ yml_obj = yaml.load(data, Loader=yaml.CSafeLoader)
32
37
  except yaml.YAMLError as exc:
33
38
  print(exc)
34
39
  sys.exit(1)