contentctl 4.3.5__tar.gz → 4.4.1__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 (175) hide show
  1. {contentctl-4.3.5 → contentctl-4.4.1}/PKG-INFO +3 -2
  2. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/build.py +1 -0
  3. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/GitService.py +10 -10
  4. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +68 -38
  5. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +5 -1
  6. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +10 -8
  7. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/inspect.py +6 -4
  8. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/new_content.py +10 -2
  9. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/release_notes.py +5 -3
  10. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/validate.py +2 -1
  11. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/enrichments/cve_enrichment.py +6 -7
  12. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/input/director.py +14 -12
  13. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/input/new_content_questions.py +9 -42
  14. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +147 -7
  15. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
  16. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/base_test_result.py +7 -7
  17. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/baseline.py +12 -18
  18. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/baseline_tags.py +2 -5
  19. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/config.py +15 -9
  20. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/constants.py +30 -0
  21. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/correlation_search.py +79 -114
  22. contentctl-4.4.1/contentctl/objects/dashboard.py +100 -0
  23. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment.py +20 -5
  24. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/detection_tags.py +22 -20
  25. contentctl-4.4.1/contentctl/objects/drilldown.py +70 -0
  26. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/enums.py +26 -22
  27. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/investigation.py +23 -15
  28. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/investigation_tags.py +4 -3
  29. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/lookup.py +8 -1
  30. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/macro.py +16 -7
  31. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/notable_event.py +6 -5
  32. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/risk_analysis_action.py +4 -4
  33. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/risk_event.py +8 -7
  34. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/story.py +4 -16
  35. contentctl-4.4.1/contentctl/objects/throttling.py +46 -0
  36. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/conf_output.py +4 -0
  37. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/conf_writer.py +20 -3
  38. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/analyticstories_detections.j2 +2 -2
  39. contentctl-4.4.1/contentctl/output/templates/analyticstories_investigations.j2 +21 -0
  40. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/analyticstories_stories.j2 +1 -1
  41. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/savedsearches_baselines.j2 +2 -3
  42. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/savedsearches_detections.j2 +12 -7
  43. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/savedsearches_investigations.j2 +3 -4
  44. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
  45. {contentctl-4.3.5 → contentctl-4.4.1}/pyproject.toml +3 -2
  46. contentctl-4.3.5/contentctl/output/templates/analyticstories_investigations.j2 +0 -21
  47. contentctl-4.3.5/contentctl/output/templates/finding_report.j2 +0 -30
  48. {contentctl-4.3.5 → contentctl-4.4.1}/LICENSE.md +0 -0
  49. {contentctl-4.3.5 → contentctl-4.4.1}/README.md +0 -0
  50. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/__init__.py +0 -0
  51. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/deploy_acs.py +0 -0
  52. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  53. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  54. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  55. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  56. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  57. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  58. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  59. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/doc_gen.py +0 -0
  60. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/initialize.py +0 -0
  61. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/initialize_old.py +0 -0
  62. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/reporting.py +0 -0
  63. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/actions/test.py +0 -0
  64. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/api.py +0 -0
  65. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/contentctl.py +0 -0
  66. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/enrichments/attack_enrichment.py +0 -0
  67. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  68. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/helper/link_validator.py +0 -0
  69. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/helper/logger.py +0 -0
  70. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/helper/splunk_app.py +0 -0
  71. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/helper/utils.py +0 -0
  72. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/input/yml_reader.py +0 -0
  73. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/alert_action.py +0 -0
  74. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/annotated_types.py +0 -0
  75. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/atomic.py +0 -0
  76. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/base_test.py +0 -0
  77. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/data_source.py +0 -0
  78. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_email.py +0 -0
  79. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_notable.py +0 -0
  80. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_phantom.py +0 -0
  81. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_rba.py +0 -0
  82. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_scheduling.py +0 -0
  83. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/deployment_slack.py +0 -0
  84. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/detection.py +0 -0
  85. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/detection_metadata.py +0 -0
  86. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/detection_stanza.py +0 -0
  87. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/errors.py +0 -0
  88. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/event_source.py +0 -0
  89. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/integration_test.py +0 -0
  90. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/integration_test_result.py +0 -0
  91. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/manual_test.py +0 -0
  92. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/manual_test_result.py +0 -0
  93. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  94. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/notable_action.py +0 -0
  95. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/observable.py +0 -0
  96. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/playbook.py +0 -0
  97. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/playbook_tags.py +0 -0
  98. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/risk_object.py +0 -0
  99. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/savedsearches_conf.py +0 -0
  100. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/security_content_object.py +0 -0
  101. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/story_tags.py +0 -0
  102. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/test_attack_data.py +0 -0
  103. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/test_group.py +0 -0
  104. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/threat_object.py +0 -0
  105. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/unit_test.py +0 -0
  106. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/unit_test_baseline.py +0 -0
  107. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/objects/unit_test_result.py +0 -0
  108. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/api_json_output.py +0 -0
  109. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/attack_nav_output.py +0 -0
  110. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/attack_nav_writer.py +0 -0
  111. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/data_source_writer.py +0 -0
  112. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/detection_writer.py +0 -0
  113. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/doc_md_output.py +0 -0
  114. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/jinja_writer.py +0 -0
  115. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/json_writer.py +0 -0
  116. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/new_content_yml_output.py +0 -0
  117. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/svg_output.py +0 -0
  118. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/app.conf.j2 +0 -0
  119. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/app.manifest.j2 +0 -0
  120. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/collections.j2 +0 -0
  121. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/content-version.j2 +0 -0
  122. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/detection_count.j2 +0 -0
  123. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/detection_coverage.j2 +0 -0
  124. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  125. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_detections.j2 +0 -0
  126. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_navigation.j2 +0 -0
  127. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  128. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  129. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  130. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_stories.j2 +0 -0
  131. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/doc_story_page.j2 +0 -0
  132. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  133. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  134. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/header.j2 +0 -0
  135. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/macros.j2 +0 -0
  136. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/panel.j2 +0 -0
  137. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/transforms.j2 +0 -0
  138. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/templates/workflow_actions.j2 +0 -0
  139. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/yml_output.py +0 -0
  140. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/output/yml_writer.py +0 -0
  141. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/README.md +0 -0
  142. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_default.yml +0 -0
  143. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  144. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  145. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  146. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/README.md +0 -0
  147. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  148. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/app.conf +0 -0
  149. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/commands.conf +0 -0
  150. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/content-version.conf +0 -0
  151. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  152. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  153. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  154. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  155. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  156. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/metadata/default.meta +0 -0
  157. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/static/appIcon.png +0 -0
  158. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  159. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  160. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  161. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  162. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/datamodels_cim.conf +0 -0
  163. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/datamodels_custom.conf +0 -0
  164. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  165. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  166. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  167. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  168. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  169. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/detections/application/.gitkeep +0 -0
  170. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  171. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/detections/network/.gitkeep +0 -0
  172. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/detections/web/.gitkeep +0 -0
  173. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  174. {contentctl-4.3.5 → contentctl-4.4.1}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  175. {contentctl-4.3.5 → contentctl-4.4.1}/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.5
3
+ Version: 4.4.1
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -10,6 +10,7 @@ Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
13
14
  Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
14
15
  Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
15
16
  Requires-Dist: attackcti (>=0.4.0,<0.5.0)
@@ -26,7 +27,7 @@ Requires-Dist: setuptools (>=69.5.1,<76.0.0)
26
27
  Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
27
28
  Requires-Dist: tqdm (>=4.66.5,<5.0.0)
28
29
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
29
- Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
30
+ Requires-Dist: xmltodict (>=0.13,<0.15)
30
31
  Description-Content-Type: text/markdown
31
32
 
32
33
 
@@ -50,6 +50,7 @@ class Build:
50
50
  updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.investigations, SecurityContentType.investigations))
51
51
  updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.lookups, SecurityContentType.lookups))
52
52
  updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.macros, SecurityContentType.macros))
53
+ updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.dashboards, SecurityContentType.dashboards))
53
54
  updated_conf_files.update(conf_output.writeAppConf())
54
55
 
55
56
  #Ensure that the conf file we just generated/update is syntactically valid
@@ -67,9 +67,9 @@ class GitService(BaseModel):
67
67
 
68
68
  #Make a filename to content map
69
69
  filepath_to_content_map = { obj.file_path:obj for (_,obj) in self.director.name_to_content_map.items()}
70
- updated_detections:List[Detection] = []
71
- updated_macros:List[Macro] = []
72
- updated_lookups:List[Lookup] =[]
70
+ updated_detections:set[Detection] = set()
71
+ updated_macros:set[Macro] = set()
72
+ updated_lookups:set[Lookup] = set()
73
73
 
74
74
  for diff in all_diffs:
75
75
  if type(diff) == pygit2.Patch:
@@ -80,14 +80,14 @@ class GitService(BaseModel):
80
80
  if decoded_path.is_relative_to(self.config.path/"detections") and decoded_path.suffix == ".yml":
81
81
  detectionObject = filepath_to_content_map.get(decoded_path, None)
82
82
  if isinstance(detectionObject, Detection):
83
- updated_detections.append(detectionObject)
83
+ updated_detections.add(detectionObject)
84
84
  else:
85
85
  raise Exception(f"Error getting detection object for file {str(decoded_path)}")
86
86
 
87
87
  elif decoded_path.is_relative_to(self.config.path/"macros") and decoded_path.suffix == ".yml":
88
88
  macroObject = filepath_to_content_map.get(decoded_path, None)
89
89
  if isinstance(macroObject, Macro):
90
- updated_macros.append(macroObject)
90
+ updated_macros.add(macroObject)
91
91
  else:
92
92
  raise Exception(f"Error getting macro object for file {str(decoded_path)}")
93
93
 
@@ -98,7 +98,7 @@ class GitService(BaseModel):
98
98
  updatedLookup = filepath_to_content_map.get(decoded_path, None)
99
99
  if not isinstance(updatedLookup,Lookup):
100
100
  raise Exception(f"Expected {decoded_path} to be type {type(Lookup)}, but instead if was {(type(updatedLookup))}")
101
- updated_lookups.append(updatedLookup)
101
+ updated_lookups.add(updatedLookup)
102
102
 
103
103
  elif decoded_path.suffix == ".csv":
104
104
  # If the CSV was updated, we want to make sure that we
@@ -125,7 +125,7 @@ class GitService(BaseModel):
125
125
  if updatedLookup is not None and updatedLookup not in updated_lookups:
126
126
  # It is possible that both the CSV and YML have been modified for the same lookup,
127
127
  # and we do not want to add it twice.
128
- updated_lookups.append(updatedLookup)
128
+ updated_lookups.add(updatedLookup)
129
129
 
130
130
  else:
131
131
  pass
@@ -136,7 +136,7 @@ class GitService(BaseModel):
136
136
 
137
137
  # If a detection has at least one dependency on changed content,
138
138
  # then we must test it again
139
- changed_macros_and_lookups = updated_macros + updated_lookups
139
+ changed_macros_and_lookups:set[SecurityContentObject] = updated_macros.union(updated_lookups)
140
140
 
141
141
  for detection in self.director.detections:
142
142
  if detection in updated_detections:
@@ -146,14 +146,14 @@ class GitService(BaseModel):
146
146
 
147
147
  for obj in changed_macros_and_lookups:
148
148
  if obj in detection.get_content_dependencies():
149
- updated_detections.append(detection)
149
+ updated_detections.add(detection)
150
150
  break
151
151
 
152
152
  #Print out the names of all modified/new content
153
153
  modifiedAndNewContentString = "\n - ".join(sorted([d.name for d in updated_detections]))
154
154
 
155
155
  print(f"[{len(updated_detections)}] Pieces of modifed and new content (this may include experimental/deprecated/manual_test content):\n - {modifiedAndNewContentString}")
156
- return updated_detections
156
+ return sorted(list(updated_detections))
157
157
 
158
158
  def getSelected(self, detectionFilenames: List[FilePath]) -> List[Detection]:
159
159
  filepath_to_content_map: dict[FilePath, SecurityContentObject] = {
@@ -13,7 +13,7 @@ from sys import stdout
13
13
  from shutil import copyfile
14
14
  from typing import Union, Optional
15
15
 
16
- from pydantic import BaseModel, PrivateAttr, Field, dataclasses
16
+ from pydantic import ConfigDict, BaseModel, PrivateAttr, Field, dataclasses
17
17
  import requests # type: ignore
18
18
  import splunklib.client as client # type: ignore
19
19
  from splunklib.binding import HTTPError # type: ignore
@@ -48,9 +48,9 @@ class SetupTestGroupResults(BaseModel):
48
48
  success: bool = True
49
49
  duration: float = 0
50
50
  start_time: float
51
-
52
- class Config:
53
- arbitrary_types_allowed = True
51
+ model_config = ConfigDict(
52
+ arbitrary_types_allowed=True
53
+ )
54
54
 
55
55
 
56
56
  class CleanupTestGroupResults(BaseModel):
@@ -68,6 +68,15 @@ class CannotRunBaselineException(Exception):
68
68
  # exception
69
69
  pass
70
70
 
71
+ class ReplayIndexDoesNotExistOnServer(Exception):
72
+ '''
73
+ In order to replay data files into the Splunk Server
74
+ for testing, they must be replayed into an index that
75
+ exists. If that index does not exist, this error will
76
+ be generated and raised before we try to do anything else
77
+ with that Data File.
78
+ '''
79
+ pass
71
80
 
72
81
  @dataclasses.dataclass(frozen=False)
73
82
  class DetectionTestingManagerOutputDto():
@@ -75,7 +84,7 @@ class DetectionTestingManagerOutputDto():
75
84
  outputQueue: list[Detection] = Field(default_factory=list)
76
85
  currentTestingQueue: dict[str, Union[Detection, None]] = Field(default_factory=dict)
77
86
  start_time: Union[datetime.datetime, None] = None
78
- replay_index: str = "CONTENTCTL_TESTING_INDEX"
87
+ replay_index: str = "contentctl_testing_index"
79
88
  replay_host: str = "CONTENTCTL_HOST"
80
89
  timeout_seconds: int = 60
81
90
  terminate: bool = False
@@ -88,12 +97,13 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
88
97
  sync_obj: DetectionTestingManagerOutputDto
89
98
  hec_token: str = ""
90
99
  hec_channel: str = ""
100
+ all_indexes_on_server: list[str] = []
91
101
  _conn: client.Service = PrivateAttr()
92
102
  pbar: tqdm.tqdm = None
93
103
  start_time: Optional[float] = None
94
-
95
- class Config:
96
- arbitrary_types_allowed = True
104
+ model_config = ConfigDict(
105
+ arbitrary_types_allowed=True
106
+ )
97
107
 
98
108
  def __init__(self, **data):
99
109
  super().__init__(**data)
@@ -131,6 +141,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
131
141
  (self.get_conn, "Waiting for App Installation"),
132
142
  (self.configure_conf_file_datamodels, "Configuring Datamodels"),
133
143
  (self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
144
+ (self.get_all_indexes, "Getting all indexes from server"),
134
145
  (self.configure_imported_roles, "Configuring Roles"),
135
146
  (self.configure_delete_indexes, "Configuring Indexes"),
136
147
  (self.configure_hec, "Configuring HEC"),
@@ -169,12 +180,11 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
169
180
  pass
170
181
 
171
182
  try:
172
-
173
183
  res = self.get_conn().inputs.create(
174
184
  name="DETECTION_TESTING_HEC",
175
185
  kind="http",
176
186
  index=self.sync_obj.replay_index,
177
- indexes=f"{self.sync_obj.replay_index},_internal,_audit",
187
+ indexes=",".join(self.all_indexes_on_server), # This allows the HEC to write to all indexes
178
188
  useACK=True,
179
189
  )
180
190
  self.hec_token = str(res.token)
@@ -183,6 +193,23 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
183
193
  except Exception as e:
184
194
  raise (Exception(f"Failure creating HEC Endpoint: {str(e)}"))
185
195
 
196
+ def get_all_indexes(self) -> None:
197
+ """
198
+ Retrieve a list of all indexes in the Splunk instance
199
+ """
200
+ try:
201
+ # We do not include the replay index because by
202
+ # the time we get to this function, it has already
203
+ # been created on the server.
204
+ indexes = []
205
+ res = self.get_conn().indexes
206
+ for index in res.list():
207
+ indexes.append(index.name)
208
+ # Retrieve all available indexes on the splunk instance
209
+ self.all_indexes_on_server = indexes
210
+ except Exception as e:
211
+ raise (Exception(f"Failure getting indexes: {str(e)}"))
212
+
186
213
  def get_conn(self) -> client.Service:
187
214
  try:
188
215
  if not self._conn:
@@ -265,39 +292,41 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
265
292
  self,
266
293
  imported_roles: list[str] = ["user", "power", "can_delete"],
267
294
  enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
268
- indexes: list[str] = ["_*", "*"],
269
- ):
270
- indexes.append(self.sync_obj.replay_index)
271
- indexes_encoded = ";".join(indexes)
295
+ ):
272
296
  try:
297
+ # Set which roles should be configured. For Enterprise Security/Integration Testing,
298
+ # we must add some extra foles.
299
+ if self.global_config.enable_integration_testing:
300
+ roles = imported_roles + enterprise_security_roles
301
+ else:
302
+ roles = imported_roles
303
+
273
304
  self.get_conn().roles.post(
274
305
  self.infrastructure.splunk_app_username,
275
- imported_roles=imported_roles + enterprise_security_roles,
276
- srchIndexesAllowed=indexes_encoded,
306
+ imported_roles=roles,
307
+ srchIndexesAllowed=";".join(self.all_indexes_on_server),
277
308
  srchIndexesDefault=self.sync_obj.replay_index,
278
309
  )
279
310
  return
280
311
  except Exception as e:
281
312
  self.pbar.write(
282
- f"Enterprise Security Roles do not exist:'{enterprise_security_roles}: {str(e)}"
313
+ f"The following role(s) do not exist:'{enterprise_security_roles}: {str(e)}"
283
314
  )
284
315
 
285
316
  self.get_conn().roles.post(
286
317
  self.infrastructure.splunk_app_username,
287
318
  imported_roles=imported_roles,
288
- srchIndexesAllowed=indexes_encoded,
319
+ srchIndexesAllowed=";".join(self.all_indexes_on_server),
289
320
  srchIndexesDefault=self.sync_obj.replay_index,
290
321
  )
291
322
 
292
- def configure_delete_indexes(self, indexes: list[str] = ["_*", "*"]):
293
- indexes.append(self.sync_obj.replay_index)
323
+ def configure_delete_indexes(self):
294
324
  endpoint = "/services/properties/authorize/default/deleteIndexesAllowed"
295
- indexes_encoded = ";".join(indexes)
296
325
  try:
297
- self.get_conn().post(endpoint, value=indexes_encoded)
326
+ self.get_conn().post(endpoint, value=";".join(self.all_indexes_on_server))
298
327
  except Exception as e:
299
328
  self.pbar.write(
300
- f"Error configuring deleteIndexesAllowed with '{indexes_encoded}': [{str(e)}]"
329
+ f"Error configuring deleteIndexesAllowed with '{self.all_indexes_on_server}': [{str(e)}]"
301
330
  )
302
331
 
303
332
  def wait_for_conf_file(self, app_name: str, conf_file_name: str):
@@ -646,8 +675,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
646
675
  # Set the mode and timeframe, if required
647
676
  kwargs = {"exec_mode": "blocking"}
648
677
 
649
-
650
-
651
678
  # Set earliest_time and latest_time appropriately if FORCE_ALL_TIME is False
652
679
  if not FORCE_ALL_TIME:
653
680
  if test.earliest_time is not None:
@@ -1027,8 +1054,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1027
1054
  # Get the start time and compute the timeout
1028
1055
  search_start_time = time.time()
1029
1056
  search_stop_time = time.time() + self.sync_obj.timeout_seconds
1030
-
1031
- # Make a copy of the search string since we may
1057
+
1058
+ # Make a copy of the search string since we may
1032
1059
  # need to make some small changes to it below
1033
1060
  search = detection.search
1034
1061
 
@@ -1080,8 +1107,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1080
1107
  # Initialize the collection of fields that are empty that shouldn't be
1081
1108
  present_threat_objects: set[str] = set()
1082
1109
  empty_fields: set[str] = set()
1083
-
1084
-
1085
1110
 
1086
1111
  # Filter out any messages in the results
1087
1112
  for result in results:
@@ -1111,7 +1136,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1111
1136
  # not populated and we should throw an error. This can happen if there is a typo
1112
1137
  # on a field. In this case, the field will appear but will not contain any values
1113
1138
  current_empty_fields: set[str] = set()
1114
-
1139
+
1115
1140
  for field in observable_fields_set:
1116
1141
  if result.get(field, 'null') == 'null':
1117
1142
  if field in risk_object_fields_set:
@@ -1131,9 +1156,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1131
1156
  if field in threat_object_fields_set:
1132
1157
  present_threat_objects.add(field)
1133
1158
  continue
1134
-
1135
1159
 
1136
-
1137
1160
  # If everything succeeded up until now, and no empty fields are found in the
1138
1161
  # current result, then the search was a success
1139
1162
  if len(current_empty_fields) == 0:
@@ -1147,8 +1170,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1147
1170
 
1148
1171
  else:
1149
1172
  empty_fields = empty_fields.union(current_empty_fields)
1150
-
1151
-
1173
+
1152
1174
  missing_threat_objects = threat_object_fields_set - present_threat_objects
1153
1175
  # Report a failure if there were empty fields in a threat object in all results
1154
1176
  if len(missing_threat_objects) > 0:
@@ -1164,7 +1186,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1164
1186
  duration=time.time() - search_start_time,
1165
1187
  )
1166
1188
  return
1167
-
1168
1189
 
1169
1190
  test.result.set_job_content(
1170
1191
  job.content,
@@ -1225,9 +1246,19 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1225
1246
  test_group: TestGroup,
1226
1247
  test_group_start_time: float,
1227
1248
  ):
1228
- tempfile = mktemp(dir=tmp_dir)
1229
-
1249
+ # Before attempting to replay the file, ensure that the index we want
1250
+ # to replay into actuall exists. If not, we should throw a detailed
1251
+ # exception that can easily be interpreted by the user.
1252
+ if attack_data_file.custom_index is not None and \
1253
+ attack_data_file.custom_index not in self.all_indexes_on_server:
1254
+ raise ReplayIndexDoesNotExistOnServer(
1255
+ f"Unable to replay data file {attack_data_file.data} "
1256
+ f"into index '{attack_data_file.custom_index}'. "
1257
+ "The index does not exist on the Splunk Server. "
1258
+ f"The only valid indexes on the server are {self.all_indexes_on_server}"
1259
+ )
1230
1260
 
1261
+ tempfile = mktemp(dir=tmp_dir)
1231
1262
  if not (str(attack_data_file.data).startswith("http://") or
1232
1263
  str(attack_data_file.data).startswith("https://")) :
1233
1264
  if pathlib.Path(str(attack_data_file.data)).is_file():
@@ -1272,7 +1303,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1272
1303
  )
1273
1304
  )
1274
1305
 
1275
-
1276
1306
  # Upload the data
1277
1307
  self.format_pbar_string(
1278
1308
  TestReportingType.GROUP,
@@ -49,13 +49,17 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
49
49
  def check_for_teardown(self):
50
50
 
51
51
  try:
52
- self.get_docker_client().containers.get(self.get_name())
52
+ container: docker.models.containers.Container = self.get_docker_client().containers.get(self.get_name())
53
53
  except Exception as e:
54
54
  if self.sync_obj.terminate is not True:
55
55
  self.pbar.write(
56
56
  f"Error: could not get container [{self.get_name()}]: {str(e)}"
57
57
  )
58
58
  self.sync_obj.terminate = True
59
+ else:
60
+ if container.status != 'running':
61
+ self.sync_obj.terminate = True
62
+ self.container = None
59
63
 
60
64
  if self.sync_obj.terminate:
61
65
  self.finish()
@@ -1,12 +1,14 @@
1
- from bottle import template, Bottle, ServerAdapter
2
- from contentctl.actions.detection_testing.views.DetectionTestingView import (
3
- DetectionTestingView,
4
- )
1
+ from threading import Thread
5
2
 
3
+ from bottle import template, Bottle, ServerAdapter
6
4
  from wsgiref.simple_server import make_server, WSGIRequestHandler
7
5
  import jinja2
8
6
  import webbrowser
9
- from threading import Thread
7
+ from pydantic import ConfigDict
8
+
9
+ from contentctl.actions.detection_testing.views.DetectionTestingView import (
10
+ DetectionTestingView,
11
+ )
10
12
 
11
13
  DEFAULT_WEB_UI_PORT = 7999
12
14
 
@@ -100,9 +102,9 @@ class SimpleWebServer(ServerAdapter):
100
102
  class DetectionTestingViewWeb(DetectionTestingView):
101
103
  bottleApp: Bottle = Bottle()
102
104
  server: SimpleWebServer = SimpleWebServer(host="0.0.0.0", port=DEFAULT_WEB_UI_PORT)
103
-
104
- class Config:
105
- arbitrary_types_allowed = True
105
+ model_config = ConfigDict(
106
+ arbitrary_types_allowed=True
107
+ )
106
108
 
107
109
  def setup(self):
108
110
  self.bottleApp.route("/", callback=self.showStatus)
@@ -297,9 +297,11 @@ class Inspect:
297
297
  validation_errors[rule_name] = []
298
298
  # No detections should be removed from build to build
299
299
  if rule_name not in current_build_conf.detection_stanzas:
300
- validation_errors[rule_name].append(DetectionMissingError(rule_name=rule_name))
300
+ if config.suppress_missing_content_exceptions:
301
+ print(f"[SUPPRESSED] {DetectionMissingError(rule_name=rule_name).long_message}")
302
+ else:
303
+ validation_errors[rule_name].append(DetectionMissingError(rule_name=rule_name))
301
304
  continue
302
-
303
305
  # Pull out the individual stanza for readability
304
306
  previous_stanza = previous_build_conf.detection_stanzas[rule_name]
305
307
  current_stanza = current_build_conf.detection_stanzas[rule_name]
@@ -335,7 +337,7 @@ class Inspect:
335
337
  )
336
338
 
337
339
  # Convert our dict mapping to a flat list of errors for use in reporting
338
- validation_error_list = [x for inner_list in validation_errors.values() for x in inner_list]
340
+ validation_error_list = [x for inner_list in validation_errors.values() for x in inner_list]
339
341
 
340
342
  # Report failure/success
341
343
  print("\nDetection Metadata Validation:")
@@ -355,4 +357,4 @@ class Inspect:
355
357
  raise ExceptionGroup(
356
358
  "Validation errors when comparing detection stanzas in current and previous build:",
357
359
  validation_error_list
358
- )
360
+ )
@@ -16,7 +16,11 @@ class NewContent:
16
16
 
17
17
  def buildDetection(self)->dict[str,Any]:
18
18
  questions = NewContentQuestions.get_questions_detection()
19
- answers = questionary.prompt(questions)
19
+ answers: dict[str,str] = questionary.prompt(
20
+ questions,
21
+ kbi_msg="User did not answer all of the prompt questions. Exiting...")
22
+ if not answers:
23
+ raise ValueError("User didn't answer one or more questions!")
20
24
  answers.update(answers)
21
25
  answers['name'] = answers['detection_name']
22
26
  del answers['detection_name']
@@ -70,7 +74,11 @@ class NewContent:
70
74
 
71
75
  def buildStory(self)->dict[str,Any]:
72
76
  questions = NewContentQuestions.get_questions_story()
73
- answers = questionary.prompt(questions)
77
+ answers = questionary.prompt(
78
+ questions,
79
+ kbi_msg="User did not answer all of the prompt questions. Exiting...")
80
+ if not answers:
81
+ raise ValueError("User didn't answer one or more questions!")
74
82
  answers['name'] = answers['story_name']
75
83
  del answers['story_name']
76
84
  answers['id'] = str(uuid.uuid4())
@@ -114,9 +114,11 @@ class ReleaseNotes:
114
114
  #If a branch name is supplied, compare against develop
115
115
  if config.latest_branch not in repo.branches:
116
116
  raise ValueError(f"latest branch {config.latest_branch} does not exist in the repository. Make sure your branch name is correct")
117
- compare_against = "develop"
117
+ if config.compare_against not in repo.branches:
118
+ raise ValueError(f"compare_against branch {config.compare_against} does not exist in the repository. Make sure your branch name is correct")
119
+
118
120
  commit1 = repo.commit(config.latest_branch)
119
- commit2 = repo.commit(compare_against)
121
+ commit2 = repo.commit(config.compare_against)
120
122
  diff_index = commit2.diff(commit1)
121
123
 
122
124
  modified_files:List[pathlib.Path] = []
@@ -189,7 +191,7 @@ class ReleaseNotes:
189
191
 
190
192
  if config.latest_branch:
191
193
  print(f"Generating release notes - \033[92m{config.latest_branch}\033[0m")
192
- print(f"Compared against - \033[92m{compare_against}\033[0m")
194
+ print(f"Compared against - \033[92m{config.compare_against}\033[0m")
193
195
  print("\n## Release notes for ESCU " + config.latest_branch)
194
196
 
195
197
  notes = [self.create_notes(config.path, stories_added, header="New Analytic Story"),
@@ -12,7 +12,7 @@ from contentctl.helper.splunk_app import SplunkApp
12
12
 
13
13
 
14
14
  class Validate:
15
- def execute(self, input_dto: validate) -> DirectorOutputDto:
15
+ def execute(self, input_dto: validate) -> DirectorOutputDto:
16
16
  director_output_dto = DirectorOutputDto(
17
17
  AtomicEnrichment.getAtomicEnrichment(input_dto),
18
18
  AttackEnrichment.getAttackEnrichment(input_dto),
@@ -26,6 +26,7 @@ class Validate:
26
26
  [],
27
27
  [],
28
28
  [],
29
+ []
29
30
  )
30
31
 
31
32
  director = Director(director_output_dto)
@@ -5,7 +5,7 @@ import os
5
5
  import shelve
6
6
  import time
7
7
  from typing import Annotated, Any, Union, TYPE_CHECKING
8
- from pydantic import BaseModel,Field, computed_field
8
+ from pydantic import ConfigDict, 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
@@ -32,13 +32,12 @@ class CveEnrichmentObj(BaseModel):
32
32
  class CveEnrichment(BaseModel):
33
33
  use_enrichment: bool = True
34
34
  cve_api_obj: Union[CVESearch,None] = None
35
-
36
35
 
37
- class Config:
38
- # Arbitrary_types are allowed to let us use the CVESearch Object
39
- arbitrary_types_allowed = True
40
- frozen = True
41
-
36
+ # Arbitrary_types are allowed to let us use the CVESearch Object
37
+ model_config = ConfigDict(
38
+ arbitrary_types_allowed=True,
39
+ frozen=True
40
+ )
42
41
 
43
42
  @staticmethod
44
43
  def getCveEnrichment(config:validate, timeout_seconds:int=10, force_disable_enrichment:bool=True)->CveEnrichment:
@@ -1,17 +1,14 @@
1
1
  import os
2
2
  import sys
3
- import pathlib
4
- from typing import Union
3
+ from pathlib import Path
5
4
  from dataclasses import dataclass, field
6
5
  from pydantic import ValidationError
7
6
  from uuid import UUID
8
7
  from contentctl.input.yml_reader import YmlReader
9
8
 
10
-
11
9
  from contentctl.objects.detection import Detection
12
10
  from contentctl.objects.story import Story
13
11
 
14
- from contentctl.objects.enums import SecurityContentProduct
15
12
  from contentctl.objects.baseline import Baseline
16
13
  from contentctl.objects.investigation import Investigation
17
14
  from contentctl.objects.playbook import Playbook
@@ -21,20 +18,15 @@ from contentctl.objects.lookup import Lookup
21
18
  from contentctl.objects.atomic import AtomicEnrichment
22
19
  from contentctl.objects.security_content_object import SecurityContentObject
23
20
  from contentctl.objects.data_source import DataSource
24
- from contentctl.objects.event_source import EventSource
25
-
21
+ from contentctl.objects.dashboard import Dashboard
26
22
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
27
23
  from contentctl.enrichments.cve_enrichment import CveEnrichment
28
24
 
29
25
  from contentctl.objects.config import validate
30
26
  from contentctl.objects.enums import SecurityContentType
31
-
32
- from contentctl.objects.enums import DetectionStatus
33
27
  from contentctl.helper.utils import Utils
34
28
 
35
29
 
36
-
37
-
38
30
  @dataclass
39
31
  class DirectorOutputDto:
40
32
  # Atomic Tests are first because parsing them
@@ -50,6 +42,8 @@ class DirectorOutputDto:
50
42
  macros: list[Macro]
51
43
  lookups: list[Lookup]
52
44
  deployments: list[Deployment]
45
+ dashboards: list[Dashboard]
46
+
53
47
  data_sources: list[DataSource]
54
48
  name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
55
49
  uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
@@ -88,6 +82,9 @@ class DirectorOutputDto:
88
82
  self.stories.append(content)
89
83
  elif isinstance(content, Detection):
90
84
  self.detections.append(content)
85
+ elif isinstance(content, Dashboard):
86
+ self.dashboards.append(content)
87
+
91
88
  elif isinstance(content, DataSource):
92
89
  self.data_sources.append(content)
93
90
  else:
@@ -115,7 +112,7 @@ class Director():
115
112
  self.createSecurityContent(SecurityContentType.data_sources)
116
113
  self.createSecurityContent(SecurityContentType.playbooks)
117
114
  self.createSecurityContent(SecurityContentType.detections)
118
-
115
+ self.createSecurityContent(SecurityContentType.dashboards)
119
116
 
120
117
  from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
121
118
  if len(MISSING_SOURCES) > 0:
@@ -137,6 +134,7 @@ class Director():
137
134
  SecurityContentType.playbooks,
138
135
  SecurityContentType.detections,
139
136
  SecurityContentType.data_sources,
137
+ SecurityContentType.dashboards
140
138
  ]:
141
139
  files = Utils.get_all_yml_files_from_directory(
142
140
  os.path.join(self.input_dto.path, str(contentType.name))
@@ -147,7 +145,7 @@ class Director():
147
145
  else:
148
146
  raise (Exception(f"Cannot createSecurityContent for unknown product {contentType}."))
149
147
 
150
- validation_errors = []
148
+ validation_errors:list[tuple[Path,ValueError]] = []
151
149
 
152
150
  already_ran = False
153
151
  progress_percent = 0
@@ -189,6 +187,10 @@ class Director():
189
187
  elif contentType == SecurityContentType.detections:
190
188
  detection = Detection.model_validate(modelDict, context={"output_dto":self.output_dto, "app":self.input_dto.app})
191
189
  self.output_dto.addContentToDictMappings(detection)
190
+
191
+ elif contentType == SecurityContentType.dashboards:
192
+ dashboard = Dashboard.model_validate(modelDict,context={"output_dto":self.output_dto})
193
+ self.output_dto.addContentToDictMappings(dashboard)
192
194
 
193
195
  elif contentType == SecurityContentType.data_sources:
194
196
  data_source = DataSource.model_validate(