contentctl 5.0.1__tar.gz → 5.0.3__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.0.1 → contentctl-5.0.3}/PKG-INFO +1 -1
  2. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/inspect.py +69 -29
  3. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/contentctl.py +34 -0
  4. contentctl-5.0.3/contentctl/objects/annotated_types.py +9 -0
  5. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/config.py +3 -1
  6. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/data_source.py +6 -2
  7. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/detection_tags.py +57 -4
  8. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/mitre_attack_enrichment.py +16 -3
  9. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/story_tags.py +6 -5
  10. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -1
  11. {contentctl-5.0.1 → contentctl-5.0.3}/pyproject.toml +1 -1
  12. contentctl-5.0.1/contentctl/objects/annotated_types.py +0 -6
  13. {contentctl-5.0.1 → contentctl-5.0.3}/LICENSE.md +0 -0
  14. {contentctl-5.0.1 → contentctl-5.0.3}/README.md +0 -0
  15. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/__init__.py +0 -0
  16. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/build.py +0 -0
  17. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/deploy_acs.py +0 -0
  18. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
  19. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/GitService.py +0 -0
  20. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
  21. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
  22. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
  23. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
  24. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/progress_bar.py +0 -0
  25. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
  26. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
  27. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
  28. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
  29. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/doc_gen.py +0 -0
  30. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/initialize.py +0 -0
  31. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/new_content.py +0 -0
  32. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/release_notes.py +0 -0
  33. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/reporting.py +0 -0
  34. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/test.py +0 -0
  35. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/actions/validate.py +0 -0
  36. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/api.py +0 -0
  37. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/enrichments/attack_enrichment.py +0 -0
  38. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/enrichments/cve_enrichment.py +0 -0
  39. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
  40. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/helper/link_validator.py +0 -0
  41. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/helper/logger.py +0 -0
  42. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/helper/splunk_app.py +0 -0
  43. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/helper/utils.py +0 -0
  44. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/input/director.py +0 -0
  45. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/input/new_content_questions.py +0 -0
  46. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/input/yml_reader.py +0 -0
  47. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +0 -0
  48. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
  49. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/alert_action.py +0 -0
  50. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/atomic.py +0 -0
  51. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/base_test.py +0 -0
  52. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/base_test_result.py +0 -0
  53. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/baseline.py +0 -0
  54. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/baseline_tags.py +0 -0
  55. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/constants.py +0 -0
  56. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/correlation_search.py +0 -0
  57. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/dashboard.py +0 -0
  58. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment.py +0 -0
  59. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_email.py +0 -0
  60. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_notable.py +0 -0
  61. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_phantom.py +0 -0
  62. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_rba.py +0 -0
  63. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_scheduling.py +0 -0
  64. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/deployment_slack.py +0 -0
  65. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/detection.py +0 -0
  66. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/detection_metadata.py +0 -0
  67. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/detection_stanza.py +0 -0
  68. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/drilldown.py +0 -0
  69. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/enums.py +0 -0
  70. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/errors.py +0 -0
  71. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/integration_test.py +0 -0
  72. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/integration_test_result.py +0 -0
  73. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/investigation.py +0 -0
  74. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/investigation_tags.py +0 -0
  75. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/lookup.py +0 -0
  76. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/macro.py +0 -0
  77. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/manual_test.py +0 -0
  78. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/manual_test_result.py +0 -0
  79. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/notable_action.py +0 -0
  80. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/notable_event.py +0 -0
  81. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/playbook.py +0 -0
  82. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/playbook_tags.py +0 -0
  83. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/rba.py +0 -0
  84. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/risk_analysis_action.py +0 -0
  85. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/risk_event.py +0 -0
  86. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/risk_object.py +0 -0
  87. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/savedsearches_conf.py +0 -0
  88. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/security_content_object.py +0 -0
  89. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/story.py +0 -0
  90. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/test_attack_data.py +0 -0
  91. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/test_group.py +0 -0
  92. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/threat_object.py +0 -0
  93. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/throttling.py +0 -0
  94. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/unit_test.py +0 -0
  95. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/unit_test_baseline.py +0 -0
  96. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/objects/unit_test_result.py +0 -0
  97. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/api_json_output.py +0 -0
  98. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/attack_nav_output.py +0 -0
  99. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/attack_nav_writer.py +0 -0
  100. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/conf_output.py +0 -0
  101. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/conf_writer.py +0 -0
  102. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/data_source_writer.py +0 -0
  103. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/doc_md_output.py +0 -0
  104. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/jinja_writer.py +0 -0
  105. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/json_writer.py +0 -0
  106. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/svg_output.py +0 -0
  107. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
  108. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
  109. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
  110. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/app.conf.j2 +0 -0
  111. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/app.manifest.j2 +0 -0
  112. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/collections.j2 +0 -0
  113. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/content-version.j2 +0 -0
  114. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/detection_count.j2 +0 -0
  115. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/detection_coverage.j2 +0 -0
  116. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_detection_page.j2 +0 -0
  117. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_detections.j2 +0 -0
  118. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_navigation.j2 +0 -0
  119. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
  120. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_playbooks.j2 +0 -0
  121. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
  122. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_stories.j2 +0 -0
  123. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/doc_story_page.j2 +0 -0
  124. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
  125. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
  126. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/header.j2 +0 -0
  127. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/macros.j2 +0 -0
  128. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/panel.j2 +0 -0
  129. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
  130. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
  131. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
  132. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/server.conf.j2 +0 -0
  133. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/transforms.j2 +0 -0
  134. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/templates/workflow_actions.j2 +0 -0
  135. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/output/yml_writer.py +0 -0
  136. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/README.md +0 -0
  137. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_default.yml +0 -0
  138. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
  139. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
  140. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
  141. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/README.md +0 -0
  142. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
  143. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/commands.conf +0 -0
  144. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
  145. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
  146. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
  147. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
  148. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
  149. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/metadata/default.meta +0 -0
  150. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/static/appIcon.png +0 -0
  151. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
  152. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
  153. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
  154. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
  155. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/datamodels_cim.conf +0 -0
  156. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/datamodels_custom.conf +0 -0
  157. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
  158. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
  159. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
  160. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
  161. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
  162. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/detections/application/.gitkeep +0 -0
  163. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/detections/cloud/.gitkeep +0 -0
  164. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/detections/network/.gitkeep +0 -0
  165. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/detections/web/.gitkeep +0 -0
  166. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/macros/security_content_ctime.yml +0 -0
  167. {contentctl-5.0.1 → contentctl-5.0.3}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
  168. {contentctl-5.0.1 → contentctl-5.0.3}/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.0.1
3
+ Version: 5.0.3
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -1,23 +1,45 @@
1
- import sys
2
- from dataclasses import dataclass
3
- import pathlib
4
- import json
5
1
  import datetime
6
- import timeit
2
+ import json
3
+ import pathlib
4
+ import sys
7
5
  import time
6
+ import timeit
7
+ from dataclasses import dataclass
8
+ from io import BufferedReader
8
9
 
9
- from requests import Session, post, get
10
+ from requests import Session, get, post
10
11
  from requests.auth import HTTPBasicAuth
11
12
 
12
13
  from contentctl.objects.config import inspect
13
- from contentctl.objects.savedsearches_conf import SavedsearchesConf
14
14
  from contentctl.objects.errors import (
15
- MetadataValidationError,
16
15
  DetectionIDError,
17
16
  DetectionMissingError,
18
- VersionDecrementedError,
17
+ MetadataValidationError,
19
18
  VersionBumpingError,
19
+ VersionDecrementedError,
20
20
  )
21
+ from contentctl.objects.savedsearches_conf import SavedsearchesConf
22
+
23
+ """
24
+ The following list includes all appinspect tags available from:
25
+ https://dev.splunk.com/enterprise/reference/appinspect/appinspecttagreference/
26
+
27
+ This allows contentctl to be as forward-leaning as possible in catching
28
+ any potential issues on the widest variety of stacks.
29
+ """
30
+ INCLUDED_TAGS_LIST = [
31
+ "aarch64_compatibility",
32
+ "ast",
33
+ "cloud",
34
+ "future",
35
+ "manual",
36
+ "packaging_standards",
37
+ "private_app",
38
+ "private_classic",
39
+ "private_victoria",
40
+ "splunk_appinspect",
41
+ ]
42
+ INCLUDED_TAGS_STRING = ",".join(INCLUDED_TAGS_LIST)
21
43
 
22
44
 
23
45
  @dataclass(frozen=True)
@@ -28,7 +50,6 @@ class InspectInputDto:
28
50
  class Inspect:
29
51
  def execute(self, config: inspect) -> str:
30
52
  if config.build_app or config.build_api:
31
- self.inspectAppCLI(config)
32
53
  appinspect_token = self.inspectAppAPI(config)
33
54
 
34
55
  if config.enable_metadata_validation:
@@ -49,10 +70,6 @@ class Inspect:
49
70
  session.auth = HTTPBasicAuth(
50
71
  config.splunk_api_username, config.splunk_api_password
51
72
  )
52
- if config.stack_type not in ["victoria", "classic"]:
53
- raise Exception(
54
- f"stack_type MUST be either 'classic' or 'victoria', NOT '{config.stack_type}'"
55
- )
56
73
 
57
74
  APPINSPECT_API_LOGIN = "https://api.splunk.com/2.0/rest/login/splunk"
58
75
 
@@ -64,10 +81,6 @@ class Inspect:
64
81
  APPINSPECT_API_VALIDATION_REQUEST = (
65
82
  "https://appinspect.splunk.com/v1/app/validate"
66
83
  )
67
- headers = {
68
- "Authorization": f"bearer {authorization_bearer}",
69
- "Cache-Control": "no-cache",
70
- }
71
84
 
72
85
  package_path = config.getPackageFilePath(include_version=False)
73
86
  if not package_path.is_file():
@@ -77,18 +90,43 @@ class Inspect:
77
90
  "trying to 'contentctl deploy_acs' the package BEFORE running 'contentctl build'?"
78
91
  )
79
92
 
80
- files = {
93
+ """
94
+ Some documentation on "files" argument for requests.post exists here:
95
+ https://docs.python-requests.org/en/latest/api/
96
+ The type (None, INCLUDED_TAGS_STRING) is intentional, and the None is important.
97
+ In curl syntax, the request we make below is equivalent to
98
+ curl -X POST \
99
+ -H "Authorization: bearer <TOKEN>" \
100
+ -H "Cache-Control: no-cache" \
101
+ -F "app_package=@<PATH/APP-PACKAGE>" \
102
+ -F "included_tags=cloud" \
103
+ --url "https://appinspect.splunk.com/v1/app/validate"
104
+
105
+ This is confirmed by the great resource:
106
+ https://curlconverter.com/
107
+ """
108
+ data: dict[str, tuple[None, str] | BufferedReader] = {
81
109
  "app_package": open(package_path, "rb"),
82
- "included_tags": (None, "cloud"),
110
+ "included_tags": (
111
+ None,
112
+ INCLUDED_TAGS_STRING,
113
+ ), # tuple with None is intentional here
83
114
  }
84
115
 
85
- res = post(APPINSPECT_API_VALIDATION_REQUEST, headers=headers, files=files)
116
+ headers = {
117
+ "Authorization": f"bearer {authorization_bearer}",
118
+ "Cache-Control": "no-cache",
119
+ }
120
+
121
+ res = post(APPINSPECT_API_VALIDATION_REQUEST, files=data, headers=headers)
86
122
 
87
123
  res.raise_for_status()
88
124
 
89
125
  request_id = res.json().get("request_id", None)
90
- APPINSPECT_API_VALIDATION_STATUS = f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}?included_tags=private_{config.stack_type}"
91
- headers = headers = {"Authorization": f"bearer {authorization_bearer}"}
126
+ APPINSPECT_API_VALIDATION_STATUS = (
127
+ f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}"
128
+ )
129
+
92
130
  startTime = timeit.default_timer()
93
131
  # the first time, wait for 40 seconds. subsequent times, wait for less.
94
132
  # this is because appinspect takes some time to return, so there is no sense
@@ -114,7 +152,9 @@ class Inspect:
114
152
  raise Exception(f"Error - Unknown Appinspect API status '{status}'")
115
153
 
116
154
  # We have finished running appinspect, so get the report
117
- APPINSPECT_API_REPORT = f"https://appinspect.splunk.com/v1/app/report/{request_id}?included_tags=private_{config.stack_type}"
155
+ APPINSPECT_API_REPORT = (
156
+ f"https://appinspect.splunk.com/v1/app/report/{request_id}"
157
+ )
118
158
  # Get human-readable HTML report
119
159
  headers = headers = {
120
160
  "Authorization": f"bearer {authorization_bearer}",
@@ -159,14 +199,14 @@ class Inspect:
159
199
  "\t - https://dev.splunk.com/enterprise/docs/developapps/testvalidate/appinspect/useappinspectclitool/"
160
200
  )
161
201
  from splunk_appinspect.main import (
162
- validate,
163
- MODE_OPTION,
164
202
  APP_PACKAGE_ARGUMENT,
165
- OUTPUT_FILE_OPTION,
166
- LOG_FILE_OPTION,
167
- INCLUDED_TAGS_OPTION,
168
203
  EXCLUDED_TAGS_OPTION,
204
+ INCLUDED_TAGS_OPTION,
205
+ LOG_FILE_OPTION,
206
+ MODE_OPTION,
207
+ OUTPUT_FILE_OPTION,
169
208
  TEST_MODE,
209
+ validate,
170
210
  )
171
211
  except Exception as e:
172
212
  print(e)
@@ -1,7 +1,9 @@
1
1
  import pathlib
2
+ import random
2
3
  import sys
3
4
  import traceback
4
5
  import warnings
6
+ from dataclasses import dataclass
5
7
 
6
8
  import tyro
7
9
 
@@ -155,6 +157,35 @@ YOU HAVE BEEN WARNED!
155
157
  """
156
158
 
157
159
 
160
+ def get_random_compliment():
161
+ compliments = [
162
+ "Your detection rules are like a zero-day shield! 🛡️",
163
+ "You catch threats like it's child's play! 🎯",
164
+ "Your correlation rules are pure genius! 🧠",
165
+ "Threat actors fear your detection engineering! ⚔️",
166
+ "You're the SOC's secret weapon! 🦾",
167
+ "Your false positive rate is impressively low! 📊",
168
+ "Malware trembles at your detection logic! 🦠",
169
+ "You're the threat hunter extraordinaire! 🔍",
170
+ "Your MITRE mappings are a work of art! 🎨",
171
+ "APTs have nightmares about your detections! 👻",
172
+ "Your content testing is bulletproof! 🎯",
173
+ "You're the detection engineering MVP! 🏆",
174
+ ]
175
+ return random.choice(compliments)
176
+
177
+
178
+ def recognize_func():
179
+ print(get_random_compliment())
180
+
181
+
182
+ @dataclass
183
+ class RecognizeCommand:
184
+ """Dummy subcommand for 'recognize' with no parameters."""
185
+
186
+ pass
187
+
188
+
158
189
  def main():
159
190
  print(CONTENTCTL_5_WARNING)
160
191
  try:
@@ -210,6 +241,7 @@ def main():
210
241
  "test_servers": test_servers.model_construct(**t.__dict__),
211
242
  "release_notes": release_notes.model_construct(**config_obj),
212
243
  "deploy_acs": deploy_acs.model_construct(**t.__dict__),
244
+ "recognize": RecognizeCommand(),
213
245
  }
214
246
  )
215
247
 
@@ -240,6 +272,8 @@ def main():
240
272
  deploy_acs_func(updated_config)
241
273
  elif type(config) is test or type(config) is test_servers:
242
274
  test_common_func(config)
275
+ elif type(config) is RecognizeCommand:
276
+ recognize_func()
243
277
  else:
244
278
  raise Exception(f"Unknown command line type '{type(config).__name__}'")
245
279
  except FileNotFoundError as e:
@@ -0,0 +1,9 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field
4
+
5
+ CVE_TYPE = Annotated[str, Field(pattern=r"^CVE-[1|2]\d{3}-\d+$")]
6
+ MITRE_ATTACK_ID_TYPE_PARENT = Annotated[str, Field(pattern=r"^T\d{4}$")]
7
+ MITRE_ATTACK_ID_TYPE_SUBTYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})$")]
8
+ MITRE_ATTACK_ID_TYPE = MITRE_ATTACK_ID_TYPE_PARENT | MITRE_ATTACK_ID_TYPE_SUBTYPE
9
+ APPID_TYPE = Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]
@@ -425,7 +425,6 @@ class inspect(build):
425
425
  "enforcement (defaults to the latest release of the app published on Splunkbase)."
426
426
  ),
427
427
  )
428
- stack_type: StackType = Field(description="The type of your Splunk Cloud Stack")
429
428
 
430
429
  @field_validator("enrichments", mode="after")
431
430
  @classmethod
@@ -496,6 +495,9 @@ class new(Config_Base):
496
495
 
497
496
  class deploy_acs(inspect):
498
497
  model_config = ConfigDict(validate_default=False, arbitrary_types_allowed=True)
498
+
499
+ stack_type: StackType = Field(description="The type of your Splunk Cloud Stack")
500
+
499
501
  # ignore linter error
500
502
  splunk_cloud_jwt_token: str = Field(
501
503
  exclude=True,
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
- from typing import Optional, Any
3
- from pydantic import Field, HttpUrl, model_serializer, BaseModel
2
+
3
+ from typing import Any, Optional
4
+
5
+ from pydantic import BaseModel, Field, HttpUrl, model_serializer
6
+
4
7
  from contentctl.objects.security_content_object import SecurityContentObject
5
8
 
6
9
 
@@ -20,6 +23,7 @@ class DataSource(SecurityContentObject):
20
23
  field_mappings: None | list = None
21
24
  convert_to_log_source: None | list = None
22
25
  example_log: None | str = None
26
+ output_fields: list[str] = []
23
27
 
24
28
  @model_serializer
25
29
  def serialize_model(self):
@@ -33,7 +33,10 @@ from contentctl.objects.enums import (
33
33
  SecurityContentProductName,
34
34
  SecurityDomain,
35
35
  )
36
- from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
36
+ from contentctl.objects.mitre_attack_enrichment import (
37
+ MitreAttackEnrichment,
38
+ MitreAttackGroup,
39
+ )
37
40
 
38
41
 
39
42
  class DetectionTags(BaseModel):
@@ -44,7 +47,7 @@ class DetectionTags(BaseModel):
44
47
  asset_type: AssetType = Field(...)
45
48
  group: list[str] = []
46
49
 
47
- mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
50
+ mitre_attack_id: list[MITRE_ATTACK_ID_TYPE] = []
48
51
  nist: list[NistCategory] = []
49
52
 
50
53
  product: list[SecurityContentProductName] = Field(..., min_length=1)
@@ -68,6 +71,15 @@ class DetectionTags(BaseModel):
68
71
  phases.add(phase)
69
72
  return sorted(list(phases))
70
73
 
74
+ # We do not want this to be included in serialization. By default, @property
75
+ # objects are not included in dumps
76
+ @property
77
+ def unique_mitre_attack_groups(self) -> list[MitreAttackGroup]:
78
+ group_set: set[MitreAttackGroup] = set()
79
+ for enrichment in self.mitre_attack_enrichments:
80
+ group_set.update(set(enrichment.mitre_attack_group_objects))
81
+ return sorted(group_set, key=lambda k: k.group)
82
+
71
83
  # enum is intentionally Cis18 even though field is named cis20 for legacy reasons
72
84
  @computed_field
73
85
  @property
@@ -134,8 +146,8 @@ class DetectionTags(BaseModel):
134
146
 
135
147
  if len(missing_tactics) > 0:
136
148
  raise ValueError(f"Missing Mitre Attack IDs. {missing_tactics} not found.")
137
- else:
138
- self.mitre_attack_enrichments = mitre_enrichments
149
+
150
+ self.mitre_attack_enrichments = mitre_enrichments
139
151
 
140
152
  return self
141
153
 
@@ -159,6 +171,44 @@ class DetectionTags(BaseModel):
159
171
  return enrichments
160
172
  """
161
173
 
174
+ @field_validator("mitre_attack_id", mode="after")
175
+ @classmethod
176
+ def sameTypeAndSubtypeNotPresent(
177
+ cls, techniques_and_subtechniques: list[MITRE_ATTACK_ID_TYPE]
178
+ ) -> list[MITRE_ATTACK_ID_TYPE]:
179
+ techniques: list[str] = [
180
+ f"{unknown_technique}."
181
+ for unknown_technique in techniques_and_subtechniques
182
+ if "." not in unknown_technique
183
+ ]
184
+ subtechniques: list[MITRE_ATTACK_ID_TYPE] = [
185
+ unknown_technique
186
+ for unknown_technique in techniques_and_subtechniques
187
+ if "." in unknown_technique
188
+ ]
189
+ subtype_and_parent_exist_exceptions: list[ValueError] = []
190
+
191
+ for subtechnique in subtechniques:
192
+ for technique in techniques:
193
+ if subtechnique.startswith(technique):
194
+ subtype_and_parent_exist_exceptions.append(
195
+ ValueError(
196
+ f" Technique : {technique.split('.')[0]}\n"
197
+ f" SubTechnique: {subtechnique}\n"
198
+ )
199
+ )
200
+
201
+ if len(subtype_and_parent_exist_exceptions):
202
+ error_string = "\n".join(
203
+ str(e) for e in subtype_and_parent_exist_exceptions
204
+ )
205
+ raise ValueError(
206
+ "Overlapping MITRE Attack ID Techniques and Subtechniques may not be defined. "
207
+ f"Remove the Technique and keep the Subtechnique:\n{error_string}"
208
+ )
209
+
210
+ return techniques_and_subtechniques
211
+
162
212
  @field_validator("analytic_story", mode="before")
163
213
  @classmethod
164
214
  def mapStoryNamesToStoryObjects(
@@ -238,3 +288,6 @@ class DetectionTags(BaseModel):
238
288
  return matched_tests + [
239
289
  AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests
240
290
  ]
291
+ return matched_tests + [
292
+ AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests
293
+ ]
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
3
- from typing import List
4
- from enum import StrEnum
2
+
5
3
  import datetime
4
+ from enum import StrEnum
5
+ from typing import List
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
8
+
6
9
  from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
7
10
 
8
11
 
@@ -84,6 +87,16 @@ class MitreAttackGroup(BaseModel):
84
87
  return []
85
88
  return contributors
86
89
 
90
+ def __lt__(self, other: MitreAttackGroup) -> bool:
91
+ if not isinstance(object, MitreAttackGroup):
92
+ raise Exception(
93
+ f"Cannot compare object of type MitreAttackGroup to object of type [{type(object).__name__}]"
94
+ )
95
+ return self.group < other.group
96
+
97
+ def __hash__(self) -> int:
98
+ return hash(self.group)
99
+
87
100
 
88
101
  class MitreAttackEnrichment(BaseModel):
89
102
  ConfigDict(extra="forbid")
@@ -1,17 +1,18 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field, model_serializer, ConfigDict
3
- from typing import List, Set, Optional
4
2
 
5
3
  from enum import Enum
4
+ from typing import List, Optional, Set
6
5
 
7
- from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
6
+ from pydantic import BaseModel, ConfigDict, Field, model_serializer
7
+
8
+ from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
8
9
  from contentctl.objects.enums import (
9
- StoryCategory,
10
10
  DataModel,
11
11
  KillChainPhase,
12
12
  SecurityContentProductName,
13
+ StoryCategory,
13
14
  )
14
- from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
15
+ from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
15
16
 
16
17
 
17
18
  class StoryUseCase(str, Enum):
@@ -60,7 +60,6 @@ tags:
60
60
  asset_type: Endpoint
61
61
  mitre_attack_id:
62
62
  - T1560.001
63
- - T1560
64
63
  product:
65
64
  - Splunk Enterprise
66
65
  - Splunk Enterprise Security
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "contentctl"
3
- version = "5.0.1"
3
+ version = "5.0.3"
4
4
 
5
5
  description = "Splunk Content Control Tool"
6
6
  authors = ["STRT <research@splunk.com>"]
@@ -1,6 +0,0 @@
1
- from pydantic import Field
2
- from typing import Annotated
3
-
4
- CVE_TYPE = Annotated[str, Field(pattern=r"^CVE-[1|2]\d{3}-\d+$")]
5
- MITRE_ATTACK_ID_TYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]
6
- APPID_TYPE = Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]
File without changes
File without changes
File without changes