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
@@ -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)
@@ -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
@@ -505,7 +522,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
505
522
  TestReportingType.GROUP,
506
523
  test_group.name,
507
524
  TestingStates.BEGINNING_GROUP,
508
- start_time=setup_start_time
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
@@ -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
@@ -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
@@ -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.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
@@ -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
@@ -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
@@ -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.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
@@ -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)):
@@ -1078,7 +1093,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1078
1093
  TestReportingType.UNIT,
1079
1094
  f"{detection.name}:{test.name}",
1080
1095
  TestingStates.PROCESSING,
1081
- start_time=start_time
1096
+ start_time=start_time,
1082
1097
  )
1083
1098
 
1084
1099
  time.sleep(1)
@@ -1094,11 +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
- # TODO (cmcginley): @ljstella you're removing this ultimately, right?
1098
- # Consolidate a set of the distinct observable field names
1099
- observable_fields_set = set([o.name for o in detection.tags.observable]) # keeping this around for later
1100
- risk_object_fields_set = set([o.name for o in detection.tags.observable if "Victim" in o.role ]) # just the "Risk Objects"
1101
- 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
+ )
1102
1131
 
1103
1132
  # Ensure the search had at least one result
1104
1133
  if int(job.content.get("resultCount", "0")) > 0:
@@ -1134,7 +1163,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1134
1163
  duration=time.time() - search_start_time,
1135
1164
  )
1136
1165
 
1137
- return
1166
+ return
1138
1167
 
1139
1168
  # If we find one or more risk object fields that contain the string "null" then they were
1140
1169
  # not populated and we should throw an error. This can happen if there is a typo
@@ -1143,10 +1172,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1143
1172
 
1144
1173
  # TODO (cmcginley): @ljstella is this something we're keeping for testing as
1145
1174
  # well?
1146
- for field in observable_fields_set:
1147
- if result.get(field, 'null') == 'null':
1175
+ for field in full_rba_field_set:
1176
+ if result.get(field, "null") == "null":
1148
1177
  if field in risk_object_fields_set:
1149
- 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
+ )
1150
1181
  test.result.set_job_content(
1151
1182
  job.content,
1152
1183
  self.infrastructure,
@@ -1177,7 +1208,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1177
1208
  else:
1178
1209
  empty_fields = empty_fields.union(current_empty_fields)
1179
1210
 
1180
- missing_threat_objects = threat_object_fields_set - present_threat_objects
1211
+ missing_threat_objects = (
1212
+ threat_object_fields_set - present_threat_objects
1213
+ )
1181
1214
  # Report a failure if there were empty fields in a threat object in all results
1182
1215
  if len(missing_threat_objects) > 0:
1183
1216
  e = Exception(
@@ -1194,12 +1227,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1194
1227
  return
1195
1228
 
1196
1229
  test.result.set_job_content(
1197
- job.content,
1198
- self.infrastructure,
1199
- TestResultStatus.PASS,
1200
- duration=time.time() - search_start_time,
1201
- )
1202
- return
1230
+ job.content,
1231
+ self.infrastructure,
1232
+ TestResultStatus.PASS,
1233
+ duration=time.time() - search_start_time,
1234
+ )
1235
+ return
1203
1236
 
1204
1237
  else:
1205
1238
  # Report a failure if there were no results at all
@@ -1221,7 +1254,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1221
1254
  splunk_search = f'search index="{index}" host="{host}" | delete'
1222
1255
  kwargs = {"exec_mode": "blocking"}
1223
1256
  try:
1224
-
1225
1257
  job = self.get_conn().jobs.create(splunk_search, **kwargs)
1226
1258
  results_stream = job.results(output_mode="json")
1227
1259
  # TODO: should we be doing something w/ this reader?
@@ -1255,8 +1287,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1255
1287
  # Before attempting to replay the file, ensure that the index we want
1256
1288
  # to replay into actuall exists. If not, we should throw a detailed
1257
1289
  # exception that can easily be interpreted by the user.
1258
- if attack_data_file.custom_index is not None and \
1259
- 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
+ ):
1260
1294
  raise ReplayIndexDoesNotExistOnServer(
1261
1295
  f"Unable to replay data file {attack_data_file.data} "
1262
1296
  f"into index '{attack_data_file.custom_index}'. "
@@ -1265,13 +1299,17 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1265
1299
  )
1266
1300
 
1267
1301
  tempfile = mktemp(dir=tmp_dir)
1268
- if not (str(attack_data_file.data).startswith("http://") or
1269
- 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
+ ):
1270
1306
  if pathlib.Path(str(attack_data_file.data)).is_file():
1271
- self.format_pbar_string(TestReportingType.GROUP,
1272
- test_group.name,
1273
- "Copying Data",
1274
- 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
+ )
1275
1313
 
1276
1314
  try:
1277
1315
  copyfile(str(attack_data_file.data), tempfile)
@@ -1296,7 +1334,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1296
1334
  TestReportingType.GROUP,
1297
1335
  test_group.name,
1298
1336
  TestingStates.DOWNLOADING,
1299
- start_time=test_group_start_time
1337
+ start_time=test_group_start_time,
1300
1338
  )
1301
1339
 
1302
1340
  Utils.download_file_from_http(
@@ -1314,7 +1352,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1314
1352
  TestReportingType.GROUP,
1315
1353
  test_group.name,
1316
1354
  TestingStates.REPLAYING,
1317
- start_time=test_group_start_time
1355
+ start_time=test_group_start_time,
1318
1356
  )
1319
1357
 
1320
1358
  self.hec_raw_replay(tempfile, attack_data_file)
@@ -1398,7 +1436,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1398
1436
  requested_acks = {"acks": [jsonResponse["ackId"]]}
1399
1437
  while True:
1400
1438
  try:
1401
-
1402
1439
  res = requests.post(
1403
1440
  url_with_hec_ack_path,
1404
1441
  json=requested_acks,
@@ -1430,7 +1467,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1430
1467
  pass
1431
1468
 
1432
1469
  def finish(self):
1433
- 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
+ )
1434
1473
  self.pbar.update()
1435
1474
  self.pbar.close()
1436
1475