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,46 +1,46 @@
1
- import time
2
- import uuid
3
1
  import abc
4
- import os.path
5
2
  import configparser
6
- import json
7
3
  import datetime
8
- import tqdm # type: ignore
4
+ import json
5
+ import os.path
9
6
  import pathlib
10
- from tempfile import TemporaryDirectory, mktemp
7
+ import time
8
+ import urllib.parse
9
+ import uuid
10
+ from shutil import copyfile
11
11
  from ssl import SSLEOFError, SSLZeroReturnError
12
12
  from sys import stdout
13
- from shutil import copyfile
14
- from typing import Union, Optional
13
+ from tempfile import TemporaryDirectory, mktemp
14
+ from typing import Optional, Union
15
15
 
16
- from pydantic import ConfigDict, BaseModel, PrivateAttr, Field, dataclasses
17
- import requests # type: ignore
18
- import splunklib.client as client # type: ignore
19
- from splunklib.binding import HTTPError # type: ignore
20
- from splunklib.results import JSONResultsReader, Message # type: ignore
16
+ import requests # type: ignore
17
+ import splunklib.client as client # type: ignore
21
18
  import splunklib.results
19
+ import tqdm # type: ignore
20
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, dataclasses
21
+ from splunklib.binding import HTTPError # type: ignore
22
+ from splunklib.results import JSONResultsReader, Message # type: ignore
22
23
  from urllib3 import disable_warnings
23
- import urllib.parse
24
24
 
25
- from contentctl.objects.config import test_common, Infrastructure
26
- from contentctl.objects.enums import PostTestBehavior, AnalyticsType
27
- from contentctl.objects.detection import Detection
25
+ from contentctl.actions.detection_testing.progress_bar import (
26
+ FinalTestingStates,
27
+ TestingStates,
28
+ TestReportingType,
29
+ format_pbar_string,
30
+ )
31
+ from contentctl.helper.utils import Utils
28
32
  from contentctl.objects.base_test import BaseTest
29
- from contentctl.objects.unit_test import UnitTest
33
+ from contentctl.objects.base_test_result import TestResultStatus
34
+ from contentctl.objects.config import Infrastructure, test_common
35
+ from contentctl.objects.correlation_search import CorrelationSearch, PbarData
36
+ from contentctl.objects.detection import Detection
37
+ from contentctl.objects.enums import AnalyticsType, PostTestBehavior
30
38
  from contentctl.objects.integration_test import IntegrationTest
31
- from contentctl.objects.test_attack_data import TestAttackData
32
- from contentctl.objects.unit_test_result import UnitTestResult
33
39
  from contentctl.objects.integration_test_result import IntegrationTestResult
40
+ from contentctl.objects.test_attack_data import TestAttackData
34
41
  from contentctl.objects.test_group import TestGroup
35
- from contentctl.objects.base_test_result import TestResultStatus
36
- from contentctl.objects.correlation_search import CorrelationSearch, PbarData
37
- from contentctl.helper.utils import Utils
38
- from contentctl.actions.detection_testing.progress_bar import (
39
- format_pbar_string,
40
- TestReportingType,
41
- FinalTestingStates,
42
- TestingStates
43
- )
42
+ from contentctl.objects.unit_test import UnitTest
43
+ from contentctl.objects.unit_test_result import UnitTestResult
44
44
 
45
45
 
46
46
  class SetupTestGroupResults(BaseModel):
@@ -48,9 +48,7 @@ class SetupTestGroupResults(BaseModel):
48
48
  success: bool = True
49
49
  duration: float = 0
50
50
  start_time: float
51
- model_config = ConfigDict(
52
- arbitrary_types_allowed=True
53
- )
51
+ model_config = ConfigDict(arbitrary_types_allowed=True)
54
52
 
55
53
 
56
54
  class CleanupTestGroupResults(BaseModel):
@@ -60,26 +58,31 @@ class CleanupTestGroupResults(BaseModel):
60
58
 
61
59
  class ContainerStoppedException(Exception):
62
60
  pass
61
+
62
+
63
63
  class CannotRunBaselineException(Exception):
64
- # Support for testing detections with baselines
64
+ # Support for testing detections with baselines
65
65
  # does not currently exist in contentctl.
66
- # As such, whenever we encounter a detection
66
+ # As such, whenever we encounter a detection
67
67
  # with baselines we should generate a descriptive
68
68
  # exception
69
69
  pass
70
70
 
71
+
71
72
  class ReplayIndexDoesNotExistOnServer(Exception):
72
- '''
73
+ """
73
74
  In order to replay data files into the Splunk Server
74
75
  for testing, they must be replayed into an index that
75
76
  exists. If that index does not exist, this error will
76
77
  be generated and raised before we try to do anything else
77
78
  with that Data File.
78
- '''
79
+ """
80
+
79
81
  pass
80
82
 
83
+
81
84
  @dataclasses.dataclass(frozen=False)
82
- class DetectionTestingManagerOutputDto():
85
+ class DetectionTestingManagerOutputDto:
83
86
  inputQueue: list[Detection] = Field(default_factory=list)
84
87
  outputQueue: list[Detection] = Field(default_factory=list)
85
88
  currentTestingQueue: dict[str, Union[Detection, None]] = Field(default_factory=dict)
@@ -101,9 +104,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
101
104
  _conn: client.Service = PrivateAttr()
102
105
  pbar: tqdm.tqdm = None
103
106
  start_time: Optional[float] = None
104
- model_config = ConfigDict(
105
- arbitrary_types_allowed=True
106
- )
107
+ model_config = ConfigDict(arbitrary_types_allowed=True)
107
108
 
108
109
  def __init__(self, **data):
109
110
  super().__init__(**data)
@@ -131,7 +132,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
131
132
  bar_format=f"{self.get_name()} starting",
132
133
  miniters=0,
133
134
  mininterval=0,
134
- file=stdout
135
+ file=stdout,
135
136
  )
136
137
 
137
138
  self.start_time = time.time()
@@ -140,14 +141,16 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
140
141
  (self.start, "Starting"),
141
142
  (self.get_conn, "Waiting for App Installation"),
142
143
  (self.configure_conf_file_datamodels, "Configuring Datamodels"),
143
- (self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
144
+ (
145
+ self.create_replay_index,
146
+ f"Create index '{self.sync_obj.replay_index}'",
147
+ ),
144
148
  (self.get_all_indexes, "Getting all indexes from server"),
145
149
  (self.configure_imported_roles, "Configuring Roles"),
146
150
  (self.configure_delete_indexes, "Configuring Indexes"),
147
151
  (self.configure_hec, "Configuring HEC"),
148
- (self.wait_for_ui_ready, "Finishing Setup")
152
+ (self.wait_for_ui_ready, "Finishing Setup"),
149
153
  ]:
150
-
151
154
  self.format_pbar_string(
152
155
  TestReportingType.SETUP,
153
156
  self.get_name(),
@@ -162,7 +165,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
162
165
  self.finish()
163
166
  return
164
167
 
165
- self.format_pbar_string(TestReportingType.SETUP, self.get_name(), "Finished Setup!")
168
+ self.format_pbar_string(
169
+ TestReportingType.SETUP, self.get_name(), "Finished Setup!"
170
+ )
166
171
 
167
172
  def wait_for_ui_ready(self):
168
173
  self.get_conn()
@@ -184,7 +189,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
184
189
  name="DETECTION_TESTING_HEC",
185
190
  kind="http",
186
191
  index=self.sync_obj.replay_index,
187
- indexes=",".join(self.all_indexes_on_server), # This allows the HEC to write to all indexes
192
+ indexes=",".join(
193
+ self.all_indexes_on_server
194
+ ), # This allows the HEC to write to all indexes
188
195
  useACK=True,
189
196
  )
190
197
  self.hec_token = str(res.token)
@@ -234,7 +241,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
234
241
  while True:
235
242
  self.check_for_teardown()
236
243
  try:
237
-
238
244
  conn = client.connect(
239
245
  host=self.infrastructure.instance_address,
240
246
  port=self.infrastructure.api_port,
@@ -277,7 +283,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
277
283
  time.sleep(1)
278
284
 
279
285
  def create_replay_index(self):
280
-
281
286
  try:
282
287
  self.get_conn().indexes.create(name=self.sync_obj.replay_index)
283
288
  except HTTPError as e:
@@ -292,7 +297,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
292
297
  self,
293
298
  imported_roles: list[str] = ["user", "power", "can_delete"],
294
299
  enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
295
- ):
300
+ ):
296
301
  try:
297
302
  # Set which roles should be configured. For Enterprise Security/Integration Testing,
298
303
  # we must add some extra foles.
@@ -334,9 +339,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
334
339
  self.check_for_teardown()
335
340
  time.sleep(1)
336
341
  try:
337
- _ = self.get_conn().get(
338
- f"configs/conf-{conf_file_name}", app=app_name
339
- )
342
+ _ = self.get_conn().get(f"configs/conf-{conf_file_name}", app=app_name)
340
343
  return
341
344
  except Exception:
342
345
  pass
@@ -366,7 +369,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
366
369
  parser.read(custom_acceleration_datamodels)
367
370
  if len(parser.keys()) > 1:
368
371
  self.pbar.write(
369
- f"Read {len(parser)-1} custom datamodels from {str(custom_acceleration_datamodels)}!"
372
+ f"Read {len(parser) - 1} custom datamodels from {str(custom_acceleration_datamodels)}!"
370
373
  )
371
374
 
372
375
  if not cim_acceleration_datamodels.is_file():
@@ -414,11 +417,15 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
414
417
  try:
415
418
  self.test_detection(detection)
416
419
  except ContainerStoppedException:
417
- self.pbar.write(f"Warning - container was stopped when trying to execute detection [{self.get_name()}]")
420
+ self.pbar.write(
421
+ f"Warning - container was stopped when trying to execute detection [{self.get_name()}]"
422
+ )
418
423
  self.finish()
419
424
  return
420
425
  except Exception as e:
421
- self.pbar.write(f"Error testing detection: {type(e).__name__}: {str(e)}")
426
+ self.pbar.write(
427
+ f"Error testing detection: {type(e).__name__}: {str(e)}"
428
+ )
422
429
  raise e
423
430
  finally:
424
431
  self.sync_obj.outputQueue.append(detection)
@@ -442,7 +449,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
442
449
  self.format_pbar_string(
443
450
  TestReportingType.GROUP,
444
451
  test_group.name,
445
- FinalTestingStates.SKIP.value,
452
+ FinalTestingStates.SKIP,
446
453
  start_time=time.time(),
447
454
  set_pbar=False,
448
455
  )
@@ -460,22 +467,32 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
460
467
  detection,
461
468
  test_group.integration_test,
462
469
  setup_results,
463
- test_group.unit_test.result
470
+ test_group.unit_test.result,
464
471
  )
465
472
 
466
473
  # cleanup
467
- cleanup_results = self.cleanup_test_group(test_group, setup_results.start_time)
474
+ cleanup_results = self.cleanup_test_group(
475
+ test_group, setup_results.start_time
476
+ )
468
477
 
469
478
  # update the results duration w/ the setup/cleanup time (for those not skipped)
470
- if (test_group.unit_test.result is not None) and (not test_group.unit_test_skipped()):
479
+ if (test_group.unit_test.result is not None) and (
480
+ not test_group.unit_test_skipped()
481
+ ):
471
482
  test_group.unit_test.result.duration = round(
472
- test_group.unit_test.result.duration + setup_results.duration + cleanup_results.duration,
473
- 2
483
+ test_group.unit_test.result.duration
484
+ + setup_results.duration
485
+ + cleanup_results.duration,
486
+ 2,
474
487
  )
475
- if (test_group.integration_test.result is not None) and (not test_group.integration_test_skipped()):
488
+ if (test_group.integration_test.result is not None) and (
489
+ not test_group.integration_test_skipped()
490
+ ):
476
491
  test_group.integration_test.result.duration = round(
477
- test_group.integration_test.result.duration + setup_results.duration + cleanup_results.duration,
478
- 2
492
+ test_group.integration_test.result.duration
493
+ + setup_results.duration
494
+ + cleanup_results.duration,
495
+ 2,
479
496
  )
480
497
 
481
498
  # Write test group status
@@ -483,7 +500,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
483
500
  self.format_pbar_string(
484
501
  TestReportingType.GROUP,
485
502
  test_group.name,
486
- TestingStates.DONE_GROUP.value,
503
+ TestingStates.DONE_GROUP,
487
504
  start_time=setup_results.start_time,
488
505
  set_pbar=False,
489
506
  )
@@ -504,8 +521,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
504
521
  self.format_pbar_string(
505
522
  TestReportingType.GROUP,
506
523
  test_group.name,
507
- TestingStates.BEGINNING_GROUP.value,
508
- start_time=setup_start_time
524
+ TestingStates.BEGINNING_GROUP,
525
+ start_time=setup_start_time,
509
526
  )
510
527
  # https://github.com/WoLpH/python-progressbar/issues/164
511
528
  # Use NullBar if there is more than 1 container or we are running
@@ -544,7 +561,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
544
561
  self.format_pbar_string(
545
562
  TestReportingType.GROUP,
546
563
  test_group.name,
547
- TestingStates.DELETING.value,
564
+ TestingStates.DELETING,
548
565
  start_time=test_group_start_time,
549
566
  )
550
567
 
@@ -554,8 +571,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
554
571
 
555
572
  # Return the cleanup metadata, adding start time and duration
556
573
  return CleanupTestGroupResults(
557
- duration=time.time() - cleanup_start_time,
558
- start_time=cleanup_start_time
574
+ duration=time.time() - cleanup_start_time, start_time=cleanup_start_time
559
575
  )
560
576
 
561
577
  def format_pbar_string(
@@ -589,17 +605,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
589
605
 
590
606
  # invoke the helper method
591
607
  new_string = format_pbar_string(
592
- self.pbar,
593
- test_reporting_type,
594
- test_name,
595
- state,
596
- start_time,
597
- set_pbar
608
+ self.pbar, test_reporting_type, test_name, state, start_time, set_pbar
598
609
  )
599
610
 
600
611
  # update sync status if needed
601
612
  if update_sync_status:
602
- self.sync_obj.currentTestingQueue[self.get_name()] = { # type: ignore
613
+ self.sync_obj.currentTestingQueue[self.get_name()] = { # type: ignore
603
614
  "name": state,
604
615
  "search": "N/A",
605
616
  }
@@ -612,7 +623,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
612
623
  detection: Detection,
613
624
  test: UnitTest,
614
625
  setup_results: SetupTestGroupResults,
615
- FORCE_ALL_TIME: bool = True
626
+ FORCE_ALL_TIME: bool = True,
616
627
  ):
617
628
  """
618
629
  Execute a unit test and set its results appropriately
@@ -632,7 +643,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
632
643
  self.format_pbar_string(
633
644
  TestReportingType.UNIT,
634
645
  f"{detection.name}:{test.name}",
635
- FinalTestingStates.SKIP.value,
646
+ FinalTestingStates.SKIP,
636
647
  start_time=test_start_time,
637
648
  set_pbar=False,
638
649
  )
@@ -656,7 +667,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
656
667
  self.infrastructure,
657
668
  TestResultStatus.ERROR,
658
669
  exception=setup_results.exception,
659
- duration=time.time() - test_start_time
670
+ duration=time.time() - test_start_time,
660
671
  )
661
672
 
662
673
  # report the failure to the CLI
@@ -664,7 +675,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
664
675
  self.format_pbar_string(
665
676
  TestReportingType.UNIT,
666
677
  f"{detection.name}:{test.name}",
667
- FinalTestingStates.ERROR.value,
678
+ FinalTestingStates.ERROR,
668
679
  start_time=test_start_time,
669
680
  set_pbar=False,
670
681
  )
@@ -686,10 +697,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
686
697
  try:
687
698
  # Iterate over baselines (if any)
688
699
  for baseline in detection.baselines:
689
- raise CannotRunBaselineException("Detection requires Execution of a Baseline, "
690
- "however Baseline execution is not "
691
- "currently supported in contentctl. Mark "
692
- "this as manual_test.")
700
+ raise CannotRunBaselineException(
701
+ "Detection requires Execution of a Baseline, "
702
+ "however Baseline execution is not "
703
+ "currently supported in contentctl. Mark "
704
+ "this as manual_test."
705
+ )
693
706
  self.retry_search_until_timeout(detection, test, kwargs, test_start_time)
694
707
  except CannotRunBaselineException as e:
695
708
  # Init the test result and record a failure if there was an issue during the search
@@ -699,7 +712,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
699
712
  self.infrastructure,
700
713
  TestResultStatus.ERROR,
701
714
  exception=e,
702
- duration=time.time() - test_start_time
715
+ duration=time.time() - test_start_time,
703
716
  )
704
717
  except ContainerStoppedException as e:
705
718
  raise e
@@ -712,7 +725,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
712
725
  self.infrastructure,
713
726
  TestResultStatus.ERROR,
714
727
  exception=e,
715
- duration=time.time() - test_start_time
728
+ duration=time.time() - test_start_time,
716
729
  )
717
730
 
718
731
  # Pause here if the terminate flag has NOT been set AND either of the below are true:
@@ -724,7 +737,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
724
737
  res = "ERROR"
725
738
  link = detection.search
726
739
  else:
727
- res = test.result.status.value.upper() # type: ignore
740
+ res = test.result.status.upper() # type: ignore
728
741
  link = test.result.get_summary_dict()["sid_link"]
729
742
 
730
743
  self.format_pbar_string(
@@ -746,7 +759,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
746
759
  test.result = UnitTestResult(
747
760
  message=message,
748
761
  exception=ValueError(message),
749
- status=TestResultStatus.ERROR
762
+ status=TestResultStatus.ERROR,
750
763
  )
751
764
 
752
765
  # Report a pass
@@ -755,7 +768,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
755
768
  self.format_pbar_string(
756
769
  TestReportingType.UNIT,
757
770
  f"{detection.name}:{test.name}",
758
- FinalTestingStates.PASS.value,
771
+ FinalTestingStates.PASS,
759
772
  start_time=test_start_time,
760
773
  set_pbar=False,
761
774
  )
@@ -766,7 +779,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
766
779
  self.format_pbar_string(
767
780
  TestReportingType.UNIT,
768
781
  f"{detection.name}:{test.name}",
769
- FinalTestingStates.SKIP.value,
782
+ FinalTestingStates.SKIP,
770
783
  start_time=test_start_time,
771
784
  set_pbar=False,
772
785
  )
@@ -777,7 +790,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
777
790
  self.format_pbar_string(
778
791
  TestReportingType.UNIT,
779
792
  f"{detection.name}:{test.name}",
780
- FinalTestingStates.FAIL.value,
793
+ FinalTestingStates.FAIL,
781
794
  start_time=test_start_time,
782
795
  set_pbar=False,
783
796
  )
@@ -788,7 +801,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
788
801
  self.format_pbar_string(
789
802
  TestReportingType.UNIT,
790
803
  f"{detection.name}:{test.name}",
791
- FinalTestingStates.ERROR.value,
804
+ FinalTestingStates.ERROR,
792
805
  start_time=test_start_time,
793
806
  set_pbar=False,
794
807
  )
@@ -811,7 +824,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
811
824
  detection: Detection,
812
825
  test: IntegrationTest,
813
826
  setup_results: SetupTestGroupResults,
814
- unit_test_result: Optional[UnitTestResult]
827
+ unit_test_result: Optional[UnitTestResult],
815
828
  ):
816
829
  """
817
830
  Executes an integration test on the detection
@@ -821,7 +834,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
821
834
  test_start_time = time.time()
822
835
 
823
836
  # First, check to see if the test should be skipped (Hunting or Correlation)
824
- if detection.type in [AnalyticsType.Hunting.value, AnalyticsType.Correlation.value]:
837
+ if detection.type in [AnalyticsType.Hunting, AnalyticsType.Correlation]:
825
838
  test.skip(
826
839
  f"TEST SKIPPED: detection is type {detection.type} and cannot be integration "
827
840
  "tested at this time"
@@ -843,11 +856,11 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
843
856
  # Determine the reporting state (we should only encounter SKIP/FAIL/ERROR)
844
857
  state: str
845
858
  if test.result.status == TestResultStatus.SKIP:
846
- state = FinalTestingStates.SKIP.value
859
+ state = FinalTestingStates.SKIP
847
860
  elif test.result.status == TestResultStatus.FAIL:
848
- state = FinalTestingStates.FAIL.value
861
+ state = FinalTestingStates.FAIL
849
862
  elif test.result.status == TestResultStatus.ERROR:
850
- state = FinalTestingStates.ERROR.value
863
+ state = FinalTestingStates.ERROR
851
864
  else:
852
865
  raise ValueError(
853
866
  f"Status for (integration) '{detection.name}:{test.name}' was preemptively set"
@@ -883,7 +896,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
883
896
  ),
884
897
  exception=setup_results.exception,
885
898
  duration=round(time.time() - test_start_time, 2),
886
- status=TestResultStatus.ERROR
899
+ status=TestResultStatus.ERROR,
887
900
  )
888
901
 
889
902
  # report the failure to the CLI
@@ -891,7 +904,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
891
904
  self.format_pbar_string(
892
905
  TestReportingType.INTEGRATION,
893
906
  f"{detection.name}:{test.name}",
894
- FinalTestingStates.FAIL.value,
907
+ FinalTestingStates.FAIL,
895
908
  start_time=test_start_time,
896
909
  set_pbar=False,
897
910
  )
@@ -905,7 +918,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
905
918
  pbar_data = PbarData(
906
919
  pbar=self.pbar,
907
920
  fq_test_name=f"{detection.name}:{test.name}",
908
- start_time=test_start_time
921
+ start_time=test_start_time,
909
922
  )
910
923
 
911
924
  # TODO (#228): consider reusing CorrelationSearch instances across test cases
@@ -923,7 +936,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
923
936
  test.result = IntegrationTestResult(
924
937
  message="TEST ERROR: unhandled exception in CorrelationSearch",
925
938
  exception=e,
926
- status=TestResultStatus.ERROR
939
+ status=TestResultStatus.ERROR,
927
940
  )
928
941
 
929
942
  # TODO (#229): when in interactive mode, cleanup should happen after user interaction
@@ -935,7 +948,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
935
948
  if test.result is None:
936
949
  res = "ERROR"
937
950
  else:
938
- res = test.result.status.value.upper() # type: ignore
951
+ res = test.result.status.upper() # type: ignore
939
952
 
940
953
  # Get the link to the saved search in this specific instance
941
954
  link = f"https://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
@@ -959,7 +972,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
959
972
  test.result = IntegrationTestResult(
960
973
  message=message,
961
974
  exception=ValueError(message),
962
- status=TestResultStatus.ERROR
975
+ status=TestResultStatus.ERROR,
963
976
  )
964
977
 
965
978
  # Report a pass
@@ -968,7 +981,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
968
981
  self.format_pbar_string(
969
982
  TestReportingType.INTEGRATION,
970
983
  f"{detection.name}:{test.name}",
971
- FinalTestingStates.PASS.value,
984
+ FinalTestingStates.PASS,
972
985
  start_time=test_start_time,
973
986
  set_pbar=False,
974
987
  )
@@ -979,7 +992,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
979
992
  self.format_pbar_string(
980
993
  TestReportingType.INTEGRATION,
981
994
  f"{detection.name}:{test.name}",
982
- FinalTestingStates.SKIP.value,
995
+ FinalTestingStates.SKIP,
983
996
  start_time=test_start_time,
984
997
  set_pbar=False,
985
998
  )
@@ -990,7 +1003,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
990
1003
  self.format_pbar_string(
991
1004
  TestReportingType.INTEGRATION,
992
1005
  f"{detection.name}:{test.name}",
993
- FinalTestingStates.FAIL.value,
1006
+ FinalTestingStates.FAIL,
994
1007
  start_time=test_start_time,
995
1008
  set_pbar=False,
996
1009
  )
@@ -1001,7 +1014,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1001
1014
  self.format_pbar_string(
1002
1015
  TestReportingType.INTEGRATION,
1003
1016
  f"{detection.name}:{test.name}",
1004
- FinalTestingStates.ERROR.value,
1017
+ FinalTestingStates.ERROR,
1005
1018
  start_time=test_start_time,
1006
1019
  set_pbar=False,
1007
1020
  )
@@ -1028,7 +1041,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1028
1041
  # check if the behavior is to always pause
1029
1042
  if self.global_config.post_test_behavior == PostTestBehavior.always_pause:
1030
1043
  return True
1031
- elif self.global_config.post_test_behavior == PostTestBehavior.pause_on_failure:
1044
+ elif (
1045
+ self.global_config.post_test_behavior
1046
+ == PostTestBehavior.pause_on_failure
1047
+ ):
1032
1048
  # If the behavior is to pause on failure, check for failure (either explicitly, or
1033
1049
  # just a lack of a result)
1034
1050
  if test.result is None or test.result.failed:
@@ -1053,15 +1069,15 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1053
1069
  """
1054
1070
  # Get the start time and compute the timeout
1055
1071
  search_start_time = time.time()
1056
- search_stop_time = time.time() + self.sync_obj.timeout_seconds
1072
+ search_stop_time = time.time() + self.sync_obj.timeout_seconds
1057
1073
 
1058
1074
  # Make a copy of the search string since we may
1059
1075
  # need to make some small changes to it below
1060
1076
  search = detection.search
1061
1077
 
1062
1078
  # Ensure searches that do not begin with '|' must begin with 'search '
1063
- if not search.strip().startswith("|"):
1064
- if not search.strip().startswith("search "):
1079
+ if not search.strip().startswith("|"):
1080
+ if not search.strip().startswith("search "):
1065
1081
  search = f"search {search}"
1066
1082
 
1067
1083
  # exponential backoff for wait time
@@ -1069,7 +1085,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1069
1085
 
1070
1086
  # Retry until timeout
1071
1087
  while time.time() < search_stop_time:
1072
-
1073
1088
  # This loop allows us to capture shutdown events without being
1074
1089
  # stuck in an extended sleep. Remember that this raises an exception
1075
1090
  for _ in range(pow(2, tick - 1)):
@@ -1077,8 +1092,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1077
1092
  self.format_pbar_string(
1078
1093
  TestReportingType.UNIT,
1079
1094
  f"{detection.name}:{test.name}",
1080
- TestingStates.PROCESSING.value,
1081
- start_time=start_time
1095
+ TestingStates.PROCESSING,
1096
+ start_time=start_time,
1082
1097
  )
1083
1098
 
1084
1099
  time.sleep(1)
@@ -1086,7 +1101,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1086
1101
  self.format_pbar_string(
1087
1102
  TestReportingType.UNIT,
1088
1103
  f"{detection.name}:{test.name}",
1089
- TestingStates.SEARCHING.value,
1104
+ TestingStates.SEARCHING,
1090
1105
  start_time=start_time,
1091
1106
  )
1092
1107
 
@@ -1094,10 +1109,25 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1094
1109
  job = self.get_conn().search(query=search, **kwargs)
1095
1110
  results = JSONResultsReader(job.results(output_mode="json"))
1096
1111
 
1097
- # Consolidate a set of the distinct observable field names
1098
- observable_fields_set = set([o.name for o in detection.tags.observable]) # keeping this around for later
1099
- risk_object_fields_set = set([o.name for o in detection.tags.observable if "Victim" in o.role ]) # just the "Risk Objects"
1100
- threat_object_fields_set = set([o.name for o in detection.tags.observable if "Attacker" in o.role]) # just the "threat objects"
1112
+ if detection.rba is not None:
1113
+ risk_object_fields_set = set(
1114
+ [o.field for o in detection.rba.risk_objects]
1115
+ ) # just the "Risk Objects"
1116
+ threat_object_fields_set = set(
1117
+ [o.field for o in detection.rba.threat_objects]
1118
+ ) # just the "threat objects"
1119
+ else:
1120
+ # For some searches, like Hunting Searches, there should
1121
+ # not be any risk or threat objects.
1122
+ risk_object_fields_set: set[str] = (
1123
+ set()
1124
+ ) # just the "Risk Objects" (of which there are none)
1125
+ threat_object_fields_set: set[str] = (
1126
+ set()
1127
+ ) # just the "threat objects" (of which there are none)
1128
+ full_rba_field_set: set[str] = risk_object_fields_set.union(
1129
+ threat_object_fields_set
1130
+ )
1101
1131
 
1102
1132
  # Ensure the search had at least one result
1103
1133
  if int(job.content.get("resultCount", "0")) > 0:
@@ -1121,7 +1151,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1121
1151
  missing_risk_objects = risk_object_fields_set - results_fields_set
1122
1152
  if len(missing_risk_objects) > 0:
1123
1153
  # Report a failure in such cases
1124
- e = Exception(f"The observable field(s) {missing_risk_objects} are missing in the detection results")
1154
+ e = Exception(
1155
+ f"The risk object field(s) {missing_risk_objects} are missing in the "
1156
+ "detection results"
1157
+ )
1125
1158
  test.result.set_job_content(
1126
1159
  job.content,
1127
1160
  self.infrastructure,
@@ -1130,17 +1163,21 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1130
1163
  duration=time.time() - search_start_time,
1131
1164
  )
1132
1165
 
1133
- return
1166
+ return
1134
1167
 
1135
1168
  # If we find one or more risk object fields that contain the string "null" then they were
1136
1169
  # not populated and we should throw an error. This can happen if there is a typo
1137
1170
  # on a field. In this case, the field will appear but will not contain any values
1138
1171
  current_empty_fields: set[str] = set()
1139
1172
 
1140
- for field in observable_fields_set:
1141
- if result.get(field, 'null') == 'null':
1173
+ # TODO (cmcginley): @ljstella is this something we're keeping for testing as
1174
+ # well?
1175
+ for field in full_rba_field_set:
1176
+ if result.get(field, "null") == "null":
1142
1177
  if field in risk_object_fields_set:
1143
- e = Exception(f"The risk object field {field} is missing in at least one result.")
1178
+ e = Exception(
1179
+ f"The risk object field {field} is missing in at least one result."
1180
+ )
1144
1181
  test.result.set_job_content(
1145
1182
  job.content,
1146
1183
  self.infrastructure,
@@ -1171,7 +1208,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1171
1208
  else:
1172
1209
  empty_fields = empty_fields.union(current_empty_fields)
1173
1210
 
1174
- missing_threat_objects = threat_object_fields_set - present_threat_objects
1211
+ missing_threat_objects = (
1212
+ threat_object_fields_set - present_threat_objects
1213
+ )
1175
1214
  # Report a failure if there were empty fields in a threat object in all results
1176
1215
  if len(missing_threat_objects) > 0:
1177
1216
  e = Exception(
@@ -1188,12 +1227,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1188
1227
  return
1189
1228
 
1190
1229
  test.result.set_job_content(
1191
- job.content,
1192
- self.infrastructure,
1193
- TestResultStatus.PASS,
1194
- duration=time.time() - search_start_time,
1195
- )
1196
- return
1230
+ job.content,
1231
+ self.infrastructure,
1232
+ TestResultStatus.PASS,
1233
+ duration=time.time() - search_start_time,
1234
+ )
1235
+ return
1197
1236
 
1198
1237
  else:
1199
1238
  # Report a failure if there were no results at all
@@ -1215,7 +1254,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1215
1254
  splunk_search = f'search index="{index}" host="{host}" | delete'
1216
1255
  kwargs = {"exec_mode": "blocking"}
1217
1256
  try:
1218
-
1219
1257
  job = self.get_conn().jobs.create(splunk_search, **kwargs)
1220
1258
  results_stream = job.results(output_mode="json")
1221
1259
  # TODO: should we be doing something w/ this reader?
@@ -1249,8 +1287,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1249
1287
  # Before attempting to replay the file, ensure that the index we want
1250
1288
  # to replay into actuall exists. If not, we should throw a detailed
1251
1289
  # exception that can easily be interpreted by the user.
1252
- if attack_data_file.custom_index is not None and \
1253
- attack_data_file.custom_index not in self.all_indexes_on_server:
1290
+ if (
1291
+ attack_data_file.custom_index is not None
1292
+ and attack_data_file.custom_index not in self.all_indexes_on_server
1293
+ ):
1254
1294
  raise ReplayIndexDoesNotExistOnServer(
1255
1295
  f"Unable to replay data file {attack_data_file.data} "
1256
1296
  f"into index '{attack_data_file.custom_index}'. "
@@ -1259,13 +1299,17 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1259
1299
  )
1260
1300
 
1261
1301
  tempfile = mktemp(dir=tmp_dir)
1262
- if not (str(attack_data_file.data).startswith("http://") or
1263
- str(attack_data_file.data).startswith("https://")) :
1302
+ if not (
1303
+ str(attack_data_file.data).startswith("http://")
1304
+ or str(attack_data_file.data).startswith("https://")
1305
+ ):
1264
1306
  if pathlib.Path(str(attack_data_file.data)).is_file():
1265
- self.format_pbar_string(TestReportingType.GROUP,
1266
- test_group.name,
1267
- "Copying Data",
1268
- test_group_start_time)
1307
+ self.format_pbar_string(
1308
+ TestReportingType.GROUP,
1309
+ test_group.name,
1310
+ "Copying Data",
1311
+ test_group_start_time,
1312
+ )
1269
1313
 
1270
1314
  try:
1271
1315
  copyfile(str(attack_data_file.data), tempfile)
@@ -1289,8 +1333,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1289
1333
  self.format_pbar_string(
1290
1334
  TestReportingType.GROUP,
1291
1335
  test_group.name,
1292
- TestingStates.DOWNLOADING.value,
1293
- start_time=test_group_start_time
1336
+ TestingStates.DOWNLOADING,
1337
+ start_time=test_group_start_time,
1294
1338
  )
1295
1339
 
1296
1340
  Utils.download_file_from_http(
@@ -1307,8 +1351,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1307
1351
  self.format_pbar_string(
1308
1352
  TestReportingType.GROUP,
1309
1353
  test_group.name,
1310
- TestingStates.REPLAYING.value,
1311
- start_time=test_group_start_time
1354
+ TestingStates.REPLAYING,
1355
+ start_time=test_group_start_time,
1312
1356
  )
1313
1357
 
1314
1358
  self.hec_raw_replay(tempfile, attack_data_file)
@@ -1392,7 +1436,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1392
1436
  requested_acks = {"acks": [jsonResponse["ackId"]]}
1393
1437
  while True:
1394
1438
  try:
1395
-
1396
1439
  res = requests.post(
1397
1440
  url_with_hec_ack_path,
1398
1441
  json=requested_acks,
@@ -1424,7 +1467,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1424
1467
  pass
1425
1468
 
1426
1469
  def finish(self):
1427
- self.pbar.bar_format = f"Finished running tests on instance: [{self.get_name()}]"
1470
+ self.pbar.bar_format = (
1471
+ f"Finished running tests on instance: [{self.get_name()}]"
1472
+ )
1428
1473
  self.pbar.update()
1429
1474
  self.pbar.close()
1430
1475