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/objects/enums.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import List
|
|
3
|
-
import
|
|
3
|
+
from enum import StrEnum, IntEnum
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class AnalyticsType(
|
|
6
|
+
class AnalyticsType(StrEnum):
|
|
7
7
|
TTP = "TTP"
|
|
8
8
|
Anomaly = "Anomaly"
|
|
9
9
|
Hunting = "Hunting"
|
|
10
10
|
Correlation = "Correlation"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
class DeploymentType(StrEnum):
|
|
13
14
|
TTP = "TTP"
|
|
14
15
|
Anomaly = "Anomaly"
|
|
15
16
|
Hunting = "Hunting"
|
|
@@ -18,9 +19,9 @@ class DeploymentType(str, enum.Enum):
|
|
|
18
19
|
Embedded = "Embedded"
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class DataModel(
|
|
22
|
+
class DataModel(StrEnum):
|
|
22
23
|
ENDPOINT = "Endpoint"
|
|
23
|
-
NETWORK_TRAFFIC
|
|
24
|
+
NETWORK_TRAFFIC = "Network_Traffic"
|
|
24
25
|
AUTHENTICATION = "Authentication"
|
|
25
26
|
CHANGE = "Change"
|
|
26
27
|
CHANGE_ANALYSIS = "Change_Analysis"
|
|
@@ -31,20 +32,21 @@ class DataModel(str,enum.Enum):
|
|
|
31
32
|
UPDATES = "Updates"
|
|
32
33
|
VULNERABILITIES = "Vulnerabilities"
|
|
33
34
|
WEB = "Web"
|
|
34
|
-
#Should the following more specific DMs be added?
|
|
35
|
-
#Or should they just fall under endpoint?
|
|
36
|
-
#ENDPOINT_PROCESSES = "Endpoint_Processes"
|
|
37
|
-
#ENDPOINT_FILESYSTEM = "Endpoint_Filesystem"
|
|
38
|
-
#ENDPOINT_REGISTRY = "Endpoint_Registry"
|
|
35
|
+
# Should the following more specific DMs be added?
|
|
36
|
+
# Or should they just fall under endpoint?
|
|
37
|
+
# ENDPOINT_PROCESSES = "Endpoint_Processes"
|
|
38
|
+
# ENDPOINT_FILESYSTEM = "Endpoint_Filesystem"
|
|
39
|
+
# ENDPOINT_REGISTRY = "Endpoint_Registry"
|
|
39
40
|
RISK = "Risk"
|
|
40
41
|
SPLUNK_AUDIT = "Splunk_Audit"
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
class PlaybookType(
|
|
44
|
+
class PlaybookType(StrEnum):
|
|
44
45
|
INVESTIGATION = "Investigation"
|
|
45
46
|
RESPONSE = "Response"
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
class SecurityContentType(IntEnum):
|
|
48
50
|
detections = 1
|
|
49
51
|
baselines = 2
|
|
50
52
|
stories = 3
|
|
@@ -68,55 +70,37 @@ class SecurityContentType(enum.Enum):
|
|
|
68
70
|
# json_objects = "json_objects"
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
class
|
|
72
|
-
SPLUNK_APP = 1
|
|
73
|
-
API = 3
|
|
74
|
-
CUSTOM = 4
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class SecurityContentProductName(str, enum.Enum):
|
|
73
|
+
class SecurityContentProductName(StrEnum):
|
|
78
74
|
SPLUNK_ENTERPRISE = "Splunk Enterprise"
|
|
79
75
|
SPLUNK_ENTERPRISE_SECURITY = "Splunk Enterprise Security"
|
|
80
76
|
SPLUNK_CLOUD = "Splunk Cloud"
|
|
81
77
|
SPLUNK_SECURITY_ANALYTICS_FOR_AWS = "Splunk Security Analytics for AWS"
|
|
82
78
|
SPLUNK_BEHAVIORAL_ANALYTICS = "Splunk Behavioral Analytics"
|
|
83
79
|
|
|
84
|
-
|
|
80
|
+
|
|
81
|
+
class SecurityContentInvestigationProductName(StrEnum):
|
|
85
82
|
SPLUNK_ENTERPRISE = "Splunk Enterprise"
|
|
86
83
|
SPLUNK_ENTERPRISE_SECURITY = "Splunk Enterprise Security"
|
|
87
84
|
SPLUNK_CLOUD = "Splunk Cloud"
|
|
88
85
|
SPLUNK_SECURITY_ANALYTICS_FOR_AWS = "Splunk Security Analytics for AWS"
|
|
89
86
|
SPLUNK_BEHAVIORAL_ANALYTICS = "Splunk Behavioral Analytics"
|
|
90
87
|
SPLUNK_PHANTOM = "Splunk Phantom"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class DetectionStatus(enum.Enum):
|
|
94
|
-
production = "production"
|
|
95
|
-
deprecated = "deprecated"
|
|
96
|
-
experimental = "experimental"
|
|
97
|
-
validation = "validation"
|
|
98
88
|
|
|
99
89
|
|
|
100
|
-
class
|
|
90
|
+
class DetectionStatus(StrEnum):
|
|
101
91
|
production = "production"
|
|
102
92
|
deprecated = "deprecated"
|
|
103
93
|
experimental = "experimental"
|
|
104
94
|
validation = "validation"
|
|
105
95
|
|
|
106
96
|
|
|
107
|
-
class LogLevel(
|
|
97
|
+
class LogLevel(StrEnum):
|
|
108
98
|
NONE = "NONE"
|
|
109
99
|
ERROR = "ERROR"
|
|
110
100
|
INFO = "INFO"
|
|
111
101
|
|
|
112
102
|
|
|
113
|
-
class
|
|
114
|
-
notable = "notable"
|
|
115
|
-
rba = "rba"
|
|
116
|
-
email = "email"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class StoryCategory(str, enum.Enum):
|
|
103
|
+
class StoryCategory(StrEnum):
|
|
120
104
|
ABUSE = "Abuse"
|
|
121
105
|
ADVERSARY_TACTICS = "Adversary Tactics"
|
|
122
106
|
BEST_PRACTICES = "Best Practices"
|
|
@@ -139,37 +123,18 @@ class StoryCategory(str, enum.Enum):
|
|
|
139
123
|
UNAUTHORIZED_SOFTWARE = "Unauthorized Software"
|
|
140
124
|
|
|
141
125
|
|
|
142
|
-
class PostTestBehavior(
|
|
126
|
+
class PostTestBehavior(StrEnum):
|
|
143
127
|
always_pause = "always_pause"
|
|
144
128
|
pause_on_failure = "pause_on_failure"
|
|
145
129
|
never_pause = "never_pause"
|
|
146
130
|
|
|
147
131
|
|
|
148
|
-
class DetectionTestingMode(
|
|
132
|
+
class DetectionTestingMode(StrEnum):
|
|
149
133
|
selected = "selected"
|
|
150
134
|
all = "all"
|
|
151
135
|
changes = "changes"
|
|
152
136
|
|
|
153
137
|
|
|
154
|
-
class DetectionTestingTargetInfrastructure(str, enum.Enum):
|
|
155
|
-
container = "container"
|
|
156
|
-
server = "server"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
class InstanceState(str, enum.Enum):
|
|
160
|
-
starting = "starting"
|
|
161
|
-
running = "running"
|
|
162
|
-
error = "error"
|
|
163
|
-
stopping = "stopping"
|
|
164
|
-
stopped = "stopped"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class SigmaConverterTarget(enum.Enum):
|
|
168
|
-
CIM = 1
|
|
169
|
-
RAW = 2
|
|
170
|
-
OCSF = 3
|
|
171
|
-
ALL = 4
|
|
172
|
-
|
|
173
138
|
# It's unclear why we use a mix of constants and enums. The following list was taken from:
|
|
174
139
|
# contentctl/contentctl/helper/constants.py.
|
|
175
140
|
# We convect it to an enum here
|
|
@@ -183,8 +148,8 @@ class SigmaConverterTarget(enum.Enum):
|
|
|
183
148
|
# "Command And Control": 6,
|
|
184
149
|
# "Actions on Objectives": 7
|
|
185
150
|
# }
|
|
186
|
-
class KillChainPhase(
|
|
187
|
-
UNKNOWN ="Unknown"
|
|
151
|
+
class KillChainPhase(StrEnum):
|
|
152
|
+
UNKNOWN = "Unknown"
|
|
188
153
|
RECONNAISSANCE = "Reconnaissance"
|
|
189
154
|
WEAPONIZATION = "Weaponization"
|
|
190
155
|
DELIVERY = "Delivery"
|
|
@@ -194,7 +159,7 @@ class KillChainPhase(str, enum.Enum):
|
|
|
194
159
|
ACTIONS_ON_OBJECTIVES = "Actions on Objectives"
|
|
195
160
|
|
|
196
161
|
|
|
197
|
-
class DataSource(
|
|
162
|
+
class DataSource(StrEnum):
|
|
198
163
|
OSQUERY_ES_PROCESS_EVENTS = "OSQuery ES Process Events"
|
|
199
164
|
POWERSHELL_4104 = "Powershell 4104"
|
|
200
165
|
SYSMON_EVENT_ID_1 = "Sysmon EventID 1"
|
|
@@ -234,7 +199,8 @@ class DataSource(str,enum.Enum):
|
|
|
234
199
|
WINDOWS_SECURITY_5145 = "Windows Security 5145"
|
|
235
200
|
WINDOWS_SYSTEM_7045 = "Windows System 7045"
|
|
236
201
|
|
|
237
|
-
|
|
202
|
+
|
|
203
|
+
class ProvidingTechnology(StrEnum):
|
|
238
204
|
AMAZON_SECURITY_LAKE = "Amazon Security Lake"
|
|
239
205
|
AMAZON_WEB_SERVICES_CLOUDTRAIL = "Amazon Web Services - Cloudtrail"
|
|
240
206
|
AZURE_AD = "Azure AD"
|
|
@@ -253,9 +219,9 @@ class ProvidingTechnology(str, enum.Enum):
|
|
|
253
219
|
SPLUNK_INTERNAL_LOGS = "Splunk Internal Logs"
|
|
254
220
|
SYMANTEC_ENDPOINT_PROTECTION = "Symantec Endpoint Protection"
|
|
255
221
|
ZEEK = "Zeek"
|
|
256
|
-
|
|
222
|
+
|
|
257
223
|
@staticmethod
|
|
258
|
-
def getProvidingTechFromSearch(search_string:str)->List[ProvidingTechnology]:
|
|
224
|
+
def getProvidingTechFromSearch(search_string: str) -> List[ProvidingTechnology]:
|
|
259
225
|
"""_summary_
|
|
260
226
|
|
|
261
227
|
Args:
|
|
@@ -267,34 +233,45 @@ class ProvidingTechnology(str, enum.Enum):
|
|
|
267
233
|
Returns:
|
|
268
234
|
List[ProvidingTechnology]: List of providing technologies (with no duplicates because
|
|
269
235
|
it is derived from a set) calculated from the search string.
|
|
270
|
-
"""
|
|
271
|
-
matched_technologies:set[ProvidingTechnology] = set()
|
|
272
|
-
#As there are many different sources that use google logs, we define the set once
|
|
273
|
-
google_logs = set(
|
|
236
|
+
"""
|
|
237
|
+
matched_technologies: set[ProvidingTechnology] = set()
|
|
238
|
+
# As there are many different sources that use google logs, we define the set once
|
|
239
|
+
google_logs = set(
|
|
240
|
+
[
|
|
241
|
+
ProvidingTechnology.GOOGLE_WORKSPACE,
|
|
242
|
+
ProvidingTechnology.GOOGLE_CLOUD_PLATFORM,
|
|
243
|
+
]
|
|
244
|
+
)
|
|
274
245
|
providing_technologies_mapping = {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
246
|
+
"`amazon_security_lake`": set([ProvidingTechnology.AMAZON_SECURITY_LAKE]),
|
|
247
|
+
"audit_searches": set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
|
|
248
|
+
"`azure_monitor_aad`": set(
|
|
249
|
+
[ProvidingTechnology.AZURE_AD, ProvidingTechnology.ENTRA_ID]
|
|
250
|
+
),
|
|
251
|
+
"`cloudtrail`": set([ProvidingTechnology.AMAZON_WEB_SERVICES_CLOUDTRAIL]),
|
|
252
|
+
# Endpoint is NOT a Macro (and this is intentional since it is to capture Endpoint Datamodel usage)
|
|
253
|
+
"Endpoint": set(
|
|
254
|
+
[
|
|
255
|
+
ProvidingTechnology.MICROSOFT_SYSMON,
|
|
256
|
+
ProvidingTechnology.MICROSOFT_WINDOWS,
|
|
257
|
+
ProvidingTechnology.CARBON_BLACK_RESPONSE,
|
|
258
|
+
ProvidingTechnology.CROWDSTRIKE_FALCON,
|
|
259
|
+
ProvidingTechnology.SYMANTEC_ENDPOINT_PROTECTION,
|
|
260
|
+
]
|
|
261
|
+
),
|
|
262
|
+
"`google_": google_logs,
|
|
263
|
+
"`gsuite": google_logs,
|
|
264
|
+
"`gws_": google_logs,
|
|
265
|
+
"`kube": set([ProvidingTechnology.KUBERNETES]),
|
|
266
|
+
"`ms_defender`": set([ProvidingTechnology.MICROSOFT_DEFENDER]),
|
|
267
|
+
"`o365_": set([ProvidingTechnology.MICROSOFT_OFFICE_365]),
|
|
268
|
+
"`okta": set([ProvidingTechnology.OKTA]),
|
|
269
|
+
"`pingid`": set([ProvidingTechnology.PING_ID]),
|
|
270
|
+
"`powershell`": set(set([ProvidingTechnology.MICROSOFT_WINDOWS])),
|
|
271
|
+
"`splunkd_": set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
|
|
272
|
+
"`sysmon`": set([ProvidingTechnology.MICROSOFT_SYSMON]),
|
|
273
|
+
"`wineventlog_security`": set([ProvidingTechnology.MICROSOFT_WINDOWS]),
|
|
274
|
+
"`zeek_": set([ProvidingTechnology.ZEEK]),
|
|
298
275
|
}
|
|
299
276
|
for key in providing_technologies_mapping:
|
|
300
277
|
if key in search_string:
|
|
@@ -302,7 +279,7 @@ class ProvidingTechnology(str, enum.Enum):
|
|
|
302
279
|
return sorted(list(matched_technologies))
|
|
303
280
|
|
|
304
281
|
|
|
305
|
-
class Cis18Value(
|
|
282
|
+
class Cis18Value(StrEnum):
|
|
306
283
|
CIS_0 = "CIS 0"
|
|
307
284
|
CIS_1 = "CIS 1"
|
|
308
285
|
CIS_2 = "CIS 2"
|
|
@@ -323,7 +300,8 @@ class Cis18Value(str,enum.Enum):
|
|
|
323
300
|
CIS_17 = "CIS 17"
|
|
324
301
|
CIS_18 = "CIS 18"
|
|
325
302
|
|
|
326
|
-
|
|
303
|
+
|
|
304
|
+
class SecurityDomain(StrEnum):
|
|
327
305
|
ENDPOINT = "endpoint"
|
|
328
306
|
NETWORK = "network"
|
|
329
307
|
THREAT = "threat"
|
|
@@ -331,7 +309,8 @@ class SecurityDomain(str, enum.Enum):
|
|
|
331
309
|
ACCESS = "access"
|
|
332
310
|
AUDIT = "audit"
|
|
333
311
|
|
|
334
|
-
|
|
312
|
+
|
|
313
|
+
class AssetType(StrEnum):
|
|
335
314
|
AWS_ACCOUNT = "AWS Account"
|
|
336
315
|
AWS_EKS_KUBERNETES_CLUSTER = "AWS EKS Kubernetes cluster"
|
|
337
316
|
AWS_FEDERATED_ACCOUNT = "AWS Federated Account"
|
|
@@ -340,9 +319,9 @@ class AssetType(str, enum.Enum):
|
|
|
340
319
|
AMAZON_EKS_KUBERNETES_CLUSTER = "Amazon EKS Kubernetes cluster"
|
|
341
320
|
AMAZON_EKS_KUBERNETES_CLUSTER_POD = "Amazon EKS Kubernetes cluster Pod"
|
|
342
321
|
AMAZON_ELASTIC_CONTAINER_REGISTRY = "Amazon Elastic Container Registry"
|
|
343
|
-
#AZURE = "Azure"
|
|
344
|
-
#AZURE_AD = "Azure AD"
|
|
345
|
-
#AZURE_AD_TENANT = "Azure AD Tenant"
|
|
322
|
+
# AZURE = "Azure"
|
|
323
|
+
# AZURE_AD = "Azure AD"
|
|
324
|
+
# AZURE_AD_TENANT = "Azure AD Tenant"
|
|
346
325
|
AZURE_TENANT = "Azure Tenant"
|
|
347
326
|
AZURE_AKS_KUBERNETES_CLUSTER = "Azure AKS Kubernetes cluster"
|
|
348
327
|
AZURE_ACTIVE_DIRECTORY = "Azure Active Directory"
|
|
@@ -369,8 +348,8 @@ class AssetType(str, enum.Enum):
|
|
|
369
348
|
INSTANCE = "Instance"
|
|
370
349
|
KUBERNETES = "Kubernetes"
|
|
371
350
|
NETWORK = "Network"
|
|
372
|
-
#OFFICE_365 = "Office 365"
|
|
373
|
-
#OFFICE_365_Tenant = "Office 365 Tenant"
|
|
351
|
+
# OFFICE_365 = "Office 365"
|
|
352
|
+
# OFFICE_365_Tenant = "Office 365 Tenant"
|
|
374
353
|
O365_TENANT = "O365 Tenant"
|
|
375
354
|
OKTA_TENANT = "Okta Tenant"
|
|
376
355
|
PROXY = "Proxy"
|
|
@@ -382,7 +361,8 @@ class AssetType(str, enum.Enum):
|
|
|
382
361
|
WEB_APPLICATION = "Web Application"
|
|
383
362
|
WINDOWS = "Windows"
|
|
384
363
|
|
|
385
|
-
|
|
364
|
+
|
|
365
|
+
class NistCategory(StrEnum):
|
|
386
366
|
ID_AM = "ID.AM"
|
|
387
367
|
ID_BE = "ID.BE"
|
|
388
368
|
ID_GV = "ID.GV"
|
|
@@ -406,7 +386,8 @@ class NistCategory(str, enum.Enum):
|
|
|
406
386
|
RC_IM = "RC.IM"
|
|
407
387
|
RC_CO = "RC.CO"
|
|
408
388
|
|
|
409
|
-
|
|
389
|
+
|
|
390
|
+
class RiskSeverity(StrEnum):
|
|
410
391
|
# Levels taken from the following documentation link
|
|
411
392
|
# https://docs.splunk.com/Documentation/ES/7.3.2/User/RiskScoring
|
|
412
393
|
# 20 - info (0-20 for us)
|
contentctl/objects/errors.py
CHANGED
|
@@ -4,21 +4,25 @@ from uuid import UUID
|
|
|
4
4
|
|
|
5
5
|
class ValidationFailed(Exception):
|
|
6
6
|
"""Indicates not an error in execution, but a validation failure"""
|
|
7
|
+
|
|
7
8
|
pass
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class IntegrationTestingError(Exception):
|
|
11
12
|
"""Base exception class for integration testing"""
|
|
13
|
+
|
|
12
14
|
pass
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class ServerError(IntegrationTestingError):
|
|
16
18
|
"""An error encounterd during integration testing, as provided by the server (Splunk instance)"""
|
|
19
|
+
|
|
17
20
|
pass
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
class ClientError(IntegrationTestingError):
|
|
21
24
|
"""An error encounterd during integration testing, on the client's side (locally)"""
|
|
25
|
+
|
|
22
26
|
pass
|
|
23
27
|
|
|
24
28
|
|
|
@@ -26,6 +30,7 @@ class MetadataValidationError(Exception, ABC):
|
|
|
26
30
|
"""
|
|
27
31
|
Base class for any errors arising from savedsearches.conf detection metadata validation
|
|
28
32
|
"""
|
|
33
|
+
|
|
29
34
|
# The name of the rule the error relates to
|
|
30
35
|
rule_name: str
|
|
31
36
|
|
|
@@ -52,11 +57,8 @@ class DetectionMissingError(MetadataValidationError):
|
|
|
52
57
|
"""
|
|
53
58
|
An error indicating a detection in the prior build could not be found in the current build
|
|
54
59
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rule_name: str,
|
|
58
|
-
*args: object
|
|
59
|
-
) -> None:
|
|
60
|
+
|
|
61
|
+
def __init__(self, rule_name: str, *args: object) -> None:
|
|
60
62
|
self.rule_name = rule_name
|
|
61
63
|
super().__init__(self.long_message, *args)
|
|
62
64
|
|
|
@@ -77,15 +79,14 @@ class DetectionMissingError(MetadataValidationError):
|
|
|
77
79
|
A short-form error message
|
|
78
80
|
:returns: a str, the message
|
|
79
81
|
"""
|
|
80
|
-
return
|
|
81
|
-
"Detection from previous build not found in current build."
|
|
82
|
-
)
|
|
82
|
+
return "Detection from previous build not found in current build."
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class DetectionIDError(MetadataValidationError):
|
|
86
86
|
"""
|
|
87
87
|
An error indicating the detection ID may have changed between builds
|
|
88
88
|
"""
|
|
89
|
+
|
|
89
90
|
# The ID from the current build
|
|
90
91
|
current_id: UUID
|
|
91
92
|
|
|
@@ -93,11 +94,7 @@ class DetectionIDError(MetadataValidationError):
|
|
|
93
94
|
previous_id: UUID
|
|
94
95
|
|
|
95
96
|
def __init__(
|
|
96
|
-
|
|
97
|
-
rule_name: str,
|
|
98
|
-
current_id: UUID,
|
|
99
|
-
previous_id: UUID,
|
|
100
|
-
*args: object
|
|
97
|
+
self, rule_name: str, current_id: UUID, previous_id: UUID, *args: object
|
|
101
98
|
) -> None:
|
|
102
99
|
self.rule_name = rule_name
|
|
103
100
|
self.current_id = current_id
|
|
@@ -122,15 +119,14 @@ class DetectionIDError(MetadataValidationError):
|
|
|
122
119
|
A short-form error message
|
|
123
120
|
:returns: a str, the message
|
|
124
121
|
"""
|
|
125
|
-
return
|
|
126
|
-
f"Detection ID {self.current_id} in current build does not match ID {self.previous_id} in previous build."
|
|
127
|
-
)
|
|
122
|
+
return f"Detection ID {self.current_id} in current build does not match ID {self.previous_id} in previous build."
|
|
128
123
|
|
|
129
124
|
|
|
130
125
|
class VersioningError(MetadataValidationError, ABC):
|
|
131
126
|
"""
|
|
132
127
|
A base class for any metadata validation errors relating to detection versioning
|
|
133
128
|
"""
|
|
129
|
+
|
|
134
130
|
# The version in the current build
|
|
135
131
|
current_version: int
|
|
136
132
|
|
|
@@ -138,11 +134,7 @@ class VersioningError(MetadataValidationError, ABC):
|
|
|
138
134
|
previous_version: int
|
|
139
135
|
|
|
140
136
|
def __init__(
|
|
141
|
-
|
|
142
|
-
rule_name: str,
|
|
143
|
-
current_version: int,
|
|
144
|
-
previous_version: int,
|
|
145
|
-
*args: object
|
|
137
|
+
self, rule_name: str, current_version: int, previous_version: int, *args: object
|
|
146
138
|
) -> None:
|
|
147
139
|
self.rule_name = rule_name
|
|
148
140
|
self.current_version = current_version
|
|
@@ -154,6 +146,7 @@ class VersionDecrementedError(VersioningError):
|
|
|
154
146
|
"""
|
|
155
147
|
An error indicating the version number went down between builds
|
|
156
148
|
"""
|
|
149
|
+
|
|
157
150
|
@property
|
|
158
151
|
def long_message(self) -> str:
|
|
159
152
|
"""
|
|
@@ -182,6 +175,7 @@ class VersionBumpingError(VersioningError):
|
|
|
182
175
|
"""
|
|
183
176
|
An error indicating the detection changed but its version wasn't bumped appropriately
|
|
184
177
|
"""
|
|
178
|
+
|
|
185
179
|
@property
|
|
186
180
|
def long_message(self) -> str:
|
|
187
181
|
"""
|
|
@@ -200,6 +194,4 @@ class VersionBumpingError(VersioningError):
|
|
|
200
194
|
A short-form error message
|
|
201
195
|
:returns: a str, the message
|
|
202
196
|
"""
|
|
203
|
-
return
|
|
204
|
-
f"Detection version in current build should be bumped to at least {self.previous_version + 1}."
|
|
205
|
-
)
|
|
197
|
+
return f"Detection version in current build should be bumped to at least {self.previous_version + 1}."
|
|
@@ -10,6 +10,7 @@ class IntegrationTest(BaseTest):
|
|
|
10
10
|
"""
|
|
11
11
|
An integration test for a detection against ES
|
|
12
12
|
"""
|
|
13
|
+
|
|
13
14
|
# The test type (integration)
|
|
14
15
|
test_type: TestType = Field(default=TestType.INTEGRATION)
|
|
15
16
|
|
|
@@ -34,7 +35,6 @@ class IntegrationTest(BaseTest):
|
|
|
34
35
|
Skip the test by setting its result status
|
|
35
36
|
:param message: the reason for skipping
|
|
36
37
|
"""
|
|
37
|
-
self.result = IntegrationTestResult(
|
|
38
|
-
message=message,
|
|
39
|
-
status=TestResultStatus.SKIP
|
|
38
|
+
self.result = IntegrationTestResult( # type: ignore
|
|
39
|
+
message=message, status=TestResultStatus.SKIP
|
|
40
40
|
)
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
|
-
from typing import List,
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from contentctl.objects.
|
|
4
|
+
from typing import Any, List, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field, computed_field, model_serializer
|
|
7
|
+
|
|
8
|
+
from contentctl.objects.config import CustomApp
|
|
8
9
|
from contentctl.objects.constants import (
|
|
9
10
|
CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
|
|
11
|
+
CONTENTCTL_MAX_STANZA_LENGTH,
|
|
10
12
|
CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE,
|
|
11
|
-
CONTENTCTL_MAX_STANZA_LENGTH
|
|
12
13
|
)
|
|
13
|
-
from contentctl.objects.
|
|
14
|
+
from contentctl.objects.enums import DataModel, DetectionStatus
|
|
15
|
+
from contentctl.objects.investigation_tags import InvestigationTags
|
|
16
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
17
|
+
|
|
14
18
|
|
|
15
|
-
# TODO (#266): disable the use_enum_values configuration
|
|
16
19
|
class Investigation(SecurityContentObject):
|
|
17
|
-
model_config = ConfigDict(
|
|
18
|
-
type: str = Field(...,pattern="^Investigation$")
|
|
19
|
-
|
|
20
|
-
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
20
|
+
model_config = ConfigDict(validate_default=False)
|
|
21
|
+
type: str = Field(..., pattern="^Investigation$")
|
|
22
|
+
name: str = Field(..., max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
21
23
|
search: str = Field(...)
|
|
22
24
|
how_to_implement: str = Field(...)
|
|
23
25
|
known_false_positives: str = Field(...)
|
|
24
|
-
|
|
25
|
-
|
|
26
26
|
tags: InvestigationTags
|
|
27
|
+
status: Literal[DetectionStatus.production, DetectionStatus.deprecated]
|
|
27
28
|
|
|
28
29
|
# enrichment
|
|
29
30
|
@computed_field
|
|
30
31
|
@property
|
|
31
|
-
def inputs(self)->List[str]:
|
|
32
|
-
#Parse out and return all inputs from the searchj
|
|
33
|
-
inputs:List[str] = []
|
|
32
|
+
def inputs(self) -> List[str]:
|
|
33
|
+
# Parse out and return all inputs from the searchj
|
|
34
|
+
inputs: List[str] = []
|
|
34
35
|
pattern = r"\$([^\s.]*)\$"
|
|
35
36
|
|
|
36
37
|
for input in re.findall(pattern, self.search):
|
|
@@ -40,27 +41,47 @@ class Investigation(SecurityContentObject):
|
|
|
40
41
|
|
|
41
42
|
@computed_field
|
|
42
43
|
@property
|
|
43
|
-
def
|
|
44
|
-
return
|
|
44
|
+
def datamodel(self) -> List[DataModel]:
|
|
45
|
+
return [dm for dm in DataModel if dm in self.search]
|
|
46
|
+
|
|
47
|
+
@computed_field
|
|
48
|
+
@property
|
|
49
|
+
def lowercase_name(self) -> str:
|
|
50
|
+
return (
|
|
51
|
+
self.name.replace(" ", "_")
|
|
52
|
+
.replace("-", "_")
|
|
53
|
+
.replace(".", "_")
|
|
54
|
+
.replace("/", "_")
|
|
55
|
+
.lower()
|
|
56
|
+
.replace(" ", "_")
|
|
57
|
+
.replace("-", "_")
|
|
58
|
+
.replace(".", "_")
|
|
59
|
+
.replace("/", "_")
|
|
60
|
+
.lower()
|
|
61
|
+
)
|
|
45
62
|
|
|
46
|
-
|
|
47
63
|
# This is a slightly modified version of the get_conf_stanza_name function from
|
|
48
64
|
# SecurityContentObject_Abstract
|
|
49
|
-
def get_response_task_name(
|
|
50
|
-
|
|
65
|
+
def get_response_task_name(
|
|
66
|
+
self, app: CustomApp, max_stanza_length: int = CONTENTCTL_MAX_STANZA_LENGTH
|
|
67
|
+
) -> str:
|
|
68
|
+
stanza_name = CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE.format(
|
|
69
|
+
app_label=app.label, detection_name=self.name
|
|
70
|
+
)
|
|
51
71
|
if len(stanza_name) > max_stanza_length:
|
|
52
|
-
raise ValueError(
|
|
53
|
-
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"conf stanza may only be {max_stanza_length} characters, "
|
|
74
|
+
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' "
|
|
75
|
+
)
|
|
54
76
|
return stanza_name
|
|
55
|
-
|
|
56
77
|
|
|
57
78
|
@model_serializer
|
|
58
79
|
def serialize_model(self):
|
|
59
|
-
#Call serializer for parent
|
|
80
|
+
# Call serializer for parent
|
|
60
81
|
super_fields = super().serialize_model()
|
|
61
|
-
|
|
62
|
-
#All fields custom to this model
|
|
63
|
-
model= {
|
|
82
|
+
|
|
83
|
+
# All fields custom to this model
|
|
84
|
+
model = {
|
|
64
85
|
"type": self.type,
|
|
65
86
|
"datamodel": self.datamodel,
|
|
66
87
|
"search": self.search,
|
|
@@ -68,18 +89,20 @@ class Investigation(SecurityContentObject):
|
|
|
68
89
|
"known_false_positives": self.known_false_positives,
|
|
69
90
|
"inputs": self.inputs,
|
|
70
91
|
"tags": self.tags.model_dump(),
|
|
71
|
-
"lowercase_name":self.lowercase_name
|
|
92
|
+
"lowercase_name": self.lowercase_name,
|
|
72
93
|
}
|
|
73
|
-
|
|
74
|
-
#Combine fields from this model with fields from parent
|
|
94
|
+
|
|
95
|
+
# Combine fields from this model with fields from parent
|
|
75
96
|
super_fields.update(model)
|
|
76
|
-
|
|
77
|
-
#return the model
|
|
78
|
-
return super_fields
|
|
79
97
|
|
|
98
|
+
# return the model
|
|
99
|
+
return super_fields
|
|
80
100
|
|
|
81
|
-
def model_post_init(self, ctx:dict[str,Any]):
|
|
101
|
+
def model_post_init(self, ctx: dict[str, Any]):
|
|
82
102
|
# Ensure we link all stories this investigation references
|
|
83
103
|
# back to itself
|
|
84
104
|
for story in self.tags.analytic_story:
|
|
85
105
|
story.investigations.append(self)
|
|
106
|
+
# back to itself
|
|
107
|
+
for story in self.tags.analytic_story:
|
|
108
|
+
story.investigations.append(self)
|