contentctl 5.5.7__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.
Files changed (65) hide show
  1. contentctl/actions/deploy_acs.py +5 -3
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +3 -3
  3. contentctl/actions/detection_testing/GitService.py +4 -4
  4. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +3 -3
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +15 -17
  6. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +9 -8
  7. contentctl/actions/detection_testing/progress_bar.py +2 -1
  8. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +4 -3
  9. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +4 -2
  10. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +7 -7
  11. contentctl/actions/doc_gen.py +1 -2
  12. contentctl/actions/release_notes.py +2 -2
  13. contentctl/actions/reporting.py +3 -3
  14. contentctl/actions/test.py +2 -3
  15. contentctl/actions/validate.py +1 -1
  16. contentctl/api.py +7 -6
  17. contentctl/contentctl.py +1 -1
  18. contentctl/enrichments/attack_enrichment.py +1 -1
  19. contentctl/enrichments/cve_enrichment.py +9 -6
  20. contentctl/enrichments/splunk_app_enrichment.py +5 -4
  21. contentctl/helper/link_validator.py +7 -7
  22. contentctl/helper/splunk_app.py +6 -6
  23. contentctl/helper/utils.py +8 -8
  24. contentctl/input/director.py +3 -2
  25. contentctl/input/new_content_questions.py +1 -0
  26. contentctl/input/yml_reader.py +2 -2
  27. contentctl/objects/abstract_security_content_objects/detection_abstract.py +1 -1
  28. contentctl/objects/alert_action.py +4 -2
  29. contentctl/objects/atomic.py +8 -5
  30. contentctl/objects/base_test.py +1 -1
  31. contentctl/objects/base_test_result.py +2 -2
  32. contentctl/objects/baseline_tags.py +7 -6
  33. contentctl/objects/config.py +5 -5
  34. contentctl/objects/correlation_search.py +156 -139
  35. contentctl/objects/dashboard.py +1 -1
  36. contentctl/objects/deployment_email.py +1 -0
  37. contentctl/objects/deployment_notable.py +3 -1
  38. contentctl/objects/deployment_phantom.py +1 -0
  39. contentctl/objects/deployment_rba.py +1 -0
  40. contentctl/objects/deployment_scheduling.py +1 -0
  41. contentctl/objects/deployment_slack.py +1 -0
  42. contentctl/objects/detection_stanza.py +1 -1
  43. contentctl/objects/integration_test.py +2 -2
  44. contentctl/objects/investigation_tags.py +6 -3
  45. contentctl/objects/manual_test.py +2 -2
  46. contentctl/objects/playbook_tags.py +5 -2
  47. contentctl/objects/risk_analysis_action.py +1 -1
  48. contentctl/objects/savedsearches_conf.py +3 -3
  49. contentctl/objects/story_tags.py +1 -1
  50. contentctl/objects/test_group.py +2 -2
  51. contentctl/objects/throttling.py +2 -1
  52. contentctl/objects/unit_test.py +2 -2
  53. contentctl/objects/unit_test_baseline.py +2 -1
  54. contentctl/objects/unit_test_result.py +4 -2
  55. contentctl/output/conf_output.py +2 -2
  56. contentctl/output/conf_writer.py +5 -5
  57. contentctl/output/doc_md_output.py +0 -1
  58. contentctl/output/jinja_writer.py +1 -0
  59. contentctl/output/json_writer.py +1 -1
  60. contentctl/output/yml_writer.py +3 -2
  61. {contentctl-5.5.7.dist-info → contentctl-5.5.8.dist-info}/METADATA +3 -3
  62. {contentctl-5.5.7.dist-info → contentctl-5.5.8.dist-info}/RECORD +65 -65
  63. {contentctl-5.5.7.dist-info → contentctl-5.5.8.dist-info}/LICENSE.md +0 -0
  64. {contentctl-5.5.7.dist-info → contentctl-5.5.8.dist-info}/WHEEL +0 -0
  65. {contentctl-5.5.7.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 = 60
76
+ BASE_SLEEP = 2
73
77
 
74
- # NOTE: Some detections take longer to generate their risk/notables than other; testing has
75
- # shown 270s to likely be sufficient for all detections in 99% of runs; however we have
76
- # encountered a handful of transient failures in the last few months. Since our success rate
77
- # is at 100% now, we will round this to a flat 300s to accomodate these outliers.
78
- # Max amount to wait before timing out during exponential backoff
79
- MAX_SLEEP = 300
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 = "*/1 * * * *"
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
- # TODO (#248): Refactor risk/notable querying to pin to a single savedsearch ID
537
- # Search for all risk events from a single scheduled search (indicated by orig_sid)
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 scheduled search (indicated by orig_sid)
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
- # TODO (#248): Refactor risk/notable querying to pin to a single savedsearch ID
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}" [datamodel Risk '
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, max_sleep: int = TimeoutConfig.MAX_SLEEP, raise_on_exc: bool = False
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.force_run()
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
- # Validate risk events
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
@@ -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}: {str(e)}")
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,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel, ConfigDict
3
4
 
4
5
 
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, ConfigDict
2
+
3
3
  from typing import List
4
4
 
5
+ from pydantic import BaseModel, ConfigDict
6
+
5
7
 
6
8
  class DeploymentNotable(BaseModel):
7
9
  model_config = ConfigDict(extra="forbid")
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel, ConfigDict
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel, ConfigDict
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel, ConfigDict
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel, ConfigDict
3
4
 
4
5
 
@@ -1,6 +1,6 @@
1
- from typing import ClassVar
2
1
  import hashlib
3
2
  from functools import cached_property
3
+ from typing import ClassVar
4
4
 
5
5
  from pydantic import BaseModel, Field, computed_field
6
6
 
@@ -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
- from contentctl.objects.story import Story
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):
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
- from typing import Optional, List
3
- from pydantic import BaseModel, Field, ConfigDict
2
+
4
3
  import enum
4
+ from typing import List, Optional
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
5
8
  from contentctl.objects.detection import Detection
6
9
 
7
10
 
@@ -1,5 +1,5 @@
1
- from typing import Any
2
1
  import json
2
+ from typing import Any
3
3
 
4
4
  from pydantic import BaseModel, field_validator
5
5
 
@@ -1,8 +1,8 @@
1
- from pathlib import Path
2
- from typing import Any, ClassVar
3
1
  import re
4
- import tempfile
5
2
  import tarfile
3
+ import tempfile
4
+ from pathlib import Path
5
+ from typing import Any, ClassVar
6
6
 
7
7
  from pydantic import BaseModel, Field, PrivateAttr
8
8
 
@@ -45,7 +45,7 @@ class StoryTags(BaseModel):
45
45
  def getCategory_conf(self) -> str:
46
46
  # if len(self.category) > 1:
47
47
  # print("Story with more than 1 category. We can only have 1 category, fix it!")
48
- return list(self.category)[0]
48
+ return next(iter(self.category))
49
49
 
50
50
  @model_serializer
51
51
  def serialize_model(self):
@@ -1,9 +1,9 @@
1
1
  from pydantic import BaseModel
2
2
 
3
- from contentctl.objects.unit_test import UnitTest
3
+ from contentctl.objects.base_test_result import TestResultStatus
4
4
  from contentctl.objects.integration_test import IntegrationTest
5
5
  from contentctl.objects.test_attack_data import TestAttackData
6
- from contentctl.objects.base_test_result import TestResultStatus
6
+ from contentctl.objects.unit_test import UnitTest
7
7
 
8
8
 
9
9
  class TestGroup(BaseModel):
@@ -1,6 +1,7 @@
1
- from pydantic import BaseModel, Field, field_validator
2
1
  from typing import Annotated
3
2
 
3
+ from pydantic import BaseModel, Field, field_validator
4
+
4
5
 
5
6
  # Alert Suppression/Throttling settings have been taken from
6
7
  # https://docs.splunk.com/Documentation/Splunk/9.2.2/Admin/Savedsearchesconf
@@ -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.unit_test_result import UnitTestResult
7
5
  from contentctl.objects.base_test import BaseTest, TestType
8
6
  from contentctl.objects.base_test_result import TestResultStatus
7
+ from contentctl.objects.test_attack_data import TestAttackData
8
+ from contentctl.objects.unit_test_result import UnitTestResult
9
9
 
10
10
 
11
11
  class UnitTest(BaseTest):
@@ -1,6 +1,7 @@
1
- from pydantic import BaseModel, ConfigDict
2
1
  from typing import Union
3
2
 
3
+ from pydantic import BaseModel, ConfigDict
4
+
4
5
 
5
6
  class UnitTestBaseline(BaseModel):
6
7
  model_config = ConfigDict(extra="forbid")
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Union, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Union
4
+
4
5
  from splunklib.data import Record
6
+
5
7
  from contentctl.objects.base_test_result import BaseTestResult, TestResultStatus
6
8
 
7
9
  if TYPE_CHECKING:
@@ -70,7 +72,7 @@ class UnitTestResult(BaseTestResult):
70
72
  elif content is None:
71
73
  self.status = TestResultStatus.ERROR
72
74
  if self.exception is not None:
73
- self.message = f"EXCEPTION: {str(self.exception)}"
75
+ self.message = f"EXCEPTION: {self.exception!s}"
74
76
  else:
75
77
  self.message = "ERROR with no more specific message available."
76
78
  self.sid_link = NO_SID
@@ -270,7 +270,7 @@ class ConfOutput:
270
270
  output_dir=pathlib.Path(self.config.getBuildDir()),
271
271
  )
272
272
  except SystemExit as e:
273
- raise Exception(f"Error building package with slim: {str(e)}")
273
+ raise Exception(f"Error building package with slim: {e!s}")
274
274
 
275
275
  except Exception as e:
276
276
  print(
@@ -278,7 +278,7 @@ class ConfOutput:
278
278
  "Packaging app with tar instead. This should still work, but appinspect may catch "
279
279
  "errors that otherwise would have been flagged by slim."
280
280
  )
281
- raise Exception(f"slim (splunk packaging toolkit) not installed: {str(e)}")
281
+ raise Exception(f"slim (splunk packaging toolkit) not installed: {e!s}")
282
282
 
283
283
  def packageApp(self, method: Callable[[ConfOutput], None] = packageAppTar) -> None:
284
284
  return method(self)