contentctl 5.5.3__tar.gz → 5.5.4__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 (171) hide show
  1. {contentctl-5.5.3 → contentctl-5.5.4}/PKG-INFO +1 -1
  2. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/enrichments/attack_enrichment.py +57 -29
  3. contentctl-5.5.4/contentctl/output/attack_nav_output.py +197 -0
  4. {contentctl-5.5.3 → contentctl-5.5.4}/pyproject.toml +1 -1
  5. contentctl-5.5.3/contentctl/output/attack_nav_output.py +0 -53
  6. {contentctl-5.5.3 → contentctl-5.5.4}/LICENSE.md +0 -0
  7. {contentctl-5.5.3 → contentctl-5.5.4}/README.md +0 -0
  8. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/__init__.py +0 -0
  9. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/build.py +0 -0
  10. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/deploy_acs.py +0 -0
  11. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  12. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/GitService.py +0 -0
  13. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  14. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
  15. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  16. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  17. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  18. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  19. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  20. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  21. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  22. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/doc_gen.py +0 -0
  23. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/initialize.py +0 -0
  24. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/inspect.py +0 -0
  25. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/new_content.py +0 -0
  26. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/release_notes.py +0 -0
  27. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/reporting.py +0 -0
  28. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/test.py +0 -0
  29. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/actions/validate.py +0 -0
  30. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/api.py +0 -0
  31. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/contentctl.py +0 -0
  32. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/enrichments/cve_enrichment.py +0 -0
  33. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  34. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/helper/link_validator.py +0 -0
  35. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/helper/logger.py +0 -0
  36. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/helper/splunk_app.py +0 -0
  37. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/helper/utils.py +0 -0
  38. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/input/director.py +0 -0
  39. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/input/new_content_questions.py +0 -0
  40. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/input/yml_reader.py +0 -0
  41. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +0 -0
  42. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  43. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/alert_action.py +0 -0
  44. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/annotated_types.py +0 -0
  45. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/atomic.py +0 -0
  46. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/base_security_event.py +0 -0
  47. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/base_test.py +0 -0
  48. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/base_test_result.py +0 -0
  49. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/baseline.py +0 -0
  50. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/baseline_tags.py +0 -0
  51. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/config.py +0 -0
  52. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/constants.py +0 -0
  53. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/content_versioning_service.py +0 -0
  54. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/correlation_search.py +0 -0
  55. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/dashboard.py +0 -0
  56. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/data_source.py +0 -0
  57. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment.py +0 -0
  58. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_email.py +0 -0
  59. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_notable.py +0 -0
  60. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_phantom.py +0 -0
  61. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_rba.py +0 -0
  62. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_scheduling.py +0 -0
  63. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/deployment_slack.py +0 -0
  64. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/detection.py +0 -0
  65. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/detection_metadata.py +0 -0
  66. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/detection_stanza.py +0 -0
  67. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/detection_tags.py +0 -0
  68. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/drilldown.py +0 -0
  69. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/enums.py +0 -0
  70. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/errors.py +0 -0
  71. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/integration_test.py +0 -0
  72. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/integration_test_result.py +0 -0
  73. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/investigation.py +0 -0
  74. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/investigation_tags.py +0 -0
  75. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/lookup.py +0 -0
  76. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/macro.py +0 -0
  77. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/manual_test.py +0 -0
  78. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/manual_test_result.py +0 -0
  79. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  80. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/notable_action.py +0 -0
  81. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/notable_event.py +0 -0
  82. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/playbook.py +0 -0
  83. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/playbook_tags.py +0 -0
  84. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/rba.py +0 -0
  85. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/removed_security_content_object.py +0 -0
  86. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/risk_analysis_action.py +0 -0
  87. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/risk_event.py +0 -0
  88. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/risk_object.py +0 -0
  89. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/savedsearches_conf.py +0 -0
  90. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/security_content_object.py +0 -0
  91. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/story.py +0 -0
  92. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/story_tags.py +0 -0
  93. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/test_attack_data.py +0 -0
  94. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/test_group.py +0 -0
  95. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/threat_object.py +0 -0
  96. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/throttling.py +0 -0
  97. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/unit_test.py +0 -0
  98. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/unit_test_baseline.py +0 -0
  99. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/objects/unit_test_result.py +0 -0
  100. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/api_json_output.py +0 -0
  101. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/attack_nav_writer.py +0 -0
  102. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/conf_output.py +0 -0
  103. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/conf_writer.py +0 -0
  104. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/doc_md_output.py +0 -0
  105. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/jinja_writer.py +0 -0
  106. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/json_writer.py +0 -0
  107. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/runtime_csv_writer.py +0 -0
  108. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/svg_output.py +0 -0
  109. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  110. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  111. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  112. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/app.conf.j2 +0 -0
  113. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/app.manifest.j2 +0 -0
  114. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/collections.j2 +0 -0
  115. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/content-version.j2 +0 -0
  116. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/detection_count.j2 +0 -0
  117. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/detection_coverage.j2 +0 -0
  118. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  119. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_detections.j2 +0 -0
  120. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_navigation.j2 +0 -0
  121. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  122. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  123. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  124. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_stories.j2 +0 -0
  125. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/doc_story_page.j2 +0 -0
  126. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  127. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  128. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/header.j2 +0 -0
  129. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/macros.j2 +0 -0
  130. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/panel.j2 +0 -0
  131. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  132. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  133. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  134. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/server.conf.j2 +0 -0
  135. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/transforms.j2 +0 -0
  136. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/templates/workflow_actions.j2 +0 -0
  137. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/output/yml_writer.py +0 -0
  138. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/README.md +0 -0
  139. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_default.yml +0 -0
  140. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  141. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  142. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  143. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/README.md +0 -0
  144. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  145. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/commands.conf +0 -0
  146. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  147. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  148. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  149. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  150. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  151. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/metadata/default.meta +0 -0
  152. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/static/appIcon.png +0 -0
  153. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  154. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  155. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  156. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  157. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/datamodels_cim.conf +0 -0
  158. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/datamodels_custom.conf +0 -0
  159. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  160. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  161. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  162. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  163. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  164. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/detections/application/.gitkeep +0 -0
  165. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  166. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
  167. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/detections/network/.gitkeep +0 -0
  168. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/detections/web/.gitkeep +0 -0
  169. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  170. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  171. {contentctl-5.5.3 → contentctl-5.5.4}/contentctl/templates/stories/cobalt_strike.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: contentctl
3
- Version: 5.5.3
3
+ Version: 5.5.4
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -1,18 +1,40 @@
1
1
  from __future__ import annotations
2
- from attackcti import attack_client
2
+
3
3
  import logging
4
- from pydantic import BaseModel
5
4
  from dataclasses import field
6
- from typing import Any
7
5
  from pathlib import Path
6
+ from typing import Any, TypedDict, cast
7
+
8
+ from attackcti import attack_client # type: ignore[reportMissingTypeStubs]
9
+ from pydantic import BaseModel
10
+
11
+ from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
12
+ from contentctl.objects.config import validate
8
13
  from contentctl.objects.mitre_attack_enrichment import (
9
14
  MitreAttackEnrichment,
10
15
  MitreTactics,
11
16
  )
12
- from contentctl.objects.config import validate
13
- from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
14
17
 
18
+ # Suppress attackcti logging
15
19
  logging.getLogger("taxii2client").setLevel(logging.CRITICAL)
20
+ logging.getLogger("stix2").setLevel(logging.CRITICAL)
21
+
22
+
23
+ class AttackPattern(TypedDict):
24
+ id: str
25
+ technique_id: str
26
+ technique: str
27
+ tactic: list[str]
28
+
29
+
30
+ class IntrusionSet(TypedDict):
31
+ id: str
32
+ group: str
33
+
34
+
35
+ class Relationship(TypedDict):
36
+ target_object: str
37
+ source_object: str
16
38
 
17
39
 
18
40
  class AttackEnrichment(BaseModel):
@@ -98,11 +120,6 @@ class AttackEnrichment(BaseModel):
98
120
  end="",
99
121
  flush=True,
100
122
  )
101
- # The existence of the input_path is validated during cli argument validation, but it is
102
- # possible that the repo is in the wrong format. If the following directories do not
103
- # exist, then attack_client will fall back to resolving via REST API. We do not
104
- # want this as it is slow and error prone, so we will force an exception to
105
- # be generated.
106
123
  enterprise_path = input_path / "enterprise-attack"
107
124
  mobile_path = input_path / "ics-attack"
108
125
  ics_path = input_path / "mobile-attack"
@@ -123,36 +140,47 @@ class AttackEnrichment(BaseModel):
123
140
  }
124
141
  )
125
142
 
126
- all_enterprise_techniques = lift.get_enterprise_techniques(
127
- stix_format=False
143
+ all_enterprise_techniques = cast(
144
+ list[AttackPattern], lift.get_enterprise_techniques(stix_format=False)
128
145
  )
129
- enterprise_relationships = lift.get_enterprise_relationships(
130
- stix_format=False
146
+ enterprise_relationships = cast(
147
+ list[Relationship], lift.get_enterprise_relationships(stix_format=False)
148
+ )
149
+ enterprise_groups = cast(
150
+ list[IntrusionSet], lift.get_enterprise_groups(stix_format=False)
131
151
  )
132
- enterprise_groups = lift.get_enterprise_groups(stix_format=False)
133
152
 
134
153
  for technique in all_enterprise_techniques:
135
154
  apt_groups: list[dict[str, Any]] = []
136
155
  for relationship in enterprise_relationships:
137
- if (
138
- relationship["target_object"] == technique["id"]
139
- ) and relationship["source_object"].startswith("intrusion-set"):
156
+ if relationship["target_object"] == technique[
157
+ "id"
158
+ ] and relationship["source_object"].startswith("intrusion-set"):
140
159
  for group in enterprise_groups:
141
160
  if relationship["source_object"] == group["id"]:
142
- apt_groups.append(group)
143
- # apt_groups.append(group['group'])
161
+ apt_groups.append(dict(group))
144
162
 
145
- tactics = []
163
+ tactics: list[MitreTactics] = []
146
164
  if "tactic" in technique:
147
165
  for tactic in technique["tactic"]:
148
- tactics.append(tactic.replace("-", " ").title())
149
-
150
- self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
151
- attack_lookup[technique["technique_id"]] = {
152
- "technique": technique["technique"],
153
- "tactics": tactics,
154
- "groups": apt_groups,
155
- }
166
+ tactics.append(
167
+ cast(MitreTactics, tactic.replace("-", " ").title())
168
+ )
169
+
170
+ self.addMitreIDViaGroupObjects(dict(technique), tactics, apt_groups)
171
+ attack_lookup[technique["technique_id"]] = (
172
+ MitreAttackEnrichment.model_validate(
173
+ {
174
+ "mitre_attack_id": technique["technique_id"],
175
+ "mitre_attack_technique": technique["technique"],
176
+ "mitre_attack_tactics": tactics,
177
+ "mitre_attack_groups": [
178
+ group["group"] for group in apt_groups
179
+ ],
180
+ "mitre_attack_group_objects": apt_groups,
181
+ }
182
+ )
183
+ )
156
184
 
157
185
  except Exception as err:
158
186
  raise Exception(f"Error getting MITRE Enrichment: {str(err)}")
@@ -0,0 +1,197 @@
1
+ # Standard library imports
2
+ import json
3
+ import pathlib
4
+ from datetime import datetime
5
+ from typing import Any, TypedDict
6
+
7
+ # Third-party imports
8
+ from contentctl.objects.detection import Detection
9
+
10
+
11
+ class TechniqueData(TypedDict):
12
+ score: int
13
+ file_paths: list[str]
14
+ links: list[dict[str, str]]
15
+
16
+
17
+ class LayerData(TypedDict):
18
+ name: str
19
+ versions: dict[str, str]
20
+ domain: str
21
+ description: str
22
+ filters: dict[str, list[str]]
23
+ sorting: int
24
+ layout: dict[str, str | bool]
25
+ hideDisabled: bool
26
+ techniques: list[dict[str, Any]]
27
+ gradient: dict[str, list[str] | int]
28
+ legendItems: list[dict[str, str]]
29
+ showTacticRowBackground: bool
30
+ tacticRowBackground: str
31
+ selectTechniquesAcrossTactics: bool
32
+ selectSubtechniquesWithParent: bool
33
+ selectVisibleTechniques: bool
34
+ metadata: list[dict[str, str]]
35
+
36
+
37
+ class AttackNavOutput:
38
+ def __init__(
39
+ self,
40
+ layer_name: str = "Splunk Detection Coverage",
41
+ layer_description: str = "MITRE ATT&CK coverage for Splunk detections",
42
+ layer_domain: str = "enterprise-attack",
43
+ ):
44
+ self.layer_name = layer_name
45
+ self.layer_description = layer_description
46
+ self.layer_domain = layer_domain
47
+
48
+ def writeObjects(
49
+ self, detections: list[Detection], output_path: pathlib.Path
50
+ ) -> None:
51
+ """
52
+ Generate MITRE ATT&CK Navigator layer file from detections
53
+ Args:
54
+ detections: List of Detection objects
55
+ output_path: Path to write the layer file
56
+ """
57
+ techniques: dict[str, TechniqueData] = {}
58
+ tactic_coverage: dict[str, set[str]] = {}
59
+
60
+ # Process each detection
61
+ for detection in detections:
62
+ if not hasattr(detection.tags, "mitre_attack_id"):
63
+ continue
64
+
65
+ for tactic in detection.tags.mitre_attack_id:
66
+ if tactic not in techniques:
67
+ techniques[tactic] = {"score": 0, "file_paths": [], "links": []}
68
+ tactic_coverage[tactic] = set()
69
+
70
+ detection_type = detection.source
71
+ detection_id = str(detection.id) # Convert UUID to string
72
+ detection_url = (
73
+ f"https://research.splunk.com/{detection_type}/{detection_id}/"
74
+ )
75
+ detection_name = detection.name.replace(
76
+ "_", " "
77
+ ).title() # Convert to Title Case
78
+ detection_info = f"{detection_name}"
79
+
80
+ techniques[tactic]["score"] += 1
81
+ techniques[tactic]["file_paths"].append(detection_info)
82
+ techniques[tactic]["links"].append(
83
+ {"label": detection_name, "url": detection_url}
84
+ )
85
+ tactic_coverage[tactic].add(detection_id)
86
+
87
+ # Create the layer file
88
+ layer: LayerData = {
89
+ "name": self.layer_name,
90
+ "versions": {
91
+ "attack": "14", # Update as needed
92
+ "navigator": "5.1.0",
93
+ "layer": "4.5",
94
+ },
95
+ "domain": self.layer_domain,
96
+ "description": self.layer_description,
97
+ "filters": {
98
+ "platforms": [
99
+ "Windows",
100
+ "Linux",
101
+ "macOS",
102
+ "AWS",
103
+ "GCP",
104
+ "Azure",
105
+ "Office 365",
106
+ "SaaS",
107
+ ]
108
+ },
109
+ "sorting": 0,
110
+ "layout": {
111
+ "layout": "flat",
112
+ "showName": True,
113
+ "showID": False,
114
+ "showAggregateScores": True,
115
+ "countUnscored": True,
116
+ "aggregateFunction": "average",
117
+ "expandedSubtechniques": "none",
118
+ },
119
+ "hideDisabled": False,
120
+ "techniques": [
121
+ {
122
+ "techniqueID": tid,
123
+ "score": data["score"],
124
+ "metadata": [
125
+ {"name": "Detection", "value": name, "divider": False}
126
+ for name in data["file_paths"]
127
+ ]
128
+ + [
129
+ {
130
+ "name": "Link",
131
+ "value": f"[View Detection]({link['url']})",
132
+ "divider": False,
133
+ }
134
+ for link in data["links"]
135
+ ],
136
+ "links": [
137
+ {"label": link["label"], "url": link["url"]}
138
+ for link in data["links"]
139
+ ],
140
+ }
141
+ for tid, data in techniques.items()
142
+ ],
143
+ "gradient": {
144
+ "colors": [
145
+ "#1a365d", # Dark blue
146
+ "#2c5282", # Medium blue
147
+ "#4299e1", # Light blue
148
+ "#48bb78", # Light green
149
+ "#38a169", # Medium green
150
+ "#276749", # Dark green
151
+ ],
152
+ "minValue": 0,
153
+ "maxValue": 5, # Adjust based on your max detections per technique
154
+ },
155
+ "legendItems": [
156
+ {"label": "1 Detection", "color": "#1a365d"},
157
+ {"label": "2 Detections", "color": "#4299e1"},
158
+ {"label": "3 Detections", "color": "#48bb78"},
159
+ {"label": "4+ Detections", "color": "#276749"},
160
+ ],
161
+ "showTacticRowBackground": True,
162
+ "tacticRowBackground": "#dddddd",
163
+ "selectTechniquesAcrossTactics": True,
164
+ "selectSubtechniquesWithParent": True,
165
+ "selectVisibleTechniques": False,
166
+ "metadata": [
167
+ {"name": "Generated", "value": datetime.now().isoformat()},
168
+ {"name": "Total Detections", "value": str(len(detections))},
169
+ {"name": "Covered Techniques", "value": str(len(techniques))},
170
+ ],
171
+ }
172
+
173
+ # Write the layer file
174
+ output_file = output_path / "coverage.json"
175
+ with open(output_file, "w") as f:
176
+ json.dump(layer, f, indent=2)
177
+
178
+ print(f"\n✅ MITRE ATT&CK Navigator layer file written to: {output_file}")
179
+ print("📊 Coverage Summary:")
180
+ print(f" Total Detections: {len(detections)}")
181
+ print(f" Covered Techniques: {len(techniques)}")
182
+ print(f" Tactics with Coverage: {len(tactic_coverage)}")
183
+ print("\n🗺️ To view the layer:")
184
+ print(" 1. Go to https://mitre-attack.github.io/attack-navigator/")
185
+ print(" 2. Click 'Open Existing Layer'")
186
+ print(f" 3. Select the file: {output_file}")
187
+
188
+ def convertNameToFileName(self, name: str) -> str:
189
+ """Convert a detection name to a valid filename"""
190
+ file_name = (
191
+ name.replace(" ", "_")
192
+ .replace("-", "_")
193
+ .replace(".", "_")
194
+ .replace("/", "_")
195
+ .lower()
196
+ )
197
+ return f"{file_name}.yml"
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
3
 
4
- version = "5.5.3"
4
+ version = "5.5.4"
5
5
 
6
6
  description = "Splunk Content Control Tool"
7
7
  authors = ["STRT <research@splunk.com>"]
@@ -1,53 +0,0 @@
1
- import pathlib
2
- from typing import List, Union
3
-
4
- from contentctl.objects.detection import Detection
5
- from contentctl.output.attack_nav_writer import AttackNavWriter
6
-
7
-
8
- class AttackNavOutput:
9
- def writeObjects(
10
- self, detections: List[Detection], output_path: pathlib.Path
11
- ) -> None:
12
- techniques: dict[str, dict[str, Union[List[str], int]]] = {}
13
-
14
- for detection in detections:
15
- for tactic in detection.tags.mitre_attack_id:
16
- if tactic not in techniques:
17
- techniques[tactic] = {"score": 0, "file_paths": []}
18
-
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)
28
-
29
- """
30
- for detection in objects:
31
- if detection.tags.mitre_attack_enrichments:
32
- for mitre_attack_enrichment in detection.tags.mitre_attack_enrichments:
33
- if not mitre_attack_enrichment.mitre_attack_id in techniques:
34
- techniques[mitre_attack_enrichment.mitre_attack_id] = {
35
- 'score': 1,
36
- 'file_paths': ['https://github.com/splunk/security_content/blob/develop/detections/' + detection.getSource() + '/' + self.convertNameToFileName(detection.name)]
37
- }
38
- else:
39
- techniques[mitre_attack_enrichment.mitre_attack_id]['score'] = techniques[mitre_attack_enrichment.mitre_attack_id]['score'] + 1
40
- techniques[mitre_attack_enrichment.mitre_attack_id]['file_paths'].append('https://github.com/splunk/security_content/blob/develop/detections/' + detection.getSource() + '/' + self.convertNameToFileName(detection.name))
41
- """
42
- AttackNavWriter.writeAttackNavFile(techniques, output_path / "coverage.json")
43
-
44
- def convertNameToFileName(self, name: str):
45
- file_name = (
46
- name.replace(" ", "_")
47
- .replace("-", "_")
48
- .replace(".", "_")
49
- .replace("/", "_")
50
- .lower()
51
- )
52
- file_name = file_name + ".yml"
53
- return file_name
File without changes
File without changes
File without changes