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.
- contentctl/actions/build.py +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
from contentctl.
|
|
26
|
-
from contentctl.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
257
|
-
#
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
"
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
traceback.print_stack()
|
|
226
|
+
#print(e)
|
|
786
227
|
sys.exit(1)
|
|
228
|
+
|