contentctl 4.4.7__py3-none-any.whl → 5.0.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 (123) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +102 -57
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
  5. contentctl/actions/detection_testing/GitService.py +134 -76
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +9 -6
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
  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 +155 -108
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +33 -28
  21. contentctl/actions/validate.py +55 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +124 -90
  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 -53
  30. contentctl/input/director.py +68 -36
  31. contentctl/input/new_content_questions.py +27 -35
  32. contentctl/input/yml_reader.py +28 -18
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
  35. contentctl/objects/alert_action.py +10 -9
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +65 -54
  38. contentctl/objects/base_test.py +5 -3
  39. contentctl/objects/base_test_result.py +19 -11
  40. contentctl/objects/baseline.py +62 -30
  41. contentctl/objects/baseline_tags.py +30 -24
  42. contentctl/objects/config.py +790 -597
  43. contentctl/objects/constants.py +33 -56
  44. contentctl/objects/correlation_search.py +150 -136
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +16 -17
  47. contentctl/objects/deployment.py +43 -44
  48. contentctl/objects/deployment_email.py +3 -2
  49. contentctl/objects/deployment_notable.py +4 -2
  50. contentctl/objects/deployment_phantom.py +7 -6
  51. contentctl/objects/deployment_rba.py +3 -2
  52. contentctl/objects/deployment_scheduling.py +3 -2
  53. contentctl/objects/deployment_slack.py +3 -2
  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 +58 -103
  58. contentctl/objects/drilldown.py +66 -34
  59. contentctl/objects/enums.py +81 -100
  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 +59 -36
  64. contentctl/objects/investigation_tags.py +30 -19
  65. contentctl/objects/lookup.py +304 -101
  66. contentctl/objects/macro.py +55 -39
  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 +23 -13
  74. contentctl/objects/rba.py +96 -0
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +110 -160
  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 +54 -49
  81. contentctl/objects/story_tags.py +56 -45
  82. contentctl/objects/test_attack_data.py +2 -1
  83. contentctl/objects/test_group.py +5 -2
  84. contentctl/objects/threat_object.py +1 -0
  85. contentctl/objects/throttling.py +27 -18
  86. contentctl/objects/unit_test.py +3 -4
  87. contentctl/objects/unit_test_baseline.py +5 -5
  88. contentctl/objects/unit_test_result.py +6 -6
  89. contentctl/output/api_json_output.py +233 -220
  90. contentctl/output/attack_nav_output.py +21 -21
  91. contentctl/output/attack_nav_writer.py +29 -37
  92. contentctl/output/conf_output.py +235 -172
  93. contentctl/output/conf_writer.py +201 -125
  94. contentctl/output/data_source_writer.py +38 -26
  95. contentctl/output/doc_md_output.py +53 -27
  96. contentctl/output/jinja_writer.py +19 -15
  97. contentctl/output/json_writer.py +21 -11
  98. contentctl/output/svg_output.py +56 -38
  99. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  100. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  101. contentctl/output/templates/collections.j2 +1 -1
  102. contentctl/output/templates/doc_detections.j2 +0 -5
  103. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  104. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  105. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  106. contentctl/output/templates/savedsearches_detections.j2 +10 -11
  107. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  108. contentctl/output/templates/transforms.j2 +6 -8
  109. contentctl/output/yml_writer.py +29 -20
  110. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  111. contentctl/templates/stories/cobalt_strike.yml +1 -0
  112. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
  113. contentctl-5.0.0.dist-info/RECORD +168 -0
  114. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
  115. contentctl/actions/initialize_old.py +0 -245
  116. contentctl/objects/event_source.py +0 -11
  117. contentctl/objects/observable.py +0 -37
  118. contentctl/output/detection_writer.py +0 -28
  119. contentctl/output/new_content_yml_output.py +0 -56
  120. contentctl/output/yml_output.py +0 -66
  121. contentctl-4.4.7.dist-info/RECORD +0 -173
  122. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
  123. {contentctl-4.4.7.dist-info → contentctl-5.0.0.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):
106
- if type(config) == test:
107
- #construct the container Infrastructure objects
114
+
115
+ def test_common_func(config: test_common):
116
+ if type(config) is test:
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,26 +220,25 @@ def main():
192
220
  with warnings.catch_warnings(action="ignore"):
193
221
  config = tyro.cli(models)
194
222
 
195
-
196
- if type(config) == init:
223
+ if type(config) is init:
197
224
  t.__dict__.update(config.__dict__)
198
225
  init_func(t)
199
- elif type(config) == validate:
226
+ elif type(config) is validate:
200
227
  validate_func(config)
201
- elif type(config) == report:
228
+ elif type(config) is report:
202
229
  report_func(config)
203
- elif type(config) == build:
230
+ elif type(config) is build:
204
231
  build_func(config)
205
- elif type(config) == new:
232
+ elif type(config) is new:
206
233
  new_func(config)
207
- elif type(config) == inspect:
234
+ elif type(config) is inspect:
208
235
  inspect_func(config)
209
- elif type(config) == release_notes:
236
+ elif type(config) is release_notes:
210
237
  release_notes_func(config)
211
- elif type(config) == deploy_acs:
238
+ elif type(config) is deploy_acs:
212
239
  updated_config = deploy_acs.model_validate(config)
213
240
  deploy_acs_func(updated_config)
214
- elif type(config) == test or type(config) == test_servers:
241
+ elif type(config) is test or type(config) is test_servers:
215
242
  test_common_func(config)
216
243
  else:
217
244
  raise Exception(f"Unknown command line type '{type(config).__name__}'")
@@ -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()
@@ -1,121 +1,161 @@
1
-
2
1
  from __future__ import annotations
3
- import sys
4
2
  from attackcti import attack_client
5
3
  import logging
6
4
  from pydantic import BaseModel
7
5
  from dataclasses import field
8
6
  from typing import Any
9
7
  from pathlib import Path
10
- from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment, MitreTactics
8
+ from contentctl.objects.mitre_attack_enrichment import (
9
+ MitreAttackEnrichment,
10
+ MitreTactics,
11
+ )
11
12
  from contentctl.objects.config import validate
12
13
  from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
13
- logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
14
+
15
+ logging.getLogger("taxii2client").setLevel(logging.CRITICAL)
14
16
 
15
17
 
16
18
  class AttackEnrichment(BaseModel):
17
19
  data: dict[str, MitreAttackEnrichment] = field(default_factory=dict)
18
- use_enrichment:bool = True
19
-
20
+ use_enrichment: bool = True
21
+
20
22
  @staticmethod
21
- def getAttackEnrichment(config:validate)->AttackEnrichment:
23
+ def getAttackEnrichment(config: validate) -> AttackEnrichment:
22
24
  enrichment = AttackEnrichment(use_enrichment=config.enrichments)
23
25
  _ = enrichment.get_attack_lookup(config.mitre_cti_repo_path, config.enrichments)
24
26
  return enrichment
25
-
26
- def getEnrichmentByMitreID(self, mitre_id:MITRE_ATTACK_ID_TYPE)->MitreAttackEnrichment:
27
+
28
+ def getEnrichmentByMitreID(
29
+ self, mitre_id: MITRE_ATTACK_ID_TYPE
30
+ ) -> MitreAttackEnrichment:
27
31
  if not self.use_enrichment:
28
- raise Exception("Error, trying to add Mitre Enrichment, but use_enrichment was set to False")
29
-
32
+ raise Exception(
33
+ "Error, trying to add Mitre Enrichment, but use_enrichment was set to False"
34
+ )
35
+
30
36
  enrichment = self.data.get(mitre_id, None)
31
37
  if enrichment is not None:
32
38
  return enrichment
33
39
  else:
34
- raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
35
-
36
- def addMitreIDViaGroupNames(self, technique:dict[str,Any], tactics:list[str], groupNames:list[str])->None:
37
- technique_id = technique['technique_id']
38
- technique_obj = technique['technique']
40
+ raise Exception(
41
+ f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}"
42
+ )
43
+
44
+ def addMitreIDViaGroupNames(
45
+ self, technique: dict[str, Any], tactics: list[str], groupNames: list[str]
46
+ ) -> None:
47
+ technique_id = technique["technique_id"]
48
+ technique_obj = technique["technique"]
39
49
  tactics.sort()
40
-
50
+
41
51
  if technique_id in self.data:
42
52
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
43
- self.data[technique_id] = MitreAttackEnrichment.model_validate({'mitre_attack_id':technique_id,
44
- 'mitre_attack_technique':technique_obj,
45
- 'mitre_attack_tactics':tactics,
46
- 'mitre_attack_groups':groupNames,
47
- 'mitre_attack_group_objects':[]})
48
-
49
- def addMitreIDViaGroupObjects(self, technique:dict[str,Any], tactics:list[MitreTactics], groupDicts:list[dict[str,Any]])->None:
50
- technique_id = technique['technique_id']
51
- technique_obj = technique['technique']
53
+ self.data[technique_id] = MitreAttackEnrichment.model_validate(
54
+ {
55
+ "mitre_attack_id": technique_id,
56
+ "mitre_attack_technique": technique_obj,
57
+ "mitre_attack_tactics": tactics,
58
+ "mitre_attack_groups": groupNames,
59
+ "mitre_attack_group_objects": [],
60
+ }
61
+ )
62
+
63
+ def addMitreIDViaGroupObjects(
64
+ self,
65
+ technique: dict[str, Any],
66
+ tactics: list[MitreTactics],
67
+ groupDicts: list[dict[str, Any]],
68
+ ) -> None:
69
+ technique_id = technique["technique_id"]
70
+ technique_obj = technique["technique"]
52
71
  tactics.sort()
53
-
54
- groupNames:list[str] = sorted([group['group'] for group in groupDicts])
55
-
72
+
73
+ groupNames: list[str] = sorted([group["group"] for group in groupDicts])
74
+
56
75
  if technique_id in self.data:
57
76
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
58
-
59
- self.data[technique_id] = MitreAttackEnrichment.model_validate({'mitre_attack_id': technique_id,
60
- 'mitre_attack_technique': technique_obj,
61
- 'mitre_attack_tactics': tactics,
62
- 'mitre_attack_groups': groupNames,
63
- 'mitre_attack_group_objects': groupDicts})
64
-
65
-
66
- def get_attack_lookup(self, input_path: Path, enrichments:bool = False) -> dict[str,MitreAttackEnrichment]:
67
- attack_lookup:dict[str,MitreAttackEnrichment] = {}
77
+
78
+ self.data[technique_id] = MitreAttackEnrichment.model_validate(
79
+ {
80
+ "mitre_attack_id": technique_id,
81
+ "mitre_attack_technique": technique_obj,
82
+ "mitre_attack_tactics": tactics,
83
+ "mitre_attack_groups": groupNames,
84
+ "mitre_attack_group_objects": groupDicts,
85
+ }
86
+ )
87
+
88
+ def get_attack_lookup(
89
+ self, input_path: Path, enrichments: bool = False
90
+ ) -> dict[str, MitreAttackEnrichment]:
91
+ attack_lookup: dict[str, MitreAttackEnrichment] = {}
68
92
  if not enrichments:
69
93
  return attack_lookup
70
-
94
+
71
95
  try:
72
- print(f"Performing MITRE Enrichment using the repository at {input_path}...",end="", flush=True)
73
- # The existence of the input_path is validated during cli argument validation, but it is
96
+ print(
97
+ f"Performing MITRE Enrichment using the repository at {input_path}...",
98
+ end="",
99
+ flush=True,
100
+ )
101
+ # The existence of the input_path is validated during cli argument validation, but it is
74
102
  # possible that the repo is in the wrong format. If the following directories do not
75
- # exist, then attack_client will fall back to resolving via REST API. We do not
76
- # want this as it is slow and error prone, so we will force an exception to
103
+ # exist, then attack_client will fall back to resolving via REST API. We do not
104
+ # want this as it is slow and error prone, so we will force an exception to
77
105
  # be generated.
78
- enterprise_path = input_path/"enterprise-attack"
79
- mobile_path = input_path/"ics-attack"
80
- ics_path = input_path/"mobile-attack"
81
- if not (enterprise_path.is_dir() and mobile_path.is_dir() and ics_path.is_dir()):
82
- raise FileNotFoundError("One or more of the following paths does not exist: "
83
- f"{[str(enterprise_path),str(mobile_path),str(ics_path)]}. "
84
- f"Please ensure that the {input_path} directory "
85
- "has been git cloned correctly.")
106
+ enterprise_path = input_path / "enterprise-attack"
107
+ mobile_path = input_path / "ics-attack"
108
+ ics_path = input_path / "mobile-attack"
109
+ if not (
110
+ enterprise_path.is_dir() and mobile_path.is_dir() and ics_path.is_dir()
111
+ ):
112
+ raise FileNotFoundError(
113
+ "One or more of the following paths does not exist: "
114
+ f"{[str(enterprise_path), str(mobile_path), str(ics_path)]}. "
115
+ f"Please ensure that the {input_path} directory "
116
+ "has been git cloned correctly."
117
+ )
86
118
  lift = attack_client(
87
- local_paths= {
88
- "enterprise":str(enterprise_path),
89
- "mobile":str(mobile_path),
90
- "ics":str(ics_path)
119
+ local_paths={
120
+ "enterprise": str(enterprise_path),
121
+ "mobile": str(mobile_path),
122
+ "ics": str(ics_path),
91
123
  }
92
124
  )
93
-
94
- all_enterprise_techniques = lift.get_enterprise_techniques(stix_format=False)
95
- enterprise_relationships = lift.get_enterprise_relationships(stix_format=False)
125
+
126
+ all_enterprise_techniques = lift.get_enterprise_techniques(
127
+ stix_format=False
128
+ )
129
+ enterprise_relationships = lift.get_enterprise_relationships(
130
+ stix_format=False
131
+ )
96
132
  enterprise_groups = lift.get_enterprise_groups(stix_format=False)
97
-
133
+
98
134
  for technique in all_enterprise_techniques:
99
- apt_groups:list[dict[str,Any]] = []
135
+ apt_groups: list[dict[str, Any]] = []
100
136
  for relationship in enterprise_relationships:
101
- if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
137
+ if (
138
+ relationship["target_object"] == technique["id"]
139
+ ) and relationship["source_object"].startswith("intrusion-set"):
102
140
  for group in enterprise_groups:
103
- if relationship['source_object'] == group['id']:
141
+ if relationship["source_object"] == group["id"]:
104
142
  apt_groups.append(group)
105
- #apt_groups.append(group['group'])
143
+ # apt_groups.append(group['group'])
106
144
 
107
145
  tactics = []
108
- if ('tactic' in technique):
109
- for tactic in technique['tactic']:
110
- tactics.append(tactic.replace('-',' ').title())
146
+ if "tactic" in technique:
147
+ for tactic in technique["tactic"]:
148
+ tactics.append(tactic.replace("-", " ").title())
111
149
 
112
150
  self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
113
- attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
114
-
151
+ attack_lookup[technique["technique_id"]] = {
152
+ "technique": technique["technique"],
153
+ "tactics": tactics,
154
+ "groups": apt_groups,
155
+ }
115
156
 
116
-
117
157
  except Exception as err:
118
158
  raise Exception(f"Error getting MITRE Enrichment: {str(err)}")
119
-
159
+
120
160
  print("Done!")
121
- return attack_lookup
161
+ return attack_lookup