contentctl 5.0.0a2__py3-none-any.whl → 5.0.0a3__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 +88 -55
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
- contentctl/actions/detection_testing/GitService.py +2 -4
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +3 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
- 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 +78 -50
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +31 -25
- contentctl/actions/validate.py +54 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +10 -10
- 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 -39
- contentctl/input/director.py +69 -37
- contentctl/input/new_content_questions.py +26 -34
- contentctl/input/yml_reader.py +22 -17
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
- contentctl/objects/alert_action.py +8 -8
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +64 -54
- contentctl/objects/base_test.py +2 -1
- contentctl/objects/base_test_result.py +16 -8
- contentctl/objects/baseline.py +41 -30
- contentctl/objects/baseline_tags.py +29 -22
- contentctl/objects/config.py +1 -1
- contentctl/objects/constants.py +29 -58
- contentctl/objects/correlation_search.py +75 -55
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +13 -13
- contentctl/objects/deployment.py +44 -37
- contentctl/objects/deployment_email.py +1 -1
- contentctl/objects/deployment_notable.py +2 -1
- contentctl/objects/deployment_phantom.py +5 -5
- contentctl/objects/deployment_rba.py +1 -1
- contentctl/objects/deployment_scheduling.py +1 -1
- contentctl/objects/deployment_slack.py +1 -1
- 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 +54 -64
- contentctl/objects/drilldown.py +66 -35
- contentctl/objects/enums.py +61 -43
- 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 +41 -26
- contentctl/objects/investigation_tags.py +29 -17
- contentctl/objects/lookup.py +234 -113
- contentctl/objects/macro.py +55 -38
- 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 +22 -16
- contentctl/objects/rba.py +14 -8
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +27 -20
- 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 +45 -44
- contentctl/objects/story_tags.py +56 -44
- 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 +4 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +22 -22
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +230 -174
- contentctl/output/data_source_writer.py +38 -25
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +20 -8
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/transforms.j2 +2 -2
- contentctl/output/yml_writer.py +18 -24
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
- contentctl-5.0.0a3.dist-info/RECORD +168 -0
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/observable.py +0 -39
- contentctl-5.0.0a2.dist-info/RECORD +0 -170
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
contentctl/objects/enums.py
CHANGED
|
@@ -9,6 +9,7 @@ class AnalyticsType(StrEnum):
|
|
|
9
9
|
Hunting = "Hunting"
|
|
10
10
|
Correlation = "Correlation"
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
class DeploymentType(StrEnum):
|
|
13
14
|
TTP = "TTP"
|
|
14
15
|
Anomaly = "Anomaly"
|
|
@@ -20,7 +21,7 @@ class DeploymentType(StrEnum):
|
|
|
20
21
|
|
|
21
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,11 +32,11 @@ class DataModel(StrEnum):
|
|
|
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
|
|
|
@@ -44,6 +45,7 @@ class PlaybookType(StrEnum):
|
|
|
44
45
|
INVESTIGATION = "Investigation"
|
|
45
46
|
RESPONSE = "Response"
|
|
46
47
|
|
|
48
|
+
|
|
47
49
|
class SecurityContentType(IntEnum):
|
|
48
50
|
detections = 1
|
|
49
51
|
baselines = 2
|
|
@@ -68,7 +70,6 @@ class SecurityContentType(IntEnum):
|
|
|
68
70
|
# json_objects = "json_objects"
|
|
69
71
|
|
|
70
72
|
|
|
71
|
-
|
|
72
73
|
class SecurityContentProductName(StrEnum):
|
|
73
74
|
SPLUNK_ENTERPRISE = "Splunk Enterprise"
|
|
74
75
|
SPLUNK_ENTERPRISE_SECURITY = "Splunk Enterprise Security"
|
|
@@ -76,6 +77,7 @@ class SecurityContentProductName(StrEnum):
|
|
|
76
77
|
SPLUNK_SECURITY_ANALYTICS_FOR_AWS = "Splunk Security Analytics for AWS"
|
|
77
78
|
SPLUNK_BEHAVIORAL_ANALYTICS = "Splunk Behavioral Analytics"
|
|
78
79
|
|
|
80
|
+
|
|
79
81
|
class SecurityContentInvestigationProductName(StrEnum):
|
|
80
82
|
SPLUNK_ENTERPRISE = "Splunk Enterprise"
|
|
81
83
|
SPLUNK_ENTERPRISE_SECURITY = "Splunk Enterprise Security"
|
|
@@ -83,7 +85,7 @@ class SecurityContentInvestigationProductName(StrEnum):
|
|
|
83
85
|
SPLUNK_SECURITY_ANALYTICS_FOR_AWS = "Splunk Security Analytics for AWS"
|
|
84
86
|
SPLUNK_BEHAVIORAL_ANALYTICS = "Splunk Behavioral Analytics"
|
|
85
87
|
SPLUNK_PHANTOM = "Splunk Phantom"
|
|
86
|
-
|
|
88
|
+
|
|
87
89
|
|
|
88
90
|
class DetectionStatus(StrEnum):
|
|
89
91
|
production = "production"
|
|
@@ -147,7 +149,7 @@ class DetectionTestingMode(StrEnum):
|
|
|
147
149
|
# "Actions on Objectives": 7
|
|
148
150
|
# }
|
|
149
151
|
class KillChainPhase(StrEnum):
|
|
150
|
-
UNKNOWN ="Unknown"
|
|
152
|
+
UNKNOWN = "Unknown"
|
|
151
153
|
RECONNAISSANCE = "Reconnaissance"
|
|
152
154
|
WEAPONIZATION = "Weaponization"
|
|
153
155
|
DELIVERY = "Delivery"
|
|
@@ -197,6 +199,7 @@ class DataSource(StrEnum):
|
|
|
197
199
|
WINDOWS_SECURITY_5145 = "Windows Security 5145"
|
|
198
200
|
WINDOWS_SYSTEM_7045 = "Windows System 7045"
|
|
199
201
|
|
|
202
|
+
|
|
200
203
|
class ProvidingTechnology(StrEnum):
|
|
201
204
|
AMAZON_SECURITY_LAKE = "Amazon Security Lake"
|
|
202
205
|
AMAZON_WEB_SERVICES_CLOUDTRAIL = "Amazon Web Services - Cloudtrail"
|
|
@@ -216,9 +219,9 @@ class ProvidingTechnology(StrEnum):
|
|
|
216
219
|
SPLUNK_INTERNAL_LOGS = "Splunk Internal Logs"
|
|
217
220
|
SYMANTEC_ENDPOINT_PROTECTION = "Symantec Endpoint Protection"
|
|
218
221
|
ZEEK = "Zeek"
|
|
219
|
-
|
|
222
|
+
|
|
220
223
|
@staticmethod
|
|
221
|
-
def getProvidingTechFromSearch(search_string:str)->List[ProvidingTechnology]:
|
|
224
|
+
def getProvidingTechFromSearch(search_string: str) -> List[ProvidingTechnology]:
|
|
222
225
|
"""_summary_
|
|
223
226
|
|
|
224
227
|
Args:
|
|
@@ -230,34 +233,45 @@ class ProvidingTechnology(StrEnum):
|
|
|
230
233
|
Returns:
|
|
231
234
|
List[ProvidingTechnology]: List of providing technologies (with no duplicates because
|
|
232
235
|
it is derived from a set) calculated from the search string.
|
|
233
|
-
"""
|
|
234
|
-
matched_technologies:set[ProvidingTechnology] = set()
|
|
235
|
-
#As there are many different sources that use google logs, we define the set once
|
|
236
|
-
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
|
+
)
|
|
237
245
|
providing_technologies_mapping = {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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]),
|
|
261
275
|
}
|
|
262
276
|
for key in providing_technologies_mapping:
|
|
263
277
|
if key in search_string:
|
|
@@ -286,6 +300,7 @@ class Cis18Value(StrEnum):
|
|
|
286
300
|
CIS_17 = "CIS 17"
|
|
287
301
|
CIS_18 = "CIS 18"
|
|
288
302
|
|
|
303
|
+
|
|
289
304
|
class SecurityDomain(StrEnum):
|
|
290
305
|
ENDPOINT = "endpoint"
|
|
291
306
|
NETWORK = "network"
|
|
@@ -294,6 +309,7 @@ class SecurityDomain(StrEnum):
|
|
|
294
309
|
ACCESS = "access"
|
|
295
310
|
AUDIT = "audit"
|
|
296
311
|
|
|
312
|
+
|
|
297
313
|
class AssetType(StrEnum):
|
|
298
314
|
AWS_ACCOUNT = "AWS Account"
|
|
299
315
|
AWS_EKS_KUBERNETES_CLUSTER = "AWS EKS Kubernetes cluster"
|
|
@@ -303,9 +319,9 @@ class AssetType(StrEnum):
|
|
|
303
319
|
AMAZON_EKS_KUBERNETES_CLUSTER = "Amazon EKS Kubernetes cluster"
|
|
304
320
|
AMAZON_EKS_KUBERNETES_CLUSTER_POD = "Amazon EKS Kubernetes cluster Pod"
|
|
305
321
|
AMAZON_ELASTIC_CONTAINER_REGISTRY = "Amazon Elastic Container Registry"
|
|
306
|
-
#AZURE = "Azure"
|
|
307
|
-
#AZURE_AD = "Azure AD"
|
|
308
|
-
#AZURE_AD_TENANT = "Azure AD Tenant"
|
|
322
|
+
# AZURE = "Azure"
|
|
323
|
+
# AZURE_AD = "Azure AD"
|
|
324
|
+
# AZURE_AD_TENANT = "Azure AD Tenant"
|
|
309
325
|
AZURE_TENANT = "Azure Tenant"
|
|
310
326
|
AZURE_AKS_KUBERNETES_CLUSTER = "Azure AKS Kubernetes cluster"
|
|
311
327
|
AZURE_ACTIVE_DIRECTORY = "Azure Active Directory"
|
|
@@ -332,8 +348,8 @@ class AssetType(StrEnum):
|
|
|
332
348
|
INSTANCE = "Instance"
|
|
333
349
|
KUBERNETES = "Kubernetes"
|
|
334
350
|
NETWORK = "Network"
|
|
335
|
-
#OFFICE_365 = "Office 365"
|
|
336
|
-
#OFFICE_365_Tenant = "Office 365 Tenant"
|
|
351
|
+
# OFFICE_365 = "Office 365"
|
|
352
|
+
# OFFICE_365_Tenant = "Office 365 Tenant"
|
|
337
353
|
O365_TENANT = "O365 Tenant"
|
|
338
354
|
OKTA_TENANT = "Okta Tenant"
|
|
339
355
|
PROXY = "Proxy"
|
|
@@ -345,6 +361,7 @@ class AssetType(StrEnum):
|
|
|
345
361
|
WEB_APPLICATION = "Web Application"
|
|
346
362
|
WINDOWS = "Windows"
|
|
347
363
|
|
|
364
|
+
|
|
348
365
|
class NistCategory(StrEnum):
|
|
349
366
|
ID_AM = "ID.AM"
|
|
350
367
|
ID_BE = "ID.BE"
|
|
@@ -369,6 +386,7 @@ class NistCategory(StrEnum):
|
|
|
369
386
|
RC_IM = "RC.IM"
|
|
370
387
|
RC_CO = "RC.CO"
|
|
371
388
|
|
|
389
|
+
|
|
372
390
|
class RiskSeverity(StrEnum):
|
|
373
391
|
# Levels taken from the following documentation link
|
|
374
392
|
# https://docs.splunk.com/Documentation/ES/7.3.2/User/RiskScoring
|
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,21 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import re
|
|
3
3
|
from typing import List, Any
|
|
4
|
-
from pydantic import computed_field, Field, ConfigDict,model_serializer
|
|
4
|
+
from pydantic import computed_field, Field, ConfigDict, model_serializer
|
|
5
5
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
6
6
|
from contentctl.objects.enums import DataModel
|
|
7
7
|
from contentctl.objects.investigation_tags import InvestigationTags
|
|
8
8
|
from contentctl.objects.constants import (
|
|
9
9
|
CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
|
|
10
10
|
CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE,
|
|
11
|
-
CONTENTCTL_MAX_STANZA_LENGTH
|
|
11
|
+
CONTENTCTL_MAX_STANZA_LENGTH,
|
|
12
12
|
)
|
|
13
13
|
from contentctl.objects.config import CustomApp
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
class Investigation(SecurityContentObject):
|
|
16
17
|
model_config = ConfigDict(validate_default=False)
|
|
17
|
-
type: str = Field(...,pattern="^Investigation$")
|
|
18
|
-
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
18
|
+
type: str = Field(..., pattern="^Investigation$")
|
|
19
|
+
name: str = Field(..., max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
19
20
|
search: str = Field(...)
|
|
20
21
|
how_to_implement: str = Field(...)
|
|
21
22
|
known_false_positives: str = Field(...)
|
|
@@ -24,9 +25,9 @@ class Investigation(SecurityContentObject):
|
|
|
24
25
|
# enrichment
|
|
25
26
|
@computed_field
|
|
26
27
|
@property
|
|
27
|
-
def inputs(self)->List[str]:
|
|
28
|
-
#Parse out and return all inputs from the searchj
|
|
29
|
-
inputs:List[str] = []
|
|
28
|
+
def inputs(self) -> List[str]:
|
|
29
|
+
# Parse out and return all inputs from the searchj
|
|
30
|
+
inputs: List[str] = []
|
|
30
31
|
pattern = r"\$([^\s.]*)\$"
|
|
31
32
|
|
|
32
33
|
for input in re.findall(pattern, self.search):
|
|
@@ -41,27 +42,42 @@ class Investigation(SecurityContentObject):
|
|
|
41
42
|
|
|
42
43
|
@computed_field
|
|
43
44
|
@property
|
|
44
|
-
def lowercase_name(self)->str:
|
|
45
|
-
return
|
|
45
|
+
def lowercase_name(self) -> str:
|
|
46
|
+
return (
|
|
47
|
+
self.name.replace(" ", "_")
|
|
48
|
+
.replace("-", "_")
|
|
49
|
+
.replace(".", "_")
|
|
50
|
+
.replace("/", "_")
|
|
51
|
+
.lower()
|
|
52
|
+
.replace(" ", "_")
|
|
53
|
+
.replace("-", "_")
|
|
54
|
+
.replace(".", "_")
|
|
55
|
+
.replace("/", "_")
|
|
56
|
+
.lower()
|
|
57
|
+
)
|
|
46
58
|
|
|
47
|
-
|
|
48
59
|
# This is a slightly modified version of the get_conf_stanza_name function from
|
|
49
60
|
# SecurityContentObject_Abstract
|
|
50
|
-
def get_response_task_name(
|
|
51
|
-
|
|
61
|
+
def get_response_task_name(
|
|
62
|
+
self, app: CustomApp, max_stanza_length: int = CONTENTCTL_MAX_STANZA_LENGTH
|
|
63
|
+
) -> str:
|
|
64
|
+
stanza_name = CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE.format(
|
|
65
|
+
app_label=app.label, detection_name=self.name
|
|
66
|
+
)
|
|
52
67
|
if len(stanza_name) > max_stanza_length:
|
|
53
|
-
raise ValueError(
|
|
54
|
-
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"conf stanza may only be {max_stanza_length} characters, "
|
|
70
|
+
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' "
|
|
71
|
+
)
|
|
55
72
|
return stanza_name
|
|
56
|
-
|
|
57
73
|
|
|
58
74
|
@model_serializer
|
|
59
75
|
def serialize_model(self):
|
|
60
|
-
#Call serializer for parent
|
|
76
|
+
# Call serializer for parent
|
|
61
77
|
super_fields = super().serialize_model()
|
|
62
|
-
|
|
63
|
-
#All fields custom to this model
|
|
64
|
-
model= {
|
|
78
|
+
|
|
79
|
+
# All fields custom to this model
|
|
80
|
+
model = {
|
|
65
81
|
"type": self.type,
|
|
66
82
|
"datamodel": self.datamodel,
|
|
67
83
|
"search": self.search,
|
|
@@ -69,17 +85,16 @@ class Investigation(SecurityContentObject):
|
|
|
69
85
|
"known_false_positives": self.known_false_positives,
|
|
70
86
|
"inputs": self.inputs,
|
|
71
87
|
"tags": self.tags.model_dump(),
|
|
72
|
-
"lowercase_name":self.lowercase_name
|
|
88
|
+
"lowercase_name": self.lowercase_name,
|
|
73
89
|
}
|
|
74
|
-
|
|
75
|
-
#Combine fields from this model with fields from parent
|
|
90
|
+
|
|
91
|
+
# Combine fields from this model with fields from parent
|
|
76
92
|
super_fields.update(model)
|
|
77
|
-
|
|
78
|
-
#return the model
|
|
79
|
-
return super_fields
|
|
80
93
|
|
|
94
|
+
# return the model
|
|
95
|
+
return super_fields
|
|
81
96
|
|
|
82
|
-
def model_post_init(self, ctx:dict[str,Any]):
|
|
97
|
+
def model_post_init(self, ctx: dict[str, Any]):
|
|
83
98
|
# Ensure we link all stories this investigation references
|
|
84
99
|
# back to itself
|
|
85
100
|
for story in self.tags.analytic_story:
|
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import List
|
|
3
|
-
from pydantic import
|
|
3
|
+
from pydantic import (
|
|
4
|
+
BaseModel,
|
|
5
|
+
Field,
|
|
6
|
+
field_validator,
|
|
7
|
+
ValidationInfo,
|
|
8
|
+
model_serializer,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
)
|
|
4
11
|
from contentctl.objects.story import Story
|
|
5
|
-
from contentctl.objects.enums import
|
|
12
|
+
from contentctl.objects.enums import (
|
|
13
|
+
SecurityContentInvestigationProductName,
|
|
14
|
+
SecurityDomain,
|
|
15
|
+
)
|
|
16
|
+
|
|
6
17
|
|
|
7
18
|
class InvestigationTags(BaseModel):
|
|
8
19
|
model_config = ConfigDict(extra="forbid")
|
|
9
|
-
analytic_story: List[Story] = Field([],min_length=1)
|
|
10
|
-
product: List[SecurityContentInvestigationProductName] = Field(...,min_length=1)
|
|
20
|
+
analytic_story: List[Story] = Field([], min_length=1)
|
|
21
|
+
product: List[SecurityContentInvestigationProductName] = Field(..., min_length=1)
|
|
11
22
|
security_domain: SecurityDomain = Field(...)
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
@field_validator('analytic_story',mode="before")
|
|
24
|
+
@field_validator("analytic_story", mode="before")
|
|
15
25
|
@classmethod
|
|
16
|
-
def mapStoryNamesToStoryObjects(
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
def mapStoryNamesToStoryObjects(
|
|
27
|
+
cls, v: list[str], info: ValidationInfo
|
|
28
|
+
) -> list[Story]:
|
|
29
|
+
return Story.mapNamesToSecurityContentObjects(
|
|
30
|
+
v, info.context.get("output_dto", None)
|
|
31
|
+
)
|
|
19
32
|
|
|
20
33
|
@model_serializer
|
|
21
34
|
def serialize_model(self):
|
|
22
|
-
#All fields custom to this model
|
|
23
|
-
model= {
|
|
35
|
+
# All fields custom to this model
|
|
36
|
+
model = {
|
|
24
37
|
"analytic_story": [story.name for story in self.analytic_story],
|
|
25
38
|
"product": self.product,
|
|
26
39
|
"security_domain": self.security_domain,
|
|
27
40
|
}
|
|
28
|
-
|
|
29
|
-
#Combine fields from this model with fields from parent
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return model
|
|
41
|
+
|
|
42
|
+
# Combine fields from this model with fields from parent
|
|
43
|
+
|
|
44
|
+
# return the model
|
|
45
|
+
return model
|