contentctl 4.4.7__py3-none-any.whl → 5.0.0a2__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 (70) hide show
  1. contentctl/actions/build.py +39 -27
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +0 -1
  3. contentctl/actions/detection_testing/GitService.py +132 -72
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +32 -26
  5. contentctl/actions/detection_testing/progress_bar.py +6 -6
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +4 -4
  7. contentctl/actions/new_content.py +98 -81
  8. contentctl/actions/test.py +4 -5
  9. contentctl/actions/validate.py +2 -1
  10. contentctl/contentctl.py +114 -80
  11. contentctl/helper/utils.py +0 -14
  12. contentctl/input/director.py +5 -5
  13. contentctl/input/new_content_questions.py +2 -2
  14. contentctl/input/yml_reader.py +11 -6
  15. contentctl/objects/abstract_security_content_objects/detection_abstract.py +228 -120
  16. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +5 -7
  17. contentctl/objects/alert_action.py +2 -1
  18. contentctl/objects/atomic.py +1 -0
  19. contentctl/objects/base_test.py +4 -3
  20. contentctl/objects/base_test_result.py +3 -3
  21. contentctl/objects/baseline.py +26 -6
  22. contentctl/objects/baseline_tags.py +2 -3
  23. contentctl/objects/config.py +789 -596
  24. contentctl/objects/constants.py +4 -1
  25. contentctl/objects/correlation_search.py +89 -95
  26. contentctl/objects/data_source.py +5 -6
  27. contentctl/objects/deployment.py +2 -10
  28. contentctl/objects/deployment_email.py +2 -1
  29. contentctl/objects/deployment_notable.py +2 -1
  30. contentctl/objects/deployment_phantom.py +2 -1
  31. contentctl/objects/deployment_rba.py +2 -1
  32. contentctl/objects/deployment_scheduling.py +2 -1
  33. contentctl/objects/deployment_slack.py +2 -1
  34. contentctl/objects/detection_tags.py +7 -42
  35. contentctl/objects/drilldown.py +1 -0
  36. contentctl/objects/enums.py +21 -58
  37. contentctl/objects/investigation.py +6 -5
  38. contentctl/objects/investigation_tags.py +2 -3
  39. contentctl/objects/lookup.py +145 -63
  40. contentctl/objects/macro.py +2 -3
  41. contentctl/objects/mitre_attack_enrichment.py +2 -2
  42. contentctl/objects/observable.py +3 -1
  43. contentctl/objects/playbook_tags.py +5 -1
  44. contentctl/objects/rba.py +90 -0
  45. contentctl/objects/risk_event.py +87 -144
  46. contentctl/objects/story_tags.py +1 -2
  47. contentctl/objects/test_attack_data.py +2 -1
  48. contentctl/objects/unit_test_baseline.py +2 -1
  49. contentctl/output/api_json_output.py +233 -220
  50. contentctl/output/conf_output.py +51 -44
  51. contentctl/output/conf_writer.py +201 -125
  52. contentctl/output/data_source_writer.py +0 -1
  53. contentctl/output/json_writer.py +2 -4
  54. contentctl/output/svg_output.py +1 -1
  55. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  56. contentctl/output/templates/collections.j2 +1 -1
  57. contentctl/output/templates/doc_detections.j2 +0 -5
  58. contentctl/output/templates/savedsearches_detections.j2 +8 -3
  59. contentctl/output/templates/transforms.j2 +4 -4
  60. contentctl/output/yml_writer.py +15 -0
  61. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  62. {contentctl-4.4.7.dist-info → contentctl-5.0.0a2.dist-info}/METADATA +5 -4
  63. {contentctl-4.4.7.dist-info → contentctl-5.0.0a2.dist-info}/RECORD +66 -69
  64. {contentctl-4.4.7.dist-info → contentctl-5.0.0a2.dist-info}/WHEEL +1 -1
  65. contentctl/objects/event_source.py +0 -11
  66. contentctl/output/detection_writer.py +0 -28
  67. contentctl/output/new_content_yml_output.py +0 -56
  68. contentctl/output/yml_output.py +0 -66
  69. {contentctl-4.4.7.dist-info → contentctl-5.0.0a2.dist-info}/LICENSE.md +0 -0
  70. {contentctl-4.4.7.dist-info → contentctl-5.0.0a2.dist-info}/entry_points.txt +0 -0
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)
@@ -220,20 +247,27 @@ def main():
220
247
  sys.exit(1)
221
248
  except Exception as e:
222
249
  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")
250
+ print(
251
+ "There was a serious issue where the config file could not be created.\n"
252
+ "The entire stack trace is provided below (please include it if filing a bug report).\n"
253
+ )
225
254
  traceback.print_exc()
226
255
  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")
256
+ print(
257
+ "Verbose error logging is ENABLED.\n"
258
+ "The entire stack trace has been provided below (please include it if filing a bug report):\n"
259
+ )
229
260
  traceback.print_exc()
230
261
  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.")
233
- print(e)
234
-
262
+ print(
263
+ "Verbose error logging is DISABLED.\n"
264
+ "Please use the --verbose command line argument if you need more context for your error or file a bug report."
265
+ )
266
+
267
+ print(e)
268
+ print(CONTENTCTL_5_WARNING)
235
269
  sys.exit(1)
236
270
 
237
271
 
238
272
  if __name__ == "__main__":
239
- main()
273
+ 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)