contentctl 4.0.5__tar.gz → 4.1.0__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 (174) hide show
  1. {contentctl-4.0.5 → contentctl-4.1.0}/PKG-INFO +7 -8
  2. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/inspect.py +1 -1
  3. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/new_content.py +6 -3
  4. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/validate.py +1 -0
  5. contentctl-4.1.0/contentctl/api.py +137 -0
  6. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/contentctl.py +28 -24
  7. contentctl-4.1.0/contentctl/enrichments/cve_enrichment.py +65 -0
  8. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/director.py +72 -72
  9. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +77 -13
  10. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -0
  11. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/baseline.py +0 -1
  12. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/config.py +4 -8
  13. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/detection_tags.py +1 -1
  14. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/macro.py +8 -7
  15. contentctl-4.1.0/contentctl/output/yml_writer.py +49 -0
  16. {contentctl-4.0.5 → contentctl-4.1.0}/pyproject.toml +7 -8
  17. contentctl-4.0.5/contentctl/actions/apav_deploy.py +0 -98
  18. contentctl-4.0.5/contentctl/actions/api_deploy.py +0 -151
  19. contentctl-4.0.5/contentctl/enrichments/cve_enrichment.py +0 -100
  20. contentctl-4.0.5/contentctl/output/yml_writer.py +0 -11
  21. contentctl-4.0.5/contentctl/templates/app_template/default/distsearch.conf +0 -5
  22. {contentctl-4.0.5 → contentctl-4.1.0}/LICENSE.md +0 -0
  23. {contentctl-4.0.5 → contentctl-4.1.0}/README.md +0 -0
  24. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/__init__.py +0 -0
  25. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/build.py +0 -0
  26. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/convert.py +0 -0
  27. /contentctl-4.0.5/contentctl/actions/acs_deploy.py → /contentctl-4.1.0/contentctl/actions/deploy_acs.py +0 -0
  28. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  29. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/GitService.py +0 -0
  30. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  31. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
  32. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  33. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  34. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  35. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  36. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  37. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  38. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  39. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/doc_gen.py +0 -0
  40. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/initialize.py +0 -0
  41. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/initialize_old.py +0 -0
  42. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/release_notes.py +0 -0
  43. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/reporting.py +0 -0
  44. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/test.py +0 -0
  45. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/enrichments/attack_enrichment.py +0 -0
  46. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  47. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/link_validator.py +0 -0
  48. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/logger.py +0 -0
  49. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/utils.py +0 -0
  50. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/backend_splunk_ba.py +0 -0
  51. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/new_content_questions.py +0 -0
  52. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/sigma_converter.py +0 -0
  53. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/ssa_detection_builder.py +0 -0
  54. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/yml_reader.py +0 -0
  55. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/alert_action.py +0 -0
  56. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/atomic.py +0 -0
  57. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/base_test.py +0 -0
  58. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/base_test_result.py +0 -0
  59. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/baseline_tags.py +0 -0
  60. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/constants.py +0 -0
  61. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/correlation_search.py +0 -0
  62. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/data_source.py +0 -0
  63. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment.py +0 -0
  64. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_email.py +0 -0
  65. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_notable.py +0 -0
  66. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_phantom.py +0 -0
  67. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_rba.py +0 -0
  68. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_scheduling.py +0 -0
  69. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_slack.py +0 -0
  70. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/detection.py +0 -0
  71. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/enums.py +0 -0
  72. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/integration_test.py +0 -0
  73. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/integration_test_result.py +0 -0
  74. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/investigation.py +0 -0
  75. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/investigation_tags.py +0 -0
  76. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/lookup.py +0 -0
  77. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  78. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/notable_action.py +0 -0
  79. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/observable.py +0 -0
  80. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/playbook.py +0 -0
  81. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/playbook_tags.py +0 -0
  82. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/risk_analysis_action.py +0 -0
  83. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/risk_object.py +0 -0
  84. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/security_content_object.py +0 -0
  85. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/ssa_detection.py +0 -0
  86. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/ssa_detection_tags.py +0 -0
  87. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/story.py +0 -0
  88. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/story_tags.py +0 -0
  89. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/test_group.py +0 -0
  90. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/threat_object.py +0 -0
  91. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test.py +0 -0
  92. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_attack_data.py +0 -0
  93. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_baseline.py +0 -0
  94. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_old.py +0 -0
  95. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_result.py +0 -0
  96. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_ssa.py +0 -0
  97. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/api_json_output.py +0 -0
  98. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/attack_nav_output.py +0 -0
  99. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/attack_nav_writer.py +0 -0
  100. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/ba_yml_output.py +0 -0
  101. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/conf_output.py +0 -0
  102. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/conf_writer.py +0 -0
  103. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/detection_writer.py +0 -0
  104. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/doc_md_output.py +0 -0
  105. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/finding_report_writer.py +0 -0
  106. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/jinja_writer.py +0 -0
  107. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/json_writer.py +0 -0
  108. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/new_content_yml_output.py +0 -0
  109. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/svg_output.py +0 -0
  110. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  111. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  112. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  113. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/app.conf.j2 +0 -0
  114. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/app.manifest.j2 +0 -0
  115. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/collections.j2 +0 -0
  116. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/content-version.j2 +0 -0
  117. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/detection_count.j2 +0 -0
  118. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
  119. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  120. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_detections.j2 +0 -0
  121. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
  122. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  123. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  124. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  125. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_stories.j2 +0 -0
  126. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
  127. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  128. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  129. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/finding_report.j2 +0 -0
  130. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/header.j2 +0 -0
  131. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/macros.j2 +0 -0
  132. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/panel.j2 +0 -0
  133. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  134. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  135. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  136. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/transforms.j2 +0 -0
  137. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
  138. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/yml_output.py +0 -0
  139. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/README +0 -0
  140. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_default.yml +0 -0
  141. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  142. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  143. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  144. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README.md +0 -0
  145. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  146. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/app.conf +0 -0
  147. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/commands.conf +0 -0
  148. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
  149. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  150. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  151. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  152. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/usage_searches.conf +0 -0
  153. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  154. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  155. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
  156. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
  157. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  158. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  159. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  160. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/datamodels_cim.conf +0 -0
  161. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/datamodels_custom.conf +0 -0
  162. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  163. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  164. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  165. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  166. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  167. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/application/.gitkeep +0 -0
  168. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  169. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
  170. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/network/.gitkeep +0 -0
  171. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/web/.gitkeep +0 -0
  172. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  173. {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  174. {contentctl-4.0.5 → contentctl-4.1.0}/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.0.5
3
+ Version: 4.1.0
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -10,25 +10,24 @@ 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
- Requires-Dist: Jinja2 (>=3.1.2,<4.0.0)
13
+ Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
14
14
  Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
15
15
  Requires-Dist: attackcti (>=0.3.7,<0.4.0)
16
16
  Requires-Dist: bottle (>=0.12.25,<0.13.0)
17
17
  Requires-Dist: docker (>=7.1.0,<8.0.0)
18
18
  Requires-Dist: gitpython (>=3.1.43,<4.0.0)
19
19
  Requires-Dist: pycvesearch (>=1.2,<2.0)
20
- Requires-Dist: pydantic (>=2.5.1,<3.0.0)
20
+ Requires-Dist: pydantic (>=2.7.1,<3.0.0)
21
21
  Requires-Dist: pygit2 (>=1.14.1,<2.0.0)
22
- Requires-Dist: pysigma (>=0.10.8,<0.11.0)
23
- Requires-Dist: pysigma-backend-splunk (>=1.0.3,<2.0.0)
22
+ Requires-Dist: pysigma (>=0.11.5,<0.12.0)
23
+ Requires-Dist: pysigma-backend-splunk (>=1.1.0,<2.0.0)
24
24
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
25
25
  Requires-Dist: requests (>=2.32.2,<2.33.0)
26
26
  Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
27
- Requires-Dist: setuptools (>=69.5.1,<70.0.0)
27
+ Requires-Dist: setuptools (>=69.5.1,<71.0.0)
28
28
  Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
29
- Requires-Dist: tqdm (>=4.66.1,<5.0.0)
29
+ Requires-Dist: tqdm (>=4.66.4,<5.0.0)
30
30
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
31
- Requires-Dist: validators (>=0.22.0,<0.23.0)
32
31
  Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
33
32
  Description-Content-Type: text/markdown
34
33
 
@@ -61,7 +61,7 @@ class Inspect:
61
61
  if not package_path.is_file():
62
62
  raise Exception(f"Cannot run Appinspect API on App '{config.app.title}' - "
63
63
  f"no package exists as expected path '{package_path}'.\nAre you "
64
- "trying to 'contentctl acs_deploy' the package BEFORE running 'contentctl build'?")
64
+ "trying to 'contentctl deploy_acs' the package BEFORE running 'contentctl build'?")
65
65
 
66
66
  files = {
67
67
  "app_package": open(package_path,"rb"),
@@ -25,7 +25,8 @@ class NewContent:
25
25
  answers['date'] = datetime.today().strftime('%Y-%m-%d')
26
26
  answers['author'] = answers['detection_author']
27
27
  del answers['detection_author']
28
- answers['data_source'] = answers['data_source']
28
+ answers['data_sources'] = answers['data_source']
29
+ del answers['data_source']
29
30
  answers['type'] = answers['detection_type']
30
31
  del answers['detection_type']
31
32
  answers['status'] = "production" #start everything as production since that's what we INTEND the content to become
@@ -49,6 +50,7 @@ class NewContent:
49
50
  answers['tags']['required_fields'] = ['UPDATE']
50
51
  answers['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
51
52
  answers['tags']['security_domain'] = answers['security_domain']
53
+ del answers["security_domain"]
52
54
  answers['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
53
55
 
54
56
  #generate the tests section
@@ -64,6 +66,7 @@ class NewContent:
64
66
  ]
65
67
  }
66
68
  ]
69
+ del answers["mitre_attack_ids"]
67
70
  return answers
68
71
 
69
72
  def buildStory(self)->dict[str,Any]:
@@ -111,12 +114,12 @@ class NewContent:
111
114
  #make sure the output folder exists for this detection
112
115
  output_folder.mkdir(exist_ok=True)
113
116
 
114
- YmlWriter.writeYmlFile(file_path, object)
117
+ YmlWriter.writeDetection(file_path, object)
115
118
  print("Successfully created detection " + file_path)
116
119
 
117
120
  elif type == NewContentType.story:
118
121
  file_path = os.path.join(self.output_path, 'stories', self.convertNameToFileName(object['name'], object['tags']['product']))
119
- YmlWriter.writeYmlFile(file_path, object)
122
+ YmlWriter.writeStory(file_path, object)
120
123
  print("Successfully created story " + file_path)
121
124
 
122
125
  else:
@@ -23,6 +23,7 @@ class Validate:
23
23
  director_output_dto = DirectorOutputDto(AtomicTest.getAtomicTestsFromArtRepo(repo_path=input_dto.getAtomicRedTeamRepoPath(),
24
24
  enabled=input_dto.enrichments),
25
25
  AttackEnrichment.getAttackEnrichment(input_dto),
26
+ CveEnrichment.getCveEnrichment(input_dto),
26
27
  [],[],[],[],[],[],[],[],[])
27
28
 
28
29
 
@@ -0,0 +1,137 @@
1
+ from pathlib import Path
2
+ from typing import Any, Union, Type
3
+ from contentctl.input.yml_reader import YmlReader
4
+ from contentctl.objects.config import test_common, test, test_servers
5
+ from contentctl.objects.security_content_object import SecurityContentObject
6
+ from contentctl.input.director import DirectorOutputDto
7
+
8
+ def config_from_file(path:Path=Path("contentctl.yml"), config: dict[str,Any]={},
9
+ configType:Type[Union[test,test_servers]]=test)->test_common:
10
+
11
+ """
12
+ Fetch a configuration object that can be used for a number of different contentctl
13
+ operations including validate, build, inspect, test, and test_servers. A file will
14
+ be used as the basis for constructing the configuration.
15
+
16
+ Args:
17
+ path (Path, optional): Relative or absolute path to a contentctl config file.
18
+ Defaults to Path("contentctl.yml"), which is the default name and location (in the current directory)
19
+ of the configuration files which are automatically generated for contentctl.
20
+ config (dict[], optional): Dictionary of values to override values read from the YML
21
+ path passed as the first argument. Defaults to {}, an empty dict meaning that nothing
22
+ will be overwritten
23
+ configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
24
+ This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
25
+ Returns:
26
+ test_common: Returns a complete contentctl test_common configuration. Note that this configuration
27
+ will have all applicable field for validate and build as well, but can also be used for easily
28
+ construction a test or test_servers object.
29
+ """
30
+
31
+ try:
32
+ yml_dict = YmlReader.load_file(path, add_fields=False)
33
+
34
+
35
+ except Exception as e:
36
+ raise Exception(f"Failed to load contentctl configuration from file '{path}': {str(e)}")
37
+
38
+ # Apply settings that have been overridden from the ones in the file
39
+ try:
40
+ yml_dict.update(config)
41
+ except Exception as e:
42
+ raise Exception(f"Failed updating dictionary of values read from file '{path}'"
43
+ f" with the dictionary of arguments passed: {str(e)}")
44
+
45
+ # The function below will throw its own descriptive exception if it fails
46
+ configObject = config_from_dict(yml_dict, configType=configType)
47
+
48
+ return configObject
49
+
50
+
51
+
52
+
53
+ def config_from_dict(config: dict[str,Any]={},
54
+ configType:Type[Union[test,test_servers]]=test)->test_common:
55
+ """
56
+ Fetch a configuration object that can be used for a number of different contentctl
57
+ operations including validate, build, inspect, test, and test_servers. A dict will
58
+ be used as the basis for constructing the configuration.
59
+
60
+ Args:
61
+ config (dict[str,Any],Optional): If a dictionary is not explicitly passed, then
62
+ an empty dict will be used to create a configuration, if possible, from default
63
+ values. Note that based on default values in the contentctl/objects/config.py
64
+ file, this may raise an exception. If so, please set appropriate default values
65
+ in the file above or supply those values via this argument.
66
+ configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
67
+ This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
68
+ Returns:
69
+ test_common: Returns a complete contentctl test_common configuration. Note that this configuration
70
+ will have all applicable field for validate and build as well, but can also be used for easily
71
+ construction a test or test_servers object.
72
+ """
73
+ try:
74
+ test_object = configType.model_validate(config)
75
+ except Exception as e:
76
+ raise Exception(f"Failed to load contentctl configuration from dict:\n{str(e)}")
77
+
78
+ return test_object
79
+
80
+
81
+ def update_config(config:Union[test,test_servers], **key_value_updates:dict[str,Any])->test_common:
82
+
83
+ """Update any relevant keys in a config file with the specified values.
84
+ Full validation will be performed after this update and descriptive errors
85
+ will be produced
86
+
87
+ Args:
88
+ config (test_common): A previously-constructed test_common object. This can be
89
+ build using the configFromDict or configFromFile functions.
90
+ key_value_updates (kwargs, optional): Additional keyword/argument pairs to update
91
+ arbitrary fields in the configuration.
92
+
93
+ Returns:
94
+ test_common: A validated object which has had the relevant fields updated.
95
+ Note that descriptive Exceptions will be generated if updated values are either
96
+ invalid (have the wrong type, or disallowed values) or you attempt to update
97
+ fields that do not exist
98
+ """
99
+ # Create a copy so we don't change the underlying model
100
+ config_copy = config.model_copy(deep=True)
101
+
102
+ # Force validation of assignment since doing so via arbitrary dict can be error prone
103
+ # Also, ensure that we do not try to add fields that are not part of the model
104
+ config_copy.model_config.update({'validate_assignment': True, 'extra': 'forbid'})
105
+
106
+
107
+
108
+ # Collect any errors that may occur
109
+ errors:list[Exception] = []
110
+
111
+ # We need to do this one by one because the extra:forbid argument does not appear to
112
+ # be respected at this time.
113
+ for key, value in key_value_updates.items():
114
+ try:
115
+ setattr(config_copy,key,value)
116
+ except Exception as e:
117
+ errors.append(e)
118
+ if len(errors) > 0:
119
+ errors_string = '\n'.join([str(e) for e in errors])
120
+ raise Exception(f"Error(s) updaitng configuration:\n{errors_string}")
121
+
122
+ return config_copy
123
+
124
+
125
+
126
+ def content_to_dict(director:DirectorOutputDto)->dict[str,list[dict[str,Any]]]:
127
+ output_dict:dict[str,list[dict[str,Any]]] = {}
128
+ for contentType in ['detections','stories','baselines','investigations',
129
+ 'playbooks','macros','lookups','deployments','ssa_detections']:
130
+
131
+ output_dict[contentType] = []
132
+ t:list[SecurityContentObject] = getattr(director,contentType)
133
+
134
+ for item in t:
135
+ output_dict[contentType].append(item.model_dump())
136
+ return output_dict
137
+
@@ -1,6 +1,11 @@
1
- from contentctl.actions.initialize import Initialize
1
+ import traceback
2
+ import sys
3
+ import warnings
4
+ import pathlib
2
5
  import tyro
3
- from contentctl.objects.config import init, validate, build, new, deploy_acs, deploy_rest, test, test_servers, inspect, report, test_common, release_notes
6
+
7
+ from contentctl.actions.initialize import Initialize
8
+ from contentctl.objects.config import init, validate, build, new, deploy_acs, test, test_servers, inspect, report, test_common, release_notes
4
9
  from contentctl.actions.validate import Validate
5
10
  from contentctl.actions.new_content import NewContent
6
11
  from contentctl.actions.detection_testing.GitService import GitService
@@ -9,14 +14,10 @@ from contentctl.actions.build import (
9
14
  DirectorOutputDto,
10
15
  Build,
11
16
  )
12
-
13
17
  from contentctl.actions.test import Test
14
18
  from contentctl.actions.test import TestInputDto
15
19
  from contentctl.actions.reporting import ReportingInputDto, Reporting
16
20
  from contentctl.actions.inspect import Inspect
17
- import sys
18
- import warnings
19
- import pathlib
20
21
  from contentctl.input.yml_reader import YmlReader
21
22
  from contentctl.actions.release_notes import ReleaseNotes
22
23
 
@@ -95,13 +96,14 @@ def new_func(config:new):
95
96
 
96
97
  def deploy_acs_func(config:deploy_acs):
97
98
  #This is a bit challenging to get to work with the default values.
98
- raise Exception("deploy acs not yet implemented")
99
-
100
- def deploy_rest_func(config:deploy_rest):
101
- raise Exception("deploy rest not yet implemented")
102
-
99
+ raise Exception("deploy acs not yet implemented")
103
100
 
104
101
  def test_common_func(config:test_common):
102
+ if type(config) == test:
103
+ #construct the container Infrastructure objects
104
+ config.getContainerInfrastructureObjects()
105
+ #otherwise, they have already been passed as servers
106
+
105
107
  director_output_dto = build_func(config)
106
108
  gitServer = GitService(director=director_output_dto,config=config)
107
109
  detections_to_test = gitServer.getContent()
@@ -175,15 +177,14 @@ def main():
175
177
  "test":test.model_validate(config_obj),
176
178
  "test_servers":test_servers.model_construct(**t.__dict__),
177
179
  "release_notes": release_notes.model_construct(**config_obj),
178
- "deploy_acs": deploy_acs.model_construct(**t.__dict__),
179
- #"deploy_rest":deploy_rest()
180
+ "deploy_acs": deploy_acs.model_construct(**t.__dict__)
180
181
  }
181
182
  )
182
183
 
183
184
 
184
185
 
185
186
 
186
-
187
+ config = None
187
188
  try:
188
189
  # Since some model(s) were constructed and not model_validated, we have to catch
189
190
  # warnings again when creating the cli
@@ -209,20 +210,23 @@ def main():
209
210
  elif type(config) == deploy_acs:
210
211
  updated_config = deploy_acs.model_validate(config)
211
212
  deploy_acs_func(updated_config)
212
- elif type(config) == deploy_rest:
213
- deploy_rest_func(config)
214
213
  elif type(config) == test or type(config) == test_servers:
215
- if type(config) == test:
216
- #construct the container Infrastructure objects
217
- config.getContainerInfrastructureObjects()
218
- #otherwise, they have already been passed as servers
219
214
  test_common_func(config)
220
215
  else:
221
216
  raise Exception(f"Unknown command line type '{type(config).__name__}'")
222
217
  except Exception as e:
223
- import traceback
224
- traceback.print_exc()
225
- traceback.print_stack()
226
- #print(e)
218
+ if config is None:
219
+ print("There was a serious issue where the config file could not be created.\n"
220
+ "The entire stack trace is provided below (please include it if filing a bug report).\n")
221
+ traceback.print_exc()
222
+ elif config.verbose:
223
+ print("Verbose error logging is ENABLED.\n"
224
+ "The entire stack trace has been provided below (please include it if filing a bug report):\n")
225
+ traceback.print_exc()
226
+ else:
227
+ print("Verbose error logging is DISABLED.\n"
228
+ "Please use the --verbose command line argument if you need more context for your error or file a bug report.")
229
+ print(e)
230
+
227
231
  sys.exit(1)
228
232
 
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+ from pycvesearch import CVESearch
3
+ import functools
4
+ import os
5
+ import shelve
6
+ import time
7
+ from typing import Annotated, Any, Union, TYPE_CHECKING
8
+ from pydantic import BaseModel,Field, computed_field
9
+ from decimal import Decimal
10
+ from requests.exceptions import ReadTimeout
11
+
12
+ if TYPE_CHECKING:
13
+ from contentctl.objects.config import validate
14
+
15
+
16
+
17
+ CVESSEARCH_API_URL = 'https://cve.circl.lu'
18
+
19
+
20
+ class CveEnrichmentObj(BaseModel):
21
+ id:Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]
22
+ cvss:Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
23
+ summary:str
24
+
25
+ @computed_field
26
+ @property
27
+ def url(self)->str:
28
+ BASE_NVD_URL = "https://nvd.nist.gov/vuln/detail/"
29
+ return f"{BASE_NVD_URL}{self.id}"
30
+
31
+
32
+ class CveEnrichment(BaseModel):
33
+ use_enrichment: bool = True
34
+ cve_api_obj: Union[CVESearch,None] = None
35
+
36
+
37
+ class Config:
38
+ # Arbitrary_types are allowed to let us use the CVESearch Object
39
+ arbitrary_types_allowed = True
40
+ frozen = True
41
+
42
+
43
+ @staticmethod
44
+ def getCveEnrichment(config:validate, timeout_seconds:int=10, force_disable_enrichment:bool=True)->CveEnrichment:
45
+ if force_disable_enrichment:
46
+ return CveEnrichment(use_enrichment=False, cve_api_obj=None)
47
+
48
+ if config.enrichments:
49
+ try:
50
+ cve_api_obj = CVESearch(CVESSEARCH_API_URL, timeout=timeout_seconds)
51
+ return CveEnrichment(use_enrichment=True, cve_api_obj=cve_api_obj)
52
+ except Exception as e:
53
+ raise Exception(f"Error setting CVE_SEARCH API to: {CVESSEARCH_API_URL}: {str(e)}")
54
+
55
+ return CveEnrichment(use_enrichment=False, cve_api_obj=None)
56
+
57
+
58
+ def enrich_cve(self, cve_id:str, raise_exception_on_failure:bool=True)->CveEnrichmentObj:
59
+
60
+ if not self.use_enrichment:
61
+ return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
62
+ else:
63
+ print("WARNING - Dynamic enrichment not supported at this time.")
64
+ return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
65
+ # Depending on needs, we may add dynamic enrichment functionality back to the tool
@@ -5,9 +5,8 @@ from dataclasses import dataclass, field
5
5
  from pydantic import ValidationError
6
6
  from uuid import UUID
7
7
  from contentctl.input.yml_reader import YmlReader
8
-
9
-
10
-
8
+
9
+
11
10
  from contentctl.objects.detection import Detection
12
11
  from contentctl.objects.story import Story
13
12
 
@@ -28,29 +27,69 @@ from contentctl.enrichments.cve_enrichment import CveEnrichment
28
27
  from contentctl.objects.config import validate
29
28
 
30
29
 
31
-
32
- @dataclass()
30
+ @dataclass
33
31
  class DirectorOutputDto:
34
- # Atomic Tests are first because parsing them
35
- # is far quicker than attack_enrichment
36
- atomic_tests: Union[list[AtomicTest],None]
37
- attack_enrichment: AttackEnrichment
38
- detections: list[Detection]
39
- stories: list[Story]
40
- baselines: list[Baseline]
41
- investigations: list[Investigation]
42
- playbooks: list[Playbook]
43
- macros: list[Macro]
44
- lookups: list[Lookup]
45
- deployments: list[Deployment]
46
- ssa_detections: list[SSADetection]
47
- #cve_enrichment: CveEnrichment
48
-
49
- name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
50
- uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
51
-
32
+ # Atomic Tests are first because parsing them
33
+ # is far quicker than attack_enrichment
34
+ atomic_tests: Union[list[AtomicTest],None]
35
+ attack_enrichment: AttackEnrichment
36
+ cve_enrichment: CveEnrichment
37
+ detections: list[Detection]
38
+ stories: list[Story]
39
+ baselines: list[Baseline]
40
+ investigations: list[Investigation]
41
+ playbooks: list[Playbook]
42
+ macros: list[Macro]
43
+ lookups: list[Lookup]
44
+ deployments: list[Deployment]
45
+ ssa_detections: list[SSADetection]
46
+
47
+ name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
48
+ uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
49
+
50
+ def addContentToDictMappings(self, content: SecurityContentObject):
51
+ content_name = content.name
52
+ if isinstance(content, SSADetection):
53
+ # Since SSA detections may have the same name as ESCU detection,
54
+ # for this function we prepend 'SSA ' to the name.
55
+ content_name = f"SSA {content_name}"
56
+ if content_name in self.name_to_content_map:
57
+ raise ValueError(
58
+ f"Duplicate name '{content_name}' with paths:\n"
59
+ f" - {content.file_path}\n"
60
+ f" - {self.name_to_content_map[content_name].file_path}"
61
+ )
62
+ elif content.id in self.uuid_to_content_map:
63
+ raise ValueError(
64
+ f"Duplicate id '{content.id}' with paths:\n"
65
+ f" - {content.file_path}\n"
66
+ f" - {self.name_to_content_map[content_name].file_path}"
67
+ )
68
+
69
+ if isinstance(content, Lookup):
70
+ self.lookups.append(content)
71
+ elif isinstance(content, Macro):
72
+ self.macros.append(content)
73
+ elif isinstance(content, Deployment):
74
+ self.deployments.append(content)
75
+ elif isinstance(content, Playbook):
76
+ self.playbooks.append(content)
77
+ elif isinstance(content, Baseline):
78
+ self.baselines.append(content)
79
+ elif isinstance(content, Investigation):
80
+ self.investigations.append(content)
81
+ elif isinstance(content, Story):
82
+ self.stories.append(content)
83
+ elif isinstance(content, Detection):
84
+ self.detections.append(content)
85
+ elif isinstance(content, SSADetection):
86
+ self.ssa_detections.append(content)
87
+ else:
88
+ raise Exception(f"Unknown security content type: {type(content)}")
52
89
 
53
90
 
91
+ self.name_to_content_map[content_name] = content
92
+ self.uuid_to_content_map[content.id] = content
54
93
 
55
94
 
56
95
  from contentctl.input.ssa_detection_builder import SSADetectionBuilder
@@ -60,13 +99,6 @@ from contentctl.objects.enums import DetectionStatus
60
99
  from contentctl.helper.utils import Utils
61
100
 
62
101
 
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
102
  class Director():
71
103
  input_dto: validate
72
104
  output_dto: DirectorOutputDto
@@ -77,27 +109,7 @@ class Director():
77
109
  def __init__(self, output_dto: DirectorOutputDto) -> None:
78
110
  self.output_dto = output_dto
79
111
  self.ssa_detection_builder = SSADetectionBuilder()
80
-
81
- def addContentToDictMappings(self, content:SecurityContentObject):
82
- content_name = content.name
83
- if isinstance(content,SSADetection):
84
- # Since SSA detections may have the same name as ESCU detection,
85
- # for this function we prepend 'SSA ' to the name.
86
- content_name = f"SSA {content_name}"
87
- if content_name in self.output_dto.name_to_content_map:
88
- raise ValueError(f"Duplicate name '{content_name}' with paths:\n"
89
- f" - {content.file_path}\n"
90
- f" - {self.output_dto.name_to_content_map[content_name].file_path}")
91
- elif content.id in self.output_dto.uuid_to_content_map:
92
- raise ValueError(f"Duplicate id '{content.id}' with paths:\n"
93
- f" - {content.file_path}\n"
94
- f" - {self.output_dto.name_to_content_map[content_name].file_path}")
95
-
96
- self.output_dto.name_to_content_map[content_name] = content
97
- self.output_dto.uuid_to_content_map[content.id] = content
98
-
99
112
 
100
-
101
113
  def execute(self, input_dto: validate) -> None:
102
114
  self.input_dto = input_dto
103
115
 
@@ -146,50 +158,41 @@ class Director():
146
158
 
147
159
  if contentType == SecurityContentType.lookups:
148
160
  lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
149
- self.output_dto.lookups.append(lookup)
150
- self.addContentToDictMappings(lookup)
161
+ self.output_dto.addContentToDictMappings(lookup)
151
162
 
152
163
  elif contentType == SecurityContentType.macros:
153
164
  macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
154
- self.output_dto.macros.append(macro)
155
- self.addContentToDictMappings(macro)
165
+ self.output_dto.addContentToDictMappings(macro)
156
166
 
157
167
  elif contentType == SecurityContentType.deployments:
158
168
  deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
159
- self.output_dto.deployments.append(deployment)
160
- self.addContentToDictMappings(deployment)
169
+ self.output_dto.addContentToDictMappings(deployment)
161
170
 
162
171
  elif contentType == SecurityContentType.playbooks:
163
172
  playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
164
- self.output_dto.playbooks.append(playbook)
165
- self.addContentToDictMappings(playbook)
173
+ self.output_dto.addContentToDictMappings(playbook)
166
174
 
167
175
  elif contentType == SecurityContentType.baselines:
168
176
  baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
169
- self.output_dto.baselines.append(baseline)
170
- self.addContentToDictMappings(baseline)
177
+ self.output_dto.addContentToDictMappings(baseline)
171
178
 
172
179
  elif contentType == SecurityContentType.investigations:
173
180
  investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
174
- self.output_dto.investigations.append(investigation)
175
- self.addContentToDictMappings(investigation)
181
+ self.output_dto.addContentToDictMappings(investigation)
176
182
 
177
183
  elif contentType == SecurityContentType.stories:
178
184
  story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
179
- self.output_dto.stories.append(story)
180
- self.addContentToDictMappings(story)
185
+ self.output_dto.addContentToDictMappings(story)
181
186
 
182
187
  elif contentType == SecurityContentType.detections:
183
- detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto})
184
- self.output_dto.detections.append(detection)
185
- self.addContentToDictMappings(detection)
188
+ detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
189
+ self.output_dto.addContentToDictMappings(detection)
186
190
 
187
191
  elif contentType == SecurityContentType.ssa_detections:
188
192
  self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
189
193
  ssa_detection = self.ssa_detection_builder.getObject()
190
194
  if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
191
- self.output_dto.ssa_detections.append(ssa_detection)
192
- self.addContentToDictMappings(ssa_detection)
195
+ self.output_dto.addContentToDictMappings(ssa_detection)
193
196
 
194
197
  else:
195
198
  raise Exception(f"Unsupported type: [{contentType}]")
@@ -228,6 +231,3 @@ class Director():
228
231
  builder.addMappings()
229
232
  builder.addUnitTest()
230
233
  builder.addRBA()
231
-
232
-
233
-