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
|
@@ -1,246 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from contentctl.objects.detection import Detection
|
|
6
|
+
from contentctl.objects.lookup import Lookup
|
|
7
|
+
from contentctl.objects.macro import Macro
|
|
8
|
+
from contentctl.objects.story import Story
|
|
9
|
+
from contentctl.objects.baseline import Baseline
|
|
10
|
+
from contentctl.objects.investigation import Investigation
|
|
11
|
+
from contentctl.objects.deployment import Deployment
|
|
12
|
+
|
|
1
13
|
import os
|
|
2
|
-
import json
|
|
3
14
|
import pathlib
|
|
4
15
|
|
|
5
16
|
from contentctl.output.json_writer import JsonWriter
|
|
6
|
-
from contentctl.objects.enums import SecurityContentType
|
|
7
|
-
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
|
|
8
|
-
SecurityContentObject_Abstract,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
class ApiJsonOutput:
|
|
20
|
+
output_path: pathlib.Path
|
|
21
|
+
app_label: str
|
|
22
|
+
|
|
23
|
+
def __init__(self, output_path: pathlib.Path, app_label: str):
|
|
24
|
+
self.output_path = output_path
|
|
25
|
+
self.app_label = app_label
|
|
14
26
|
|
|
15
|
-
def
|
|
27
|
+
def writeDetections(
|
|
16
28
|
self,
|
|
17
|
-
objects: list[
|
|
18
|
-
output_path: pathlib.Path,
|
|
19
|
-
app_label:str = "ESCU",
|
|
20
|
-
contentType: SecurityContentType = None
|
|
29
|
+
objects: list[Detection],
|
|
21
30
|
) -> None:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"search",
|
|
44
|
-
"how_to_implement",
|
|
45
|
-
"known_false_positives",
|
|
46
|
-
"references",
|
|
47
|
-
"datamodel",
|
|
48
|
-
"macros",
|
|
49
|
-
"lookups",
|
|
50
|
-
"source",
|
|
51
|
-
"nes_fields",
|
|
52
|
-
]
|
|
53
|
-
)
|
|
31
|
+
detections = [
|
|
32
|
+
detection.model_dump(
|
|
33
|
+
include=set(
|
|
34
|
+
[
|
|
35
|
+
"name",
|
|
36
|
+
"author",
|
|
37
|
+
"date",
|
|
38
|
+
"version",
|
|
39
|
+
"id",
|
|
40
|
+
"description",
|
|
41
|
+
"tags",
|
|
42
|
+
"search",
|
|
43
|
+
"how_to_implement",
|
|
44
|
+
"known_false_positives",
|
|
45
|
+
"references",
|
|
46
|
+
"datamodel",
|
|
47
|
+
"macros",
|
|
48
|
+
"lookups",
|
|
49
|
+
"source",
|
|
50
|
+
"nes_fields",
|
|
51
|
+
]
|
|
54
52
|
)
|
|
55
|
-
for detection in objects
|
|
56
|
-
]
|
|
57
|
-
#Only a subset of macro fields are required:
|
|
58
|
-
# for detection in detections:
|
|
59
|
-
# new_macros = []
|
|
60
|
-
# for macro in detection.get("macros",[]):
|
|
61
|
-
# new_macro_fields = {}
|
|
62
|
-
# new_macro_fields["name"] = macro.get("name")
|
|
63
|
-
# new_macro_fields["definition"] = macro.get("definition")
|
|
64
|
-
# new_macro_fields["description"] = macro.get("description")
|
|
65
|
-
# if len(macro.get("arguments", [])) > 0:
|
|
66
|
-
# new_macro_fields["arguments"] = macro.get("arguments")
|
|
67
|
-
# new_macros.append(new_macro_fields)
|
|
68
|
-
# detection["macros"] = new_macros
|
|
69
|
-
# del()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
JsonWriter.writeJsonObject(
|
|
73
|
-
os.path.join(output_path, "detections.json"), "detections", detections
|
|
74
53
|
)
|
|
54
|
+
for detection in objects
|
|
55
|
+
]
|
|
56
|
+
# Only a subset of macro fields are required:
|
|
57
|
+
# for detection in detections:
|
|
58
|
+
# new_macros = []
|
|
59
|
+
# for macro in detection.get("macros",[]):
|
|
60
|
+
# new_macro_fields = {}
|
|
61
|
+
# new_macro_fields["name"] = macro.get("name")
|
|
62
|
+
# new_macro_fields["definition"] = macro.get("definition")
|
|
63
|
+
# new_macro_fields["description"] = macro.get("description")
|
|
64
|
+
# if len(macro.get("arguments", [])) > 0:
|
|
65
|
+
# new_macro_fields["arguments"] = macro.get("arguments")
|
|
66
|
+
# new_macros.append(new_macro_fields)
|
|
67
|
+
# detection["macros"] = new_macros
|
|
68
|
+
# del()
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for macro in objects
|
|
80
|
-
]
|
|
81
|
-
for macro in macros:
|
|
82
|
-
for k in ["author", "date","version","id","references"]:
|
|
83
|
-
if k in macro:
|
|
84
|
-
del(macro[k])
|
|
85
|
-
JsonWriter.writeJsonObject(
|
|
86
|
-
os.path.join(output_path, "macros.json"), "macros", macros
|
|
87
|
-
)
|
|
70
|
+
JsonWriter.writeJsonObject(
|
|
71
|
+
os.path.join(self.output_path, "detections.json"), "detections", detections
|
|
72
|
+
)
|
|
88
73
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
74
|
+
def writeMacros(
|
|
75
|
+
self,
|
|
76
|
+
objects: list[Macro],
|
|
77
|
+
) -> None:
|
|
78
|
+
macros = [
|
|
79
|
+
macro.model_dump(include=set(["definition", "description", "name"]))
|
|
80
|
+
for macro in objects
|
|
81
|
+
]
|
|
82
|
+
for macro in macros:
|
|
83
|
+
for k in ["author", "date", "version", "id", "references"]:
|
|
84
|
+
if k in macro:
|
|
85
|
+
del macro[k]
|
|
86
|
+
JsonWriter.writeJsonObject(
|
|
87
|
+
os.path.join(self.output_path, "macros.json"), "macros", macros
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def writeStories(
|
|
91
|
+
self,
|
|
92
|
+
objects: list[Story],
|
|
93
|
+
) -> None:
|
|
94
|
+
stories = [
|
|
95
|
+
story.model_dump(
|
|
96
|
+
include=set(
|
|
97
|
+
[
|
|
98
|
+
"name",
|
|
99
|
+
"author",
|
|
100
|
+
"date",
|
|
101
|
+
"version",
|
|
102
|
+
"id",
|
|
103
|
+
"description",
|
|
104
|
+
"narrative",
|
|
105
|
+
"references",
|
|
106
|
+
"tags",
|
|
107
|
+
"detections_names",
|
|
108
|
+
"investigation_names",
|
|
109
|
+
"baseline_names",
|
|
110
|
+
"detections",
|
|
111
|
+
]
|
|
109
112
|
)
|
|
110
|
-
for story in objects
|
|
111
|
-
]
|
|
112
|
-
# Only get certain fields from detections
|
|
113
|
-
for story in stories:
|
|
114
|
-
# Only use a small subset of fields from the detection
|
|
115
|
-
story["detections"] = [
|
|
116
|
-
{
|
|
117
|
-
"name": detection["name"],
|
|
118
|
-
"source": detection["source"],
|
|
119
|
-
"type": detection["type"],
|
|
120
|
-
"tags": detection["tags"].get("mitre_attack_enrichments", []),
|
|
121
|
-
}
|
|
122
|
-
for detection in story["detections"]
|
|
123
|
-
]
|
|
124
|
-
story["detection_names"] = [f"{app_label} - {name} - Rule" for name in story["detection_names"]]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
JsonWriter.writeJsonObject(
|
|
128
|
-
os.path.join(output_path, "stories.json"), "stories", stories
|
|
129
113
|
)
|
|
114
|
+
for story in objects
|
|
115
|
+
]
|
|
116
|
+
# Only get certain fields from detections
|
|
117
|
+
for story in stories:
|
|
118
|
+
# Only use a small subset of fields from the detection
|
|
119
|
+
story["detections"] = [
|
|
120
|
+
{
|
|
121
|
+
"name": detection["name"],
|
|
122
|
+
"source": detection["source"],
|
|
123
|
+
"type": detection["type"],
|
|
124
|
+
"tags": detection["tags"].get("mitre_attack_enrichments", []),
|
|
125
|
+
}
|
|
126
|
+
for detection in story["detections"]
|
|
127
|
+
]
|
|
128
|
+
story["detection_names"] = [
|
|
129
|
+
f"{self.app_label} - {name} - Rule" for name in story["detection_names"]
|
|
130
|
+
]
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
print('wait')
|
|
158
|
-
|
|
159
|
-
JsonWriter.writeJsonObject(
|
|
160
|
-
os.path.join(output_path, "baselines.json"), "baselines", baselines
|
|
132
|
+
JsonWriter.writeJsonObject(
|
|
133
|
+
os.path.join(self.output_path, "stories.json"), "stories", stories
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def writeBaselines(
|
|
137
|
+
self,
|
|
138
|
+
objects: list[Baseline],
|
|
139
|
+
) -> None:
|
|
140
|
+
baselines = [
|
|
141
|
+
baseline.model_dump(
|
|
142
|
+
include=set(
|
|
143
|
+
[
|
|
144
|
+
"name",
|
|
145
|
+
"author",
|
|
146
|
+
"date",
|
|
147
|
+
"version",
|
|
148
|
+
"id",
|
|
149
|
+
"description",
|
|
150
|
+
"type",
|
|
151
|
+
"datamodel",
|
|
152
|
+
"search",
|
|
153
|
+
"how_to_implement",
|
|
154
|
+
"known_false_positives",
|
|
155
|
+
"references",
|
|
156
|
+
"tags",
|
|
157
|
+
]
|
|
161
158
|
)
|
|
159
|
+
)
|
|
160
|
+
for baseline in objects
|
|
161
|
+
]
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
163
|
+
JsonWriter.writeJsonObject(
|
|
164
|
+
os.path.join(self.output_path, "baselines.json"), "baselines", baselines
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def writeInvestigations(
|
|
168
|
+
self,
|
|
169
|
+
objects: list[Investigation],
|
|
170
|
+
) -> None:
|
|
171
|
+
investigations = [
|
|
172
|
+
investigation.model_dump(
|
|
173
|
+
include=set(
|
|
174
|
+
[
|
|
175
|
+
"name",
|
|
176
|
+
"author",
|
|
177
|
+
"date",
|
|
178
|
+
"version",
|
|
179
|
+
"id",
|
|
180
|
+
"description",
|
|
181
|
+
"type",
|
|
182
|
+
"datamodel",
|
|
183
|
+
"search",
|
|
184
|
+
"how_to_implemnet",
|
|
185
|
+
"known_false_positives",
|
|
186
|
+
"references",
|
|
187
|
+
"inputs",
|
|
188
|
+
"tags",
|
|
189
|
+
"lowercase_name",
|
|
190
|
+
]
|
|
185
191
|
)
|
|
186
|
-
for investigation in objects
|
|
187
|
-
]
|
|
188
|
-
JsonWriter.writeJsonObject(
|
|
189
|
-
os.path.join(output_path, "response_tasks.json"),
|
|
190
|
-
"response_tasks",
|
|
191
|
-
investigations,
|
|
192
192
|
)
|
|
193
|
+
for investigation in objects
|
|
194
|
+
]
|
|
195
|
+
JsonWriter.writeJsonObject(
|
|
196
|
+
os.path.join(self.output_path, "response_tasks.json"),
|
|
197
|
+
"response_tasks",
|
|
198
|
+
investigations,
|
|
199
|
+
)
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
201
|
+
def writeLookups(
|
|
202
|
+
self,
|
|
203
|
+
objects: list[Lookup],
|
|
204
|
+
) -> None:
|
|
205
|
+
lookups = [
|
|
206
|
+
lookup.model_dump(
|
|
207
|
+
include=set(
|
|
208
|
+
[
|
|
209
|
+
"name",
|
|
210
|
+
"description",
|
|
211
|
+
"collection",
|
|
212
|
+
"fields_list",
|
|
213
|
+
"filename",
|
|
214
|
+
"default_match",
|
|
215
|
+
"match_type",
|
|
216
|
+
"min_matches",
|
|
217
|
+
"case_sensitive_match",
|
|
218
|
+
]
|
|
210
219
|
)
|
|
211
|
-
for lookup in objects
|
|
212
|
-
]
|
|
213
|
-
for lookup in lookups:
|
|
214
|
-
for k in ["author","date","version","id","references"]:
|
|
215
|
-
if k in lookup:
|
|
216
|
-
del(lookup[k])
|
|
217
|
-
JsonWriter.writeJsonObject(
|
|
218
|
-
os.path.join(output_path, "lookups.json"), "lookups", lookups
|
|
219
220
|
)
|
|
221
|
+
for lookup in objects
|
|
222
|
+
]
|
|
223
|
+
for lookup in lookups:
|
|
224
|
+
for k in ["author", "date", "version", "id", "references"]:
|
|
225
|
+
if k in lookup:
|
|
226
|
+
del lookup[k]
|
|
227
|
+
JsonWriter.writeJsonObject(
|
|
228
|
+
os.path.join(self.output_path, "lookups.json"), "lookups", lookups
|
|
229
|
+
)
|
|
220
230
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
def writeDeployments(
|
|
232
|
+
self,
|
|
233
|
+
objects: list[Deployment],
|
|
234
|
+
) -> None:
|
|
235
|
+
deployments = [
|
|
236
|
+
deployment.model_dump(
|
|
237
|
+
include=set(
|
|
238
|
+
[
|
|
239
|
+
"name",
|
|
240
|
+
"author",
|
|
241
|
+
"date",
|
|
242
|
+
"version",
|
|
243
|
+
"id",
|
|
244
|
+
"description",
|
|
245
|
+
"scheduling",
|
|
246
|
+
"rba",
|
|
247
|
+
"tags",
|
|
248
|
+
]
|
|
237
249
|
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
)
|
|
251
|
+
for deployment in objects
|
|
252
|
+
]
|
|
253
|
+
# references are not to be included, but have been deleted in the
|
|
254
|
+
# model_serialization logic
|
|
255
|
+
JsonWriter.writeJsonObject(
|
|
256
|
+
os.path.join(self.output_path, "deployments.json"),
|
|
257
|
+
"deployments",
|
|
258
|
+
deployments,
|
|
259
|
+
)
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import List,Union
|
|
1
|
+
from typing import List, Union
|
|
3
2
|
import pathlib
|
|
4
3
|
|
|
5
4
|
from contentctl.objects.detection import Detection
|
|
6
5
|
from contentctl.output.attack_nav_writer import AttackNavWriter
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
class AttackNavOutput
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
class AttackNavOutput:
|
|
9
|
+
def writeObjects(
|
|
10
|
+
self, detections: List[Detection], output_path: pathlib.Path
|
|
11
|
+
) -> None:
|
|
12
|
+
techniques: dict[str, dict[str, Union[List[str], int]]] = {}
|
|
13
13
|
for detection in detections:
|
|
14
14
|
for tactic in detection.tags.mitre_attack_id:
|
|
15
15
|
if tactic not in techniques:
|
|
16
|
-
techniques[tactic] = {
|
|
17
|
-
|
|
16
|
+
techniques[tactic] = {"score": 0, "file_paths": []}
|
|
17
|
+
|
|
18
18
|
detection_url = f"https://github.com/splunk/security_content/blob/develop/detections/{detection.source}/{detection.file_path.name}"
|
|
19
|
-
techniques[tactic][
|
|
20
|
-
techniques[tactic][
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
techniques[tactic]["score"] += 1
|
|
20
|
+
techniques[tactic]["file_paths"].append(detection_url)
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
23
|
for detection in objects:
|
|
24
24
|
if detection.tags.mitre_attack_enrichments:
|
|
25
25
|
for mitre_attack_enrichment in detection.tags.mitre_attack_enrichments:
|
|
@@ -31,16 +31,16 @@ class AttackNavOutput():
|
|
|
31
31
|
else:
|
|
32
32
|
techniques[mitre_attack_enrichment.mitre_attack_id]['score'] = techniques[mitre_attack_enrichment.mitre_attack_id]['score'] + 1
|
|
33
33
|
techniques[mitre_attack_enrichment.mitre_attack_id]['file_paths'].append('https://github.com/splunk/security_content/blob/develop/detections/' + detection.getSource() + '/' + self.convertNameToFileName(detection.name))
|
|
34
|
-
|
|
35
|
-
AttackNavWriter.writeAttackNavFile(techniques, output_path /
|
|
36
|
-
|
|
34
|
+
"""
|
|
35
|
+
AttackNavWriter.writeAttackNavFile(techniques, output_path / "coverage.json")
|
|
37
36
|
|
|
38
37
|
def convertNameToFileName(self, name: str):
|
|
39
|
-
file_name =
|
|
40
|
-
.replace(
|
|
41
|
-
.replace(
|
|
42
|
-
.replace(
|
|
43
|
-
.replace(
|
|
38
|
+
file_name = (
|
|
39
|
+
name.replace(" ", "_")
|
|
40
|
+
.replace("-", "_")
|
|
41
|
+
.replace(".", "_")
|
|
42
|
+
.replace("/", "_")
|
|
44
43
|
.lower()
|
|
45
|
-
|
|
44
|
+
)
|
|
45
|
+
file_name = file_name + ".yml"
|
|
46
46
|
return file_name
|
|
@@ -1,75 +1,67 @@
|
|
|
1
|
-
|
|
2
1
|
import json
|
|
3
2
|
from typing import Union, List
|
|
4
3
|
import pathlib
|
|
4
|
+
|
|
5
5
|
VERSION = "4.3"
|
|
6
6
|
NAME = "Detection Coverage"
|
|
7
7
|
DESCRIPTION = "security_content detection coverage"
|
|
8
8
|
DOMAIN = "mitre-enterprise"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class AttackNavWriter
|
|
12
|
-
|
|
11
|
+
class AttackNavWriter:
|
|
13
12
|
@staticmethod
|
|
14
|
-
def writeAttackNavFile(
|
|
13
|
+
def writeAttackNavFile(
|
|
14
|
+
mitre_techniques: dict[str, dict[str, Union[List[str], int]]],
|
|
15
|
+
output_path: pathlib.Path,
|
|
16
|
+
) -> None:
|
|
15
17
|
max_count = 0
|
|
16
18
|
for technique_id in mitre_techniques.keys():
|
|
17
|
-
if mitre_techniques[technique_id][
|
|
18
|
-
max_count = mitre_techniques[technique_id][
|
|
19
|
-
|
|
19
|
+
if mitre_techniques[technique_id]["score"] > max_count:
|
|
20
|
+
max_count = mitre_techniques[technique_id]["score"]
|
|
21
|
+
|
|
20
22
|
layer_json = {
|
|
21
23
|
"version": VERSION,
|
|
22
24
|
"name": NAME,
|
|
23
25
|
"description": DESCRIPTION,
|
|
24
26
|
"domain": DOMAIN,
|
|
25
|
-
"techniques": []
|
|
27
|
+
"techniques": [],
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
layer_json["gradient"] = {
|
|
29
|
-
"colors": [
|
|
30
|
-
"#ffffff",
|
|
31
|
-
"#66b1ff",
|
|
32
|
-
"#096ed7"
|
|
33
|
-
],
|
|
31
|
+
"colors": ["#ffffff", "#66b1ff", "#096ed7"],
|
|
34
32
|
"minValue": 0,
|
|
35
|
-
"maxValue": max_count
|
|
33
|
+
"maxValue": max_count,
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
layer_json["filters"] = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
"platforms": [
|
|
38
|
+
"Windows",
|
|
39
|
+
"Linux",
|
|
40
|
+
"macOS",
|
|
41
|
+
"AWS",
|
|
42
|
+
"GCP",
|
|
43
|
+
"Azure",
|
|
44
|
+
"Office 365",
|
|
45
|
+
"SaaS",
|
|
46
|
+
]
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
layer_json["legendItems"] = [
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
"color": "#ffffff"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"label": "Some detections available",
|
|
58
|
-
"color": "#66b1ff"
|
|
59
|
-
}
|
|
50
|
+
{"label": "NO available detections", "color": "#ffffff"},
|
|
51
|
+
{"label": "Some detections available", "color": "#66b1ff"},
|
|
60
52
|
]
|
|
61
53
|
|
|
62
|
-
layer_json[
|
|
63
|
-
layer_json[
|
|
54
|
+
layer_json["showTacticRowBackground"] = True
|
|
55
|
+
layer_json["tacticRowBackground"] = "#dddddd"
|
|
64
56
|
layer_json["sorting"] = 3
|
|
65
57
|
|
|
66
58
|
for technique_id in mitre_techniques.keys():
|
|
67
59
|
layer_technique = {
|
|
68
60
|
"techniqueID": technique_id,
|
|
69
|
-
"score": mitre_techniques[technique_id][
|
|
70
|
-
"comment": "\n\n".join(mitre_techniques[technique_id][
|
|
61
|
+
"score": mitre_techniques[technique_id]["score"],
|
|
62
|
+
"comment": "\n\n".join(mitre_techniques[technique_id]["file_paths"]),
|
|
71
63
|
}
|
|
72
64
|
layer_json["techniques"].append(layer_technique)
|
|
73
65
|
|
|
74
|
-
with open(output_path,
|
|
66
|
+
with open(output_path, "w") as outfile:
|
|
75
67
|
json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
|