contentctl 5.5.6__py3-none-any.whl → 5.5.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentctl/actions/deploy_acs.py +5 -3
- contentctl/actions/detection_testing/DetectionTestingManager.py +3 -3
- contentctl/actions/detection_testing/GitService.py +4 -4
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +3 -3
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +15 -17
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +9 -8
- contentctl/actions/detection_testing/progress_bar.py +2 -1
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +4 -3
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +4 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +7 -7
- contentctl/actions/doc_gen.py +1 -2
- contentctl/actions/release_notes.py +2 -2
- contentctl/actions/reporting.py +3 -3
- contentctl/actions/test.py +2 -3
- contentctl/actions/validate.py +1 -1
- contentctl/api.py +7 -6
- contentctl/contentctl.py +1 -1
- contentctl/enrichments/attack_enrichment.py +1 -1
- contentctl/enrichments/cve_enrichment.py +9 -6
- contentctl/enrichments/splunk_app_enrichment.py +5 -4
- contentctl/helper/link_validator.py +7 -7
- contentctl/helper/splunk_app.py +6 -6
- contentctl/helper/utils.py +8 -8
- contentctl/input/director.py +3 -2
- contentctl/input/new_content_questions.py +1 -0
- contentctl/input/yml_reader.py +2 -2
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +1 -1
- contentctl/objects/alert_action.py +4 -2
- contentctl/objects/atomic.py +8 -5
- contentctl/objects/base_test.py +1 -1
- contentctl/objects/base_test_result.py +2 -2
- contentctl/objects/baseline_tags.py +7 -6
- contentctl/objects/config.py +5 -5
- contentctl/objects/correlation_search.py +156 -139
- contentctl/objects/dashboard.py +1 -1
- contentctl/objects/deployment_email.py +1 -0
- contentctl/objects/deployment_notable.py +3 -1
- contentctl/objects/deployment_phantom.py +1 -0
- contentctl/objects/deployment_rba.py +1 -0
- contentctl/objects/deployment_scheduling.py +1 -0
- contentctl/objects/deployment_slack.py +1 -0
- contentctl/objects/detection_stanza.py +1 -1
- contentctl/objects/integration_test.py +2 -2
- contentctl/objects/investigation_tags.py +6 -3
- contentctl/objects/manual_test.py +2 -2
- contentctl/objects/playbook_tags.py +247 -10
- contentctl/objects/risk_analysis_action.py +1 -1
- contentctl/objects/savedsearches_conf.py +3 -3
- contentctl/objects/story_tags.py +1 -1
- contentctl/objects/test_group.py +2 -2
- contentctl/objects/throttling.py +2 -1
- contentctl/objects/unit_test.py +2 -2
- contentctl/objects/unit_test_baseline.py +2 -1
- contentctl/objects/unit_test_result.py +4 -2
- contentctl/output/conf_output.py +2 -2
- contentctl/output/conf_writer.py +5 -5
- contentctl/output/doc_md_output.py +0 -1
- contentctl/output/jinja_writer.py +1 -0
- contentctl/output/json_writer.py +1 -1
- contentctl/output/yml_writer.py +3 -2
- {contentctl-5.5.6.dist-info → contentctl-5.5.8.dist-info}/METADATA +3 -3
- {contentctl-5.5.6.dist-info → contentctl-5.5.8.dist-info}/RECORD +65 -65
- {contentctl-5.5.6.dist-info → contentctl-5.5.8.dist-info}/LICENSE.md +0 -0
- {contentctl-5.5.6.dist-info → contentctl-5.5.8.dist-info}/WHEEL +0 -0
- {contentctl-5.5.6.dist-info → contentctl-5.5.8.dist-info}/entry_points.txt +0 -0
|
@@ -68,15 +68,20 @@ class TimeoutConfig(IntEnum):
|
|
|
68
68
|
Configuration values for the exponential backoff timer
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
|
+
# NOTE: Some detections take longer to generate their risk/notables than other; testing has
|
|
72
|
+
# shown that in a single run, 99% detections could generate risk/notables within 30s and less than 1%
|
|
73
|
+
# detections (20 to 30 detections) would need 60 to 90s to wait for risk/notables.
|
|
74
|
+
|
|
71
75
|
# base amount to sleep for before beginning exponential backoff during testing
|
|
72
|
-
BASE_SLEEP =
|
|
76
|
+
BASE_SLEEP = 2
|
|
73
77
|
|
|
74
|
-
# NOTE:
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
# NOTE: Based on testing, there are 45 detections couldn't generate risk/notables within single dispatch, and
|
|
79
|
+
# they needed to be retried; 90s is a reasonable wait time before retrying dispatching the SavedSearch
|
|
80
|
+
# Wait time before retrying dispatching the SavedSearch
|
|
81
|
+
RETRY_DISPATCH = 90
|
|
82
|
+
|
|
83
|
+
# Time elapsed before adding additional wait time
|
|
84
|
+
ADD_WAIT_TIME = 30
|
|
80
85
|
|
|
81
86
|
|
|
82
87
|
# TODO (#226): evaluate sane defaults for timeframe for integration testing (e.g. 5y is good
|
|
@@ -88,7 +93,7 @@ class ScheduleConfig(StrEnum):
|
|
|
88
93
|
|
|
89
94
|
EARLIEST_TIME = "-5y@y"
|
|
90
95
|
LATEST_TIME = "-1m@m"
|
|
91
|
-
CRON_SCHEDULE = "
|
|
96
|
+
CRON_SCHEDULE = "0 0 1 1 *"
|
|
92
97
|
|
|
93
98
|
|
|
94
99
|
class ResultIterator:
|
|
@@ -202,6 +207,9 @@ class CorrelationSearch(BaseModel):
|
|
|
202
207
|
# cleanup of this index
|
|
203
208
|
test_index: str | None = Field(default=None, min_length=1)
|
|
204
209
|
|
|
210
|
+
# The search ID of the last dispatched search; this is used to query for risk/notable events
|
|
211
|
+
sid: str | None = Field(default=None)
|
|
212
|
+
|
|
205
213
|
# The logger to use (logs all go to a null pipe unless ENABLE_LOGGING is set to True, so as not
|
|
206
214
|
# to conflict w/ tqdm)
|
|
207
215
|
logger: logging.Logger = Field(
|
|
@@ -437,6 +445,34 @@ class CorrelationSearch(BaseModel):
|
|
|
437
445
|
if refresh:
|
|
438
446
|
self.refresh()
|
|
439
447
|
|
|
448
|
+
def dispatch(self) -> splunklib.Job:
|
|
449
|
+
"""Dispatches the SavedSearch
|
|
450
|
+
|
|
451
|
+
Dispatches the SavedSearch entity, returning a Job object representing the search job.
|
|
452
|
+
:return: a splunklib.Job object representing the search job when the SavedSearch is finished running
|
|
453
|
+
"""
|
|
454
|
+
self.logger.debug(f"Dispatching {self.name}...")
|
|
455
|
+
try:
|
|
456
|
+
job = self.saved_search.dispatch(trigger_actions=True)
|
|
457
|
+
|
|
458
|
+
time_to_execute = 0
|
|
459
|
+
# Check if the job is finished
|
|
460
|
+
while not job.is_done():
|
|
461
|
+
self.logger.debug(f"Job {job.sid} is still running...")
|
|
462
|
+
time.sleep(1)
|
|
463
|
+
time_to_execute += 1
|
|
464
|
+
self.logger.debug(
|
|
465
|
+
f"Job {job.sid} has finished running in {time_to_execute} seconds."
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
self.sid = job.sid
|
|
469
|
+
|
|
470
|
+
return job # type: ignore
|
|
471
|
+
except HTTPError as e:
|
|
472
|
+
raise ServerError(
|
|
473
|
+
f"HTTP error encountered while dispatching detection: {e}"
|
|
474
|
+
)
|
|
475
|
+
|
|
440
476
|
def disable(self, refresh: bool = True) -> None:
|
|
441
477
|
"""Disables the SavedSearch
|
|
442
478
|
|
|
@@ -486,22 +522,6 @@ class CorrelationSearch(BaseModel):
|
|
|
486
522
|
if refresh:
|
|
487
523
|
self.refresh()
|
|
488
524
|
|
|
489
|
-
def force_run(self, refresh: bool = True) -> None:
|
|
490
|
-
"""Forces a detection run
|
|
491
|
-
|
|
492
|
-
Enables the detection, adjusts the cron schedule to run every 1 minute, and widens the earliest/latest window
|
|
493
|
-
to run on test data.
|
|
494
|
-
:param refresh: a bool indicating whether to refresh the metadata for the detection (default True)
|
|
495
|
-
"""
|
|
496
|
-
self.update_timeframe(refresh=False)
|
|
497
|
-
if not self.enabled:
|
|
498
|
-
self.enable(refresh=False)
|
|
499
|
-
else:
|
|
500
|
-
self.logger.warning(f"Detection '{self.name}' was already enabled")
|
|
501
|
-
|
|
502
|
-
if refresh:
|
|
503
|
-
self.refresh()
|
|
504
|
-
|
|
505
525
|
def risk_event_exists(self) -> bool:
|
|
506
526
|
"""Whether at least one matching risk event exists
|
|
507
527
|
|
|
@@ -533,12 +553,8 @@ class CorrelationSearch(BaseModel):
|
|
|
533
553
|
)
|
|
534
554
|
return self._risk_events
|
|
535
555
|
|
|
536
|
-
#
|
|
537
|
-
|
|
538
|
-
query = (
|
|
539
|
-
f'search index=risk search_name="{self.name}" [search index=risk search '
|
|
540
|
-
f'search_name="{self.name}" | tail 1 | fields orig_sid] | tojson'
|
|
541
|
-
)
|
|
556
|
+
# Search for all risk events from a single search (indicated by orig_sid)
|
|
557
|
+
query = f'search index=risk search_name="{self.name}" orig_sid="{self.sid}" | tojson'
|
|
542
558
|
result_iterator = self._search(query)
|
|
543
559
|
|
|
544
560
|
# Iterate over the events, storing them in a list and checking for any errors
|
|
@@ -610,11 +626,8 @@ class CorrelationSearch(BaseModel):
|
|
|
610
626
|
)
|
|
611
627
|
return self._notable_events
|
|
612
628
|
|
|
613
|
-
# Search for all notable events from a single
|
|
614
|
-
query =
|
|
615
|
-
f'search index=notable search_name="{self.name}" [search index=notable search '
|
|
616
|
-
f'search_name="{self.name}" | tail 1 | fields orig_sid] | tojson'
|
|
617
|
-
)
|
|
629
|
+
# Search for all notable events from a single search (indicated by orig_sid)
|
|
630
|
+
query = f'search index=notable search_name="{self.name}" orig_sid="{self.sid}" | tojson'
|
|
618
631
|
result_iterator = self._search(query)
|
|
619
632
|
|
|
620
633
|
# Iterate over the events, storing them in a list and checking for any errors
|
|
@@ -688,12 +701,10 @@ class CorrelationSearch(BaseModel):
|
|
|
688
701
|
)
|
|
689
702
|
return self._risk_dm_events
|
|
690
703
|
|
|
691
|
-
#
|
|
692
|
-
# Search for all risk data model events from a single scheduled search (indicated by
|
|
704
|
+
# Search for all risk data model events from a single search (indicated by
|
|
693
705
|
# orig_sid)
|
|
694
706
|
query = (
|
|
695
|
-
f'datamodel Risk All_Risk flat | search search_name="{self.name}"
|
|
696
|
-
f'All_Risk flat | search search_name="{self.name}" | tail 1 | fields orig_sid] '
|
|
707
|
+
f'datamodel Risk All_Risk flat | search search_name="{self.name}" orig_sid="{self.sid}" '
|
|
697
708
|
"| tojson"
|
|
698
709
|
)
|
|
699
710
|
result_iterator = self._search(query)
|
|
@@ -881,10 +892,105 @@ class CorrelationSearch(BaseModel):
|
|
|
881
892
|
return True
|
|
882
893
|
return False
|
|
883
894
|
|
|
895
|
+
def validate_ara_events(self) -> None:
|
|
896
|
+
"""
|
|
897
|
+
Validate the risk and notable events created by the saved search.
|
|
898
|
+
An exception is raised if the validation fails for either risk or notable events.
|
|
899
|
+
|
|
900
|
+
:raises ValidationFailed: If the expected risk events are not found or validation fails.
|
|
901
|
+
"""
|
|
902
|
+
# Validate risk events
|
|
903
|
+
if self.has_risk_analysis_action:
|
|
904
|
+
self.logger.debug("Checking for matching risk events")
|
|
905
|
+
if self.risk_event_exists():
|
|
906
|
+
# TODO (PEX-435): should this in the retry loop? or outside it?
|
|
907
|
+
# -> I've observed there being a missing risk event (15/16) on
|
|
908
|
+
# the first few tries, so this does help us check for true
|
|
909
|
+
# positives; BUT, if we have lots of failing detections, this
|
|
910
|
+
# will definitely add to the total wait time
|
|
911
|
+
# -> certain types of failures (e.g. risk message, or any value
|
|
912
|
+
# checking) should fail testing automatically
|
|
913
|
+
# -> other types, like those based on counts of risk events,
|
|
914
|
+
# should happen should fail more slowly as more events may be
|
|
915
|
+
# produced
|
|
916
|
+
self.validate_risk_events()
|
|
917
|
+
else:
|
|
918
|
+
raise ValidationFailed(
|
|
919
|
+
f"TEST FAILED: No matching risk event created for: {self.name}"
|
|
920
|
+
)
|
|
921
|
+
else:
|
|
922
|
+
self.logger.debug(f"No risk action defined for '{self.name}'")
|
|
923
|
+
|
|
924
|
+
# Validate notable events
|
|
925
|
+
if self.has_notable_action:
|
|
926
|
+
self.logger.debug("Checking for matching notable events")
|
|
927
|
+
# NOTE: because we check this last, if both fail, the error message about notables will
|
|
928
|
+
# always be the last to be added and thus the one surfaced to the user
|
|
929
|
+
if self.notable_event_exists():
|
|
930
|
+
# TODO (PEX-435): should this in the retry loop? or outside it?
|
|
931
|
+
self.validate_notable_events()
|
|
932
|
+
pass
|
|
933
|
+
else:
|
|
934
|
+
raise ValidationFailed(
|
|
935
|
+
f"TEST FAILED: No matching notable event created for: {self.name}"
|
|
936
|
+
)
|
|
937
|
+
else:
|
|
938
|
+
self.logger.debug(f"No notable action defined for '{self.name}'")
|
|
939
|
+
|
|
940
|
+
def dispatch_and_validate(self, elapsed_sleep_time: dict[str, int]) -> None:
|
|
941
|
+
"""Dispatch the saved search and validate the risk/notable events
|
|
942
|
+
|
|
943
|
+
Dispatches the saved search and validates the risk/notable events created by it. If any
|
|
944
|
+
validation fails, raises a ValidationFailed exception.
|
|
945
|
+
|
|
946
|
+
:param elapsed_sleep_time: Dictionary tracking the total elapsed sleep time across retries.
|
|
947
|
+
:type elapsed_sleep_time: dict[str, int]
|
|
948
|
+
|
|
949
|
+
:raises ValidationFailed: If validation of risk/notable events fails after all retries.
|
|
950
|
+
"""
|
|
951
|
+
self.dispatch()
|
|
952
|
+
|
|
953
|
+
wait_time = TimeoutConfig.BASE_SLEEP
|
|
954
|
+
time_elapsed = 0
|
|
955
|
+
validation_error = None
|
|
956
|
+
|
|
957
|
+
while time_elapsed <= TimeoutConfig.RETRY_DISPATCH:
|
|
958
|
+
validation_start_time = time.time()
|
|
959
|
+
|
|
960
|
+
# reset validation_error for each iteration
|
|
961
|
+
validation_error = None
|
|
962
|
+
|
|
963
|
+
# wait at least 30 seconds before adding to the wait time (we expect the vast majority of detections to show results w/in that window)
|
|
964
|
+
if time_elapsed > TimeoutConfig.ADD_WAIT_TIME:
|
|
965
|
+
time.sleep(wait_time)
|
|
966
|
+
elapsed_sleep_time["elapsed_sleep_time"] += wait_time
|
|
967
|
+
wait_time = min(
|
|
968
|
+
TimeoutConfig.RETRY_DISPATCH - int(time_elapsed), wait_time * 2
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
try:
|
|
972
|
+
self.validate_ara_events()
|
|
973
|
+
except ValidationFailed as e:
|
|
974
|
+
self.logger.error(f"Validation failed: {e}")
|
|
975
|
+
validation_error = e
|
|
976
|
+
# break out of the loop if validation passes
|
|
977
|
+
if validation_error is None:
|
|
978
|
+
self.logger.info(
|
|
979
|
+
f"Validation passed for {self.name} after {elapsed_sleep_time['elapsed_sleep_time']} seconds"
|
|
980
|
+
)
|
|
981
|
+
break
|
|
982
|
+
|
|
983
|
+
validation_end_time = time.time()
|
|
984
|
+
time_elapsed += validation_end_time - validation_start_time
|
|
985
|
+
|
|
986
|
+
if validation_error is not None:
|
|
987
|
+
raise validation_error
|
|
988
|
+
|
|
884
989
|
# NOTE: it would be more ideal to switch this to a system which gets the handle of the saved search job and polls
|
|
885
990
|
# it for completion, but that seems more tricky
|
|
886
991
|
def test(
|
|
887
|
-
self,
|
|
992
|
+
self,
|
|
993
|
+
raise_on_exc: bool = False,
|
|
888
994
|
) -> IntegrationTestResult:
|
|
889
995
|
"""Execute the integration test
|
|
890
996
|
|
|
@@ -892,43 +998,17 @@ class CorrelationSearch(BaseModel):
|
|
|
892
998
|
and clear the indexes if so. Then, we force a run of the detection, wait for `sleep` seconds, and finally we
|
|
893
999
|
validate that the appropriate risk/notable events seem to have been created. NOTE: assumes the data already
|
|
894
1000
|
exists in the instance
|
|
895
|
-
:param max_sleep: max number of seconds to sleep for after enabling the detection before we check for created
|
|
896
|
-
events; re-checks are made upon failures using an exponential backoff until the max is reached
|
|
897
1001
|
:param raise_on_exc: bool flag indicating if an exception should be raised when caught by the test routine, or
|
|
898
1002
|
if the error state should just be recorded for the test
|
|
899
1003
|
"""
|
|
900
|
-
# max_sleep must be greater than the base value we must wait for the scheduled searchjob to run (jobs run every
|
|
901
|
-
# 60s)
|
|
902
|
-
if max_sleep < TimeoutConfig.BASE_SLEEP:
|
|
903
|
-
raise ClientError(
|
|
904
|
-
f"max_sleep value of {max_sleep} is less than the base sleep required "
|
|
905
|
-
f"({TimeoutConfig.BASE_SLEEP})"
|
|
906
|
-
)
|
|
907
1004
|
|
|
908
1005
|
# initialize result as None
|
|
909
1006
|
result: IntegrationTestResult | None = None
|
|
910
1007
|
|
|
911
1008
|
# keep track of time slept and number of attempts for exponential backoff (base 2)
|
|
912
|
-
elapsed_sleep_time = 0
|
|
913
|
-
num_tries = 0
|
|
914
|
-
|
|
915
|
-
# set the initial base sleep time
|
|
916
|
-
time_to_sleep = TimeoutConfig.BASE_SLEEP
|
|
1009
|
+
elapsed_sleep_time = {"elapsed_sleep_time": 0}
|
|
917
1010
|
|
|
918
1011
|
try:
|
|
919
|
-
# first make sure the indexes are currently empty and the detection is starting from a disabled state
|
|
920
|
-
self.logger.debug("Cleaning up any pre-existing risk/notable events...")
|
|
921
|
-
self.update_pbar(TestingStates.PRE_CLEANUP)
|
|
922
|
-
if self.risk_event_exists():
|
|
923
|
-
self.logger.warning(
|
|
924
|
-
f"Risk events matching '{self.name}' already exist; marking for deletion"
|
|
925
|
-
)
|
|
926
|
-
if self.notable_event_exists():
|
|
927
|
-
self.logger.warning(
|
|
928
|
-
f"Notable events matching '{self.name}' already exist; marking for deletion"
|
|
929
|
-
)
|
|
930
|
-
self.cleanup()
|
|
931
|
-
|
|
932
1012
|
# skip test if no risk or notable action defined
|
|
933
1013
|
if not self.has_risk_analysis_action and not self.has_notable_action:
|
|
934
1014
|
message = (
|
|
@@ -944,95 +1024,32 @@ class CorrelationSearch(BaseModel):
|
|
|
944
1024
|
# force the detection to run
|
|
945
1025
|
self.logger.info(f"Forcing a run on {self.name}")
|
|
946
1026
|
self.update_pbar(TestingStates.FORCE_RUN)
|
|
947
|
-
self.
|
|
948
|
-
|
|
949
|
-
# loop so long as the elapsed time is less than max_sleep
|
|
950
|
-
while elapsed_sleep_time < max_sleep:
|
|
951
|
-
# sleep so the detection job can finish
|
|
952
|
-
self.logger.info(
|
|
953
|
-
f"Waiting {time_to_sleep} for {self.name} so it can finish"
|
|
954
|
-
)
|
|
955
|
-
self.update_pbar(TestingStates.VALIDATING)
|
|
956
|
-
time.sleep(time_to_sleep)
|
|
957
|
-
elapsed_sleep_time += time_to_sleep
|
|
958
|
-
|
|
959
|
-
self.logger.info(
|
|
960
|
-
f"Validating detection (attempt #{num_tries + 1} - {elapsed_sleep_time} seconds elapsed of "
|
|
961
|
-
f"{max_sleep} max)"
|
|
962
|
-
)
|
|
1027
|
+
self.update_timeframe(refresh=False)
|
|
1028
|
+
self.enable(refresh=False)
|
|
963
1029
|
|
|
1030
|
+
attempt = 1
|
|
1031
|
+
while attempt <= 3:
|
|
964
1032
|
# reset the result to None on each loop iteration
|
|
965
1033
|
result = None
|
|
966
1034
|
|
|
1035
|
+
attempt += 1
|
|
967
1036
|
try:
|
|
968
|
-
|
|
969
|
-
if self.has_risk_analysis_action:
|
|
970
|
-
self.logger.debug("Checking for matching risk events")
|
|
971
|
-
if self.risk_event_exists():
|
|
972
|
-
# TODO (PEX-435): should this in the retry loop? or outside it?
|
|
973
|
-
# -> I've observed there being a missing risk event (15/16) on
|
|
974
|
-
# the first few tries, so this does help us check for true
|
|
975
|
-
# positives; BUT, if we have lots of failing detections, this
|
|
976
|
-
# will definitely add to the total wait time
|
|
977
|
-
# -> certain types of failures (e.g. risk message, or any value
|
|
978
|
-
# checking) should fail testing automatically
|
|
979
|
-
# -> other types, like those based on counts of risk events,
|
|
980
|
-
# should happen should fail more slowly as more events may be
|
|
981
|
-
# produced
|
|
982
|
-
self.validate_risk_events()
|
|
983
|
-
else:
|
|
984
|
-
raise ValidationFailed(
|
|
985
|
-
f"TEST FAILED: No matching risk event created for: {self.name}"
|
|
986
|
-
)
|
|
987
|
-
else:
|
|
988
|
-
self.logger.debug(
|
|
989
|
-
f"No risk action defined for '{self.name}'"
|
|
990
|
-
)
|
|
991
|
-
|
|
992
|
-
# Validate notable events
|
|
993
|
-
if self.has_notable_action:
|
|
994
|
-
self.logger.debug("Checking for matching notable events")
|
|
995
|
-
# NOTE: because we check this last, if both fail, the error message about notables will
|
|
996
|
-
# always be the last to be added and thus the one surfaced to the user
|
|
997
|
-
if self.notable_event_exists():
|
|
998
|
-
# TODO (PEX-435): should this in the retry loop? or outside it?
|
|
999
|
-
self.validate_notable_events()
|
|
1000
|
-
pass
|
|
1001
|
-
else:
|
|
1002
|
-
raise ValidationFailed(
|
|
1003
|
-
f"TEST FAILED: No matching notable event created for: {self.name}"
|
|
1004
|
-
)
|
|
1005
|
-
else:
|
|
1006
|
-
self.logger.debug(
|
|
1007
|
-
f"No notable action defined for '{self.name}'"
|
|
1008
|
-
)
|
|
1037
|
+
self.dispatch_and_validate(elapsed_sleep_time)
|
|
1009
1038
|
except ValidationFailed as e:
|
|
1010
1039
|
self.logger.error(f"Risk/notable validation failed: {e}")
|
|
1011
1040
|
result = IntegrationTestResult(
|
|
1012
1041
|
status=TestResultStatus.FAIL,
|
|
1013
1042
|
message=f"TEST FAILED: {e}",
|
|
1014
|
-
wait_duration=elapsed_sleep_time,
|
|
1043
|
+
wait_duration=elapsed_sleep_time["elapsed_sleep_time"],
|
|
1015
1044
|
)
|
|
1016
|
-
|
|
1017
|
-
# if result is still None, then all checks passed and we can break the loop
|
|
1018
1045
|
if result is None:
|
|
1019
1046
|
result = IntegrationTestResult(
|
|
1020
1047
|
status=TestResultStatus.PASS,
|
|
1021
1048
|
message=f"TEST PASSED: Expected risk and/or notable events were created for: {self.name}",
|
|
1022
|
-
wait_duration=elapsed_sleep_time,
|
|
1049
|
+
wait_duration=elapsed_sleep_time["elapsed_sleep_time"],
|
|
1023
1050
|
)
|
|
1024
1051
|
break
|
|
1025
1052
|
|
|
1026
|
-
# increment number of attempts to validate detection
|
|
1027
|
-
num_tries += 1
|
|
1028
|
-
|
|
1029
|
-
# compute the next time to sleep for
|
|
1030
|
-
time_to_sleep = 2**num_tries
|
|
1031
|
-
|
|
1032
|
-
# if the computed time to sleep will exceed max_sleep, adjust appropriately
|
|
1033
|
-
if (elapsed_sleep_time + time_to_sleep) > max_sleep:
|
|
1034
|
-
time_to_sleep = max_sleep - elapsed_sleep_time
|
|
1035
|
-
|
|
1036
1053
|
# TODO (PEX-436): should cleanup be in a finally block so it runs even on exception?
|
|
1037
1054
|
# cleanup the created events, disable the detection and return the result
|
|
1038
1055
|
self.logger.debug("Cleaning up any created risk/notable events...")
|
|
@@ -1043,7 +1060,7 @@ class CorrelationSearch(BaseModel):
|
|
|
1043
1060
|
result = IntegrationTestResult(
|
|
1044
1061
|
status=TestResultStatus.ERROR,
|
|
1045
1062
|
message=f"TEST FAILED (ERROR): Exception raised during integration test: {e}",
|
|
1046
|
-
wait_duration=elapsed_sleep_time,
|
|
1063
|
+
wait_duration=elapsed_sleep_time["elapsed_sleep_time"],
|
|
1047
1064
|
exception=e,
|
|
1048
1065
|
)
|
|
1049
1066
|
self.logger.exception(result.message) # type: ignore
|
contentctl/objects/dashboard.py
CHANGED
|
@@ -79,7 +79,7 @@ class Dashboard(SecurityContentObject):
|
|
|
79
79
|
try:
|
|
80
80
|
json_obj: dict[str, Any] = json.load(jsonFilePointer)
|
|
81
81
|
except Exception as e:
|
|
82
|
-
raise ValueError(f"Unable to load data from {json_file_path}: {
|
|
82
|
+
raise ValueError(f"Unable to load data from {json_file_path}: {e!s}")
|
|
83
83
|
|
|
84
84
|
name_from_file = data.get("name", None)
|
|
85
85
|
name_from_json = json_obj.get("title", None)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
2
|
|
|
3
3
|
from contentctl.objects.base_test import BaseTest, TestType
|
|
4
|
-
from contentctl.objects.unit_test import UnitTest
|
|
5
|
-
from contentctl.objects.integration_test_result import IntegrationTestResult
|
|
6
4
|
from contentctl.objects.base_test_result import TestResultStatus
|
|
5
|
+
from contentctl.objects.integration_test_result import IntegrationTestResult
|
|
6
|
+
from contentctl.objects.unit_test import UnitTest
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class IntegrationTest(BaseTest):
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import List
|
|
4
|
+
|
|
3
5
|
from pydantic import (
|
|
4
6
|
BaseModel,
|
|
7
|
+
ConfigDict,
|
|
5
8
|
Field,
|
|
6
|
-
field_validator,
|
|
7
9
|
ValidationInfo,
|
|
10
|
+
field_validator,
|
|
8
11
|
model_serializer,
|
|
9
|
-
ConfigDict,
|
|
10
12
|
)
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
from contentctl.objects.enums import (
|
|
13
15
|
SecurityContentInvestigationProductName,
|
|
14
16
|
SecurityDomain,
|
|
15
17
|
)
|
|
18
|
+
from contentctl.objects.story import Story
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class InvestigationTags(BaseModel):
|
|
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
-
from contentctl.objects.test_attack_data import TestAttackData
|
|
6
|
-
from contentctl.objects.manual_test_result import ManualTestResult
|
|
7
5
|
from contentctl.objects.base_test import BaseTest, TestType
|
|
8
6
|
from contentctl.objects.base_test_result import TestResultStatus
|
|
7
|
+
from contentctl.objects.manual_test_result import ManualTestResult
|
|
8
|
+
from contentctl.objects.test_attack_data import TestAttackData
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ManualTest(BaseTest):
|