contentctl 4.3.3__tar.gz → 4.3.4__tar.gz

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 (172) hide show
  1. {contentctl-4.3.3 → contentctl-4.3.4}/PKG-INFO +2 -2
  2. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -6
  3. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/enrichments/attack_enrichment.py +2 -1
  4. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/enrichments/cve_enrichment.py +2 -2
  5. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +3 -2
  6. contentctl-4.3.4/contentctl/objects/annotated_types.py +6 -0
  7. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/config.py +3 -3
  8. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/correlation_search.py +35 -28
  9. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/detection_tags.py +5 -3
  10. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/mitre_attack_enrichment.py +2 -1
  11. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/risk_event.py +94 -76
  12. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/story_tags.py +3 -3
  13. {contentctl-4.3.3 → contentctl-4.3.4}/pyproject.toml +2 -2
  14. {contentctl-4.3.3 → contentctl-4.3.4}/LICENSE.md +0 -0
  15. {contentctl-4.3.3 → contentctl-4.3.4}/README.md +0 -0
  16. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/__init__.py +0 -0
  17. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/build.py +0 -0
  18. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/deploy_acs.py +0 -0
  19. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  20. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/GitService.py +0 -0
  21. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  22. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  23. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  24. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  25. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  26. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  27. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  28. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  29. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/doc_gen.py +0 -0
  30. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/initialize.py +0 -0
  31. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/initialize_old.py +0 -0
  32. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/inspect.py +0 -0
  33. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/new_content.py +0 -0
  34. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/release_notes.py +0 -0
  35. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/reporting.py +0 -0
  36. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/test.py +0 -0
  37. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/actions/validate.py +0 -0
  38. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/api.py +0 -0
  39. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/contentctl.py +0 -0
  40. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  41. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/helper/link_validator.py +0 -0
  42. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/helper/logger.py +0 -0
  43. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/helper/splunk_app.py +0 -0
  44. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/helper/utils.py +0 -0
  45. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/input/director.py +0 -0
  46. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/input/new_content_questions.py +0 -0
  47. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/input/yml_reader.py +0 -0
  48. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  49. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/alert_action.py +0 -0
  50. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/atomic.py +0 -0
  51. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/base_test.py +0 -0
  52. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/base_test_result.py +0 -0
  53. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/baseline.py +0 -0
  54. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/baseline_tags.py +0 -0
  55. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/constants.py +0 -0
  56. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/data_source.py +0 -0
  57. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment.py +0 -0
  58. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_email.py +0 -0
  59. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_notable.py +0 -0
  60. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_phantom.py +0 -0
  61. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_rba.py +0 -0
  62. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_scheduling.py +0 -0
  63. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/deployment_slack.py +0 -0
  64. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/detection.py +0 -0
  65. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/enums.py +0 -0
  66. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/errors.py +0 -0
  67. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/event_source.py +0 -0
  68. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/integration_test.py +0 -0
  69. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/integration_test_result.py +0 -0
  70. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/investigation.py +0 -0
  71. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/investigation_tags.py +0 -0
  72. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/lookup.py +0 -0
  73. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/macro.py +0 -0
  74. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/manual_test.py +0 -0
  75. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/manual_test_result.py +0 -0
  76. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/notable_action.py +0 -0
  77. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/notable_event.py +0 -0
  78. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/observable.py +0 -0
  79. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/playbook.py +0 -0
  80. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/playbook_tags.py +0 -0
  81. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/risk_analysis_action.py +0 -0
  82. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/risk_object.py +0 -0
  83. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/security_content_object.py +0 -0
  84. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/ssa_detection.py +0 -0
  85. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/ssa_detection_tags.py +0 -0
  86. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/story.py +0 -0
  87. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/test_attack_data.py +0 -0
  88. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/test_group.py +0 -0
  89. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/threat_object.py +0 -0
  90. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/unit_test.py +0 -0
  91. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/unit_test_baseline.py +0 -0
  92. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/unit_test_old.py +0 -0
  93. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/unit_test_result.py +0 -0
  94. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/objects/unit_test_ssa.py +0 -0
  95. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/api_json_output.py +0 -0
  96. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/attack_nav_output.py +0 -0
  97. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/attack_nav_writer.py +0 -0
  98. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/conf_output.py +0 -0
  99. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/conf_writer.py +0 -0
  100. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/data_source_writer.py +0 -0
  101. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/detection_writer.py +0 -0
  102. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/doc_md_output.py +0 -0
  103. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/jinja_writer.py +0 -0
  104. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/json_writer.py +0 -0
  105. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/new_content_yml_output.py +0 -0
  106. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/svg_output.py +0 -0
  107. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  108. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  109. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  110. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/app.conf.j2 +0 -0
  111. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/app.manifest.j2 +0 -0
  112. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/collections.j2 +0 -0
  113. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/content-version.j2 +0 -0
  114. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/detection_count.j2 +0 -0
  115. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/detection_coverage.j2 +0 -0
  116. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  117. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_detections.j2 +0 -0
  118. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_navigation.j2 +0 -0
  119. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  120. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  121. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  122. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_stories.j2 +0 -0
  123. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/doc_story_page.j2 +0 -0
  124. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  125. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  126. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/finding_report.j2 +0 -0
  127. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/header.j2 +0 -0
  128. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/macros.j2 +0 -0
  129. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/panel.j2 +0 -0
  130. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  131. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  132. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  133. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/transforms.j2 +0 -0
  134. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/templates/workflow_actions.j2 +0 -0
  135. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/yml_output.py +0 -0
  136. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/output/yml_writer.py +0 -0
  137. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/README.md +0 -0
  138. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_default.yml +0 -0
  139. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  140. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  141. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  142. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/README.md +0 -0
  143. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  144. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/app.conf +0 -0
  145. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/commands.conf +0 -0
  146. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/content-version.conf +0 -0
  147. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  148. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  149. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  150. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  151. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  152. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/metadata/default.meta +0 -0
  153. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/static/appIcon.png +0 -0
  154. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  155. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  156. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  157. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  158. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/datamodels_cim.conf +0 -0
  159. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/datamodels_custom.conf +0 -0
  160. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  161. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  162. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  163. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  164. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  165. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/detections/application/.gitkeep +0 -0
  166. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  167. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
  168. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/detections/network/.gitkeep +0 -0
  169. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/detections/web/.gitkeep +0 -0
  170. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  171. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  172. {contentctl-4.3.3 → contentctl-4.3.4}/contentctl/templates/stories/cobalt_strike.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: contentctl
3
- Version: 4.3.3
3
+ Version: 4.3.4
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -22,7 +22,7 @@ Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
22
22
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
23
23
  Requires-Dist: requests (>=2.32.3,<2.33.0)
24
24
  Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
25
- Requires-Dist: setuptools (>=69.5.1,<74.0.0)
25
+ Requires-Dist: setuptools (>=69.5.1,<75.0.0)
26
26
  Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
27
27
  Requires-Dist: tqdm (>=4.66.5,<5.0.0)
28
28
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
@@ -374,12 +374,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
374
374
  return
375
375
 
376
376
  try:
377
- # NOTE: (THIS CODE HAS MOVED) we handle skipping entire detections differently than
378
- # we do skipping individual test cases; we skip entire detections by excluding
379
- # them to an entirely separate queue, while we skip individual test cases via the
380
- # BaseTest.skip() method, such as when we are skipping all integration tests (see
381
- # DetectionBuilder.skipIntegrationTests)
382
- # TODO: are we skipping by production status elsewhere?
383
377
  detection = self.sync_obj.inputQueue.pop()
384
378
  self.sync_obj.currentTestingQueue[self.get_name()] = detection
385
379
  except IndexError:
@@ -10,6 +10,7 @@ from dataclasses import field
10
10
  from typing import Annotated,Any
11
11
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
12
12
  from contentctl.objects.config import validate
13
+ from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
13
14
  logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
14
15
 
15
16
 
@@ -23,7 +24,7 @@ class AttackEnrichment(BaseModel):
23
24
  _ = enrichment.get_attack_lookup(str(config.path))
24
25
  return enrichment
25
26
 
26
- def getEnrichmentByMitreID(self, mitre_id:Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")])->MitreAttackEnrichment:
27
+ def getEnrichmentByMitreID(self, mitre_id:MITRE_ATTACK_ID_TYPE)->MitreAttackEnrichment:
27
28
  if not self.use_enrichment:
28
29
  raise Exception(f"Error, trying to add Mitre Enrichment, but use_enrichment was set to False")
29
30
 
@@ -8,7 +8,7 @@ from typing import Annotated, Any, Union, TYPE_CHECKING
8
8
  from pydantic import BaseModel,Field, computed_field
9
9
  from decimal import Decimal
10
10
  from requests.exceptions import ReadTimeout
11
-
11
+ from contentctl.objects.annotated_types import CVE_TYPE
12
12
  if TYPE_CHECKING:
13
13
  from contentctl.objects.config import validate
14
14
 
@@ -18,7 +18,7 @@ CVESSEARCH_API_URL = 'https://cve.circl.lu'
18
18
 
19
19
 
20
20
  class CveEnrichmentObj(BaseModel):
21
- id: Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]
21
+ id: CVE_TYPE
22
22
  cvss: Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
23
23
  summary: str
24
24
 
@@ -322,12 +322,13 @@ class Detection_Abstract(SecurityContentObject):
322
322
  @property
323
323
  def providing_technologies(self) -> List[ProvidingTechnology]:
324
324
  return ProvidingTechnology.getProvidingTechFromSearch(self.search)
325
-
326
-
325
+
326
+ # TODO (#247): Refactor the risk property of detection_abstract
327
327
  @computed_field
328
328
  @property
329
329
  def risk(self) -> list[dict[str, Any]]:
330
330
  risk_objects: list[dict[str, str | int]] = []
331
+ # TODO (#246): "User Name" type should map to a "user" risk object and not "other"
331
332
  risk_object_user_types = {'user', 'username', 'email address'}
332
333
  risk_object_system_types = {'device', 'endpoint', 'hostname', 'ip address'}
333
334
  process_threat_object_types = {'process name', 'process'}
@@ -0,0 +1,6 @@
1
+ from pydantic import Field
2
+ from typing import Annotated
3
+
4
+ CVE_TYPE = Annotated[str, Field(pattern=r"^CVE-[1|2]\d{3}-\d+$")]
5
+ MITRE_ATTACK_ID_TYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]
6
+ APPID_TYPE = Annotated[str,Field(pattern="^[a-zA-Z0-9_-]+$")]
@@ -18,7 +18,7 @@ from urllib.parse import urlparse
18
18
  from abc import ABC, abstractmethod
19
19
  from contentctl.objects.enums import PostTestBehavior, DetectionTestingMode
20
20
  from contentctl.objects.detection import Detection
21
-
21
+ from contentctl.objects.annotated_types import APPID_TYPE
22
22
  import tqdm
23
23
  from functools import partialmethod
24
24
 
@@ -33,7 +33,7 @@ class App_Base(BaseModel,ABC):
33
33
  model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
34
34
  uid: Optional[int] = Field(default=None)
35
35
  title: str = Field(description="Human-readable name used by the app. This can have special characters.")
36
- appid: Optional[Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]]= Field(default=None,description="Internal name used by your app. "
36
+ appid: Optional[APPID_TYPE]= Field(default=None,description="Internal name used by your app. "
37
37
  "It may ONLY have characters, numbers, and underscores. No other characters are allowed.")
38
38
  version: str = Field(description="The version of your Content Pack. This must follow semantic versioning guidelines.")
39
39
  description: Optional[str] = Field(default="description of app",description="Free text description of the Content Pack.")
@@ -101,7 +101,7 @@ class CustomApp(App_Base):
101
101
  # https://docs.splunk.com/Documentation/Splunk/9.0.4/Admin/Appconf
102
102
  uid: int = Field(ge=2, lt=100000, default_factory=lambda:random.randint(20000,100000))
103
103
  title: str = Field(default="Content Pack",description="Human-readable name used by the app. This can have special characters.")
104
- appid: Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]= Field(default="ContentPack",description="Internal name used by your app. "
104
+ appid: APPID_TYPE = Field(default="ContentPack",description="Internal name used by your app. "
105
105
  "It may ONLY have characters, numbers, and underscores. No other characters are allowed.")
106
106
  version: str = Field(default="0.0.1",description="The version of your Content Pack. This must follow semantic versioning guidelines.", validate_default=True)
107
107
 
@@ -575,10 +575,11 @@ class CorrelationSearch(BaseModel):
575
575
  self.logger.debug(f"Using cached risk events ({len(self._risk_events)} total).")
576
576
  return self._risk_events
577
577
 
578
+ # TODO (#248): Refactor risk/notable querying to pin to a single savedsearch ID
578
579
  # Search for all risk events from a single scheduled search (indicated by orig_sid)
579
580
  query = (
580
581
  f'search index=risk search_name="{self.name}" [search index=risk search '
581
- f'search_name="{self.name}" | head 1 | fields orig_sid] | tojson'
582
+ f'search_name="{self.name}" | tail 1 | fields orig_sid] | tojson'
582
583
  )
583
584
  result_iterator = self._search(query)
584
585
 
@@ -643,7 +644,7 @@ class CorrelationSearch(BaseModel):
643
644
  # Search for all notable events from a single scheduled search (indicated by orig_sid)
644
645
  query = (
645
646
  f'search index=notable search_name="{self.name}" [search index=notable search '
646
- f'search_name="{self.name}" | head 1 | fields orig_sid] | tojson'
647
+ f'search_name="{self.name}" | tail 1 | fields orig_sid] | tojson'
647
648
  )
648
649
  result_iterator = self._search(query)
649
650
 
@@ -686,15 +687,17 @@ class CorrelationSearch(BaseModel):
686
687
  check the risks/notables
687
688
  :returns: an IntegrationTestResult on failure; None on success
688
689
  """
689
- # TODO (PEX-433): Re-enable this check once we have refined the logic and reduced the false
690
- # positive rate in risk/obseravble matching
691
690
  # Create a mapping of the relevant observables to counters
692
- # observables = CorrelationSearch._get_relevant_observables(self.detection.tags.observable)
693
- # observable_counts: dict[str, int] = {str(x): 0 for x in observables}
694
- # if len(observables) != len(observable_counts):
695
- # raise ClientError(
696
- # f"At least two observables in '{self.detection.name}' have the same name."
697
- # )
691
+ observables = CorrelationSearch._get_relevant_observables(self.detection.tags.observable)
692
+ observable_counts: dict[str, int] = {str(x): 0 for x in observables}
693
+
694
+ # NOTE: we intentionally want this to be an error state and not a failure state, as
695
+ # ultimately this validation should be handled during the build process
696
+ if len(observables) != len(observable_counts):
697
+ raise ClientError(
698
+ f"At least two observables in '{self.detection.name}' have the same name; "
699
+ "each observable for a detection should be unique."
700
+ )
698
701
 
699
702
  # Get the risk events; note that we use the cached risk events, expecting they were
700
703
  # saved by a prior call to risk_event_exists
@@ -710,25 +713,29 @@ class CorrelationSearch(BaseModel):
710
713
  )
711
714
  event.validate_against_detection(self.detection)
712
715
 
713
- # TODO (PEX-433): Re-enable this check once we have refined the logic and reduced the
714
- # false positive rate in risk/obseravble matching
715
716
  # Update observable count based on match
716
- # matched_observable = event.get_matched_observable(self.detection.tags.observable)
717
- # self.logger.debug(
718
- # f"Matched risk event ({event.risk_object}, {event.risk_object_type}) to observable "
719
- # f"({matched_observable.name}, {matched_observable.type}, {matched_observable.role})"
720
- # )
721
- # observable_counts[str(matched_observable)] += 1
722
-
723
- # TODO (PEX-433): test my new contentctl logic against an old ESCU build; my logic should
724
- # detect the faulty attacker events -> this was the issue from the 4.28/4.27 release;
725
- # recreate by testing against one of those old builds w/ the bad config
726
- # TODO (PEX-433): Re-enable this check once we have refined the logic and reduced the false
727
- # positive
728
- # rate in risk/obseravble matching
729
- # TODO (PEX-433): I foresee issues here if for example a parent and child process share a
730
- # name (matched observable could be either) -> these issues are confirmed to exist, e.g.
731
- # `Windows Steal Authentication Certificates Export Certificate`
717
+ matched_observable = event.get_matched_observable(self.detection.tags.observable)
718
+ self.logger.debug(
719
+ f"Matched risk event (object={event.risk_object}, type={event.risk_object_type}) "
720
+ f"to observable (name={matched_observable.name}, type={matched_observable.type}, "
721
+ f"role={matched_observable.role}) using the source field "
722
+ f"'{event.source_field_name}'"
723
+ )
724
+ observable_counts[str(matched_observable)] += 1
725
+
726
+ # Report any observables which did not have at least one match to a risk event
727
+ for observable in observables:
728
+ self.logger.debug(
729
+ f"Matched observable (name={observable.name}, type={observable.type}, "
730
+ f"role={observable.role}) to {observable_counts[str(observable)]} risk events."
731
+ )
732
+ if observable_counts[str(observable)] == 0:
733
+ raise ValidationFailed(
734
+ f"Observable (name={observable.name}, type={observable.type}, "
735
+ f"role={observable.role}) was not matched to any risk events."
736
+ )
737
+
738
+ # TODO (#250): Re-enable and refactor code that validates the specific risk counts
732
739
  # Validate risk events in aggregate; we should have an equal amount of risk events for each
733
740
  # relevant observable, and the total count should match the total number of events
734
741
  # individual_count: Optional[int] = None
@@ -33,7 +33,7 @@ from contentctl.objects.enums import (
33
33
  SecurityContentProductName
34
34
  )
35
35
  from contentctl.objects.atomic import AtomicTest
36
-
36
+ from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE, CVE_TYPE
37
37
 
38
38
  # TODO (#266): disable the use_enum_values configuration
39
39
  class DetectionTags(BaseModel):
@@ -50,8 +50,10 @@ class DetectionTags(BaseModel):
50
50
  def risk_score(self) -> int:
51
51
  return round((self.confidence * self.impact)/100)
52
52
 
53
- mitre_attack_id: List[Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]] = []
53
+ mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
54
54
  nist: list[NistCategory] = []
55
+
56
+ # TODO (#249): Add pydantic validator to ensure observables are unique within a detection
55
57
  observable: List[Observable] = []
56
58
  message: str = Field(...)
57
59
  product: list[SecurityContentProductName] = Field(..., min_length=1)
@@ -69,7 +71,7 @@ class DetectionTags(BaseModel):
69
71
  else:
70
72
  return RiskSeverity('low')
71
73
 
72
- cve: List[Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]] = []
74
+ cve: List[CVE_TYPE] = []
73
75
  atomic_guid: List[AtomicTest] = []
74
76
  drilldown_search: Optional[str] = None
75
77
 
@@ -3,6 +3,7 @@ from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
3
3
  from typing import List, Annotated
4
4
  from enum import StrEnum
5
5
  import datetime
6
+ from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
6
7
 
7
8
  class MitreTactics(StrEnum):
8
9
  RECONNAISSANCE = "Reconnaissance"
@@ -85,7 +86,7 @@ class MitreAttackGroup(BaseModel):
85
86
  # TODO (#266): disable the use_enum_values configuration
86
87
  class MitreAttackEnrichment(BaseModel):
87
88
  ConfigDict(use_enum_values=True)
88
- mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
89
+ mitre_attack_id: MITRE_ATTACK_ID_TYPE = Field(...)
89
90
  mitre_attack_technique: str = Field(...)
90
91
  mitre_attack_tactics: List[MitreTactics] = Field(...)
91
92
  mitre_attack_groups: List[str] = Field(...)
@@ -1,32 +1,51 @@
1
1
  import re
2
- from typing import Union, Optional
3
2
 
4
- from pydantic import BaseModel, Field, PrivateAttr, field_validator
3
+ from pydantic import BaseModel, Field, PrivateAttr, field_validator, computed_field
5
4
 
6
5
  from contentctl.objects.errors import ValidationFailed
7
6
  from contentctl.objects.detection import Detection
8
7
  from contentctl.objects.observable import Observable
9
8
 
10
- # TODO (PEX-433): use SES_OBSERVABLE_TYPE_MAPPING
9
+ # TODO (#259): Map our observable types to more than user/system
10
+ # TODO (#247): centralize this mapping w/ usage of SES_OBSERVABLE_TYPE_MAPPING (see
11
+ # observable.py) and the ad hoc mapping made in detection_abstract.py (see the risk property func)
11
12
  TYPE_MAP: dict[str, list[str]] = {
12
- "user": ["User"],
13
- "system": ["Hostname", "IP Address", "Endpoint"],
14
- "other": ["Process", "URL String", "Unknown", "Process Name"],
13
+ "system": [
14
+ "Hostname",
15
+ "IP Address",
16
+ "Endpoint"
17
+ ],
18
+ "user": [
19
+ "User",
20
+ "User Name",
21
+ "Email Address",
22
+ "Email"
23
+ ],
24
+ "hash_values": [],
25
+ "network_artifacts": [],
26
+ "host_artifacts": [],
27
+ "tools": [],
28
+ "other": [
29
+ "Process",
30
+ "URL String",
31
+ "Unknown",
32
+ "Process Name",
33
+ "MAC Address",
34
+ "File Name",
35
+ "File Hash",
36
+ "Resource UID",
37
+ "Uniform Resource Locator",
38
+ "File",
39
+ "Geo Location",
40
+ "Container",
41
+ "Registry Key",
42
+ "Registry Value",
43
+ "Other"
44
+ ]
15
45
  }
16
- # TODO (PEX-433): 'Email Address', 'File Name', 'File Hash', 'Other', 'User Name', 'File',
17
- # 'Process Name'
18
46
 
19
- # TODO (PEX-433): use SES_OBSERVABLE_ROLE_MAPPING
47
+ # Roles that should not generate risks
20
48
  IGNORE_ROLES: list[str] = ["Attacker"]
21
- # Known valid roles: Victim, Parent Process, Child Process
22
- # TODO (PEX-433): 'Other', 'Target', 'Unknown'
23
- # TODO (PEX-433): is Other a valid role
24
-
25
- # TODO (PEX-433): do we need User Name in conjunction w/ User? User Name doesn't get mapped to
26
- # "user" in risk events
27
- # TODO (PEX-433): similarly, do we need Process and Process Name?
28
-
29
- RESERVED_FIELDS = ["host"]
30
49
 
31
50
 
32
51
  class RiskEvent(BaseModel):
@@ -36,7 +55,7 @@ class RiskEvent(BaseModel):
36
55
  search_name: str
37
56
 
38
57
  # The subject of the risk event (e.g. a username, process name, system name, account ID, etc.)
39
- risk_object: Union[int, str]
58
+ risk_object: int | str
40
59
 
41
60
  # The type of the risk object (e.g. user, system, or other)
42
61
  risk_object_type: str
@@ -59,8 +78,12 @@ class RiskEvent(BaseModel):
59
78
  default=[]
60
79
  )
61
80
 
81
+ # Contributing events search query (we use this to derive the corresponding field from the
82
+ # observables)
83
+ contributing_events_search: str
84
+
62
85
  # Private attribute caching the observable this RiskEvent is mapped to
63
- _matched_observable: Optional[Observable] = PrivateAttr(default=None)
86
+ _matched_observable: Observable | None = PrivateAttr(default=None)
64
87
 
65
88
  class Config:
66
89
  # Allowing fields that aren't explicitly defined to be passed since some of the risk event's
@@ -69,7 +92,7 @@ class RiskEvent(BaseModel):
69
92
 
70
93
  @field_validator("annotations_mitre_attack", "analyticstories", mode="before")
71
94
  @classmethod
72
- def _convert_str_value_to_singleton(cls, v: Union[str, list[str]]) -> list[str]:
95
+ def _convert_str_value_to_singleton(cls, v: str | list[str]) -> list[str]:
73
96
  """
74
97
  Given a value, determine if its a list or a single str value; if a single value, return as a
75
98
  singleton. Do nothing if anything else.
@@ -79,6 +102,25 @@ class RiskEvent(BaseModel):
79
102
  else:
80
103
  return [v]
81
104
 
105
+ @computed_field
106
+ @property
107
+ def source_field_name(self) -> str:
108
+ """
109
+ A cached derivation of the source field name the risk event corresponds to in the relevant
110
+ event(s). Useful for mapping back to an observable in the detection.
111
+ """
112
+ pattern = re.compile(
113
+ r"\| savedsearch \"" + self.search_name + r"\" \| search (?P<field>[^=]+)=.+"
114
+ )
115
+ match = pattern.search(self.contributing_events_search)
116
+ if match is None:
117
+ raise ValueError(
118
+ "Unable to parse source field name from risk event using "
119
+ f"'contributing_events_search' ('{self.contributing_events_search}') using "
120
+ f"pattern: {pattern}"
121
+ )
122
+ return match.group("field")
123
+
82
124
  def validate_against_detection(self, detection: Detection) -> None:
83
125
  """
84
126
  Given the associated detection, validate the risk event against its fields
@@ -108,10 +150,8 @@ class RiskEvent(BaseModel):
108
150
  # Check risk_message
109
151
  self.validate_risk_message(detection)
110
152
 
111
- # TODO (PEX-433): Re-enable this check once we have refined the logic and reduced the false
112
- # positive rate in risk/obseravble matching
113
153
  # Check several conditions against the observables
114
- # self.validate_risk_against_observables(detection.tags.observable)
154
+ self.validate_risk_against_observables(detection.tags.observable)
115
155
 
116
156
  def validate_mitre_ids(self, detection: Detection) -> None:
117
157
  """
@@ -199,7 +239,11 @@ class RiskEvent(BaseModel):
199
239
  if self.risk_object_type != expected_type:
200
240
  raise ValidationFailed(
201
241
  f"The risk object type ({self.risk_object_type}) does not match the expected type "
202
- f"based on the matched observable ({matched_observable.type}=={expected_type})."
242
+ f"based on the matched observable ({matched_observable.type}->{expected_type}): "
243
+ f"risk=(object={self.risk_object}, type={self.risk_object_type}, "
244
+ f"source_field_name={self.source_field_name}), "
245
+ f"observable=(name={matched_observable.name}, type={matched_observable.type}, "
246
+ f"role={matched_observable.role})"
203
247
  )
204
248
 
205
249
  @staticmethod
@@ -220,8 +264,6 @@ class RiskEvent(BaseModel):
220
264
  f"Observable type {observable_type} does not have a mapping to a risk type in TYPE_MAP"
221
265
  )
222
266
 
223
- # TODO (PEX-433): should this be an observable instance method? It feels less relevant to
224
- # observables themselves, as it's really only relevant to the handling of risk events
225
267
  @staticmethod
226
268
  def ignore_observable(observable: Observable) -> bool:
227
269
  """
@@ -230,8 +272,6 @@ class RiskEvent(BaseModel):
230
272
  :param observable: the Observable object we are checking the roles of
231
273
  :returns: a bool indicating whether this observable should be ignored or not
232
274
  """
233
- # TODO (PEX-433): could there be a case where an observable has both an Attacker and Victim
234
- # (or equivalent) role? If so, how should we handle ignoring it?
235
275
  ignore = False
236
276
  for role in observable.role:
237
277
  if role in IGNORE_ROLES:
@@ -239,29 +279,6 @@ class RiskEvent(BaseModel):
239
279
  break
240
280
  return ignore
241
281
 
242
- # TODO (PEX-433): two possibilities: alway check for the field itself and the field prefixed
243
- # w/ "orig_" OR more explicitly maintain a list of known "reserved fields", like "host". I
244
- # think I like option 2 better as it can have fewer unknown side effects
245
- def matches_observable(self, observable: Observable) -> bool:
246
- """
247
- Given an observable, check if the risk event matches is
248
- :param observable: the Observable object we are comparing the risk event against
249
- :returns: bool indicating a match or not
250
- """
251
- # When field names collide w/ reserved fields in Splunk events (e.g. sourcetype or host)
252
- # they get prefixed w/ "orig_"
253
- attribute_name = observable.name
254
- if attribute_name in RESERVED_FIELDS:
255
- attribute_name = f"orig_{attribute_name}"
256
-
257
- # Retrieve the value of this attribute and see if it matches the risk_object
258
- value: Union[str, list[str]] = getattr(self, attribute_name)
259
- if isinstance(value, str):
260
- value = [value]
261
-
262
- # The value of the attribute may be a list of values, so check for any matches
263
- return self.risk_object in value
264
-
265
282
  def get_matched_observable(self, observables: list[Observable]) -> Observable:
266
283
  """
267
284
  Given a list of observables, return the one this risk event matches
@@ -274,40 +291,41 @@ class RiskEvent(BaseModel):
274
291
  if self._matched_observable is not None:
275
292
  return self._matched_observable
276
293
 
277
- matched_observable: Optional[Observable] = None
294
+ matched_observable: Observable | None = None
278
295
 
279
296
  # Iterate over the obervables and check for a match
280
297
  for observable in observables:
298
+ # TODO (#252): Refactor and re-enable per-field validation of risk events
281
299
  # Each the field name used in each observable shoud be present in the risk event
282
- # TODO (PEX-433): this check is redundant I think; earlier in the unit test, observable
283
- # field
284
- # names are compared against the search result set, ensuring all are present; if all
285
- # are present in the result set, all are present in the risk event
286
- if not hasattr(self, observable.name):
287
- raise ValidationFailed(
288
- f"Observable field \"{observable.name}\" not found in risk event."
289
- )
300
+ # if not hasattr(self, observable.name):
301
+ # raise ValidationFailed(
302
+ # f"Observable field \"{observable.name}\" not found in risk event."
303
+ # )
290
304
 
291
305
  # Try to match the risk_object against a specific observable for the obervables with
292
- # a valid role (some, like Attacker, don't get converted to risk events)
293
- if not RiskEvent.ignore_observable(observable):
294
- if self.matches_observable(observable):
295
- # TODO (PEX-433): This check fails as there are some instances where this is
296
- # true (e.g. we have an observable for process and parent_process and both
297
- # have the same name like "cmd.exe")
298
- if matched_observable is not None:
299
- raise ValueError(
300
- "Unexpected conditon: we don't expect the value corresponding to an "
301
- "observables field name to be repeated"
302
- )
303
- # NOTE: we explicitly do not break early as we want to check each observable
304
- matched_observable = observable
306
+ # a valid role (some, like Attacker, shouldn't get converted to risk events)
307
+ if self.source_field_name == observable.name:
308
+ if matched_observable is not None:
309
+ raise ValueError(
310
+ "Unexpected conditon: we don't expect the source event field "
311
+ "corresponding to an observables field name to be repeated."
312
+ )
313
+
314
+ # Report any risk events we find that shouldn't be there
315
+ if RiskEvent.ignore_observable(observable):
316
+ raise ValidationFailed(
317
+ "Risk event matched an observable with an invalid role: "
318
+ f"(name={observable.name}, type={observable.type}, role={observable.role})")
319
+ # NOTE: we explicitly do not break early as we want to check each observable
320
+ matched_observable = observable
305
321
 
306
322
  # Ensure we were able to match the risk event to a specific observable
307
323
  if matched_observable is None:
308
324
  raise ValidationFailed(
309
- f"Unable to match risk event ({self.risk_object}, {self.risk_object_type}) to an "
310
- "appropriate observable"
325
+ f"Unable to match risk event (object={self.risk_object}, type="
326
+ f"{self.risk_object_type}, source_field_name={self.source_field_name}) to an "
327
+ "observable; please check for errors in the observable roles/types for this "
328
+ "detection, as well as the risk event build process in contentctl."
311
329
  )
312
330
 
313
331
  # Cache and return the matched observable
@@ -6,7 +6,7 @@ from enum import Enum
6
6
 
7
7
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
8
8
  from contentctl.objects.enums import StoryCategory, DataModel, KillChainPhase, SecurityContentProductName
9
-
9
+ from contentctl.objects.annotated_types import CVE_TYPE,MITRE_ATTACK_ID_TYPE
10
10
 
11
11
  class StoryUseCase(str,Enum):
12
12
  FRAUD_DETECTION = "Fraud Detection"
@@ -27,10 +27,10 @@ class StoryTags(BaseModel):
27
27
 
28
28
  # enrichment
29
29
  mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
30
- mitre_attack_tactics: Optional[Set[Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]]] = None
30
+ mitre_attack_tactics: Optional[Set[MITRE_ATTACK_ID_TYPE]] = None
31
31
  datamodels: Optional[Set[DataModel]] = None
32
32
  kill_chain_phases: Optional[Set[KillChainPhase]] = None
33
- cve: List[Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]] = []
33
+ cve: List[CVE_TYPE] = []
34
34
  group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
35
35
 
36
36
  def getCategory_conf(self) -> str:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
- version = "4.3.3"
3
+ version = "4.3.4"
4
4
  description = "Splunk Content Control Tool"
5
5
  authors = ["STRT <research@splunk.com>"]
6
6
  license = "Apache 2.0"
@@ -27,7 +27,7 @@ tqdm = "^4.66.5"
27
27
  pygit2 = "^1.15.1"
28
28
  tyro = "^0.8.3"
29
29
  gitpython = "^3.1.43"
30
- setuptools = ">=69.5.1,<74.0.0"
30
+ setuptools = ">=69.5.1,<75.0.0"
31
31
  [tool.poetry.dev-dependencies]
32
32
 
33
33
  [build-system]
File without changes
File without changes
File without changes