contentctl 4.4.7__py3-none-any.whl → 5.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +102 -57
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
  5. contentctl/actions/detection_testing/GitService.py +134 -76
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +9 -6
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +155 -108
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +33 -28
  21. contentctl/actions/validate.py +55 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +124 -90
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -53
  30. contentctl/input/director.py +68 -36
  31. contentctl/input/new_content_questions.py +27 -35
  32. contentctl/input/yml_reader.py +28 -18
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
  35. contentctl/objects/alert_action.py +10 -9
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +65 -54
  38. contentctl/objects/base_test.py +5 -3
  39. contentctl/objects/base_test_result.py +19 -11
  40. contentctl/objects/baseline.py +62 -30
  41. contentctl/objects/baseline_tags.py +30 -24
  42. contentctl/objects/config.py +790 -597
  43. contentctl/objects/constants.py +33 -56
  44. contentctl/objects/correlation_search.py +150 -136
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +16 -17
  47. contentctl/objects/deployment.py +43 -44
  48. contentctl/objects/deployment_email.py +3 -2
  49. contentctl/objects/deployment_notable.py +4 -2
  50. contentctl/objects/deployment_phantom.py +7 -6
  51. contentctl/objects/deployment_rba.py +3 -2
  52. contentctl/objects/deployment_scheduling.py +3 -2
  53. contentctl/objects/deployment_slack.py +3 -2
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +58 -103
  58. contentctl/objects/drilldown.py +66 -34
  59. contentctl/objects/enums.py +81 -100
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +59 -36
  64. contentctl/objects/investigation_tags.py +30 -19
  65. contentctl/objects/lookup.py +304 -101
  66. contentctl/objects/macro.py +55 -39
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +23 -13
  74. contentctl/objects/rba.py +96 -0
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +110 -160
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +54 -49
  81. contentctl/objects/story_tags.py +56 -45
  82. contentctl/objects/test_attack_data.py +2 -1
  83. contentctl/objects/test_group.py +5 -2
  84. contentctl/objects/threat_object.py +1 -0
  85. contentctl/objects/throttling.py +27 -18
  86. contentctl/objects/unit_test.py +3 -4
  87. contentctl/objects/unit_test_baseline.py +5 -5
  88. contentctl/objects/unit_test_result.py +6 -6
  89. contentctl/output/api_json_output.py +233 -220
  90. contentctl/output/attack_nav_output.py +21 -21
  91. contentctl/output/attack_nav_writer.py +29 -37
  92. contentctl/output/conf_output.py +235 -172
  93. contentctl/output/conf_writer.py +201 -125
  94. contentctl/output/data_source_writer.py +38 -26
  95. contentctl/output/doc_md_output.py +53 -27
  96. contentctl/output/jinja_writer.py +19 -15
  97. contentctl/output/json_writer.py +21 -11
  98. contentctl/output/svg_output.py +56 -38
  99. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  100. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  101. contentctl/output/templates/collections.j2 +1 -1
  102. contentctl/output/templates/doc_detections.j2 +0 -5
  103. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  104. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  105. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  106. contentctl/output/templates/savedsearches_detections.j2 +10 -11
  107. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  108. contentctl/output/templates/transforms.j2 +6 -8
  109. contentctl/output/yml_writer.py +29 -20
  110. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  111. contentctl/templates/stories/cobalt_strike.yml +1 -0
  112. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
  113. contentctl-5.0.0.dist-info/RECORD +168 -0
  114. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
  115. contentctl/actions/initialize_old.py +0 -245
  116. contentctl/objects/event_source.py +0 -11
  117. contentctl/objects/observable.py +0 -37
  118. contentctl/output/detection_writer.py +0 -28
  119. contentctl/output/new_content_yml_output.py +0 -56
  120. contentctl/output/yml_output.py +0 -66
  121. contentctl-4.4.7.dist-info/RECORD +0 -173
  122. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
  123. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
  from typing import List
3
- import enum
3
+ from enum import StrEnum, IntEnum
4
4
 
5
5
 
6
- class AnalyticsType(str, enum.Enum):
6
+ class AnalyticsType(StrEnum):
7
7
  TTP = "TTP"
8
8
  Anomaly = "Anomaly"
9
9
  Hunting = "Hunting"
10
10
  Correlation = "Correlation"
11
11
 
12
- class DeploymentType(str, enum.Enum):
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(str,enum.Enum):
22
+ class DataModel(StrEnum):
22
23
  ENDPOINT = "Endpoint"
23
- NETWORK_TRAFFIC = "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(str, enum.Enum):
44
+ class PlaybookType(StrEnum):
44
45
  INVESTIGATION = "Investigation"
45
46
  RESPONSE = "Response"
46
47
 
47
- class SecurityContentType(enum.Enum):
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 SecurityContentProduct(enum.Enum):
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
- class SecurityContentInvestigationProductName(str, enum.Enum):
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 DetectionStatusSSA(enum.Enum):
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(enum.Enum):
97
+ class LogLevel(StrEnum):
108
98
  NONE = "NONE"
109
99
  ERROR = "ERROR"
110
100
  INFO = "INFO"
111
101
 
112
102
 
113
- class AlertActions(enum.Enum):
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(str, enum.Enum):
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(str, enum.Enum):
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(str, enum.Enum):
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(str,enum.Enum):
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
- class ProvidingTechnology(str, enum.Enum):
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([ProvidingTechnology.GOOGLE_WORKSPACE, ProvidingTechnology.GOOGLE_CLOUD_PLATFORM])
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
- '`amazon_security_lake`': set([ProvidingTechnology.AMAZON_SECURITY_LAKE]),
276
- 'audit_searches': set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
277
- '`azure_monitor_aad`': set([ProvidingTechnology.AZURE_AD, ProvidingTechnology.ENTRA_ID]),
278
- '`cloudtrail`': set([ProvidingTechnology.AMAZON_WEB_SERVICES_CLOUDTRAIL]),
279
- #Endpoint is NOT a Macro (and this is intentional since it is to capture Endpoint Datamodel usage)
280
- 'Endpoint': set([ProvidingTechnology.MICROSOFT_SYSMON,
281
- ProvidingTechnology.MICROSOFT_WINDOWS,
282
- ProvidingTechnology.CARBON_BLACK_RESPONSE,
283
- ProvidingTechnology.CROWDSTRIKE_FALCON,
284
- ProvidingTechnology.SYMANTEC_ENDPOINT_PROTECTION]),
285
- '`google_': google_logs,
286
- '`gsuite': google_logs,
287
- '`gws_': google_logs,
288
- '`kube': set([ProvidingTechnology.KUBERNETES]),
289
- '`ms_defender`': set([ProvidingTechnology.MICROSOFT_DEFENDER]),
290
- '`o365_': set([ProvidingTechnology.MICROSOFT_OFFICE_365]),
291
- '`okta': set([ProvidingTechnology.OKTA]),
292
- '`pingid`': set([ProvidingTechnology.PING_ID]),
293
- '`powershell`': set(set([ProvidingTechnology.MICROSOFT_WINDOWS])),
294
- '`splunkd_': set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
295
- '`sysmon`': set([ProvidingTechnology.MICROSOFT_SYSMON]),
296
- '`wineventlog_security`': set([ProvidingTechnology.MICROSOFT_WINDOWS]),
297
- '`zeek_': set([ProvidingTechnology.ZEEK]),
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(str,enum.Enum):
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
- class SecurityDomain(str, enum.Enum):
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
- class AssetType(str, enum.Enum):
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
- class NistCategory(str, enum.Enum):
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
- class RiskSeverity(str,enum.Enum):
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)
@@ -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
- def __init__(
56
- self,
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
- self,
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
- self,
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( # type: ignore
38
- message=message,
39
- status=TestResultStatus.SKIP
38
+ self.result = IntegrationTestResult( # type: ignore
39
+ message=message, status=TestResultStatus.SKIP
40
40
  )
@@ -5,5 +5,6 @@ class IntegrationTestResult(BaseTestResult):
5
5
  """
6
6
  An integration test result
7
7
  """
8
+
8
9
  # the total time we slept waiting for the detection to fire after activating it
9
10
  wait_duration: int | None = None
@@ -1,36 +1,37 @@
1
1
  from __future__ import annotations
2
+
2
3
  import re
3
- from typing import List, Any
4
- from pydantic import computed_field, Field, ConfigDict,model_serializer
5
- from contentctl.objects.security_content_object import SecurityContentObject
6
- from contentctl.objects.enums import DataModel
7
- from contentctl.objects.investigation_tags import InvestigationTags
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.config import CustomApp
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(use_enum_values=True,validate_default=False)
18
- type: str = Field(...,pattern="^Investigation$")
19
- datamodel: list[DataModel] = Field(...)
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 lowercase_name(self)->str:
44
- return self.name.replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower().replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
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(self, app:CustomApp, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH)->str:
50
- stanza_name = CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
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(f"conf stanza may only be {max_stanza_length} characters, "
53
- f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
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)