contentctl 5.1.0__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 (168) hide show
  1. {contentctl-5.1.0 → contentctl-5.2.0}/PKG-INFO +1 -1
  2. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +1 -1
  3. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +28 -1
  4. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/data_source.py +2 -0
  5. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/lookup.py +16 -3
  6. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/attack_nav_output.py +11 -4
  7. contentctl-5.2.0/contentctl/output/attack_nav_writer.py +83 -0
  8. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/transforms.j2 +2 -2
  9. {contentctl-5.1.0 → contentctl-5.2.0}/pyproject.toml +2 -2
  10. contentctl-5.1.0/contentctl/output/attack_nav_writer.py +0 -67
  11. {contentctl-5.1.0 → contentctl-5.2.0}/LICENSE.md +0 -0
  12. {contentctl-5.1.0 → contentctl-5.2.0}/README.md +0 -0
  13. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/__init__.py +0 -0
  14. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/build.py +0 -0
  15. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/deploy_acs.py +0 -0
  16. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  17. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
  18. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  19. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  20. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  21. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  22. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  23. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  24. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  25. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  26. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/doc_gen.py +0 -0
  27. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/initialize.py +0 -0
  28. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/inspect.py +0 -0
  29. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/new_content.py +0 -0
  30. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/release_notes.py +0 -0
  31. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/reporting.py +0 -0
  32. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/test.py +0 -0
  33. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/validate.py +0 -0
  34. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/api.py +0 -0
  35. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/contentctl.py +0 -0
  36. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
  37. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
  38. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  39. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/link_validator.py +0 -0
  40. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/logger.py +0 -0
  41. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/splunk_app.py +0 -0
  42. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/utils.py +0 -0
  43. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/director.py +0 -0
  44. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/new_content_questions.py +0 -0
  45. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/yml_reader.py +0 -0
  46. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  47. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/alert_action.py +0 -0
  48. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/annotated_types.py +0 -0
  49. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/atomic.py +0 -0
  50. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/base_test.py +0 -0
  51. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/base_test_result.py +0 -0
  52. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/baseline.py +0 -0
  53. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/baseline_tags.py +0 -0
  54. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/config.py +0 -0
  55. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/constants.py +0 -0
  56. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/correlation_search.py +0 -0
  57. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/dashboard.py +0 -0
  58. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment.py +0 -0
  59. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_email.py +0 -0
  60. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_notable.py +0 -0
  61. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_phantom.py +0 -0
  62. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_rba.py +0 -0
  63. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
  64. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_slack.py +0 -0
  65. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection.py +0 -0
  66. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_metadata.py +0 -0
  67. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_stanza.py +0 -0
  68. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_tags.py +0 -0
  69. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/drilldown.py +0 -0
  70. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/enums.py +0 -0
  71. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/errors.py +0 -0
  72. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/integration_test.py +0 -0
  73. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/integration_test_result.py +0 -0
  74. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/investigation.py +0 -0
  75. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/investigation_tags.py +0 -0
  76. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/macro.py +0 -0
  77. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/manual_test.py +0 -0
  78. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/manual_test_result.py +0 -0
  79. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
  80. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/notable_action.py +0 -0
  81. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/notable_event.py +0 -0
  82. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/playbook.py +0 -0
  83. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/playbook_tags.py +0 -0
  84. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/rba.py +0 -0
  85. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
  86. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_event.py +0 -0
  87. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_object.py +0 -0
  88. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/savedsearches_conf.py +0 -0
  89. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/security_content_object.py +0 -0
  90. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/story.py +0 -0
  91. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/story_tags.py +0 -0
  92. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/test_attack_data.py +0 -0
  93. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/test_group.py +0 -0
  94. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/threat_object.py +0 -0
  95. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/throttling.py +0 -0
  96. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test.py +0 -0
  97. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
  98. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test_result.py +0 -0
  99. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/api_json_output.py +0 -0
  100. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/conf_output.py +0 -0
  101. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/conf_writer.py +0 -0
  102. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/data_source_writer.py +0 -0
  103. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/doc_md_output.py +0 -0
  104. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/jinja_writer.py +0 -0
  105. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/json_writer.py +0 -0
  106. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/svg_output.py +0 -0
  107. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  108. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  109. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  110. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
  111. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
  112. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/collections.j2 +0 -0
  113. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/content-version.j2 +0 -0
  114. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
  115. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
  116. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  117. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
  118. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
  119. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  120. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  121. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  122. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
  123. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
  124. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  125. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  126. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/header.j2 +0 -0
  127. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/macros.j2 +0 -0
  128. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/panel.j2 +0 -0
  129. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  130. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  131. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  132. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/server.conf.j2 +0 -0
  133. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
  134. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/yml_writer.py +0 -0
  135. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/README.md +0 -0
  136. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_default.yml +0 -0
  137. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  138. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  139. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  140. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README.md +0 -0
  141. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  142. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
  143. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  144. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  145. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  146. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  147. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  148. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
  149. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
  150. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  151. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  152. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  153. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  154. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
  155. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
  156. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  157. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  158. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  159. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  160. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  161. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
  162. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  163. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
  164. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
  165. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
  166. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  167. {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  168. {contentctl-5.1.0 → contentctl-5.2.0}/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.1.0
3
+ Version: 5.2.0
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -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
 
@@ -474,7 +474,7 @@ class Detection_Abstract(SecurityContentObject):
474
474
  "name": lookup.name,
475
475
  "description": lookup.description,
476
476
  "filename": lookup.filename.name,
477
- "default_match": "true" if lookup.default_match else "false",
477
+ "default_match": lookup.default_match,
478
478
  "case_sensitive_match": "true"
479
479
  if lookup.case_sensitive_match
480
480
  else "false",
@@ -1055,3 +1055,30 @@ class Detection_Abstract(SecurityContentObject):
1055
1055
  # Return the summary
1056
1056
 
1057
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,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
3
 
4
- version = "5.1.0"
4
+ version = "5.2.0"
5
5
 
6
6
  description = "Splunk Content Control Tool"
7
7
  authors = ["STRT <research@splunk.com>"]
@@ -33,7 +33,7 @@ gitpython = "^3.1.43"
33
33
  setuptools = ">=69.5.1,<76.0.0"
34
34
 
35
35
  [tool.poetry.group.dev.dependencies]
36
- ruff = "^0.9.2"
36
+ ruff = "^0.9.10"
37
37
 
38
38
  [build-system]
39
39
  requires = ["poetry-core>=1.0.0"]
@@ -1,67 +0,0 @@
1
- import json
2
- from typing import Union, List
3
- import pathlib
4
-
5
- VERSION = "4.3"
6
- NAME = "Detection Coverage"
7
- DESCRIPTION = "security_content detection coverage"
8
- DOMAIN = "mitre-enterprise"
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 = 0
18
- for technique_id in mitre_techniques.keys():
19
- if mitre_techniques[technique_id]["score"] > max_count:
20
- max_count = mitre_techniques[technique_id]["score"]
21
-
22
- layer_json = {
23
- "version": VERSION,
24
- "name": NAME,
25
- "description": DESCRIPTION,
26
- "domain": DOMAIN,
27
- "techniques": [],
28
- }
29
-
30
- layer_json["gradient"] = {
31
- "colors": ["#ffffff", "#66b1ff", "#096ed7"],
32
- "minValue": 0,
33
- "maxValue": max_count,
34
- }
35
-
36
- layer_json["filters"] = {
37
- "platforms": [
38
- "Windows",
39
- "Linux",
40
- "macOS",
41
- "AWS",
42
- "GCP",
43
- "Azure",
44
- "Office 365",
45
- "SaaS",
46
- ]
47
- }
48
-
49
- layer_json["legendItems"] = [
50
- {"label": "NO available detections", "color": "#ffffff"},
51
- {"label": "Some detections available", "color": "#66b1ff"},
52
- ]
53
-
54
- layer_json["showTacticRowBackground"] = True
55
- layer_json["tacticRowBackground"] = "#dddddd"
56
- layer_json["sorting"] = 3
57
-
58
- for technique_id in mitre_techniques.keys():
59
- layer_technique = {
60
- "techniqueID": technique_id,
61
- "score": mitre_techniques[technique_id]["score"],
62
- "comment": "\n\n".join(mitre_techniques[technique_id]["file_paths"]),
63
- }
64
- layer_json["techniques"].append(layer_technique)
65
-
66
- with open(output_path, "w") as outfile:
67
- json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
File without changes
File without changes
File without changes