contentctl 4.1.5__tar.gz → 4.2.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 (173) hide show
  1. {contentctl-4.1.5 → contentctl-4.2.0}/PKG-INFO +1 -1
  2. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/build.py +14 -1
  3. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/initialize.py +1 -0
  4. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/validate.py +0 -1
  5. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/director.py +39 -56
  6. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/yml_reader.py +2 -0
  7. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +45 -23
  8. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +29 -2
  9. contentctl-4.2.0/contentctl/objects/data_source.py +42 -0
  10. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/enums.py +0 -1
  11. contentctl-4.2.0/contentctl/objects/event_source.py +11 -0
  12. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/story.py +12 -5
  13. contentctl-4.2.0/contentctl/output/data_source_writer.py +40 -0
  14. contentctl-4.2.0/contentctl/templates/data_sources/sysmon_eventid_1.yml +171 -0
  15. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +1 -1
  16. {contentctl-4.1.5 → contentctl-4.2.0}/pyproject.toml +1 -1
  17. contentctl-4.1.5/contentctl/objects/data_source.py +0 -28
  18. contentctl-4.1.5/contentctl/objects/event_source.py +0 -10
  19. {contentctl-4.1.5 → contentctl-4.2.0}/LICENSE.md +0 -0
  20. {contentctl-4.1.5 → contentctl-4.2.0}/README.md +0 -0
  21. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/__init__.py +0 -0
  22. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/convert.py +0 -0
  23. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/deploy_acs.py +0 -0
  24. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  25. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
  26. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  27. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
  28. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  29. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  30. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  31. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  32. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  33. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  34. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  35. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/doc_gen.py +0 -0
  36. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/initialize_old.py +0 -0
  37. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/inspect.py +0 -0
  38. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/new_content.py +0 -0
  39. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/release_notes.py +0 -0
  40. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/reporting.py +0 -0
  41. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/actions/test.py +0 -0
  42. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/api.py +0 -0
  43. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/contentctl.py +0 -0
  44. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
  45. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
  46. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  47. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/link_validator.py +0 -0
  48. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/logger.py +0 -0
  49. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/helper/utils.py +0 -0
  50. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/backend_splunk_ba.py +0 -0
  51. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/new_content_questions.py +0 -0
  52. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/sigma_converter.py +0 -0
  53. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/input/ssa_detection_builder.py +0 -0
  54. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/alert_action.py +0 -0
  55. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/atomic.py +0 -0
  56. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/base_test.py +0 -0
  57. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/base_test_result.py +0 -0
  58. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/baseline.py +0 -0
  59. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/baseline_tags.py +0 -0
  60. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/config.py +0 -0
  61. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/constants.py +0 -0
  62. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/correlation_search.py +0 -0
  63. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment.py +0 -0
  64. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_email.py +0 -0
  65. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_notable.py +0 -0
  66. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_phantom.py +0 -0
  67. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_rba.py +0 -0
  68. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
  69. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/deployment_slack.py +0 -0
  70. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/detection.py +0 -0
  71. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/detection_tags.py +0 -0
  72. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/integration_test.py +0 -0
  73. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/integration_test_result.py +0 -0
  74. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/investigation.py +0 -0
  75. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/investigation_tags.py +0 -0
  76. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/lookup.py +0 -0
  77. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/macro.py +0 -0
  78. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  79. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/notable_action.py +0 -0
  80. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/observable.py +0 -0
  81. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/playbook.py +0 -0
  82. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/playbook_tags.py +0 -0
  83. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
  84. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/risk_object.py +0 -0
  85. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/security_content_object.py +0 -0
  86. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/ssa_detection.py +0 -0
  87. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/ssa_detection_tags.py +0 -0
  88. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/story_tags.py +0 -0
  89. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/test_group.py +0 -0
  90. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/threat_object.py +0 -0
  91. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test.py +0 -0
  92. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_attack_data.py +0 -0
  93. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
  94. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_old.py +0 -0
  95. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_result.py +0 -0
  96. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/objects/unit_test_ssa.py +0 -0
  97. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/api_json_output.py +0 -0
  98. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/attack_nav_output.py +0 -0
  99. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/attack_nav_writer.py +0 -0
  100. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/ba_yml_output.py +0 -0
  101. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/conf_output.py +0 -0
  102. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/conf_writer.py +0 -0
  103. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/detection_writer.py +0 -0
  104. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/doc_md_output.py +0 -0
  105. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/finding_report_writer.py +0 -0
  106. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/jinja_writer.py +0 -0
  107. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/json_writer.py +0 -0
  108. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/new_content_yml_output.py +0 -0
  109. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/svg_output.py +0 -0
  110. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  111. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  112. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  113. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
  114. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
  115. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/collections.j2 +0 -0
  116. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/content-version.j2 +0 -0
  117. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
  118. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
  119. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  120. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
  121. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
  122. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  123. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  124. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  125. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
  126. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
  127. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  128. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  129. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/finding_report.j2 +0 -0
  130. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/header.j2 +0 -0
  131. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/macros.j2 +0 -0
  132. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/panel.j2 +0 -0
  133. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  134. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  135. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  136. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/transforms.j2 +0 -0
  137. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
  138. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/yml_output.py +0 -0
  139. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/output/yml_writer.py +0 -0
  140. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/README +0 -0
  141. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_default.yml +0 -0
  142. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  143. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  144. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  145. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/README.md +0 -0
  146. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  147. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/app.conf +0 -0
  148. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
  149. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
  150. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  151. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  152. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  153. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  154. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  155. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
  156. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
  157. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  158. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  159. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  160. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
  161. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
  162. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  163. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  164. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  165. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  166. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  167. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
  168. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  169. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
  170. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
  171. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  172. {contentctl-4.1.5 → contentctl-4.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  173. {contentctl-4.1.5 → contentctl-4.2.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.1.5
3
+ Version: 4.2.0
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -10,6 +10,8 @@ from contentctl.output.conf_output import ConfOutput
10
10
  from contentctl.output.conf_writer import ConfWriter
11
11
  from contentctl.output.ba_yml_output import BAYmlOutput
12
12
  from contentctl.output.api_json_output import ApiJsonOutput
13
+ from contentctl.output.data_source_writer import DataSourceWriter
14
+ from contentctl.objects.lookup import Lookup
13
15
  import pathlib
14
16
  import json
15
17
  import datetime
@@ -28,9 +30,20 @@ class Build:
28
30
 
29
31
 
30
32
  def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
31
- if input_dto.config.build_app:
33
+ if input_dto.config.build_app:
34
+
32
35
  updated_conf_files:set[pathlib.Path] = set()
33
36
  conf_output = ConfOutput(input_dto.config)
37
+
38
+ # Construct a special lookup whose CSV is created at runtime and
39
+ # written directly into the output folder. It is created with model_construct,
40
+ # not model_validate, because the CSV does not exist yet.
41
+ data_sources_lookup_csv_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.csv"
42
+ DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
43
+ input_dto.director_output_dto.addContentToDictMappings(Lookup.model_construct(description= "A lookup file that will contain the data source objects for detections.",
44
+ filename=data_sources_lookup_csv_path,
45
+ name="data_sources"))
46
+
34
47
  updated_conf_files.update(conf_output.writeHeaders())
35
48
  updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.detections, SecurityContentType.detections))
36
49
  updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.stories, SecurityContentType.stories))
@@ -28,6 +28,7 @@ class Initialize:
28
28
  ('../templates/app_template/', 'app_template'),
29
29
  ('../templates/deployments/', 'deployments'),
30
30
  ('../templates/detections/', 'detections'),
31
+ ('../templates/data_sources/', 'data_sources'),
31
32
  ('../templates/macros/','macros'),
32
33
  ('../templates/stories/', 'stories'),
33
34
  ]:
@@ -28,7 +28,6 @@ class Validate:
28
28
  [],
29
29
  [],
30
30
  [],
31
- [],
32
31
  )
33
32
 
34
33
  director = Director(director_output_dto)
@@ -58,7 +58,6 @@ class DirectorOutputDto:
58
58
  deployments: list[Deployment]
59
59
  ssa_detections: list[SSADetection]
60
60
  data_sources: list[DataSource]
61
- event_sources: list[EventSource]
62
61
  name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
63
62
  uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
64
63
 
@@ -68,17 +67,19 @@ class DirectorOutputDto:
68
67
  # Since SSA detections may have the same name as ESCU detection,
69
68
  # for this function we prepend 'SSA ' to the name.
70
69
  content_name = f"SSA {content_name}"
70
+
71
71
  if content_name in self.name_to_content_map:
72
72
  raise ValueError(
73
73
  f"Duplicate name '{content_name}' with paths:\n"
74
74
  f" - {content.file_path}\n"
75
75
  f" - {self.name_to_content_map[content_name].file_path}"
76
76
  )
77
- elif content.id in self.uuid_to_content_map:
77
+
78
+ if content.id in self.uuid_to_content_map:
78
79
  raise ValueError(
79
80
  f"Duplicate id '{content.id}' with paths:\n"
80
81
  f" - {content.file_path}\n"
81
- f" - {self.name_to_content_map[content_name].file_path}"
82
+ f" - {self.uuid_to_content_map[content.id].file_path}"
82
83
  )
83
84
 
84
85
  if isinstance(content, Lookup):
@@ -99,9 +100,10 @@ class DirectorOutputDto:
99
100
  self.detections.append(content)
100
101
  elif isinstance(content, SSADetection):
101
102
  self.ssa_detections.append(content)
103
+ elif isinstance(content, DataSource):
104
+ self.data_sources.append(content)
102
105
  else:
103
- raise Exception(f"Unknown security content type: {type(content)}")
104
-
106
+ raise Exception(f"Unknown security content type: {type(content)}")
105
107
 
106
108
  self.name_to_content_map[content_name] = content
107
109
  self.uuid_to_content_map[content.id] = content
@@ -124,41 +126,27 @@ class Director():
124
126
  self.createSecurityContent(SecurityContentType.stories)
125
127
  self.createSecurityContent(SecurityContentType.baselines)
126
128
  self.createSecurityContent(SecurityContentType.investigations)
127
- self.createSecurityContent(SecurityContentType.event_sources)
128
129
  self.createSecurityContent(SecurityContentType.data_sources)
129
130
  self.createSecurityContent(SecurityContentType.playbooks)
130
131
  self.createSecurityContent(SecurityContentType.detections)
131
132
  self.createSecurityContent(SecurityContentType.ssa_detections)
132
133
 
134
+
135
+ from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
136
+ if len(MISSING_SOURCES) > 0:
137
+ missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES)))
138
+ print("WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
139
+ "This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
140
+ f"{missing_sources_string}")
141
+ else:
142
+ print("No missing data_sources!")
143
+
133
144
  def createSecurityContent(self, contentType: SecurityContentType) -> None:
134
145
  if contentType == SecurityContentType.ssa_detections:
135
146
  files = Utils.get_all_yml_files_from_directory(
136
147
  os.path.join(self.input_dto.path, "ssa_detections")
137
148
  )
138
149
  security_content_files = [f for f in files if f.name.startswith("ssa___")]
139
-
140
- elif contentType == SecurityContentType.data_sources:
141
- security_content_files = (
142
- Utils.get_all_yml_files_from_directory_one_layer_deep(
143
- os.path.join(self.input_dto.path, "data_sources")
144
- )
145
- )
146
-
147
- elif contentType == SecurityContentType.event_sources:
148
- security_content_files = Utils.get_all_yml_files_from_directory(
149
- os.path.join(self.input_dto.path, "data_sources", "cloud", "event_sources")
150
- )
151
- security_content_files.extend(
152
- Utils.get_all_yml_files_from_directory(
153
- os.path.join(self.input_dto.path, "data_sources", "endpoint", "event_sources")
154
- )
155
- )
156
- security_content_files.extend(
157
- Utils.get_all_yml_files_from_directory(
158
- os.path.join(self.input_dto.path, "data_sources", "network", "event_sources")
159
- )
160
- )
161
-
162
150
  elif contentType in [
163
151
  SecurityContentType.deployments,
164
152
  SecurityContentType.lookups,
@@ -168,6 +156,7 @@ class Director():
168
156
  SecurityContentType.investigations,
169
157
  SecurityContentType.playbooks,
170
158
  SecurityContentType.detections,
159
+ SecurityContentType.data_sources,
171
160
  ]:
172
161
  files = Utils.get_all_yml_files_from_directory(
173
162
  os.path.join(self.input_dto.path, str(contentType.name))
@@ -190,54 +179,48 @@ class Director():
190
179
  modelDict = YmlReader.load_file(file)
191
180
 
192
181
  if contentType == SecurityContentType.lookups:
193
- lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
194
- self.output_dto.addContentToDictMappings(lookup)
182
+ lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
183
+ self.output_dto.addContentToDictMappings(lookup)
195
184
 
196
185
  elif contentType == SecurityContentType.macros:
197
- macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
198
- self.output_dto.addContentToDictMappings(macro)
186
+ macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
187
+ self.output_dto.addContentToDictMappings(macro)
199
188
 
200
189
  elif contentType == SecurityContentType.deployments:
201
- deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
202
- self.output_dto.addContentToDictMappings(deployment)
190
+ deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
191
+ self.output_dto.addContentToDictMappings(deployment)
203
192
 
204
193
  elif contentType == SecurityContentType.playbooks:
205
- playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
206
- self.output_dto.addContentToDictMappings(playbook)
194
+ playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
195
+ self.output_dto.addContentToDictMappings(playbook)
207
196
 
208
197
  elif contentType == SecurityContentType.baselines:
209
- baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
210
- self.output_dto.addContentToDictMappings(baseline)
198
+ baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
199
+ self.output_dto.addContentToDictMappings(baseline)
211
200
 
212
201
  elif contentType == SecurityContentType.investigations:
213
- investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
214
- self.output_dto.addContentToDictMappings(investigation)
202
+ investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
203
+ self.output_dto.addContentToDictMappings(investigation)
215
204
 
216
205
  elif contentType == SecurityContentType.stories:
217
- story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
218
- self.output_dto.addContentToDictMappings(story)
206
+ story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
207
+ self.output_dto.addContentToDictMappings(story)
219
208
 
220
209
  elif contentType == SecurityContentType.detections:
221
- detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
222
- self.output_dto.addContentToDictMappings(detection)
210
+ detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
211
+ self.output_dto.addContentToDictMappings(detection)
223
212
 
224
213
  elif contentType == SecurityContentType.ssa_detections:
225
- self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
226
- ssa_detection = self.ssa_detection_builder.getObject()
227
- if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
228
- self.output_dto.addContentToDictMappings(ssa_detection)
214
+ self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
215
+ ssa_detection = self.ssa_detection_builder.getObject()
216
+ if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
217
+ self.output_dto.addContentToDictMappings(ssa_detection)
229
218
 
230
219
  elif contentType == SecurityContentType.data_sources:
231
220
  data_source = DataSource.model_validate(
232
221
  modelDict, context={"output_dto": self.output_dto}
233
222
  )
234
- self.output_dto.data_sources.append(data_source)
235
-
236
- elif contentType == SecurityContentType.event_sources:
237
- event_source = EventSource.model_validate(
238
- modelDict, context={"output_dto": self.output_dto}
239
- )
240
- self.output_dto.event_sources.append(event_source)
223
+ self.output_dto.addContentToDictMappings(data_source)
241
224
 
242
225
  else:
243
226
  raise Exception(f"Unsupported type: [{contentType}]")
@@ -40,6 +40,8 @@ class YmlReader():
40
40
  if add_fields == False:
41
41
  return yml_obj
42
42
 
43
+
43
44
  yml_obj['file_path'] = str(file_path)
45
+
44
46
 
45
47
  return yml_obj
@@ -22,12 +22,14 @@ from contentctl.objects.deployment import Deployment
22
22
  from contentctl.objects.unit_test import UnitTest
23
23
  from contentctl.objects.test_group import TestGroup
24
24
  from contentctl.objects.integration_test import IntegrationTest
25
-
25
+ from contentctl.objects.event_source import EventSource
26
+ from contentctl.objects.data_source import DataSource
26
27
 
27
28
  #from contentctl.objects.playbook import Playbook
28
- from contentctl.objects.enums import DataSource,ProvidingTechnology
29
+ from contentctl.objects.enums import ProvidingTechnology
29
30
  from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
30
31
 
32
+ MISSING_SOURCES:set[str] = set()
31
33
 
32
34
  class Detection_Abstract(SecurityContentObject):
33
35
  model_config = ConfigDict(use_enum_values=True)
@@ -35,12 +37,11 @@ class Detection_Abstract(SecurityContentObject):
35
37
  #contentType: SecurityContentType = SecurityContentType.detections
36
38
  type: AnalyticsType = Field(...)
37
39
  status: DetectionStatus = Field(...)
38
- data_source: Optional[List[str]] = None
40
+ data_source: list[str] = []
39
41
  tags: DetectionTags = Field(...)
40
42
  search: Union[str, dict[str,Any]] = Field(...)
41
43
  how_to_implement: str = Field(..., min_length=4)
42
44
  known_false_positives: str = Field(..., min_length=4)
43
- data_source_objects: Optional[List[DataSource]] = None
44
45
 
45
46
  enabled_by_default: bool = False
46
47
  file_path: FilePath = Field(...)
@@ -53,6 +54,8 @@ class Detection_Abstract(SecurityContentObject):
53
54
  # A list of groups of tests, relying on the same data
54
55
  test_groups: Union[list[TestGroup], None] = Field(None,validate_default=True)
55
56
 
57
+ data_source_objects: list[DataSource] = []
58
+
56
59
 
57
60
  @field_validator("search", mode="before")
58
61
  @classmethod
@@ -138,6 +141,7 @@ class Detection_Abstract(SecurityContentObject):
138
141
  else:
139
142
  return []
140
143
 
144
+
141
145
  @computed_field
142
146
  @property
143
147
  def source(self)->str:
@@ -161,10 +165,12 @@ class Detection_Abstract(SecurityContentObject):
161
165
  annotations_dict["type"] = self.type
162
166
  #annotations_dict["version"] = self.version
163
167
 
168
+ annotations_dict["data_source"] = self.data_source
169
+
164
170
  #The annotations object is a superset of the mappings object.
165
171
  # So start with the mapping object.
166
172
  annotations_dict.update(self.mappings)
167
-
173
+
168
174
  #Make sure that the results are sorted for readability/easier diffs
169
175
  return dict(sorted(annotations_dict.items(), key=lambda item: item[0]))
170
176
 
@@ -384,23 +390,37 @@ class Detection_Abstract(SecurityContentObject):
384
390
  raise ValueError(f"Error, failed to replace detection reference in Baseline '{baseline.name}' to detection '{self.name}'")
385
391
  baseline.tags.detections = new_detections
386
392
 
387
- self.data_source_objects = []
388
- for data_source_obj in director.data_sources:
389
- for detection_data_source in self.data_source:
390
- if data_source_obj.name in detection_data_source:
391
- self.data_source_objects.append(data_source_obj)
392
-
393
- # Remove duplicate data source objects based on their 'name' property
394
- unique_data_sources = {}
395
- for data_source_obj in self.data_source_objects:
396
- if data_source_obj.name not in unique_data_sources:
397
- unique_data_sources[data_source_obj.name] = data_source_obj
398
- self.data_source_objects = list(unique_data_sources.values())
393
+ # Data source may be defined 1 on each line, OR they may be defined as
394
+ # SOUCE_1 AND ANOTHERSOURCE AND A_THIRD_SOURCE
395
+ # if more than 1 data source is required for a detection (for example, because it includes a join)
396
+ # Parse and update the list to resolve individual names and remove potential duplicates
397
+ updated_data_source_names:set[str] = set()
398
+
399
+ for ds in self.data_source:
400
+ split_data_sources = {d.strip() for d in ds.split('AND')}
401
+ updated_data_source_names.update(split_data_sources)
402
+
403
+ sources = sorted(list(updated_data_source_names))
404
+
405
+ matched_data_sources:list[DataSource] = []
406
+ missing_sources:list[str] = []
407
+ for source in sources:
408
+ try:
409
+ matched_data_sources += DataSource.mapNamesToSecurityContentObjects([source], director)
410
+ except Exception as data_source_mapping_exception:
411
+ # We gobble this up and add it to a global set so that we
412
+ # can print it ONCE at the end of the build of datasources.
413
+ # This will be removed later as per the note below
414
+ MISSING_SOURCES.add(source)
415
+
416
+ if len(missing_sources) > 0:
417
+ # This will be changed to ValueError when we have a complete list of data sources
418
+ print(f"WARNING: The following exception occurred when mapping the data_source field to DataSource objects:{missing_sources}")
419
+
420
+ self.data_source_objects = matched_data_sources
399
421
 
400
422
  for story in self.tags.analytic_story:
401
- story.detections.append(self)
402
- story.data_sources.extend(self.data_source_objects)
403
-
423
+ story.detections.append(self)
404
424
  return self
405
425
 
406
426
 
@@ -424,14 +444,16 @@ class Detection_Abstract(SecurityContentObject):
424
444
  raise ValueError("Error, baselines are constructed automatically at runtime. Please do not include this field.")
425
445
 
426
446
 
427
- name:Union[str,dict] = info.data.get("name",None)
447
+ name:Union[str,None] = info.data.get("name",None)
428
448
  if name is None:
429
449
  raise ValueError("Error, cannot get Baselines because the Detection does not have a 'name' defined.")
430
-
450
+
431
451
  director:DirectorOutputDto = info.context.get("output_dto",None)
432
452
  baselines:List[Baseline] = []
433
453
  for baseline in director.baselines:
434
- if name in baseline.tags.detections:
454
+ # This matching is a bit strange, because baseline.tags.detections starts as a list of strings, but
455
+ # is eventually updated to a list of Detections as we construct all of the detection objects.
456
+ if name in [detection_name for detection_name in baseline.tags.detections if isinstance(detection_name,str)]:
435
457
  baselines.append(baseline)
436
458
 
437
459
  return baselines
@@ -125,9 +125,9 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
125
125
  errors:list[str] = []
126
126
  if len(missing_objects) > 0:
127
127
  errors.append(f"Failed to find the following '{cls.__name__}': {missing_objects}")
128
- if len(missing_objects) > 0:
128
+ if len(mistyped_objects) > 0:
129
129
  for mistyped_object in mistyped_objects:
130
- errors.append(f"'{mistyped_object.name}' expected to have type '{type(Self)}', but actually had type '{type(mistyped_object)}'")
130
+ errors.append(f"'{mistyped_object.name}' expected to have type '{cls}', but actually had type '{type(mistyped_object)}'")
131
131
 
132
132
  if len(errors) > 0:
133
133
  error_string = "\n - ".join(errors)
@@ -194,6 +194,33 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
194
194
 
195
195
  def __str__(self)->str:
196
196
  return(self.__repr__())
197
+
198
+ def __lt__(self, other:object)->bool:
199
+ if not isinstance(other,SecurityContentObject_Abstract):
200
+ raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
201
+ return self.name < other.name
202
+
203
+ def __eq__(self, other:object)->bool:
204
+ if not isinstance(other,SecurityContentObject_Abstract):
205
+ raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
206
+
207
+ if id(self) == id(other) and self.name == other.name and self.id == other.id:
208
+ # Yes, this is the same object
209
+ return True
210
+
211
+ elif id(self) == id(other) or self.name == other.name or self.id == other.id:
212
+ raise Exception("Attempted to compare two SecurityContentObjects, but their fields indicate they were not globally unique:"
213
+ f"\n\tid(obj1) : {id(self)}"
214
+ f"\n\tid(obj2) : {id(other)}"
215
+ f"\n\tobj1.name : {self.name}"
216
+ f"\n\tobj2.name : {other.name}"
217
+ f"\n\tobj1.id : {self.id}"
218
+ f"\n\tobj2.id : {other.id}")
219
+ else:
220
+ return False
221
+
222
+ def __hash__(self) -> NonNegativeInt:
223
+ return id(self)
197
224
 
198
225
 
199
226
 
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, Any
3
+ from pydantic import Field, FilePath, model_serializer
4
+ from contentctl.objects.security_content_object import SecurityContentObject
5
+ from contentctl.objects.event_source import EventSource
6
+
7
+ class DataSource(SecurityContentObject):
8
+ source: str = Field(...)
9
+ sourcetype: str = Field(...)
10
+ separator: Optional[str] = None
11
+ configuration: Optional[str] = None
12
+ supported_TA: Optional[list] = None
13
+ fields: Optional[list] = None
14
+ field_mappings: Optional[list] = None
15
+ convert_to_log_source: Optional[list] = None
16
+ example_log: Optional[str] = None
17
+
18
+
19
+ @model_serializer
20
+ def serialize_model(self):
21
+ #Call serializer for parent
22
+ super_fields = super().serialize_model()
23
+
24
+ #All fields custom to this model
25
+ model:dict[str,Any] = {
26
+ "source": self.source,
27
+ "sourcetype": self.sourcetype,
28
+ "separator": self.separator,
29
+ "configuration": self.configuration,
30
+ "supported_TA": self.supported_TA,
31
+ "fields": self.fields,
32
+ "field_mappings": self.field_mappings,
33
+ "convert_to_log_source": self.convert_to_log_source,
34
+ "example_log":self.example_log
35
+ }
36
+
37
+
38
+ #Combine fields from this model with fields from parent
39
+ super_fields.update(model)
40
+
41
+ #return the model
42
+ return super_fields
@@ -56,7 +56,6 @@ class SecurityContentType(enum.Enum):
56
56
  unit_tests = 9
57
57
  ssa_detections = 10
58
58
  data_sources = 11
59
- event_sources = 12
60
59
 
61
60
  # Bringing these changes back in line will take some time after
62
61
  # the initial merge is complete
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+ from typing import Union, Optional, List
3
+ from pydantic import BaseModel, Field
4
+
5
+ from contentctl.objects.security_content_object import SecurityContentObject
6
+
7
+ class EventSource(SecurityContentObject):
8
+ fields: Optional[list[str]] = None
9
+ field_mappings: Optional[list[dict]] = None
10
+ convert_to_log_source: Optional[list[dict]] = None
11
+ example_log: Optional[str] = None
@@ -33,7 +33,18 @@ class Story(SecurityContentObject):
33
33
  detections:List[Detection] = []
34
34
  investigations: List[Investigation] = []
35
35
  baselines: List[Baseline] = []
36
- data_sources: List[DataSource] = []
36
+
37
+
38
+ @computed_field
39
+ @property
40
+ def data_sources(self)-> list[DataSource]:
41
+ # Only add a data_source if it does not already exist in the story
42
+ data_source_objects:set[DataSource] = set()
43
+ for detection in self.detections:
44
+ data_source_objects.update(set(detection.data_source_objects))
45
+
46
+ return sorted(list(data_source_objects))
47
+
37
48
 
38
49
  def storyAndInvestigationNamesWithApp(self, app_name:str)->List[str]:
39
50
  return [f"{app_name} - {name} - Rule" for name in self.detection_names] + \
@@ -141,7 +152,3 @@ class Story(SecurityContentObject):
141
152
  def baseline_names(self)->List[str]:
142
153
  return [baseline.name for baseline in self.baselines]
143
154
 
144
-
145
-
146
-
147
-
@@ -0,0 +1,40 @@
1
+ import csv
2
+ from contentctl.objects.data_source import DataSource
3
+ from contentctl.objects.event_source import EventSource
4
+ from typing import List
5
+ import pathlib
6
+
7
+ class DataSourceWriter:
8
+
9
+ @staticmethod
10
+ def writeDataSourceCsv(data_source_objects: List[DataSource], file_path: pathlib.Path):
11
+ with open(file_path, mode='w', newline='') as file:
12
+ writer = csv.writer(file)
13
+ # Write the header
14
+ writer.writerow([
15
+ "name", "id", "author", "source", "sourcetype", "separator",
16
+ "supported_TA_name", "supported_TA_version", "supported_TA_url",
17
+ "description"
18
+ ])
19
+ # Write the data
20
+ for data_source in data_source_objects:
21
+ if data_source.supported_TA and isinstance(data_source.supported_TA, list) and len(data_source.supported_TA) > 0:
22
+ supported_TA_name = data_source.supported_TA[0].get('name', '')
23
+ supported_TA_version = data_source.supported_TA[0].get('version', '')
24
+ supported_TA_url = data_source.supported_TA[0].get('url', '')
25
+ else:
26
+ supported_TA_name = ''
27
+ supported_TA_version = ''
28
+ supported_TA_url = ''
29
+ writer.writerow([
30
+ data_source.name,
31
+ data_source.id,
32
+ data_source.author,
33
+ data_source.source,
34
+ data_source.sourcetype,
35
+ data_source.separator,
36
+ supported_TA_name,
37
+ supported_TA_version,
38
+ supported_TA_url,
39
+ data_source.description,
40
+ ])