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.
- contentctl/__init__.py +1 -1
- contentctl/actions/build.py +102 -57
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
- contentctl/actions/detection_testing/GitService.py +134 -76
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +9 -6
- contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +155 -108
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +33 -28
- contentctl/actions/validate.py +55 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +124 -90
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -53
- contentctl/input/director.py +68 -36
- contentctl/input/new_content_questions.py +27 -35
- contentctl/input/yml_reader.py +28 -18
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
- contentctl/objects/alert_action.py +10 -9
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +65 -54
- contentctl/objects/base_test.py +5 -3
- contentctl/objects/base_test_result.py +19 -11
- contentctl/objects/baseline.py +62 -30
- contentctl/objects/baseline_tags.py +30 -24
- contentctl/objects/config.py +790 -597
- contentctl/objects/constants.py +33 -56
- contentctl/objects/correlation_search.py +150 -136
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +16 -17
- contentctl/objects/deployment.py +43 -44
- contentctl/objects/deployment_email.py +3 -2
- contentctl/objects/deployment_notable.py +4 -2
- contentctl/objects/deployment_phantom.py +7 -6
- contentctl/objects/deployment_rba.py +3 -2
- contentctl/objects/deployment_scheduling.py +3 -2
- contentctl/objects/deployment_slack.py +3 -2
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +58 -103
- contentctl/objects/drilldown.py +66 -34
- contentctl/objects/enums.py +81 -100
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +59 -36
- contentctl/objects/investigation_tags.py +30 -19
- contentctl/objects/lookup.py +304 -101
- contentctl/objects/macro.py +55 -39
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +23 -13
- contentctl/objects/rba.py +96 -0
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +110 -160
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -45
- contentctl/objects/test_attack_data.py +2 -1
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +5 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +233 -220
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +235 -172
- contentctl/output/conf_writer.py +201 -125
- contentctl/output/data_source_writer.py +38 -26
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +21 -11
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/collections.j2 +1 -1
- contentctl/output/templates/doc_detections.j2 +0 -5
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +10 -11
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +6 -8
- contentctl/output/yml_writer.py +29 -20
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
- contentctl-5.0.0.dist-info/RECORD +168 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/event_source.py +0 -11
- contentctl/objects/observable.py +0 -37
- contentctl/output/detection_writer.py +0 -28
- contentctl/output/new_content_yml_output.py +0 -56
- contentctl/output/yml_output.py +0 -66
- contentctl-4.4.7.dist-info/RECORD +0 -173
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
- {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
|
|
1
|
+
import pathlib
|
|
2
2
|
import sys
|
|
3
|
+
import traceback
|
|
3
4
|
import warnings
|
|
4
|
-
|
|
5
|
+
|
|
5
6
|
import tyro
|
|
6
7
|
|
|
7
|
-
from contentctl.actions.
|
|
8
|
-
from contentctl.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
148
|
-
|
|
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)
|
|
226
|
+
elif type(config) is validate:
|
|
200
227
|
validate_func(config)
|
|
201
|
-
elif type(config)
|
|
228
|
+
elif type(config) is report:
|
|
202
229
|
report_func(config)
|
|
203
|
-
elif type(config)
|
|
230
|
+
elif type(config) is build:
|
|
204
231
|
build_func(config)
|
|
205
|
-
elif type(config)
|
|
232
|
+
elif type(config) is new:
|
|
206
233
|
new_func(config)
|
|
207
|
-
elif type(config)
|
|
234
|
+
elif type(config) is inspect:
|
|
208
235
|
inspect_func(config)
|
|
209
|
-
elif type(config)
|
|
236
|
+
elif type(config) is release_notes:
|
|
210
237
|
release_notes_func(config)
|
|
211
|
-
elif type(config)
|
|
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)
|
|
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(
|
|
224
|
-
|
|
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(
|
|
228
|
-
|
|
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(
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
27
|
+
|
|
28
|
+
def getEnrichmentByMitreID(
|
|
29
|
+
self, mitre_id: MITRE_ATTACK_ID_TYPE
|
|
30
|
+
) -> MitreAttackEnrichment:
|
|
27
31
|
if not self.use_enrichment:
|
|
28
|
-
raise Exception(
|
|
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(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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[
|
|
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(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
73
|
-
|
|
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 (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
95
|
-
|
|
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 (
|
|
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[
|
|
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
|
|
109
|
-
for tactic in technique[
|
|
110
|
-
tactics.append(tactic.replace(
|
|
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[
|
|
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
|