contentctl 5.0.5__tar.gz → 5.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 (170) hide show
  1. contentctl-5.2.0/PKG-INFO +74 -0
  2. contentctl-5.2.0/README.md +41 -0
  3. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +1 -1
  4. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/contentctl.py +0 -16
  5. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/director.py +16 -31
  6. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +32 -25
  7. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/data_source.py +2 -0
  8. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/lookup.py +16 -3
  9. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/attack_nav_output.py +11 -4
  10. contentctl-5.2.0/contentctl/output/attack_nav_writer.py +83 -0
  11. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/transforms.j2 +2 -2
  12. {contentctl-5.0.5 → contentctl-5.2.0}/pyproject.toml +3 -2
  13. contentctl-5.0.5/PKG-INFO +0 -263
  14. contentctl-5.0.5/README.md +0 -230
  15. contentctl-5.0.5/contentctl/output/attack_nav_writer.py +0 -67
  16. {contentctl-5.0.5 → contentctl-5.2.0}/LICENSE.md +0 -0
  17. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/__init__.py +0 -0
  18. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/build.py +0 -0
  19. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/deploy_acs.py +0 -0
  20. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  21. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
  22. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  23. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  24. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  25. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  26. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  27. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  28. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  29. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  30. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/doc_gen.py +0 -0
  31. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/initialize.py +0 -0
  32. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/inspect.py +0 -0
  33. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/new_content.py +0 -0
  34. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/release_notes.py +0 -0
  35. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/reporting.py +0 -0
  36. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/test.py +0 -0
  37. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/validate.py +0 -0
  38. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/api.py +0 -0
  39. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
  40. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
  41. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  42. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/link_validator.py +0 -0
  43. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/logger.py +0 -0
  44. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/splunk_app.py +0 -0
  45. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/utils.py +0 -0
  46. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/new_content_questions.py +0 -0
  47. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/yml_reader.py +0 -0
  48. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  49. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/alert_action.py +0 -0
  50. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/annotated_types.py +0 -0
  51. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/atomic.py +0 -0
  52. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/base_test.py +0 -0
  53. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/base_test_result.py +0 -0
  54. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/baseline.py +0 -0
  55. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/baseline_tags.py +0 -0
  56. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/config.py +0 -0
  57. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/constants.py +0 -0
  58. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/correlation_search.py +0 -0
  59. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/dashboard.py +0 -0
  60. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment.py +0 -0
  61. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_email.py +0 -0
  62. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_notable.py +0 -0
  63. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_phantom.py +0 -0
  64. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_rba.py +0 -0
  65. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
  66. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_slack.py +0 -0
  67. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection.py +0 -0
  68. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_metadata.py +0 -0
  69. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_stanza.py +0 -0
  70. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_tags.py +0 -0
  71. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/drilldown.py +0 -0
  72. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/enums.py +0 -0
  73. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/errors.py +0 -0
  74. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/integration_test.py +0 -0
  75. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/integration_test_result.py +0 -0
  76. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/investigation.py +0 -0
  77. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/investigation_tags.py +0 -0
  78. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/macro.py +0 -0
  79. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/manual_test.py +0 -0
  80. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/manual_test_result.py +0 -0
  81. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  82. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/notable_action.py +0 -0
  83. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/notable_event.py +0 -0
  84. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/playbook.py +0 -0
  85. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/playbook_tags.py +0 -0
  86. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/rba.py +0 -0
  87. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
  88. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_event.py +0 -0
  89. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_object.py +0 -0
  90. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/savedsearches_conf.py +0 -0
  91. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/security_content_object.py +0 -0
  92. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/story.py +0 -0
  93. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/story_tags.py +0 -0
  94. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/test_attack_data.py +0 -0
  95. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/test_group.py +0 -0
  96. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/threat_object.py +0 -0
  97. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/throttling.py +0 -0
  98. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test.py +0 -0
  99. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
  100. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test_result.py +0 -0
  101. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/api_json_output.py +0 -0
  102. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/conf_output.py +0 -0
  103. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/conf_writer.py +0 -0
  104. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/data_source_writer.py +0 -0
  105. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/doc_md_output.py +0 -0
  106. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/jinja_writer.py +0 -0
  107. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/json_writer.py +0 -0
  108. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/svg_output.py +0 -0
  109. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  110. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  111. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  112. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
  113. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
  114. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/collections.j2 +0 -0
  115. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/content-version.j2 +0 -0
  116. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
  117. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
  118. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  119. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
  120. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
  121. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  122. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  123. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  124. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
  125. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
  126. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  127. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  128. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/header.j2 +0 -0
  129. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/macros.j2 +0 -0
  130. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/panel.j2 +0 -0
  131. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  132. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  133. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  134. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/server.conf.j2 +0 -0
  135. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
  136. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/yml_writer.py +0 -0
  137. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/README.md +0 -0
  138. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_default.yml +0 -0
  139. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  140. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  141. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  142. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README.md +0 -0
  143. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  144. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
  145. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  146. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  147. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  148. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  149. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  150. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
  151. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
  152. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  153. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  154. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  155. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  156. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
  157. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
  158. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  159. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  160. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  161. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  162. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  163. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
  164. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  165. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
  166. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
  167. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
  168. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  169. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  170. {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/stories/cobalt_strike.yml +0 -0
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.3
2
+ Name: contentctl
3
+ Version: 5.2.0
4
+ Summary: Splunk Content Control Tool
5
+ License: Apache 2.0
6
+ Author: STRT
7
+ Author-email: research@splunk.com
8
+ Requires-Python: >=3.11,<3.14
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
15
+ Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
16
+ Requires-Dist: attackcti (>=0.4.0,<0.5.0)
17
+ Requires-Dist: bottle (>=0.12.25,<0.14.0)
18
+ Requires-Dist: docker (>=7.1.0,<8.0.0)
19
+ Requires-Dist: gitpython (>=3.1.43,<4.0.0)
20
+ Requires-Dist: pycvesearch (>=1.2,<2.0)
21
+ Requires-Dist: pydantic (>=2.9.2,<2.10.0)
22
+ Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
23
+ Requires-Dist: questionary (>=2.0.1,<3.0.0)
24
+ Requires-Dist: requests (>=2.32.3,<2.33.0)
25
+ Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
26
+ Requires-Dist: setuptools (>=69.5.1,<76.0.0)
27
+ Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
28
+ Requires-Dist: tqdm (>=4.66.5,<5.0.0)
29
+ Requires-Dist: tyro (>=0.9.2,<0.10.0)
30
+ Requires-Dist: xmltodict (>=0.13,<0.15)
31
+ Description-Content-Type: text/markdown
32
+
33
+ # contentctl
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/splunk/contentctl/refs/heads/main/docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="the logo for the contentctl project, which depicts a doodled 4 legged animal that is supposed to represent a capybara, with the name of the project below it" width="250" height="250"></p>
36
+
37
+ > [!NOTE]
38
+ > Looking to migrate from an earlier release to the new contentctl v5+ ? Check out our migration guide [here](docs/contentctl_v5_migration_guide.md).
39
+
40
+ ## What is contentctl?
41
+ `contentctl` is a tool developed by the Splunk Threat Research Team to help with managing the content living in [splunk/security_content](https://github.com/splunk/security_content) and producing the Enterprise Security Content Update app for Splunk. While its development is largely driven by STRT's needs, it has been somewhat genericized and can be used by customers and partners to package their own content. Simply put, `contentctl` is the workhorse that packages detections, macros, lookups, dashboards into a Splunk app that you can use, and that understands the YAML structure and project layout we've selected to keep development clean.
42
+
43
+ ## Quick Start Guide
44
+ Check out our [User Guide](docs/UserGuide.md) to get started!
45
+
46
+ ## Content Testing
47
+ Read more about how `contentctl` can help test and validate your content in a real Splunk instance [here](docs/ContentTestingGuide.md).
48
+
49
+ ## Sample CICD Workflows
50
+ Already using `contentctl`, or looking to get started with it already configured in GitHub Actions? [Our guide](docs/Sample_CICD_Templates.md) includes workflows to help you build and test your app.
51
+
52
+ ## Contribution Guide
53
+ Read [the Contribution Guidelines](CONTRIBUTING.md) for this project before opening a Pull Request.
54
+
55
+ ## Ecosystem
56
+ | Project | Description |
57
+ | --------------------- | ------------------------------------------------------- |
58
+ | [Splunk Security Content](https://github.com/splunk/security_content) | Splunk Threat Research Team's Content included in the [Enterprise Security Content Update App (ESCU)](https://splunkbase.splunk.com/app/3449)|
59
+ | [Splunk Attack Range](https://github.com/splunk/attack_range) | Easily deploy a preconfigured Splunk Environment locally or on AWS containing a Splunk Instance, Windows and Linux Machines, and Attacker Tools like Kali Linux. Automatically simulate attacks or run your own|
60
+ | [Splunk Attack Data](https://github.com/splunk/attack_data) | Repository of Attack Simulation Data for writing and Testing Detections| |
61
+ | [Splunk contentctl](https://github.com/splunk/contentctl) | Generate, validate, build, test, and deploy custom Security Content|
62
+ | [SigmaHQ Sigma Rules](https://github.com/SigmaHQ/sigma) | Official Repository for Sigma Rules. These rules are an excellent starting point for new content. |
63
+ | [PurpleSharp Attack Simulation](https://github.com/mvelazc0/PurpleSharp) | Open source adversary simulation tool for Windows Active Directory environments (integrated into Attack Range)|
64
+ | [Red Canary Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) | Library of attack simulations mapped to the MITRE ATT&CK® framework (integrated into Attack Range)|
65
+
66
+ ## License
67
+ Copyright 2023 Splunk Inc.
68
+
69
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
70
+
71
+ http://www.apache.org/licenses/LICENSE-2.0
72
+
73
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
74
+
@@ -0,0 +1,41 @@
1
+ # contentctl
2
+ <p align="center">
3
+ <img src="https://raw.githubusercontent.com/splunk/contentctl/refs/heads/main/docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="the logo for the contentctl project, which depicts a doodled 4 legged animal that is supposed to represent a capybara, with the name of the project below it" width="250" height="250"></p>
4
+
5
+ > [!NOTE]
6
+ > Looking to migrate from an earlier release to the new contentctl v5+ ? Check out our migration guide [here](docs/contentctl_v5_migration_guide.md).
7
+
8
+ ## What is contentctl?
9
+ `contentctl` is a tool developed by the Splunk Threat Research Team to help with managing the content living in [splunk/security_content](https://github.com/splunk/security_content) and producing the Enterprise Security Content Update app for Splunk. While its development is largely driven by STRT's needs, it has been somewhat genericized and can be used by customers and partners to package their own content. Simply put, `contentctl` is the workhorse that packages detections, macros, lookups, dashboards into a Splunk app that you can use, and that understands the YAML structure and project layout we've selected to keep development clean.
10
+
11
+ ## Quick Start Guide
12
+ Check out our [User Guide](docs/UserGuide.md) to get started!
13
+
14
+ ## Content Testing
15
+ Read more about how `contentctl` can help test and validate your content in a real Splunk instance [here](docs/ContentTestingGuide.md).
16
+
17
+ ## Sample CICD Workflows
18
+ Already using `contentctl`, or looking to get started with it already configured in GitHub Actions? [Our guide](docs/Sample_CICD_Templates.md) includes workflows to help you build and test your app.
19
+
20
+ ## Contribution Guide
21
+ Read [the Contribution Guidelines](CONTRIBUTING.md) for this project before opening a Pull Request.
22
+
23
+ ## Ecosystem
24
+ | Project | Description |
25
+ | --------------------- | ------------------------------------------------------- |
26
+ | [Splunk Security Content](https://github.com/splunk/security_content) | Splunk Threat Research Team's Content included in the [Enterprise Security Content Update App (ESCU)](https://splunkbase.splunk.com/app/3449)|
27
+ | [Splunk Attack Range](https://github.com/splunk/attack_range) | Easily deploy a preconfigured Splunk Environment locally or on AWS containing a Splunk Instance, Windows and Linux Machines, and Attacker Tools like Kali Linux. Automatically simulate attacks or run your own|
28
+ | [Splunk Attack Data](https://github.com/splunk/attack_data) | Repository of Attack Simulation Data for writing and Testing Detections| |
29
+ | [Splunk contentctl](https://github.com/splunk/contentctl) | Generate, validate, build, test, and deploy custom Security Content|
30
+ | [SigmaHQ Sigma Rules](https://github.com/SigmaHQ/sigma) | Official Repository for Sigma Rules. These rules are an excellent starting point for new content. |
31
+ | [PurpleSharp Attack Simulation](https://github.com/mvelazc0/PurpleSharp) | Open source adversary simulation tool for Windows Active Directory environments (integrated into Attack Range)|
32
+ | [Red Canary Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) | Library of attack simulations mapped to the MITRE ATT&CK® framework (integrated into Attack Range)|
33
+
34
+ ## License
35
+ Copyright 2023 Splunk Inc.
36
+
37
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
38
+
39
+ http://www.apache.org/licenses/LICENSE-2.0
40
+
41
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
@@ -89,7 +89,7 @@ class DetectionTestingManagerOutputDto:
89
89
  start_time: Union[datetime.datetime, None] = None
90
90
  replay_index: str = "contentctl_testing_index"
91
91
  replay_host: str = "CONTENTCTL_HOST"
92
- timeout_seconds: int = 60
92
+ timeout_seconds: int = 120
93
93
  terminate: bool = False
94
94
 
95
95
 
@@ -143,20 +143,6 @@ def test_common_func(config: test_common):
143
143
  raise Exception("There was at least one unsuccessful test")
144
144
 
145
145
 
146
- CONTENTCTL_5_WARNING = """
147
- *****************************************************************************
148
- WARNING - THIS IS AN ALPHA BUILD OF CONTENTCTL 5.
149
- THERE HAVE BEEN NUMEROUS CHANGES IN CONTENTCTL (ESPECIALLY TO YML FORMATS).
150
- YOU ALMOST CERTAINLY DO NOT WANT TO USE THIS BUILD.
151
- IF YOU ENCOUNTER ERRORS, PLEASE USE THE LATEST CURRENTYLY SUPPORTED RELEASE:
152
-
153
- CONTENTCTL==4.4.7
154
-
155
- YOU HAVE BEEN WARNED!
156
- *****************************************************************************
157
- """
158
-
159
-
160
146
  def get_random_compliment():
161
147
  compliments = [
162
148
  "Your detection rules are like a zero-day shield! 🛡️",
@@ -187,7 +173,6 @@ class RecognizeCommand:
187
173
 
188
174
 
189
175
  def main():
190
- print(CONTENTCTL_5_WARNING)
191
176
  try:
192
177
  configFile = pathlib.Path("contentctl.yml")
193
178
 
@@ -299,7 +284,6 @@ def main():
299
284
  )
300
285
 
301
286
  print(e)
302
- print(CONTENTCTL_5_WARNING)
303
287
  sys.exit(1)
304
288
 
305
289
 
@@ -1,30 +1,29 @@
1
1
  import os
2
2
  import sys
3
- from pathlib import Path
4
3
  from dataclasses import dataclass, field
5
- from pydantic import ValidationError
4
+ from pathlib import Path
6
5
  from uuid import UUID
7
- from contentctl.input.yml_reader import YmlReader
8
6
 
9
- from contentctl.objects.detection import Detection
10
- from contentctl.objects.story import Story
7
+ from pydantic import ValidationError
11
8
 
12
- from contentctl.objects.baseline import Baseline
13
- from contentctl.objects.investigation import Investigation
14
- from contentctl.objects.playbook import Playbook
15
- from contentctl.objects.deployment import Deployment
16
- from contentctl.objects.macro import Macro
17
- from contentctl.objects.lookup import LookupAdapter, Lookup
18
- from contentctl.objects.atomic import AtomicEnrichment
19
- from contentctl.objects.security_content_object import SecurityContentObject
20
- from contentctl.objects.data_source import DataSource
21
- from contentctl.objects.dashboard import Dashboard
22
9
  from contentctl.enrichments.attack_enrichment import AttackEnrichment
23
10
  from contentctl.enrichments.cve_enrichment import CveEnrichment
24
-
11
+ from contentctl.helper.utils import Utils
12
+ from contentctl.input.yml_reader import YmlReader
13
+ from contentctl.objects.atomic import AtomicEnrichment
14
+ from contentctl.objects.baseline import Baseline
25
15
  from contentctl.objects.config import validate
16
+ from contentctl.objects.dashboard import Dashboard
17
+ from contentctl.objects.data_source import DataSource
18
+ from contentctl.objects.deployment import Deployment
19
+ from contentctl.objects.detection import Detection
26
20
  from contentctl.objects.enums import SecurityContentType
27
- from contentctl.helper.utils import Utils
21
+ from contentctl.objects.investigation import Investigation
22
+ from contentctl.objects.lookup import Lookup, LookupAdapter
23
+ from contentctl.objects.macro import Macro
24
+ from contentctl.objects.playbook import Playbook
25
+ from contentctl.objects.security_content_object import SecurityContentObject
26
+ from contentctl.objects.story import Story
28
27
 
29
28
 
30
29
  @dataclass
@@ -113,20 +112,6 @@ class Director:
113
112
  self.createSecurityContent(SecurityContentType.detections)
114
113
  self.createSecurityContent(SecurityContentType.dashboards)
115
114
 
116
- from contentctl.objects.abstract_security_content_objects.detection_abstract import (
117
- MISSING_SOURCES,
118
- )
119
-
120
- if len(MISSING_SOURCES) > 0:
121
- missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES)))
122
- print(
123
- "WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
124
- "This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
125
- f"{missing_sources_string}"
126
- )
127
- else:
128
- print("No missing data_sources!")
129
-
130
115
  def createSecurityContent(self, contentType: SecurityContentType) -> None:
131
116
  if contentType in [
132
117
  SecurityContentType.deployments,
@@ -51,8 +51,6 @@ from contentctl.objects.security_content_object import SecurityContentObject
51
51
  from contentctl.objects.test_group import TestGroup
52
52
  from contentctl.objects.unit_test import UnitTest
53
53
 
54
- MISSING_SOURCES: set[str] = set()
55
-
56
54
  # Those AnalyticsTypes that we do not test via contentctl
57
55
  SKIPPED_ANALYTICS_TYPES: set[str] = {AnalyticsType.Correlation}
58
56
 
@@ -476,7 +474,7 @@ class Detection_Abstract(SecurityContentObject):
476
474
  "name": lookup.name,
477
475
  "description": lookup.description,
478
476
  "filename": lookup.filename.name,
479
- "default_match": "true" if lookup.default_match else "false",
477
+ "default_match": lookup.default_match,
480
478
  "case_sensitive_match": "true"
481
479
  if lookup.case_sensitive_match
482
480
  else "false",
@@ -514,7 +512,7 @@ class Detection_Abstract(SecurityContentObject):
514
512
  baseline.tags.detections = new_detections
515
513
 
516
514
  # Data source may be defined 1 on each line, OR they may be defined as
517
- # SOUCE_1 AND ANOTHERSOURCE AND A_THIRD_SOURCE
515
+ # SOURCE_1 AND ANOTHERSOURCE AND A_THIRD_SOURCE
518
516
  # if more than 1 data source is required for a detection (for example, because it includes a join)
519
517
  # Parse and update the list to resolve individual names and remove potential duplicates
520
518
  updated_data_source_names: set[str] = set()
@@ -524,27 +522,9 @@ class Detection_Abstract(SecurityContentObject):
524
522
  updated_data_source_names.update(split_data_sources)
525
523
 
526
524
  sources = sorted(list(updated_data_source_names))
527
-
528
- matched_data_sources: list[DataSource] = []
529
- missing_sources: list[str] = []
530
- for source in sources:
531
- try:
532
- matched_data_sources += DataSource.mapNamesToSecurityContentObjects(
533
- [source], director
534
- )
535
- except Exception:
536
- # We gobble this up and add it to a global set so that we
537
- # can print it ONCE at the end of the build of datasources.
538
- # This will be removed later as per the note below
539
- MISSING_SOURCES.add(source)
540
-
541
- if len(missing_sources) > 0:
542
- # This will be changed to ValueError when we have a complete list of data sources
543
- print(
544
- "WARNING: The following exception occurred when mapping the data_source field to "
545
- f"DataSource objects:{missing_sources}"
546
- )
547
-
525
+ matched_data_sources = DataSource.mapNamesToSecurityContentObjects(
526
+ sources, director
527
+ )
548
528
  self.data_source_objects = matched_data_sources
549
529
 
550
530
  for story in self.tags.analytic_story:
@@ -1075,3 +1055,30 @@ class Detection_Abstract(SecurityContentObject):
1075
1055
  # Return the summary
1076
1056
 
1077
1057
  return summary_dict
1058
+
1059
+ @model_validator(mode="after")
1060
+ def validate_data_source_output_fields(self):
1061
+ # Skip validation for Hunting and Correlation types, or non-production detections
1062
+ if self.status != DetectionStatus.production or self.type in {
1063
+ AnalyticsType.Hunting,
1064
+ AnalyticsType.Correlation,
1065
+ }:
1066
+ return self
1067
+
1068
+ # Validate that all required output fields are present in the search
1069
+ for data_source in self.data_source_objects:
1070
+ if not data_source.output_fields:
1071
+ continue
1072
+
1073
+ missing_fields = [
1074
+ field for field in data_source.output_fields if field not in self.search
1075
+ ]
1076
+
1077
+ if missing_fields:
1078
+ raise ValueError(
1079
+ f"Data source '{data_source.name}' has output fields "
1080
+ f"{missing_fields} that are not present in the search "
1081
+ f"for detection '{self.name}'"
1082
+ )
1083
+
1084
+ return self
@@ -17,10 +17,12 @@ class DataSource(SecurityContentObject):
17
17
  source: str = Field(...)
18
18
  sourcetype: str = Field(...)
19
19
  separator: Optional[str] = None
20
+ separator_value: None | str = None
20
21
  configuration: Optional[str] = None
21
22
  supported_TA: list[TA] = []
22
23
  fields: None | list = None
23
24
  field_mappings: None | list = None
25
+ mitre_components: list[str] = []
24
26
  convert_to_log_source: None | list = None
25
27
  example_log: None | str = None
26
28
  output_fields: list[str] = []
@@ -6,9 +6,10 @@ import pathlib
6
6
  import re
7
7
  from enum import StrEnum, auto
8
8
  from functools import cached_property
9
- from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Self
9
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, Self
10
10
 
11
11
  from pydantic import (
12
+ BeforeValidator,
12
13
  Field,
13
14
  FilePath,
14
15
  NonNegativeInt,
@@ -69,7 +70,19 @@ class Lookup_Type(StrEnum):
69
70
 
70
71
  # TODO (#220): Split Lookup into 2 classes
71
72
  class Lookup(SecurityContentObject, abc.ABC):
72
- default_match: Optional[bool] = None
73
+ # We need to make sure that this is converted to a string because we widely
74
+ # use the string "False" in our lookup content. However, PyYAML reads this
75
+ # as a BOOL and this causes parsing to fail. As such, we will always
76
+ # convert this to a string if it is passed as a bool
77
+ default_match: Annotated[
78
+ str, BeforeValidator(lambda dm: str(dm).lower() if isinstance(dm, bool) else dm)
79
+ ] = Field(
80
+ default="",
81
+ description="This field is given a default value of ''"
82
+ "because it is the default value specified in the transforms.conf "
83
+ "docs. Giving it a type of str rather than str | None simplifies "
84
+ "the typing for the field.",
85
+ )
73
86
  # Per the documentation for transforms.conf, EXACT should not be specified in this list,
74
87
  # so we include only WILDCARD and CIDR
75
88
  match_type: list[Annotated[str, Field(pattern=r"(^WILDCARD|CIDR)\(.+\)$")]] = Field(
@@ -88,7 +101,7 @@ class Lookup(SecurityContentObject, abc.ABC):
88
101
 
89
102
  # All fields custom to this model
90
103
  model = {
91
- "default_match": "true" if self.default_match is True else "false",
104
+ "default_match": self.default_match,
92
105
  "match_type": self.match_type_to_conf_format,
93
106
  "min_matches": self.min_matches,
94
107
  "max_matches": self.max_matches,
@@ -1,5 +1,5 @@
1
- from typing import List, Union
2
1
  import pathlib
2
+ from typing import List, Union
3
3
 
4
4
  from contentctl.objects.detection import Detection
5
5
  from contentctl.output.attack_nav_writer import AttackNavWriter
@@ -10,14 +10,21 @@ class AttackNavOutput:
10
10
  self, detections: List[Detection], output_path: pathlib.Path
11
11
  ) -> None:
12
12
  techniques: dict[str, dict[str, Union[List[str], int]]] = {}
13
+
13
14
  for detection in detections:
14
15
  for tactic in detection.tags.mitre_attack_id:
15
16
  if tactic not in techniques:
16
17
  techniques[tactic] = {"score": 0, "file_paths": []}
17
18
 
18
- detection_url = f"https://github.com/splunk/security_content/blob/develop/detections/{detection.source}/{detection.file_path.name}"
19
- techniques[tactic]["score"] += 1
20
- techniques[tactic]["file_paths"].append(detection_url)
19
+ detection_type = detection.source
20
+ detection_id = detection.id
21
+
22
+ # Store all three pieces of information separately
23
+ detection_info = f"{detection_type}|{detection_id}|{detection.name}"
24
+
25
+ techniques[tactic]["score"] = techniques[tactic].get("score", 0) + 1
26
+ if isinstance(techniques[tactic]["file_paths"], list):
27
+ techniques[tactic]["file_paths"].append(detection_info)
21
28
 
22
29
  """
23
30
  for detection in objects:
@@ -0,0 +1,83 @@
1
+ import json
2
+ import pathlib
3
+ from typing import List, Union
4
+
5
+ VERSION = "4.5"
6
+ NAME = "Detection Coverage"
7
+ DESCRIPTION = "Security Content Detection Coverage"
8
+ DOMAIN = "enterprise-attack"
9
+
10
+
11
+ class AttackNavWriter:
12
+ @staticmethod
13
+ def writeAttackNavFile(
14
+ mitre_techniques: dict[str, dict[str, Union[List[str], int]]],
15
+ output_path: pathlib.Path,
16
+ ) -> None:
17
+ max_count = max(
18
+ (technique["score"] for technique in mitre_techniques.values()), default=0
19
+ )
20
+
21
+ layer_json = {
22
+ "versions": {"attack": "16", "navigator": "5.1.0", "layer": VERSION},
23
+ "name": NAME,
24
+ "description": DESCRIPTION,
25
+ "domain": DOMAIN,
26
+ "techniques": [],
27
+ "gradient": {
28
+ "colors": ["#ffffff", "#66b1ff", "#096ed7"],
29
+ "minValue": 0,
30
+ "maxValue": max_count,
31
+ },
32
+ "filters": {
33
+ "platforms": [
34
+ "Windows",
35
+ "Linux",
36
+ "macOS",
37
+ "Network",
38
+ "AWS",
39
+ "GCP",
40
+ "Azure",
41
+ "Azure AD",
42
+ "Office 365",
43
+ "SaaS",
44
+ ]
45
+ },
46
+ "layout": {
47
+ "layout": "side",
48
+ "showName": True,
49
+ "showID": True,
50
+ "showAggregateScores": False,
51
+ },
52
+ "legendItems": [
53
+ {"label": "No detections", "color": "#ffffff"},
54
+ {"label": "Has detections", "color": "#66b1ff"},
55
+ ],
56
+ "showTacticRowBackground": True,
57
+ "tacticRowBackground": "#dddddd",
58
+ "selectTechniquesAcrossTactics": True,
59
+ }
60
+
61
+ for technique_id, data in mitre_techniques.items():
62
+ links = []
63
+ for detection_info in data["file_paths"]:
64
+ # Split the detection info into its components
65
+ detection_type, detection_id, detection_name = detection_info.split("|")
66
+
67
+ # Construct research website URL (without the name)
68
+ research_url = (
69
+ f"https://research.splunk.com/{detection_type}/{detection_id}/"
70
+ )
71
+
72
+ links.append({"label": detection_name, "url": research_url})
73
+
74
+ layer_technique = {
75
+ "techniqueID": technique_id,
76
+ "score": data["score"],
77
+ "enabled": True,
78
+ "links": links,
79
+ }
80
+ layer_json["techniques"].append(layer_technique)
81
+
82
+ with open(output_path, "w") as outfile:
83
+ json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
@@ -7,8 +7,8 @@ filename = {{ lookup.app_filename.name }}
7
7
  collection = {{ lookup.collection }}
8
8
  external_type = kvstore
9
9
  {% endif %}
10
- {% if lookup.default_match is defined and lookup.default_match != None %}
11
- default_match = {{ lookup.default_match | lower }}
10
+ {% if lookup.default_match != '' %}
11
+ default_match = {{ lookup.default_match }}
12
12
  {% endif %}
13
13
  {% if lookup.case_sensitive_match is defined and lookup.case_sensitive_match != None %}
14
14
  case_sensitive_match = {{ lookup.case_sensitive_match | lower }}
@@ -1,6 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
- version = "5.0.5"
3
+
4
+ version = "5.2.0"
4
5
 
5
6
  description = "Splunk Content Control Tool"
6
7
  authors = ["STRT <research@splunk.com>"]
@@ -32,7 +33,7 @@ gitpython = "^3.1.43"
32
33
  setuptools = ">=69.5.1,<76.0.0"
33
34
 
34
35
  [tool.poetry.group.dev.dependencies]
35
- ruff = "^0.9.2"
36
+ ruff = "^0.9.10"
36
37
 
37
38
  [build-system]
38
39
  requires = ["poetry-core>=1.0.0"]