contentctl 5.0.0a2__py3-none-any.whl → 5.0.1__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 (114) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +2 -4
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  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 +83 -53
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +10 -10
  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 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +255 -323
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +111 -46
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +47 -35
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +32 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  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 +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  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 +53 -31
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  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 +22 -16
  74. contentctl/objects/rba.py +68 -11
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  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 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  98. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  99. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  100. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  101. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  102. contentctl/output/templates/savedsearches_detections.j2 +2 -8
  103. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  104. contentctl/output/templates/transforms.j2 +2 -4
  105. contentctl/output/yml_writer.py +18 -24
  106. contentctl/templates/stories/cobalt_strike.yml +1 -0
  107. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/METADATA +1 -1
  108. contentctl-5.0.1.dist-info/RECORD +168 -0
  109. contentctl/actions/initialize_old.py +0 -245
  110. contentctl/objects/observable.py +0 -39
  111. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  112. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/LICENSE.md +0 -0
  113. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/WHEEL +0 -0
  114. {contentctl-5.0.0a2.dist-info → contentctl-5.0.1.dist-info}/entry_points.txt +0 -0
@@ -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 = "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([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
+ )
237
245
  providing_technologies_mapping = {
238
- '`amazon_security_lake`': set([ProvidingTechnology.AMAZON_SECURITY_LAKE]),
239
- 'audit_searches': set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
240
- '`azure_monitor_aad`': set([ProvidingTechnology.AZURE_AD, ProvidingTechnology.ENTRA_ID]),
241
- '`cloudtrail`': set([ProvidingTechnology.AMAZON_WEB_SERVICES_CLOUDTRAIL]),
242
- #Endpoint is NOT a Macro (and this is intentional since it is to capture Endpoint Datamodel usage)
243
- 'Endpoint': set([ProvidingTechnology.MICROSOFT_SYSMON,
244
- ProvidingTechnology.MICROSOFT_WINDOWS,
245
- ProvidingTechnology.CARBON_BLACK_RESPONSE,
246
- ProvidingTechnology.CROWDSTRIKE_FALCON,
247
- ProvidingTechnology.SYMANTEC_ENDPOINT_PROTECTION]),
248
- '`google_': google_logs,
249
- '`gsuite': google_logs,
250
- '`gws_': google_logs,
251
- '`kube': set([ProvidingTechnology.KUBERNETES]),
252
- '`ms_defender`': set([ProvidingTechnology.MICROSOFT_DEFENDER]),
253
- '`o365_': set([ProvidingTechnology.MICROSOFT_OFFICE_365]),
254
- '`okta': set([ProvidingTechnology.OKTA]),
255
- '`pingid`': set([ProvidingTechnology.PING_ID]),
256
- '`powershell`': set(set([ProvidingTechnology.MICROSOFT_WINDOWS])),
257
- '`splunkd_': set([ProvidingTechnology.SPLUNK_INTERNAL_LOGS]),
258
- '`sysmon`': set([ProvidingTechnology.MICROSOFT_SYSMON]),
259
- '`wineventlog_security`': set([ProvidingTechnology.MICROSOFT_WINDOWS]),
260
- '`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]),
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
@@ -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,32 +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
19
  class Investigation(SecurityContentObject):
16
20
  model_config = ConfigDict(validate_default=False)
17
- type: str = Field(...,pattern="^Investigation$")
18
- name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
21
+ type: str = Field(..., pattern="^Investigation$")
22
+ name: str = Field(..., max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
19
23
  search: str = Field(...)
20
24
  how_to_implement: str = Field(...)
21
25
  known_false_positives: str = Field(...)
22
26
  tags: InvestigationTags
27
+ status: Literal[DetectionStatus.production, DetectionStatus.deprecated]
23
28
 
24
29
  # enrichment
25
30
  @computed_field
26
31
  @property
27
- def inputs(self)->List[str]:
28
- #Parse out and return all inputs from the searchj
29
- inputs:List[str] = []
32
+ def inputs(self) -> List[str]:
33
+ # Parse out and return all inputs from the searchj
34
+ inputs: List[str] = []
30
35
  pattern = r"\$([^\s.]*)\$"
31
36
 
32
37
  for input in re.findall(pattern, self.search):
@@ -41,27 +46,42 @@ class Investigation(SecurityContentObject):
41
46
 
42
47
  @computed_field
43
48
  @property
44
- def lowercase_name(self)->str:
45
- return self.name.replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower().replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
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
+ )
46
62
 
47
-
48
63
  # This is a slightly modified version of the get_conf_stanza_name function from
49
64
  # SecurityContentObject_Abstract
50
- def get_response_task_name(self, app:CustomApp, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH)->str:
51
- 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
+ )
52
71
  if len(stanza_name) > max_stanza_length:
53
- raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
54
- 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
+ )
55
76
  return stanza_name
56
-
57
77
 
58
78
  @model_serializer
59
79
  def serialize_model(self):
60
- #Call serializer for parent
80
+ # Call serializer for parent
61
81
  super_fields = super().serialize_model()
62
-
63
- #All fields custom to this model
64
- model= {
82
+
83
+ # All fields custom to this model
84
+ model = {
65
85
  "type": self.type,
66
86
  "datamodel": self.datamodel,
67
87
  "search": self.search,
@@ -69,18 +89,20 @@ class Investigation(SecurityContentObject):
69
89
  "known_false_positives": self.known_false_positives,
70
90
  "inputs": self.inputs,
71
91
  "tags": self.tags.model_dump(),
72
- "lowercase_name":self.lowercase_name
92
+ "lowercase_name": self.lowercase_name,
73
93
  }
74
-
75
- #Combine fields from this model with fields from parent
94
+
95
+ # Combine fields from this model with fields from parent
76
96
  super_fields.update(model)
77
-
78
- #return the model
79
- return super_fields
80
97
 
98
+ # return the model
99
+ return super_fields
81
100
 
82
- def model_post_init(self, ctx:dict[str,Any]):
101
+ def model_post_init(self, ctx: dict[str, Any]):
83
102
  # Ensure we link all stories this investigation references
84
103
  # back to itself
85
104
  for story in self.tags.analytic_story:
86
105
  story.investigations.append(self)
106
+ # back to itself
107
+ for story in self.tags.analytic_story:
108
+ story.investigations.append(self)
@@ -1,33 +1,45 @@
1
1
  from __future__ import annotations
2
2
  from typing import List
3
- from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_serializer,ConfigDict
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 SecurityContentInvestigationProductName, SecurityDomain
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(cls, v:list[str], info:ValidationInfo)->list[Story]:
17
- return Story.mapNamesToSecurityContentObjects(v, info.context.get("output_dto",None))
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
- #return the model
33
- return model
41
+
42
+ # Combine fields from this model with fields from parent
43
+
44
+ # return the model
45
+ return model