contentctl 3.6.0__py3-none-any.whl → 4.0.2__py3-none-any.whl

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 (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
contentctl/contentctl.py CHANGED
@@ -1,786 +1,228 @@
1
- import sys
2
- import argparse
3
- import os
4
- import tqdm
5
- import functools
6
- from typing import Union
7
- import pathlib
8
- import yaml
9
- from contentctl.actions.detection_testing.GitService import (
10
- GitService,
11
- )
12
- from contentctl.actions.validate import ValidateInputDto, Validate
13
- from contentctl.actions.generate import (
14
- GenerateInputDto,
15
- DirectorOutputDto,
16
- Generate,
1
+ from contentctl.actions.initialize import Initialize
2
+ import tyro
3
+ from contentctl.objects.config import init, validate, build, new, deploy_acs, deploy_rest, test, test_servers, inspect, report, test_common, release_notes
4
+ from contentctl.actions.validate import Validate
5
+ from contentctl.actions.new_content import NewContent
6
+ from contentctl.actions.detection_testing.GitService import GitService
7
+ from contentctl.actions.build import (
8
+ BuildInputDto,
9
+ DirectorOutputDto,
10
+ Build,
17
11
  )
18
- from contentctl.actions.acs_deploy import ACSDeployInputDto, Deploy
19
12
 
13
+ from contentctl.actions.test import Test
14
+ from contentctl.actions.test import TestInputDto
20
15
  from contentctl.actions.reporting import ReportingInputDto, Reporting
21
- from contentctl.actions.new_content import NewContentInputDto, NewContent
22
- from contentctl.actions.doc_gen import DocGenInputDto, DocGen
23
- from contentctl.actions.initialize import Initialize, InitializeInputDto
24
- from contentctl.actions.api_deploy import API_Deploy, API_DeployInputDto
25
- from contentctl.actions.release_notes import ReleaseNotesInputDto, ReleaseNotes
26
- from contentctl.input.director import DirectorInputDto
27
- from contentctl.objects.enums import (
28
- SecurityContentType,
29
- SecurityContentProduct,
30
- DetectionTestingMode,
31
- PostTestBehavior,
32
- DetectionTestingTargetInfrastructure,
33
- SigmaConverterTarget
34
- )
35
- from contentctl.input.new_content_generator import NewContentGeneratorInputDto
36
- from contentctl.helper.config_handler import ConfigHandler
37
-
38
- from contentctl.objects.config import Config
39
-
40
- from contentctl.objects.app import App
41
- from contentctl.objects.test_config import Infrastructure
42
- from contentctl.actions.test import Test, TestInputDto
43
- from contentctl.input.sigma_converter import SigmaConverterInputDto
44
- from contentctl.actions.convert import ConvertInputDto, Convert
45
-
46
-
47
- SERVER_ARGS_ENV_VARIABLE = "CONTENTCTL_TEST_INFRASTRUCTURES"
48
-
49
-
50
- def configure_unattended(args: argparse.Namespace) -> argparse.Namespace:
51
- # disable all calls to tqdm - this is so that CI/CD contexts don't
52
- # have a large amount of output due to progress bar updates.
53
- tqdm.tqdm.__init__ = functools.partialmethod(
54
- tqdm.tqdm.__init__, disable=args.unattended
55
- )
56
- if args.unattended:
57
- if args.behavior != PostTestBehavior.never_pause.name:
58
- print(
59
- f"For unattended mode, --behavior MUST be {PostTestBehavior.never_pause.name}.\n"
60
- f"Updating the behavior from '{args.behavior}' to "
61
- f"'{PostTestBehavior.never_pause.name}'"
62
- )
63
- args.behavior = PostTestBehavior.never_pause.name
64
-
65
- return args
66
-
67
-
68
- def print_ascii_art():
69
- print(
70
- """
71
- Running Splunk Security Content Control Tool (contentctl)
72
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
73
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⠛⡇⠀⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
74
- ⠀⠀⠀⠀⠀⠀⠀⠀⣀⠼⠖⠛⠋⠉⠉⠓⠢⣴⡻⣾⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
75
- ⠀⠀⠀⢀⡠⠔⠊⠁⠀⠀⠀⠀⠀⠀⣠⣤⣄⠻⠟⣏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
76
- ⠀⣠⠞⠁⠀⠀⠀⡄⠀⠀⠀⠀⠀⠀⢻⣿⣿⠀⢀⠘⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
77
- ⢸⡇⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠀⠈⠁⠘⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
78
- ⢸⡉⠓⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢄⠀⠀⠀⠈⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
79
- ⠈⡇⠀⢠⠀⠀⠀⠀⠀⠀⠀⠈⡷⣄⠀⠀⢀⠈⠀⠀⠑⢄⠀⠑⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
80
- ⠀⠹⡄⠘⡄⠀⠀⠀⠀⢀⡠⠊⠀⠙⠀⠀⠈⢣⠀⠀⠀⢀⠀⠀⠀⠉⠒⠤⣀⠀⠀⠀⠀⠀⠀⠀⠀
81
- ⠀⠀⠉⠁⠛⠲⢶⡒⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⠀⠉⠂⠀⠀⠀⠀⠤⡙⠢⣄⠀⠀⠀⠀⠀
82
- ⠀⠀⠀⠀⠀⠀⠀⢹⠀⠀⡀⠀⠀⢸⠀⠀⠀⠀⠘⠇⠀⠀⠀⠀⠀⠀⠀⠀⢀⠈⠀⠈⠳⡄⠀⠀⠀
83
- ⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠣⠀⠀⠈⠀⢀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⢀⡀⠀⠑⠄⠈⠣⡘⢆⠀⠀
84
- ⠀⠀⠀⠀⠀⠀⠀⠀⢧⠀⠀⠀⠀⠀⠀⠿⠀⠀⠀⠀⣠⠞⠉⠀⠀⠀⠀⠙⢆⠀⠀⡀⠀⠁⠈⢇⠀
85
- ⠀⠀⠀⠀⠀⠀⠀⠀⢹⠀⢤⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠙⡄⠀⡀⠈⡆
86
- ⠀⠀⠀⠀⠀⠀⠀⠀⠸⡆⠘⠃⠀⠀⠀⢀⡄⠀⠀⡇⠀⠀⡄⠀⠀⠀⠰⡀⠀⠀⡄⠀⠉⠀⠃⠀⢱
87
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⡀⠀⠀⡆⠀⠸⠇⠀⠀⢳⠀⠀⠈⠀⠀⠀⠐⠓⠀⠀⢸⡄⠀⠀⠀⡀⢸
88
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⢻⠀⠀⠀⠀⢰⠛⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⡆⠀⠃⡼
89
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣷⣤⣽⣧⠀⠀⠀⡜⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃
90
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣇⡿⠹⣷⣄⣬⡗⠢⣤⠖⠛⢳⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠃⠀
91
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠋⢠⣾⢿⡏⣸⠀⠀⠈⠋⠛⠧⠤⠘⠛⠉⠙⠒⠒⠒⠒⠉⠀⠀⠀
92
- ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠻⠶⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
93
-
94
- By: Splunk Threat Research Team [STRT] - research@splunk.com
95
- """
96
- )
97
-
98
-
99
- def start(args: argparse.Namespace, read_test_file: bool = False) -> Config:
100
- base_config = ConfigHandler.read_config(args)
101
- if read_test_file:
102
- base_config.test = ConfigHandler.read_test_config(args)
103
- return base_config
104
-
105
-
106
- def initialize(args) -> None:
107
- Initialize().execute(InitializeInputDto(path=pathlib.Path(args.path), demo=args.demo))
108
-
109
-
110
- def build(args, config: Union[Config, None] = None) -> DirectorOutputDto:
111
- if config is None:
112
- config = start(args)
113
- if args.type == "app":
114
- product_type = SecurityContentProduct.SPLUNK_APP
115
- elif args.type == "ssa":
116
- product_type = SecurityContentProduct.SSA
117
- elif args.type == "api":
118
- product_type = SecurityContentProduct.API
119
- else:
120
- print("Invalid build type. Valid options app, ssa or api")
121
- sys.exit(1)
122
- director_input_dto = DirectorInputDto(
123
- input_path=pathlib.Path(os.path.abspath(args.path)),
124
- product=product_type,
125
- config=config
126
- )
127
- generate_input_dto = GenerateInputDto(
128
- director_input_dto,
129
- args.splunk_api_username,
130
- args.splunk_api_password,
131
- )
132
-
133
- generate = Generate()
134
-
135
- return generate.execute(generate_input_dto)
136
-
137
-
138
- def api_deploy(args) -> None:
139
- config = start(args)
140
- deploy_input_dto = API_DeployInputDto(path=pathlib.Path(args.path), config=config)
141
- deploy = API_Deploy()
142
-
143
- deploy.execute(deploy_input_dto)
144
-
16
+ from contentctl.actions.inspect import Inspect
17
+ import sys
18
+ import warnings
19
+ import pathlib
20
+ from contentctl.input.yml_reader import YmlReader
21
+ from contentctl.actions.release_notes import ReleaseNotes
22
+
23
+ # def print_ascii_art():
24
+ # print(
25
+ # """
26
+ # Running Splunk Security Content Control Tool (contentctl)
27
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
28
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⠛⡇⠀⠀⠀⠀⠀⠀⣠⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
29
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⣀⠼⠖⠛⠋⠉⠉⠓⠢⣴⡻⣾⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
30
+ # ⠀⠀⠀⢀⡠⠔⠊⠁⠀⠀⠀⠀⠀⠀⣠⣤⣄⠻⠟⣏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
31
+ # ⠀⣠⠞⠁⠀⠀⠀⡄⠀⠀⠀⠀⠀⠀⢻⣿⣿⠀⢀⠘⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
32
+ # ⢸⡇⠀⠀⠀⡠⠊⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠀⠈⠁⠘⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
33
+ # ⢸⡉⠓⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢄⠀⠀⠀⠈⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
34
+ # ⠈⡇⠀⢠⠀⠀⠀⠀⠀⠀⠀⠈⡷⣄⠀⠀⢀⠈⠀⠀⠑⢄⠀⠑⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
35
+ # ⠀⠹⡄⠘⡄⠀⠀⠀⠀⢀⡠⠊⠀⠙⠀⠀⠈⢣⠀⠀⠀⢀⠀⠀⠀⠉⠒⠤⣀⠀⠀⠀⠀⠀⠀⠀⠀
36
+ # ⠀⠀⠉⠁⠛⠲⢶⡒⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⡄⠀⠀⠉⠂⠀⠀⠀⠀⠤⡙⠢⣄⠀⠀⠀⠀⠀
37
+ # ⠀⠀⠀⠀⠀⠀⠀⢹⠀⠀⡀⠀⠀⢸⠀⠀⠀⠀⠘⠇⠀⠀⠀⠀⠀⠀⠀⠀⢀⠈⠀⠈⠳⡄⠀⠀⠀
38
+ # ⠀⠀⠀⠀⠀⠀⠀⠈⡇⠀⠣⠀⠀⠈⠀⢀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⢀⡀⠀⠑⠄⠈⠣⡘⢆⠀⠀
39
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⢧⠀⠀⠀⠀⠀⠀⠿⠀⠀⠀⠀⣠⠞⠉⠀⠀⠀⠀⠙⢆⠀⠀⡀⠀⠁⠈⢇⠀
40
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⢹⠀⢤⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠙⡄⠀⡀⠈⡆
41
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠸⡆⠘⠃⠀⠀⠀⢀⡄⠀⠀⡇⠀⠀⡄⠀⠀⠀⠰⡀⠀⠀⡄⠀⠉⠀⠃⠀⢱
42
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⡀⠀⠀⡆⠀⠸⠇⠀⠀⢳⠀⠀⠈⠀⠀⠀⠐⠓⠀⠀⢸⡄⠀⠀⠀⡀⢸
43
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢳⡀⠀⢻⠀⠀⠀⠀⢰⠛⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⠀⡆⠀⠃⡼
44
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣷⣤⣽⣧⠀⠀⠀⡜⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠃
45
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣇⡿⠹⣷⣄⣬⡗⠢⣤⠖⠛⢳⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠃⠀
46
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠋⢠⣾⢿⡏⣸⠀⠀⠈⠋⠛⠧⠤⠘⠛⠉⠙⠒⠒⠒⠒⠉⠀⠀⠀
47
+ # ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠻⠶⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
48
+
49
+ # By: Splunk Threat Research Team [STRT] - research@splunk.com
50
+ # """
51
+ # )
52
+
53
+
54
+
55
+
56
+ def init_func(config:test):
57
+ Initialize().execute(config)
58
+
59
+
60
+ def validate_func(config:validate)->DirectorOutputDto:
61
+ validate = Validate()
62
+ return validate.execute(config)
145
63
 
146
- def acs_deploy(args) -> None:
147
- config = start(args)
148
- director_input_dto = DirectorInputDto(
149
- input_path=pathlib.Path(os.path.abspath(args.path)),
150
- product=SecurityContentProduct.SPLUNK_APP,
151
- config=config
152
- )
153
- acs_deply_dto = ACSDeployInputDto(director_input_dto,
154
- args.splunk_api_username,
155
- args.splunk_api_password,
156
- args.splunk_cloud_jwt_token,
157
- args.splunk_cloud_stack,
158
- args.stack_type)
64
+ def report_func(config:report)->None:
65
+ # First, perform validation. Remember that the validate
66
+ # configuration is actually a subset of the build configuration
67
+ director_output_dto = validate_func(config)
159
68
 
160
- deploy = Deploy()
161
- deploy.execute(acs_deply_dto)
162
-
69
+ r = Reporting()
70
+ return r.execute(ReportingInputDto(director_output_dto=director_output_dto,
71
+ config=config))
163
72
 
164
73
 
74
+ def build_func(config:build)->DirectorOutputDto:
75
+ # First, perform validation. Remember that the validate
76
+ # configuration is actually a subset of the build configuration
77
+ director_output_dto = validate_func(config)
78
+ builder = Build()
79
+ return builder.execute(BuildInputDto(director_output_dto, config))
80
+
81
+ def inspect_func(config:inspect)->str:
82
+ #Make sure that we have built the most recent version of the app
83
+ _ = build_func(config)
84
+ inspect_token = Inspect().execute(config)
85
+ return inspect_token
86
+
165
87
 
166
- def test(args: argparse.Namespace):
167
- args = configure_unattended(args)
168
-
169
- config = start(args, read_test_file=True)
170
- #Don't do enrichment
171
- if args.dry_run:
172
- config.enrichments.attack_enrichment = False
173
- config.enrichments.cve_enrichment = False
174
- config.enrichments.splunk_app_enrichment = False
175
-
176
- if config.test is None:
177
- raise Exception("Error parsing test configuration. Test Object was None.")
178
-
179
- if args.test_branch is not None:
180
- if config.test.version_control_config is not None:
181
- config.test.version_control_config.test_branch = args.test_branch
182
- else:
183
- raise Exception("Test argument 'test_branch' passed on the command line, but 'version_control_config' is not defined in contentctl_test.yml.")
184
- if args.target_branch is not None:
185
- if config.test.version_control_config is not None:
186
- config.test.version_control_config.target_branch = args.target_branch
187
- else:
188
- raise Exception("Test argument 'target_branch' passed on the command line, but 'version_control_config' is not defined in contentctl_test.yml.")
189
-
190
- # set some arguments that are not
191
- # yet exposed/written properly in
192
- # the config file
193
- if args.infrastructure is not None:
194
- config.test.infrastructure_config.infrastructure_type = DetectionTestingTargetInfrastructure(
195
- args.infrastructure
196
- )
197
- if args.mode is not None:
198
- config.test.mode = DetectionTestingMode(args.mode)
199
- if args.behavior is not None:
200
- config.test.post_test_behavior = PostTestBehavior(args.behavior)
201
- if args.detections_list is not None:
202
- config.test.detections_list = args.detections_list
203
- if args.enable_integration_testing or config.test.enable_integration_testing:
204
- config.test.enable_integration_testing = True
205
-
206
- # validate and setup according to infrastructure type
207
- if config.test.infrastructure_config.infrastructure_type == DetectionTestingTargetInfrastructure.container:
208
- if args.num_containers is None:
209
- raise Exception(
210
- "Error - trying to start a test using container infrastructure but no value for --num_containers was "
211
- "found"
212
- )
213
- config.test.infrastructure_config.infrastructures = Infrastructure.get_infrastructure_containers(
214
- args.num_containers
215
- )
216
- elif config.test.infrastructure_config.infrastructure_type == DetectionTestingTargetInfrastructure.server:
217
- if args.server_info is None and os.environ.get(SERVER_ARGS_ENV_VARIABLE) is None:
218
- if len(config.test.infrastructure_config.infrastructures) == 0:
219
- raise Exception(
220
- "Error - trying to start a test using server infrastructure, but server information was not stored "
221
- "in contentctl_test.yml or passed on the command line. Please see the documentation for "
222
- "--server_info at the command line or 'infrastructures' in contentctl.yml."
223
- )
224
- else:
225
- print("Using server configuration from: [contentctl_test.yml infrastructures section]")
88
+ def release_notes_func(config:release_notes)->None:
89
+ ReleaseNotes().release_notes(config)
226
90
 
227
- else:
228
- if args.server_info is not None:
229
- print("Using server configuration from: [command line]")
230
- pass
231
- elif os.environ.get(SERVER_ARGS_ENV_VARIABLE) is not None:
232
- args.server_info = os.environ.get(SERVER_ARGS_ENV_VARIABLE, "").split(';')
233
- print(f"Using server configuration from: [{SERVER_ARGS_ENV_VARIABLE} environment variable]")
234
- else:
235
- raise Exception(
236
- "Server infrastructure information not passed in contentctl_test.yml file, using --server_info "
237
- f"switch on the command line, or in the {SERVER_ARGS_ENV_VARIABLE} environment variable"
238
- )
239
- # if server info was provided on the command line, us that. Otherwise use the env
91
+ def new_func(config:new):
92
+ NewContent().execute(config)
240
93
 
241
- config.test.infrastructure_config.infrastructures = []
242
94
 
243
- for server in args.server_info:
244
- address, username, password, web_ui_port, hec_port, api_port = server.split(",")
245
- config.test.infrastructure_config.infrastructures.append(
246
- Infrastructure(
247
- splunk_app_username=username,
248
- splunk_app_password=password,
249
- instance_address=address,
250
- hec_port=int(hec_port),
251
- web_ui_port=int(web_ui_port),
252
- api_port=int(api_port)
253
- )
254
- )
255
95
 
256
- # We do this before generating the app to save some time if options are incorrect.
257
- # For example, if the detection(s) we are trying to test do not exist
258
- gitService = GitService(config.test)
96
+ def deploy_acs_func(config:deploy_acs):
97
+ #This is a bit challenging to get to work with the default values.
98
+ raise Exception("deploy acs not yet implemented")
259
99
 
100
+ def deploy_rest_func(config:deploy_rest):
101
+ raise Exception("deploy rest not yet implemented")
260
102
 
261
103
 
262
- director_output_dto = build(args, config)
104
+ def test_common_func(config:test_common):
105
+ director_output_dto = build_func(config)
106
+ gitServer = GitService(director=director_output_dto,config=config)
107
+ detections_to_test = gitServer.getContent()
263
108
 
264
- test_director_output_dto = gitService.get_all_content(director_output_dto)
265
109
 
266
- if args.dry_run:
267
- #set the proper values in the config
268
- config.test.mode = DetectionTestingMode.selected
269
- config.test.detections_list = [d.file_path for d in test_director_output_dto.detections]
270
- config.test.apps = []
271
- config.test.post_test_behavior = PostTestBehavior.never_pause
272
-
273
- #Disable enrichments to save time
274
- config.enrichments.attack_enrichment = False
275
- config.enrichments.cve_enrichment = False
276
- config.enrichments.splunk_app_enrichment = False
277
-
278
- #Create a directory for artifacts.
279
- dry_run_config_dir = pathlib.Path("dry_run_config")
280
-
281
- #It's okay if it already exists
282
- dry_run_config_dir.mkdir(exist_ok=True)
283
-
284
- #Write out the test plan file
285
- with open(dry_run_config_dir/"contentctl_test.yml", "w") as test_plan_config:
286
- d = config.test.dict()
287
- d['infrastructure_config']['infrastructure_type'] = d['infrastructure_config']['infrastructure_type'].value
288
- d['mode'] = d['mode'].value
289
- d['post_test_behavior'] = d['post_test_behavior'].value
290
- yaml.safe_dump(d, test_plan_config)
291
-
292
- with open(dry_run_config_dir/"contentctl.yml", "w") as contentctl_cfg:
293
- d = config.dict()
294
- del d["test"]
295
- yaml.safe_dump(d, contentctl_cfg)
296
-
297
-
298
-
299
- print(f"Wrote test plan to '{dry_run_config_dir/'contentctl_test.yml'}' and '{dry_run_config_dir/'contentctl.yml'}'")
300
- return
301
-
302
-
303
-
304
- else:
305
- # All this information will later come from the config, so we will
306
- # be able to do it in Test().execute. For now, we will do it here
307
- app = App(
308
- uid=9999,
309
- appid=config.build.name,
310
- title=config.build.title,
311
- release=config.build.version,
312
- http_path=None,
313
- local_path=str(pathlib.Path(config.build.path_root)/f"{config.build.name}-{config.build.version}.tar.gz"),
314
- description=config.build.description,
315
- splunkbase_path=None,
316
- force_local=True
317
- )
318
-
319
- # We need to do this instead of appending to retrigger validation.
320
- # It does not happen the first time since validation does not run for default values
321
- # unless we use always=True in the validator
322
- # we always want to keep CIM as the last app installed
323
-
324
- config.test.apps = [app] + config.test.apps
325
-
326
- test_input_dto = TestInputDto(
327
- test_director_output_dto=test_director_output_dto,
328
- gitService=gitService,
329
- config=config.test,
330
- )
331
-
332
- test = Test()
333
-
334
- result = test.execute(test_input_dto)
335
- # This return code is important. Even if testing
336
- # fully completes, if everything does not pass then
337
- # we want to return a nonzero status code
338
- if result:
339
- return
340
- else:
341
- sys.exit(1)
342
-
343
-
344
- def validate(args) -> None:
345
- config = start(args)
346
- if args.type == "app":
347
- product_type = SecurityContentProduct.SPLUNK_APP
348
- elif args.type == "ssa":
349
- product_type = SecurityContentProduct.SSA
350
- elif args.type == "api":
351
- product_type = SecurityContentProduct.API
352
- else:
353
- print("Invalid build type. Valid options app, ssa or api")
354
- sys.exit(1)
355
- director_input_dto = DirectorInputDto(
356
- input_path=pathlib.Path(args.path),
357
- product=product_type,
358
- config=config
359
- )
360
- validate_input_dto = ValidateInputDto(director_input_dto=director_input_dto)
361
- validate = Validate()
362
- return validate.execute(validate_input_dto)
363
-
364
- def release_notes(args)-> None:
365
-
366
- config = start(args)
367
- director_input_dto = DirectorInputDto(
368
- input_path=pathlib.Path(args.path), product=SecurityContentProduct.SPLUNK_APP, config=config
369
- )
370
-
371
- release_notes_input_dto = ReleaseNotesInputDto(director_input_dto=director_input_dto)
372
-
373
- release_notes = ReleaseNotes()
374
- release_notes.release_notes(release_notes_input_dto, args.old_tag, args.new_tag, args.latest_branch)
375
-
376
- def doc_gen(args) -> None:
377
- config = start(args)
378
- director_input_dto = DirectorInputDto(
379
- input_path=pathlib.Path(args.path), product=SecurityContentProduct.SPLUNK_APP, config=config
380
- )
381
-
382
- doc_gen_input_dto = DocGenInputDto(director_input_dto=director_input_dto)
383
-
384
- doc_gen = DocGen()
385
- doc_gen.execute(doc_gen_input_dto)
386
-
387
-
388
- def new_content(args) -> None:
389
110
 
390
- if args.type == "detection":
391
- contentType = SecurityContentType.detections
392
- elif args.type == "story":
393
- contentType = SecurityContentType.stories
394
- else:
395
- print("ERROR: type " + args.type + " not supported")
396
- sys.exit(1)
397
-
398
- new_content_generator_input_dto = NewContentGeneratorInputDto(type=contentType)
399
- new_content_input_dto = NewContentInputDto(
400
- new_content_generator_input_dto, os.path.abspath(args.path)
401
- )
402
- new_content = NewContent()
403
- new_content.execute(new_content_input_dto)
404
-
405
-
406
- def reporting(args) -> None:
407
- config = start(args)
408
- director_input_dto = DirectorInputDto(
409
- input_path=args.path, product=SecurityContentProduct.SPLUNK_APP, config=config
410
- )
411
-
412
- reporting_input_dto = ReportingInputDto(director_input_dto=director_input_dto)
413
-
414
- reporting = Reporting()
415
- reporting.execute(reporting_input_dto)
416
-
417
-
418
- def convert(args) -> None:
419
- if args.data_model == 'cim':
420
- data_model = SigmaConverterTarget.CIM
421
- elif args.data_model == 'raw':
422
- data_model = SigmaConverterTarget.RAW
423
- elif args.data_model == 'ocsf':
424
- data_model = SigmaConverterTarget.OCSF
425
- else:
426
- print("ERROR: data model " + args.data_model + " not supported")
427
- sys.exit(1)
428
-
429
- sigma_converter_input_dto = SigmaConverterInputDto(
430
- data_model=data_model,
431
- detection_path=args.detection_path,
432
- detection_folder=args.detection_folder,
433
- input_path=args.path,
434
- log_source=args.log_source
435
- )
436
-
437
- convert_input_dto = ConvertInputDto(
438
- sigma_converter_input_dto=sigma_converter_input_dto,
439
- output_path=os.path.abspath(args.output)
440
- )
441
- convert = Convert()
442
- convert.execute(convert_input_dto)
443
-
444
-
445
- def main():
446
- """
447
- main function parses the arguments passed to the script and calls the respctive method.
448
- :param args: arguments passed by the user on command line while calling the script.
449
- :return: returns the output of the function called.
450
- """
451
-
452
- # grab arguments
453
- parser = argparse.ArgumentParser(
454
- description="Use `contentctl action -h` to get help with any Splunk content action"
455
- )
456
- parser.add_argument(
457
- "-p",
458
- "--path",
459
- required=False,
460
- default=".",
461
- help="path to the content path containing the contentctl.yml",
462
- )
463
-
464
- parser.add_argument(
465
- "--enable_enrichment",
466
- required=False,
467
- action="store_true",
468
- help="Enrichment is only REQUIRED when building a release (or testing a release). In most cases, it is not required. Disabling enrichment BY DEFAULT (which is the default setting in contentctl.yml) is a signifcant time savings."
469
- )
470
-
471
- parser.set_defaults(func=lambda _: parser.print_help())
472
- actions_parser = parser.add_subparsers(
473
- title="Splunk content actions", dest="action"
474
- )
475
-
476
- # available actions
477
- init_parser = actions_parser.add_parser(
478
- "init",
479
- help="initialize a Splunk content pack using and customizes a configuration under contentctl.yml",
480
- )
481
- validate_parser = actions_parser.add_parser(
482
- "validate", help="validates a Splunk content pack"
483
- )
484
- build_parser = actions_parser.add_parser(
485
- "build", help="builds a Splunk content pack package to be distributed"
486
- )
487
-
488
- acs_deploy_parser = actions_parser.add_parser(
489
- "acs_deploy", help="Deploys a previously built package via ACS. Note that 'contentctl build' command MUST have been run prior to running this command. It will NOT build a package itself."
490
- )
491
-
492
- new_content_parser = actions_parser.add_parser(
493
- "new", help="create new Splunk content object (detection, or story)"
494
- )
495
- reporting_parser = actions_parser.add_parser(
496
- "report", help="create Splunk content report of the current pack"
497
- )
498
-
499
- api_deploy_parser = actions_parser.add_parser(
500
- "api_deploy", help="Deploy content via API to a target Splunk Instance."
501
- )
502
-
503
- docs_parser = actions_parser.add_parser(
504
- "docs", help="create documentation in docs folder"
505
- )
506
- release_notes_parser = actions_parser.add_parser(
507
- "release_notes",
508
- help="Compares two tags and create release notes of what ESCU/BA content is added"
509
- )
510
-
511
- test_parser = actions_parser.add_parser(
512
- "test",
513
- help="Run a test of the detections against a Splunk Server or Splunk Docker Container",
514
- )
515
-
516
- convert_parser = actions_parser.add_parser("convert", help="Convert a sigma detection to a Splunk ESCU detection.")
517
-
518
- init_parser.set_defaults(func=initialize)
519
- init_parser.add_argument(
520
- "--demo",
521
- action=argparse.BooleanOptionalAction,
522
- help=(
523
- "Use this flag to pre-populate the content pack "
524
- "with one additional detection that will fail 'contentctl validate' "
525
- "and on detection that will fail 'contentctl test'. This is useful "
526
- "for demonstrating contentctl functionality."
527
- )
528
- )
529
-
530
- validate_parser.add_argument(
531
- "-t",
532
- "--type",
533
- required=False,
534
- type=str,
535
- default="app",
536
- help="Type of package: app, ssa or api"
537
- )
538
- validate_parser.set_defaults(func=validate)
539
-
540
- build_parser.add_argument(
541
- "-t",
542
- "--type",
543
- required=False,
544
- type=str,
545
- default="app",
546
- help="Type of package: app, ssa or api"
547
- )
548
-
549
- build_parser.add_argument(
550
- "--splunk_api_username",
551
- required=False,
552
- type=str,
553
- default=None,
554
- help=(
555
- f"Username for running AppInspect and, if desired, installing your app via Admin Config Service (ACS). For documentation, "
556
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref and https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps"
557
- )
558
- )
559
- build_parser.add_argument(
560
- "--splunk_api_password",
561
- required=False,
562
- type=str,
563
- default=None,
564
- help=(
565
- f"Username for running AppInspect and, if desired, installing your app via Admin Config Service (ACS). For documentation, "
566
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref and https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps"
567
- )
568
- )
569
-
570
-
571
- build_parser.set_defaults(func=build)
572
-
573
-
574
- acs_deploy_parser.add_argument(
575
- "--splunk_api_username",
576
- required=True,
577
- type=str,
578
- help=(
579
- f"Username for running AppInspect and, if desired, installing your app via Admin Config Service (ACS). For documentation, "
580
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref and https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps"
581
- )
582
- )
583
- acs_deploy_parser.add_argument(
584
- "--splunk_api_password",
585
- required=True,
586
- type=str,
587
- help=(
588
- f"Username for running AppInspect and, if desired, installing your app via Admin Config Service (ACS). For documentation, "
589
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref and https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps"
590
- )
591
- )
111
+ test_input_dto = TestInputDto(detections_to_test, config)
592
112
 
593
- acs_deploy_parser.add_argument(
594
- "--splunk_cloud_jwt_token",
595
- required=True,
596
- type=str,
597
- help=(
598
- f"Target Splunk Cloud Stack JWT Token for app deployment. Note that your stack MUST Support Admin Config Server (ACS) and Automated Private App Vetting (APAV). For documentation, "
599
- "on creating this token, please review https://docs.splunk.com/Documentation/SplunkCloud/9.1.2312/Security/CreateAuthTokens#Use_Splunk_Web_to_create_authentication_tokens"
600
- )
601
- )
602
-
603
- acs_deploy_parser.add_argument(
604
- "--splunk_cloud_stack",
605
- required=True,
606
- type=str,
607
- help=(
608
- f"Target Splunk Cloud Stack for app deployment. Note that your stack MUST Support Admin Config Server (ACS) and Automated Private App Vetting (APAV). For documentation, "
609
- "please review https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps"
610
- )
611
- )
612
-
613
- acs_deploy_parser.add_argument(
614
- "--stack_type",
615
- required=True,
616
- type=str,
617
- choices=["classic","victoria"],
618
- help="Identifies your Splunk Cloud Stack as 'classic' or 'victoria' experience"
619
- )
620
-
621
-
622
- acs_deploy_parser.set_defaults(func=acs_deploy)
623
-
624
- docs_parser.set_defaults(func=doc_gen)
625
-
626
- new_content_parser.add_argument(
627
- "-t",
628
- "--type",
629
- required=True,
630
- type=str,
631
- help="Type of security content object, choose between `detection`, `story`",
632
- )
633
- new_content_parser.set_defaults(func=new_content)
634
-
635
- reporting_parser.set_defaults(func=reporting)
636
-
637
- api_deploy_parser.set_defaults(func=api_deploy)
638
-
639
- test_parser.add_argument(
640
- "-t",
641
- "--type",
642
- required=False,
643
- type=str,
644
- default="app",
645
- help="Type of package: app, ssa or api"
646
- )
647
- test_parser.add_argument(
648
- "--mode",
649
- required=False,
650
- default=None,
651
- type=str,
652
- choices=DetectionTestingMode._member_names_,
653
- help="Controls which detections to test. 'all' will test all detections in the repo."
654
- "'selected' will test a list of detections that have "
655
- "been provided via the --selected command line argument (see for more details).",
656
- )
657
- test_parser.add_argument(
658
- "--behavior",
659
- required=False,
660
- default=None,
661
- type=str,
662
- choices=PostTestBehavior._member_names_,
663
- help="Controls what to do when a test completes. 'always_pause' means that the state of "
664
- "the test will always pause after a test, allowing the user to log into the "
665
- "server and experiment with the search and data before it is removed. 'pause_on_failure' "
666
- "will pause execution ONLY when a test fails. The user may press ENTER in the terminal "
667
- "running the test to move on to the next test. 'never_pause' will never stop testing, "
668
- "even if a test fails. Please note that 'never_pause' MUST be used for a test to "
669
- "run in an unattended manner or in a CI/CD system - otherwise a single failed test "
670
- "will result in the testing never finishing as the tool waits for input.",
671
- )
672
- test_parser.add_argument(
673
- "-d",
674
- "--detections_list",
675
- required=False,
676
- nargs="+",
677
- default=None,
678
- type=str,
679
- help="An explicit list "
680
- "of detections to test. Their paths should be relative to the app path.",
681
- )
682
-
683
- test_parser.add_argument("--unattended", action=argparse.BooleanOptionalAction)
684
-
685
- test_parser.add_argument(
686
- "--infrastructure",
687
- required=False,
688
- type=str,
689
- choices=DetectionTestingTargetInfrastructure._member_names_,
690
- default=None,
691
- help=(
692
- "Determines what infrastructure to use for testing. The options are "
693
- "container and server. Container will set up Splunk Container(s) at runtime, "
694
- "install all relevant apps, and perform configurations. Server will use "
695
- "preconfigured server(s) either specified on the command line or in "
696
- "contentctl_test.yml."
697
- )
698
- )
699
- test_parser.add_argument("--num_containers", required=False, default=1, type=int)
700
- test_parser.add_argument("--server_info", required=False, default=None, type=str, nargs='+')
113
+ t = Test()
701
114
 
702
- test_parser.add_argument("--target_branch", required=False, default=None, type=str)
703
- test_parser.add_argument("--test_branch", required=False, default=None, type=str)
704
- test_parser.add_argument("--dry_run", action=argparse.BooleanOptionalAction, help="Used to emit dry_run_config/contentctl_test.yml "\
705
- "and dry_run_config/contentctl.yml files. These are used for CI/CD-driven internal testing workflows and are not intended for public use at this time.")
115
+ # Remove detections that we do not want to test because they are
116
+ # not production, the correct type, or manual_test only
117
+ filted_test_input_dto = t.filter_detections(test_input_dto)
706
118
 
707
- # Even though these are also options to build, make them available to test_parser
708
- # as well to make the tool easier to use
709
- test_parser.add_argument(
710
- "--splunk_api_username",
711
- required=False,
712
- type=str,
713
- default=None,
714
- help=(
715
- f"Username for running AppInspect on {SecurityContentProduct.SPLUNK_APP.name} ONLY. For documentation, "
716
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref"
717
- )
718
- )
719
- test_parser.add_argument(
720
- "--splunk_api_password",
721
- required=False,
722
- type=str,
723
- default=None,
724
- help=(
725
- f"Password for running AppInspect on {SecurityContentProduct.SPLUNK_APP.name} ONLY. For documentation, "
726
- "please review https://dev.splunk.com/enterprise/reference/appinspect/appinspectapiepref"
727
- )
728
- )
729
- test_parser.add_argument(
730
- "--enable_integration_testing",
731
- required=False,
732
- action="store_true",
733
- help="Whether integration testing should be enabled, in addition to unit testing (requires a configured Splunk "
734
- "instance with ES installed)"
735
- )
736
-
737
- # TODO (cmcginley): add flag for enabling logging for correlation_search logging
738
- # TODO (cmcginley): add flag for changing max_sleep time for integration tests
739
- # TODO (cmcginley): add setting to skip listing skips -> test_config.TestConfig,
740
- # contentctl.test, contentctl.main
741
-
742
-
743
-
744
- test_parser.set_defaults(func=test)
119
+ if config.plan_only:
120
+ #Emit the test plan and quit. Do not actually run the test
121
+ config.dumpCICDPlanAndQuit(gitServer.getHash(),filted_test_input_dto.detections)
122
+ return
123
+
124
+ success = t.execute(filted_test_input_dto)
125
+
126
+ if success:
127
+ #Everything passed!
128
+ print("All tests have run successfully or been marked as 'skipped'")
129
+ return
130
+ raise Exception("There was at least one unsuccessful test")
745
131
 
746
- convert_parser.add_argument(
747
- "-dm",
748
- "--data_model",
749
- required=False,
750
- type=str,
751
- default="cim",
752
- help="converter target, choose between cim, raw, ocsf"
753
- )
754
- convert_parser.add_argument("-lo", "--log_source", required=False, type=str, help="converter log source")
755
- convert_parser.add_argument("-dp", "--detection_path", required=False, type=str, help="path to a single detection")
756
- convert_parser.add_argument(
757
- "-df",
758
- "--detection_folder",
759
- required=False,
760
- type=str,
761
- help="path to a detection folder"
132
+ def main():
133
+ try:
134
+ configFile = pathlib.Path("contentctl.yml")
135
+
136
+ # We MUST load a config (with testing info) object so that we can
137
+ # properly construct the command line, including 'contentctl test' parameters.
138
+ if not configFile.is_file():
139
+ if "init" not in sys.argv and "--help" not in sys.argv and "-h" not in sys.argv:
140
+ raise Exception(f"'{configFile}' not found in the current directory.\n"
141
+ "Please ensure you are in the correct directory or run 'contentctl init' to create a new content pack.")
142
+
143
+ if "--help" in sys.argv or "-h" in sys.argv:
144
+ print("Warning - contentctl.yml is missing from this directory. The configuration values showed at the default and are informational only.\n"
145
+ "Please ensure that contentctl.yml exists by manually creating it or running 'contentctl init'")
146
+ # Otherwise generate a stub config file.
147
+ # It will be used during init workflow
148
+
149
+ t = test()
150
+ config_obj = t.model_dump()
151
+
152
+ else:
153
+ #The file exists, so load it up!
154
+ config_obj = YmlReader().load_file(configFile)
155
+ t = test.model_validate(config_obj)
156
+ except Exception as e:
157
+ print(f"Error validating 'contentctl.yml':\n{str(e)}")
158
+ sys.exit(1)
159
+
160
+
161
+ # For ease of generating the constructor, we want to allow construction
162
+ # of an object from default values WITHOUT requiring all fields to be declared
163
+ # with defaults OR in the config file. As such, we construct the model rather
164
+ # than model_validating it so that validation does not run on missing required fields.
165
+ # Note that we HAVE model_validated the test object fields already above
166
+
167
+ models = tyro.extras.subcommand_type_from_defaults(
168
+ {
169
+ "init":init.model_validate(config_obj),
170
+ "validate": validate.model_validate(config_obj),
171
+ "report": report.model_validate(config_obj),
172
+ "build":build.model_validate(config_obj),
173
+ "inspect": inspect.model_construct(**t.__dict__),
174
+ "new":new.model_validate(config_obj),
175
+ "test":test.model_validate(config_obj),
176
+ "test_servers":test_servers.model_construct(**t.__dict__),
177
+ "release_notes": release_notes.model_construct(**config_obj),
178
+ "deploy_acs": deploy_acs.model_construct(**t.__dict__),
179
+ #"deploy_rest":deploy_rest()
180
+ }
762
181
  )
763
- convert_parser.add_argument("-o", "--output", required=True, type=str, help="output path to store the detections")
764
- convert_parser.set_defaults(func=convert)
765
-
766
- release_notes_parser.add_argument("--old_tag", "--old_tag", required=False, type=str, help="Choose the tag and compare with previous tag")
767
- release_notes_parser.add_argument("--new_tag", "--new_tag", required=False, type=str, help="Choose the tag and compare with previous tag")
768
- release_notes_parser.add_argument("--latest_branch", "--latest_branch", required=False, type=str, help="Choose the tag and compare with previous tag")
769
182
 
770
- release_notes_parser.set_defaults(func=release_notes)
771
183
 
772
184
 
773
-
774
- # parse them
775
- args = parser.parse_args()
185
+
776
186
 
777
-
778
- print_ascii_art()
779
187
  try:
780
- args.func(args)
188
+ # Since some model(s) were constructed and not model_validated, we have to catch
189
+ # warnings again when creating the cli
190
+ with warnings.catch_warnings(action="ignore"):
191
+ config = tyro.cli(models)
192
+
193
+
194
+ if type(config) == init:
195
+ t.__dict__.update(config.__dict__)
196
+ init_func(t)
197
+ elif type(config) == validate:
198
+ validate_func(config)
199
+ elif type(config) == report:
200
+ report_func(config)
201
+ elif type(config) == build:
202
+ build_func(config)
203
+ elif type(config) == new:
204
+ new_func(config)
205
+ elif type(config) == inspect:
206
+ inspect_func(config)
207
+ elif type(config) == release_notes:
208
+ release_notes_func(config)
209
+ elif type(config) == deploy_acs:
210
+ updated_config = deploy_acs.model_validate(config)
211
+ deploy_acs_func(updated_config)
212
+ elif type(config) == deploy_rest:
213
+ deploy_rest_func(config)
214
+ elif type(config) == test or type(config) == test_servers:
215
+ if type(config) == test:
216
+ #construct the container Infrastructure objects
217
+ config.getContainerInfrastructureObjects()
218
+ #otherwise, they have already been passed as servers
219
+ test_common_func(config)
220
+ else:
221
+ raise Exception(f"Unknown command line type '{type(config).__name__}'")
781
222
  except Exception as e:
782
- print(f"Error during contentctl:\n{str(e)}")
783
223
  import traceback
784
224
  traceback.print_exc()
785
- # traceback.print_stack()
225
+ traceback.print_stack()
226
+ #print(e)
786
227
  sys.exit(1)
228
+