contentctl 4.3.0__tar.gz → 4.3.2__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-4.3.0 → contentctl-4.3.2}/PKG-INFO +9 -9
  2. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +60 -24
  3. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/enrichments/attack_enrichment.py +24 -11
  4. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +5 -0
  5. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/constants.py +5 -0
  6. contentctl-4.3.2/contentctl/objects/mitre_attack_enrichment.py +95 -0
  7. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/observable.py +5 -3
  8. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +2 -2
  9. {contentctl-4.3.0 → contentctl-4.3.2}/pyproject.toml +9 -9
  10. contentctl-4.3.0/contentctl/objects/mitre_attack_enrichment.py +0 -32
  11. {contentctl-4.3.0 → contentctl-4.3.2}/LICENSE.md +0 -0
  12. {contentctl-4.3.0 → contentctl-4.3.2}/README.md +0 -0
  13. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/__init__.py +0 -0
  14. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/build.py +0 -0
  15. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/deploy_acs.py +0 -0
  16. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  17. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/GitService.py +0 -0
  18. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  19. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  20. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  21. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  22. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  23. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  24. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  25. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  26. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/doc_gen.py +0 -0
  27. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/initialize.py +0 -0
  28. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/initialize_old.py +0 -0
  29. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/inspect.py +0 -0
  30. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/new_content.py +0 -0
  31. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/release_notes.py +0 -0
  32. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/reporting.py +0 -0
  33. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/test.py +0 -0
  34. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/actions/validate.py +0 -0
  35. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/api.py +0 -0
  36. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/contentctl.py +0 -0
  37. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/enrichments/cve_enrichment.py +0 -0
  38. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  39. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/helper/link_validator.py +0 -0
  40. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/helper/logger.py +0 -0
  41. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/helper/splunk_app.py +0 -0
  42. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/helper/utils.py +0 -0
  43. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/input/director.py +0 -0
  44. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/input/new_content_questions.py +0 -0
  45. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/input/yml_reader.py +0 -0
  46. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  47. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/alert_action.py +0 -0
  48. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/atomic.py +0 -0
  49. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/base_test.py +0 -0
  50. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/base_test_result.py +0 -0
  51. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/baseline.py +0 -0
  52. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/baseline_tags.py +0 -0
  53. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/config.py +0 -0
  54. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/correlation_search.py +0 -0
  55. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/data_source.py +0 -0
  56. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment.py +0 -0
  57. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_email.py +0 -0
  58. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_notable.py +0 -0
  59. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_phantom.py +0 -0
  60. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_rba.py +0 -0
  61. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_scheduling.py +0 -0
  62. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/deployment_slack.py +0 -0
  63. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/detection.py +0 -0
  64. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/detection_tags.py +0 -0
  65. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/enums.py +0 -0
  66. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/errors.py +0 -0
  67. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/event_source.py +0 -0
  68. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/integration_test.py +0 -0
  69. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/integration_test_result.py +0 -0
  70. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/investigation.py +0 -0
  71. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/investigation_tags.py +0 -0
  72. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/lookup.py +0 -0
  73. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/macro.py +0 -0
  74. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/notable_action.py +0 -0
  75. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/notable_event.py +0 -0
  76. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/playbook.py +0 -0
  77. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/playbook_tags.py +0 -0
  78. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/risk_analysis_action.py +0 -0
  79. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/risk_event.py +0 -0
  80. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/risk_object.py +0 -0
  81. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/security_content_object.py +0 -0
  82. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/ssa_detection.py +0 -0
  83. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/ssa_detection_tags.py +0 -0
  84. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/story.py +0 -0
  85. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/story_tags.py +0 -0
  86. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/test_group.py +0 -0
  87. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/threat_object.py +0 -0
  88. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test.py +0 -0
  89. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test_attack_data.py +0 -0
  90. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test_baseline.py +0 -0
  91. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test_old.py +0 -0
  92. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test_result.py +0 -0
  93. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/objects/unit_test_ssa.py +0 -0
  94. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/api_json_output.py +0 -0
  95. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/attack_nav_output.py +0 -0
  96. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/attack_nav_writer.py +0 -0
  97. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/conf_output.py +0 -0
  98. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/conf_writer.py +0 -0
  99. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/data_source_writer.py +0 -0
  100. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/detection_writer.py +0 -0
  101. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/doc_md_output.py +0 -0
  102. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/jinja_writer.py +0 -0
  103. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/json_writer.py +0 -0
  104. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/new_content_yml_output.py +0 -0
  105. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/svg_output.py +0 -0
  106. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  107. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  108. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  109. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/app.conf.j2 +0 -0
  110. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/app.manifest.j2 +0 -0
  111. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/collections.j2 +0 -0
  112. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/content-version.j2 +0 -0
  113. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/detection_count.j2 +0 -0
  114. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/detection_coverage.j2 +0 -0
  115. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  116. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_detections.j2 +0 -0
  117. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_navigation.j2 +0 -0
  118. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  119. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  120. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  121. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_stories.j2 +0 -0
  122. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/doc_story_page.j2 +0 -0
  123. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  124. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  125. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/finding_report.j2 +0 -0
  126. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/header.j2 +0 -0
  127. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/macros.j2 +0 -0
  128. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/panel.j2 +0 -0
  129. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  130. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  131. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  132. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/transforms.j2 +0 -0
  133. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/templates/workflow_actions.j2 +0 -0
  134. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/yml_output.py +0 -0
  135. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/output/yml_writer.py +0 -0
  136. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/README.md +0 -0
  137. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_default.yml +0 -0
  138. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  139. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  140. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  141. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/README.md +0 -0
  142. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  143. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/app.conf +0 -0
  144. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/commands.conf +0 -0
  145. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/content-version.conf +0 -0
  146. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  147. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  148. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  149. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  150. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  151. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/metadata/default.meta +0 -0
  152. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/static/appIcon.png +0 -0
  153. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  154. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  155. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  156. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  157. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/datamodels_cim.conf +0 -0
  158. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/datamodels_custom.conf +0 -0
  159. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  160. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  161. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  162. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  163. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  164. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/detections/application/.gitkeep +0 -0
  165. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  166. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/detections/network/.gitkeep +0 -0
  167. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/detections/web/.gitkeep +0 -0
  168. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  169. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  170. {contentctl-4.3.0 → contentctl-4.3.2}/contentctl/templates/stories/cobalt_strike.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: contentctl
3
- Version: 4.3.0
3
+ Version: 4.3.2
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -11,20 +11,20 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
14
- Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
15
- Requires-Dist: attackcti (>=0.3.7,<0.5.0)
14
+ Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
15
+ Requires-Dist: attackcti (>=0.4.0,<0.5.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.7.1,<3.0.0)
21
- Requires-Dist: pygit2 (>=1.14.1,<2.0.0)
20
+ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
21
+ Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
22
22
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
23
- Requires-Dist: requests (>=2.32.2,<2.33.0)
23
+ Requires-Dist: requests (>=2.32.3,<2.33.0)
24
24
  Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
25
- Requires-Dist: setuptools (>=69.5.1,<73.0.0)
26
- Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
27
- Requires-Dist: tqdm (>=4.66.4,<5.0.0)
25
+ Requires-Dist: setuptools (>=69.5.1,<74.0.0)
26
+ Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
27
+ Requires-Dist: tqdm (>=4.66.5,<5.0.0)
28
28
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
29
29
  Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
30
30
  Description-Content-Type: text/markdown
@@ -1060,7 +1060,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1060
1060
  results = JSONResultsReader(job.results(output_mode="json"))
1061
1061
 
1062
1062
  # Consolidate a set of the distinct observable field names
1063
- observable_fields_set = set([o.name for o in detection.tags.observable])
1063
+ observable_fields_set = set([o.name for o in detection.tags.observable]) # keeping this around for later
1064
+ risk_object_fields_set = set([o.name for o in detection.tags.observable if "Victim" in o.role ]) # just the "Risk Objects"
1065
+ threat_object_fields_set = set([o.name for o in detection.tags.observable if "Attacker" in o.role]) # just the "threat objects"
1064
1066
 
1065
1067
  # Ensure the search had at least one result
1066
1068
  if int(job.content.get("resultCount", "0")) > 0:
@@ -1068,7 +1070,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1068
1070
  test.result = UnitTestResult()
1069
1071
 
1070
1072
  # Initialize the collection of fields that are empty that shouldn't be
1073
+ present_threat_objects: set[str] = set()
1071
1074
  empty_fields: set[str] = set()
1075
+
1076
+
1072
1077
 
1073
1078
  # Filter out any messages in the results
1074
1079
  for result in results:
@@ -1077,30 +1082,50 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1077
1082
 
1078
1083
  # If not a message, it is a dict and we will process it
1079
1084
  results_fields_set = set(result.keys())
1085
+ # Guard against first events (relevant later)
1080
1086
 
1081
- # Identify any observable fields that are not available in the results
1082
- missing_fields = observable_fields_set - results_fields_set
1083
- if len(missing_fields) > 0:
1087
+ # Identify any risk object fields that are not available in the results
1088
+ missing_risk_objects = risk_object_fields_set - results_fields_set
1089
+ if len(missing_risk_objects) > 0:
1084
1090
  # Report a failure in such cases
1085
- e = Exception(f"The observable field(s) {missing_fields} are missing in the detection results")
1091
+ e = Exception(f"The observable field(s) {missing_risk_objects} are missing in the detection results")
1086
1092
  test.result.set_job_content(
1087
1093
  job.content,
1088
1094
  self.infrastructure,
1089
- TestResultStatus.ERROR,
1095
+ TestResultStatus.FAIL,
1090
1096
  exception=e,
1091
1097
  duration=time.time() - search_start_time,
1092
1098
  )
1093
1099
 
1094
- return
1100
+ return
1095
1101
 
1096
- # If we find one or more fields that contain the string "null" then they were
1102
+ # If we find one or more risk object fields that contain the string "null" then they were
1097
1103
  # not populated and we should throw an error. This can happen if there is a typo
1098
1104
  # on a field. In this case, the field will appear but will not contain any values
1099
- current_empty_fields = set()
1105
+ current_empty_fields: set[str] = set()
1106
+
1100
1107
  for field in observable_fields_set:
1101
1108
  if result.get(field, 'null') == 'null':
1102
- current_empty_fields.add(field)
1103
-
1109
+ if field in risk_object_fields_set:
1110
+ e = Exception(f"The risk object field {field} is missing in at least one result.")
1111
+ test.result.set_job_content(
1112
+ job.content,
1113
+ self.infrastructure,
1114
+ TestResultStatus.FAIL,
1115
+ exception=e,
1116
+ duration=time.time() - search_start_time,
1117
+ )
1118
+ return
1119
+ else:
1120
+ if field in threat_object_fields_set:
1121
+ current_empty_fields.add(field)
1122
+ else:
1123
+ if field in threat_object_fields_set:
1124
+ present_threat_objects.add(field)
1125
+ continue
1126
+
1127
+
1128
+
1104
1129
  # If everything succeeded up until now, and no empty fields are found in the
1105
1130
  # current result, then the search was a success
1106
1131
  if len(current_empty_fields) == 0:
@@ -1114,21 +1139,32 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
1114
1139
 
1115
1140
  else:
1116
1141
  empty_fields = empty_fields.union(current_empty_fields)
1117
-
1118
- # Report a failure if there were empty fields in all results
1119
- e = Exception(
1120
- f"One or more required observable fields {empty_fields} contained 'null' values. Is the "
1121
- "data being parsed correctly or is there an error in the naming of a field?"
1142
+
1143
+
1144
+ missing_threat_objects = threat_object_fields_set - present_threat_objects
1145
+ # Report a failure if there were empty fields in a threat object in all results
1146
+ if len(missing_threat_objects) > 0:
1147
+ e = Exception(
1148
+ f"One or more required threat object fields {missing_threat_objects} contained 'null' values in all events. "
1149
+ "Is the data being parsed correctly or is there an error in the naming of a field?"
1122
1150
  )
1123
- test.result.set_job_content(
1124
- job.content,
1125
- self.infrastructure,
1126
- TestResultStatus.ERROR,
1127
- exception=e,
1128
- duration=time.time() - search_start_time,
1129
- )
1151
+ test.result.set_job_content(
1152
+ job.content,
1153
+ self.infrastructure,
1154
+ TestResultStatus.FAIL,
1155
+ exception=e,
1156
+ duration=time.time() - search_start_time,
1157
+ )
1158
+ return
1159
+
1130
1160
 
1131
- return
1161
+ test.result.set_job_content(
1162
+ job.content,
1163
+ self.infrastructure,
1164
+ TestResultStatus.PASS,
1165
+ duration=time.time() - search_start_time,
1166
+ )
1167
+ return
1132
1168
 
1133
1169
  else:
1134
1170
  # Report a failure if there were no results at all
@@ -7,7 +7,7 @@ from attackcti import attack_client
7
7
  import logging
8
8
  from pydantic import BaseModel, Field
9
9
  from dataclasses import field
10
- from typing import Annotated
10
+ from typing import Annotated,Any
11
11
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
12
12
  from contentctl.objects.config import validate
13
13
  logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
@@ -33,21 +33,33 @@ class AttackEnrichment(BaseModel):
33
33
  else:
34
34
  raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
35
35
 
36
-
37
- def addMitreID(self, technique:dict, tactics:list[str], groups:list[str])->None:
38
-
36
+ def addMitreIDViaGroupNames(self, technique:dict, tactics:list[str], groupNames:list[str])->None:
39
37
  technique_id = technique['technique_id']
40
38
  technique_obj = technique['technique']
41
39
  tactics.sort()
42
- groups.sort()
43
-
40
+
44
41
  if technique_id in self.data:
45
42
  raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
43
+ self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
44
+ mitre_attack_technique=technique_obj,
45
+ mitre_attack_tactics=tactics,
46
+ mitre_attack_groups=groupNames,
47
+ mitre_attack_group_objects=[])
48
+
49
+ def addMitreIDViaGroupObjects(self, technique:dict, tactics:list[str], groupObjects:list[dict[str,Any]])->None:
50
+ technique_id = technique['technique_id']
51
+ technique_obj = technique['technique']
52
+ tactics.sort()
46
53
 
54
+ groupNames:list[str] = sorted([group['group'] for group in groupObjects])
55
+
56
+ if technique_id in self.data:
57
+ raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
47
58
  self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
48
59
  mitre_attack_technique=technique_obj,
49
60
  mitre_attack_tactics=tactics,
50
- mitre_attack_groups=groups)
61
+ mitre_attack_groups=groupNames,
62
+ mitre_attack_group_objects=groupObjects)
51
63
 
52
64
 
53
65
  def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
@@ -86,19 +98,20 @@ class AttackEnrichment(BaseModel):
86
98
  progress_percent = ((index+1)/len(all_enterprise_techniques)) * 100
87
99
  if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
88
100
  print(f"\r\t{'MITRE Technique Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
89
- apt_groups = []
101
+ apt_groups:list[dict[str,Any]] = []
90
102
  for relationship in enterprise_relationships:
91
103
  if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
92
104
  for group in enterprise_groups:
93
105
  if relationship['source_object'] == group['id']:
94
- apt_groups.append(group['group'])
106
+ apt_groups.append(group)
107
+ #apt_groups.append(group['group'])
95
108
 
96
109
  tactics = []
97
110
  if ('tactic' in technique):
98
111
  for tactic in technique['tactic']:
99
112
  tactics.append(tactic.replace('-',' ').title())
100
113
 
101
- self.addMitreID(technique, tactics, apt_groups)
114
+ self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
102
115
  attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
103
116
 
104
117
  if store_csv:
@@ -131,7 +144,7 @@ class AttackEnrichment(BaseModel):
131
144
  technique_input = {'technique_id': key , 'technique': attack_lookup[key]['technique'] }
132
145
  tactics_input = attack_lookup[key]['tactics']
133
146
  groups_input = attack_lookup[key]['groups']
134
- self.addMitreID(technique=technique_input, tactics=tactics_input, groups=groups_input)
147
+ self.addMitreIDViaGroupNames(technique=technique_input, tactics=tactics_input, groups=groups_input)
135
148
 
136
149
 
137
150
 
@@ -290,6 +290,11 @@ class Detection_Abstract(SecurityContentObject):
290
290
  risk_object['threat_object_field'] = entity.name
291
291
  risk_object['threat_object_type'] = "url"
292
292
  risk_objects.append(risk_object)
293
+
294
+ elif 'Attacker' in entity.role:
295
+ risk_object['threat_object_field'] = entity.name
296
+ risk_object['threat_object_type'] = entity.type.lower()
297
+ risk_objects.append(risk_object)
293
298
 
294
299
  else:
295
300
  risk_object['risk_object_type'] = 'other'
@@ -132,3 +132,8 @@ SES_ATTACK_TACTICS_ID_MAPPING = {
132
132
  "Exfiltration": "TA0010",
133
133
  "Impact": "TA0040"
134
134
  }
135
+
136
+ RBA_OBSERVABLE_ROLE_MAPPING = {
137
+ "Attacker": 0,
138
+ "Victim": 1
139
+ }
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
3
+ from typing import List, Annotated
4
+ from enum import StrEnum
5
+ import datetime
6
+
7
+ class MitreTactics(StrEnum):
8
+ RECONNAISSANCE = "Reconnaissance"
9
+ RESOURCE_DEVELOPMENT = "Resource Development"
10
+ INITIAL_ACCESS = "Initial Access"
11
+ EXECUTION = "Execution"
12
+ PERSISTENCE = "Persistence"
13
+ PRIVILEGE_ESCALATION = "Privilege Escalation"
14
+ DEFENSE_EVASION = "Defense Evasion"
15
+ CREDENTIAL_ACCESS = "Credential Access"
16
+ DISCOVERY = "Discovery"
17
+ LATERAL_MOVEMENT = "Lateral Movement"
18
+ COLLECTION = "Collection"
19
+ COMMAND_AND_CONTROL = "Command And Control"
20
+ EXFILTRATION = "Exfiltration"
21
+ IMPACT = "Impact"
22
+
23
+
24
+ class AttackGroupMatrix(StrEnum):
25
+ enterprise_attack = "enterprise-attack"
26
+ ics_attack = "ics-attack"
27
+ mobile_attack = "mobile-attack"
28
+
29
+
30
+ class AttackGroupType(StrEnum):
31
+ intrusion_set = "intrusion-set"
32
+
33
+ class MitreExternalReference(BaseModel):
34
+ model_config = ConfigDict(extra='forbid')
35
+ source_name: str
36
+ external_id: None | str = None
37
+ url: None | HttpUrl = None
38
+ description: None | str = None
39
+
40
+
41
+ class MitreAttackGroup(BaseModel):
42
+ model_config = ConfigDict(extra='forbid')
43
+ contributors: list[str] = []
44
+ created: datetime.datetime
45
+ created_by_ref: str
46
+ external_references: list[MitreExternalReference]
47
+ group: str
48
+ group_aliases: list[str]
49
+ group_description: str
50
+ group_id: str
51
+ id: str
52
+ matrix: list[AttackGroupMatrix]
53
+ mitre_attack_spec_version: None | str
54
+ mitre_version: str
55
+ #assume that if the deprecated field is not present, then the group is not deprecated
56
+ mitre_deprecated: bool
57
+ modified: datetime.datetime
58
+ modified_by_ref: str
59
+ object_marking_refs: list[str]
60
+ type: AttackGroupType
61
+ url: HttpUrl
62
+
63
+
64
+ @field_validator("mitre_deprecated", mode="before")
65
+ def standardize_mitre_deprecated(cls, mitre_deprecated:bool | None) -> bool:
66
+ '''
67
+ For some reason, the API will return either a bool for mitre_deprecated OR
68
+ None. We simplify our typing by converting None to False, and assuming that
69
+ if deprecated is None, then the group is not deprecated.
70
+ '''
71
+ if mitre_deprecated is None:
72
+ return False
73
+ return mitre_deprecated
74
+
75
+ @field_validator("contributors", mode="before")
76
+ def standardize_contributors(cls, contributors:list[str] | None) -> list[str]:
77
+ '''
78
+ For some reason, the API will return either a list of strings for contributors OR
79
+ None. We simplify our typing by converting None to an empty list.
80
+ '''
81
+ if contributors is None:
82
+ return []
83
+ return contributors
84
+
85
+ class MitreAttackEnrichment(BaseModel):
86
+ ConfigDict(use_enum_values=True)
87
+ mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
88
+ mitre_attack_technique: str = Field(...)
89
+ mitre_attack_tactics: List[MitreTactics] = Field(...)
90
+ mitre_attack_groups: List[str] = Field(...)
91
+ #Exclude this field from serialization - it is very large and not useful in JSON objects
92
+ mitre_attack_group_objects: list[MitreAttackGroup] = Field(..., exclude=True)
93
+ def __hash__(self) -> int:
94
+ return id(self)
95
+
@@ -1,5 +1,5 @@
1
1
  from pydantic import BaseModel, field_validator
2
- from contentctl.objects.constants import SES_OBSERVABLE_TYPE_MAPPING, SES_OBSERVABLE_ROLE_MAPPING
2
+ from contentctl.objects.constants import SES_OBSERVABLE_TYPE_MAPPING, RBA_OBSERVABLE_ROLE_MAPPING
3
3
 
4
4
 
5
5
  class Observable(BaseModel):
@@ -26,10 +26,12 @@ class Observable(BaseModel):
26
26
  def check_roles(cls, v: list[str]):
27
27
  if len(v) == 0:
28
28
  raise ValueError("Error, at least 1 role must be listed for Observable.")
29
+ if len(v) > 1:
30
+ raise ValueError("Error, each Observable can only have one role.")
29
31
  for role in v:
30
- if role not in SES_OBSERVABLE_ROLE_MAPPING.keys():
32
+ if role not in RBA_OBSERVABLE_ROLE_MAPPING.keys():
31
33
  raise ValueError(
32
34
  f"Invalid role '{role}' provided for observable. Valid observable types are "
33
- f"{SES_OBSERVABLE_ROLE_MAPPING.keys()}"
35
+ f"{RBA_OBSERVABLE_ROLE_MAPPING.keys()}"
34
36
  )
35
37
  return v
@@ -53,11 +53,11 @@ tags:
53
53
  - name: parent_process_name
54
54
  type: Process
55
55
  role:
56
- - Parent Process
56
+ - Attacker
57
57
  - name: process_name
58
58
  type: Process
59
59
  role:
60
- - Child Process
60
+ - Attacker
61
61
  product:
62
62
  - Splunk Enterprise
63
63
  - Splunk Enterprise Security
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
- version = "4.3.0"
3
+ version = "4.3.2"
4
4
  description = "Splunk Content Control Tool"
5
5
  authors = ["STRT <research@splunk.com>"]
6
6
  license = "Apache 2.0"
@@ -11,23 +11,23 @@ contentctl = 'contentctl.contentctl:main'
11
11
 
12
12
  [tool.poetry.dependencies]
13
13
  python = "^3.11"
14
- pydantic = "^2.7.1"
15
- PyYAML = "^6.0.1"
16
- requests = "~2.32.2"
14
+ pydantic = "^2.8.2"
15
+ PyYAML = "^6.0.2"
16
+ requests = "~2.32.3"
17
17
  pycvesearch = "^1.2"
18
18
  xmltodict = "^0.13.0"
19
- attackcti = ">=0.3.7,<0.5.0"
19
+ attackcti = "^0.4.0"
20
20
  Jinja2 = "^3.1.4"
21
21
  questionary = "^2.0.1"
22
22
  docker = "^7.1.0"
23
- splunk-sdk = "^2.0.1"
23
+ splunk-sdk = "^2.0.2"
24
24
  semantic-version = "^2.10.0"
25
25
  bottle = "^0.12.25"
26
- tqdm = "^4.66.4"
27
- pygit2 = "^1.14.1"
26
+ tqdm = "^4.66.5"
27
+ pygit2 = "^1.15.1"
28
28
  tyro = "^0.8.3"
29
29
  gitpython = "^3.1.43"
30
- setuptools = ">=69.5.1,<73.0.0"
30
+ setuptools = ">=69.5.1,<74.0.0"
31
31
  [tool.poetry.dev-dependencies]
32
32
 
33
33
  [build-system]
@@ -1,32 +0,0 @@
1
- from __future__ import annotations
2
- from pydantic import BaseModel, Field, ConfigDict
3
- from typing import List, Annotated
4
- from enum import StrEnum
5
-
6
-
7
- class MitreTactics(StrEnum):
8
- RECONNAISSANCE = "Reconnaissance"
9
- RESOURCE_DEVELOPMENT = "Resource Development"
10
- INITIAL_ACCESS = "Initial Access"
11
- EXECUTION = "Execution"
12
- PERSISTENCE = "Persistence"
13
- PRIVILEGE_ESCALATION = "Privilege Escalation"
14
- DEFENSE_EVASION = "Defense Evasion"
15
- CREDENTIAL_ACCESS = "Credential Access"
16
- DISCOVERY = "Discovery"
17
- LATERAL_MOVEMENT = "Lateral Movement"
18
- COLLECTION = "Collection"
19
- COMMAND_AND_CONTROL = "Command And Control"
20
- EXFILTRATION = "Exfiltration"
21
- IMPACT = "Impact"
22
-
23
-
24
- class MitreAttackEnrichment(BaseModel):
25
- ConfigDict(use_enum_values=True)
26
- mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
27
- mitre_attack_technique: str = Field(...)
28
- mitre_attack_tactics: List[MitreTactics] = Field(...)
29
- mitre_attack_groups: List[str] = Field(...)
30
-
31
- def __hash__(self) -> int:
32
- return id(self)
File without changes
File without changes
File without changes