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
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
# Needed for a staticmethod to be able to return an instance of the class it belongs to
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import git
|
|
5
|
-
import validators
|
|
6
|
-
import pathlib
|
|
7
|
-
import yaml
|
|
8
|
-
import os
|
|
9
|
-
from pydantic import BaseModel, validator, root_validator, Extra, Field
|
|
10
|
-
from typing import Union
|
|
11
|
-
import re
|
|
12
|
-
import docker
|
|
13
|
-
import docker.errors
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from contentctl.objects.enums import (
|
|
17
|
-
PostTestBehavior,
|
|
18
|
-
DetectionTestingMode,
|
|
19
|
-
DetectionTestingTargetInfrastructure,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
from contentctl.objects.app import App, ENVIRONMENT_PATH_NOT_SET
|
|
23
|
-
from contentctl.helper.utils import Utils
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ALWAYS_PULL_REPO = False
|
|
27
|
-
PREVIOUSLY_ALLOCATED_PORTS: set[int] = set()
|
|
28
|
-
|
|
29
|
-
LOCAL_APP_DIR = pathlib.Path("apps")
|
|
30
|
-
CONTAINER_APP_DIR = pathlib.Path("/tmp/apps")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def getTestConfigFromYMLFile(path: pathlib.Path):
|
|
34
|
-
try:
|
|
35
|
-
with open(path, "r") as config_handle:
|
|
36
|
-
cfg = yaml.safe_load(config_handle)
|
|
37
|
-
return TestConfig.parse_obj(cfg)
|
|
38
|
-
|
|
39
|
-
except Exception as e:
|
|
40
|
-
print(f"Error loading test configuration file '{path}': {str(e)}")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class Infrastructure(BaseModel, extra=Extra.forbid, validate_assignment=True):
|
|
44
|
-
splunk_app_username: Union[str, None] = Field(
|
|
45
|
-
default="admin", title="The name of the user for testing"
|
|
46
|
-
)
|
|
47
|
-
splunk_app_password: Union[str, None] = Field(
|
|
48
|
-
default="password", title="Password for logging into Splunk Server"
|
|
49
|
-
)
|
|
50
|
-
instance_address: str = Field(
|
|
51
|
-
default="127.0.0.1",
|
|
52
|
-
title="Domain name of IP address of Splunk server to be used for testing. Do NOT use a protocol, like http(s):// or 'localhost'",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
instance_name: str = Field(
|
|
56
|
-
default="Splunk_Server_Name",
|
|
57
|
-
title="Template to be used for naming the Splunk Test Containers or referring to Test Servers.",
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
hec_port: int = Field(default=8088, title="HTTP Event Collector Port")
|
|
61
|
-
web_ui_port: int = Field(default=8000, title="Web UI Port")
|
|
62
|
-
api_port: int = Field(default=8089, title="REST API Port")
|
|
63
|
-
|
|
64
|
-
@staticmethod
|
|
65
|
-
def get_infrastructure_containers(num_containers:int=1, splunk_app_username:str="admin", splunk_app_password:str="password", instance_name_template="splunk_contentctl_{index}")->list[Infrastructure]:
|
|
66
|
-
containers:list[Infrastructure] = []
|
|
67
|
-
if num_containers < 0:
|
|
68
|
-
raise ValueError(f"Error - you must specifiy 1 or more containers, not {num_containers}.")
|
|
69
|
-
|
|
70
|
-
#Get the starting ports
|
|
71
|
-
i = Infrastructure() #Instantiate to get the base port numbers
|
|
72
|
-
|
|
73
|
-
for index in range(0, num_containers):
|
|
74
|
-
containers.append(Infrastructure(splunk_app_username=splunk_app_username,
|
|
75
|
-
splunk_app_password=splunk_app_password,
|
|
76
|
-
instance_name=instance_name_template.format(index=index),
|
|
77
|
-
hec_port=i.hec_port+(index*2),
|
|
78
|
-
web_ui_port=i.web_ui_port+index,
|
|
79
|
-
api_port=i.api_port+(index*2)))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return containers
|
|
83
|
-
|
|
84
|
-
@validator("instance_name")
|
|
85
|
-
def validate_instance_name(cls,v,values):
|
|
86
|
-
if not re.fullmatch("[a-zA-Z0-9][a-zA-Z0-9_.-]*", v):
|
|
87
|
-
raise ValueError(f"The instance_name '{v}' is not valid. Please use an instance name which matches the regular expression '[a-zA-Z0-9][a-zA-Z0-9_.-]*'")
|
|
88
|
-
else:
|
|
89
|
-
return v
|
|
90
|
-
|
|
91
|
-
@validator("instance_address")
|
|
92
|
-
def validate_instance_address(cls, v, values):
|
|
93
|
-
try:
|
|
94
|
-
if v.startswith("http"):
|
|
95
|
-
raise (Exception("should not begin with http"))
|
|
96
|
-
is_ipv4 = validators.ipv4(v)
|
|
97
|
-
if bool(is_ipv4):
|
|
98
|
-
return v
|
|
99
|
-
is_domain_name = validators.domain(v)
|
|
100
|
-
if bool(is_domain_name):
|
|
101
|
-
import socket
|
|
102
|
-
|
|
103
|
-
try:
|
|
104
|
-
socket.gethostbyname(v)
|
|
105
|
-
return v
|
|
106
|
-
except Exception as e:
|
|
107
|
-
pass
|
|
108
|
-
raise (Exception("DNS Lookup failed"))
|
|
109
|
-
raise (Exception(f"not an IPV4 address or a domain name"))
|
|
110
|
-
except Exception as e:
|
|
111
|
-
raise (
|
|
112
|
-
Exception(
|
|
113
|
-
f"Error, failed to validate instance_address '{v}': {str(e)}"
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
@validator("splunk_app_password")
|
|
120
|
-
def validate_splunk_app_password(cls, v):
|
|
121
|
-
if v == None:
|
|
122
|
-
# No app password was provided, so generate one
|
|
123
|
-
v = Utils.get_random_password()
|
|
124
|
-
else:
|
|
125
|
-
MIN_PASSWORD_LENGTH = 6
|
|
126
|
-
if len(v) < MIN_PASSWORD_LENGTH:
|
|
127
|
-
raise (
|
|
128
|
-
ValueError(
|
|
129
|
-
f"Password is less than {MIN_PASSWORD_LENGTH} characters long. This password is extremely weak, please change it."
|
|
130
|
-
)
|
|
131
|
-
)
|
|
132
|
-
return v
|
|
133
|
-
|
|
134
|
-
@validator("hec_port", "web_ui_port", "api_port", each_item=True)
|
|
135
|
-
def validate_ports_range(cls, v):
|
|
136
|
-
if v < 2:
|
|
137
|
-
raise (
|
|
138
|
-
ValueError(
|
|
139
|
-
f"Error, invalid Port number. Port must be between 2-65535: {v}"
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
elif v > 65535:
|
|
143
|
-
raise (
|
|
144
|
-
ValueError(
|
|
145
|
-
f"Error, invalid Port number. Port must be between 2-65535: {v}"
|
|
146
|
-
)
|
|
147
|
-
)
|
|
148
|
-
return v
|
|
149
|
-
|
|
150
|
-
@validator("hec_port", "web_ui_port", "api_port", each_item=False)
|
|
151
|
-
def validate_ports_overlap(cls, v):
|
|
152
|
-
|
|
153
|
-
if type(v) is not list:
|
|
154
|
-
# Otherwise this throws error when we update a single field
|
|
155
|
-
return v
|
|
156
|
-
if len(set(v)) != len(v):
|
|
157
|
-
raise (ValueError(f"Duplicate ports detected: [{v}]"))
|
|
158
|
-
|
|
159
|
-
return v
|
|
160
|
-
|
|
161
|
-
class InfrastructureConfig(BaseModel, extra=Extra.forbid, validate_assignment=True):
|
|
162
|
-
infrastructure_type: DetectionTestingTargetInfrastructure = Field(
|
|
163
|
-
default=DetectionTestingTargetInfrastructure.container,
|
|
164
|
-
title=f"Control where testing should be launched. Choose one of {DetectionTestingTargetInfrastructure._member_names_}",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
persist_and_reuse_container:bool = True
|
|
168
|
-
|
|
169
|
-
full_image_path: str = Field(
|
|
170
|
-
default="registry.hub.docker.com/splunk/splunk:latest",
|
|
171
|
-
title="Full path to the container image to be used",
|
|
172
|
-
)
|
|
173
|
-
infrastructures: list[Infrastructure] = []
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@validator("infrastructure_type")
|
|
177
|
-
def validate_infrastructure_type(cls, v, values):
|
|
178
|
-
if v == DetectionTestingTargetInfrastructure.server:
|
|
179
|
-
# No need to validate that the docker client is available
|
|
180
|
-
return v
|
|
181
|
-
elif v == DetectionTestingTargetInfrastructure.container:
|
|
182
|
-
# we need to make sure we can actually get the docker client from the environment
|
|
183
|
-
try:
|
|
184
|
-
docker.client.from_env()
|
|
185
|
-
except Exception as e:
|
|
186
|
-
raise (
|
|
187
|
-
Exception(
|
|
188
|
-
f"Error, failed to get docker client. Is Docker Installed and running "
|
|
189
|
-
f"and are docker environment variables set properly? Error:\n\t{str(e)}"
|
|
190
|
-
)
|
|
191
|
-
)
|
|
192
|
-
return v
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
@validator("full_image_path")
|
|
198
|
-
def validate_full_image_path(cls, v, values):
|
|
199
|
-
if (
|
|
200
|
-
values.get("infrastructure_type", None)
|
|
201
|
-
== DetectionTestingTargetInfrastructure.server.value
|
|
202
|
-
):
|
|
203
|
-
print(
|
|
204
|
-
f"No need to validate target image path {v}, testing target is preconfigured server"
|
|
205
|
-
)
|
|
206
|
-
return v
|
|
207
|
-
# This behavior may change if we start supporting local/offline containers and
|
|
208
|
-
# the logic to build them
|
|
209
|
-
if ":" not in v:
|
|
210
|
-
raise (
|
|
211
|
-
ValueError(
|
|
212
|
-
f"Error, the image_name {v} does not include a tag. A tagged container MUST be included to ensure consistency when testing"
|
|
213
|
-
)
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
# Check to make sure we have the latest version of the image
|
|
217
|
-
# We have this as a wrapped, nested try/except because if we
|
|
218
|
-
# encounter some error in trying to get the latest version, but
|
|
219
|
-
# we do have some version already, we will allow the test to continue.
|
|
220
|
-
# For example, this may occur if an image has been previously downloaded,
|
|
221
|
-
# but the server no longer has internet connectivity and can't get the
|
|
222
|
-
# image again. in this case, don't fail - continue with the test
|
|
223
|
-
try:
|
|
224
|
-
try:
|
|
225
|
-
# connectivity to docker server is validated previously
|
|
226
|
-
client = docker.from_env()
|
|
227
|
-
print(
|
|
228
|
-
f"Getting the latest version of the container image: {v}...",
|
|
229
|
-
end="",
|
|
230
|
-
flush=True,
|
|
231
|
-
)
|
|
232
|
-
client.images.pull(v, platform="linux/amd64")
|
|
233
|
-
print("done")
|
|
234
|
-
except docker.errors.APIError as e:
|
|
235
|
-
print("error")
|
|
236
|
-
if e.is_client_error():
|
|
237
|
-
if "invalid reference format" in str(e.explanation):
|
|
238
|
-
simple_explanation = f"The format of the docker image reference is incorrect. Please use a valid image reference"
|
|
239
|
-
else:
|
|
240
|
-
simple_explanation = (
|
|
241
|
-
f"The most likely cause of this error is that the image/tag "
|
|
242
|
-
"does not exist or it is stored in a private repository and you are not logged in."
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
elif e.is_server_error():
|
|
246
|
-
simple_explanation = (
|
|
247
|
-
f"The mostly likely cause is that the server cannot be reached. "
|
|
248
|
-
"Please ensure that the server hosting your docker image is available "
|
|
249
|
-
"and you have internet access, if required."
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
else:
|
|
253
|
-
simple_explanation = f"Unable to pull image {v} for UNKNOWN reason. Please consult the detailed error below."
|
|
254
|
-
|
|
255
|
-
verbose_explanation = e.explanation
|
|
256
|
-
|
|
257
|
-
raise (
|
|
258
|
-
ValueError(
|
|
259
|
-
f"Error Pulling Docker Image '{v}'\n - EXPLANATION: {simple_explanation} (full error text: '{verbose_explanation}'"
|
|
260
|
-
)
|
|
261
|
-
)
|
|
262
|
-
except Exception as e:
|
|
263
|
-
print("error")
|
|
264
|
-
raise (ValueError(f"Uknown error pulling Docker Image '{v}': {str(e)}"))
|
|
265
|
-
|
|
266
|
-
except Exception as e:
|
|
267
|
-
# There was some exception that prevented us from getting the latest version
|
|
268
|
-
# of the image. However, if we already have it, use the current version and
|
|
269
|
-
# down fully raise the exception - just use it
|
|
270
|
-
client = docker.from_env()
|
|
271
|
-
try:
|
|
272
|
-
client.api.inspect_image(v)
|
|
273
|
-
print(e)
|
|
274
|
-
print(
|
|
275
|
-
f"We will default to using the version of the image {v} which has "
|
|
276
|
-
"already been downloaded to this machine. Please note that it may be out of date."
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
except Exception as e2:
|
|
280
|
-
raise (
|
|
281
|
-
ValueError(
|
|
282
|
-
f"{str(e)}Image is not previously cached, so we could not use an old version."
|
|
283
|
-
)
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
return v
|
|
287
|
-
|
|
288
|
-
@validator("infrastructures", always=True)
|
|
289
|
-
def validate_infrastructures(cls, v, values):
|
|
290
|
-
MAX_RECOMMENDED_CONTAINERS_BEFORE_WARNING = 2
|
|
291
|
-
if values.get("infrastructure_type",None) == DetectionTestingTargetInfrastructure.container and len(v) == 0:
|
|
292
|
-
v = [Infrastructure()]
|
|
293
|
-
|
|
294
|
-
if len(v) < 1:
|
|
295
|
-
#print("Fix number of infrastructure validation later")
|
|
296
|
-
return v
|
|
297
|
-
raise (
|
|
298
|
-
ValueError(
|
|
299
|
-
f"Error validating infrastructures. Test must be run with AT LEAST 1 infrastructure, not {len(v)}"
|
|
300
|
-
)
|
|
301
|
-
)
|
|
302
|
-
if (values.get("infrastructure_type", None) == DetectionTestingTargetInfrastructure.container.value) and len(v) > MAX_RECOMMENDED_CONTAINERS_BEFORE_WARNING:
|
|
303
|
-
print(
|
|
304
|
-
f"You requested to run with [{v}] containers which may use a very large amount of resources "
|
|
305
|
-
"as they all run in parallel. The maximum suggested number of parallel containers is "
|
|
306
|
-
f"[{MAX_RECOMMENDED_CONTAINERS_BEFORE_WARNING}]. We will do what you asked, but be warned!"
|
|
307
|
-
)
|
|
308
|
-
return v
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
@validator("infrastructures", each_item=False)
|
|
312
|
-
def validate_ports_overlap(cls, v, values):
|
|
313
|
-
ports = set()
|
|
314
|
-
if values.get("infrastructure_type", None) == DetectionTestingTargetInfrastructure.server.value:
|
|
315
|
-
#ports are allowed to overlap, they are on different servers
|
|
316
|
-
return v
|
|
317
|
-
|
|
318
|
-
if len(v) == 0:
|
|
319
|
-
raise ValueError("Error, there must be at least one test infrastructure defined in infrastructures.")
|
|
320
|
-
for infrastructure in v:
|
|
321
|
-
for k in ["hec_port", "web_ui_port", "api_port"]:
|
|
322
|
-
if getattr(infrastructure, k) in ports:
|
|
323
|
-
raise ValueError(f"Port {getattr(infrastructure, k)} used more than once in container infrastructure ports")
|
|
324
|
-
ports.add(getattr(infrastructure, k))
|
|
325
|
-
return v
|
|
326
|
-
|
|
327
|
-
class VersionControlConfig(BaseModel, extra=Extra.forbid, validate_assignment=True):
|
|
328
|
-
repo_path: str = Field(default=".", title="Path to the root of your app")
|
|
329
|
-
repo_url: str = Field(
|
|
330
|
-
default="https://github.com/your_organization/your_repo",
|
|
331
|
-
title="HTTP(s) path to the repo for repo_path. If this field is blank, it will be inferred from the repo",
|
|
332
|
-
)
|
|
333
|
-
target_branch: str = Field(default="main", title="Main branch of the repo or target of a Pull Request/Merge Request.")
|
|
334
|
-
test_branch: str = Field(default="main", title="Branch of the repo to be tested, if applicable.")
|
|
335
|
-
commit_hash: Union[str,None] = Field(default=None, title="Commit hash of the repo state to be tested, if applicable")
|
|
336
|
-
pr_number: Union[int,None] = Field(default=None, title="The number of the PR to test")
|
|
337
|
-
|
|
338
|
-
@validator('repo_path')
|
|
339
|
-
def validate_repo_path(cls,v):
|
|
340
|
-
print(f"checking repo path '{v}'")
|
|
341
|
-
try:
|
|
342
|
-
path = pathlib.Path(v)
|
|
343
|
-
except Exception as e:
|
|
344
|
-
|
|
345
|
-
raise(ValueError(f"Error, the provided path is is not a valid path: '{v}'"))
|
|
346
|
-
|
|
347
|
-
try:
|
|
348
|
-
r = git.Repo(path)
|
|
349
|
-
except Exception as e:
|
|
350
|
-
|
|
351
|
-
raise(ValueError(f"Error, the provided path is not a valid git repo: '{path}'"))
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
|
|
355
|
-
if ALWAYS_PULL_REPO:
|
|
356
|
-
r.remotes.origin.pull()
|
|
357
|
-
except Exception as e:
|
|
358
|
-
raise ValueError(f"Error pulling git repository {v}: {str(e)}")
|
|
359
|
-
print("repo path looks good")
|
|
360
|
-
return v
|
|
361
|
-
|
|
362
|
-
@validator('repo_url')
|
|
363
|
-
def validate_repo_url(cls, v, values):
|
|
364
|
-
#First try to get the value from the repo
|
|
365
|
-
try:
|
|
366
|
-
remotes = git.Repo(values['repo_path']).remotes
|
|
367
|
-
except Exception as e:
|
|
368
|
-
raise ValueError(f"Error - repo at {values['repo_path']} has no remotes. Repo must be tracked in a remote git repo.")
|
|
369
|
-
|
|
370
|
-
try:
|
|
371
|
-
remote_url_from_repo = remotes.origin.url
|
|
372
|
-
except Exception as e:
|
|
373
|
-
raise(ValueError(f"Error reading remote_url from the repo located at '{values['repo_path']}'"))
|
|
374
|
-
|
|
375
|
-
if v is not None and remote_url_from_repo != v:
|
|
376
|
-
raise(ValueError(f"The url of the remote repo supplied in the config file {v} does not "\
|
|
377
|
-
f"match the value read from the repository at {values['repo_path']}, {remote_url_from_repo}"))
|
|
378
|
-
|
|
379
|
-
if v is None:
|
|
380
|
-
v = remote_url_from_repo
|
|
381
|
-
|
|
382
|
-
#Ensure that the url is the proper format
|
|
383
|
-
# try:
|
|
384
|
-
# if bool(validators.url(v)) == False:
|
|
385
|
-
# raise(Exception)
|
|
386
|
-
# except:
|
|
387
|
-
# raise(ValueError(f"Error validating the repo_url. The url is not valid: {v}"))
|
|
388
|
-
|
|
389
|
-
return v
|
|
390
|
-
|
|
391
|
-
@validator('target_branch')
|
|
392
|
-
def valid_target_branch(cls, v, values):
|
|
393
|
-
if v is None:
|
|
394
|
-
print(f"target_branch is not supplied. Inferring from '{values['repo_path']}'...",end='')
|
|
395
|
-
|
|
396
|
-
target_branch = Utils.get_default_branch_name(values['repo_path'], values['repo_url'])
|
|
397
|
-
print(f"target_branch name '{target_branch}' inferred'")
|
|
398
|
-
#continue with the validation
|
|
399
|
-
v = target_branch
|
|
400
|
-
|
|
401
|
-
try:
|
|
402
|
-
Utils.validate_git_branch_name(values['repo_path'],values['repo_url'], v)
|
|
403
|
-
except Exception as e:
|
|
404
|
-
raise ValueError(f"Error validating target_branch: {str(e)}")
|
|
405
|
-
return v
|
|
406
|
-
|
|
407
|
-
@validator('test_branch')
|
|
408
|
-
def validate_test_branch(cls, v, values):
|
|
409
|
-
if v is None:
|
|
410
|
-
print(f"No test_branch provided, so we will default to using the target_branch '{values['target_branch']}'")
|
|
411
|
-
v = values['target_branch']
|
|
412
|
-
try:
|
|
413
|
-
Utils.validate_git_branch_name(values['repo_path'],values['repo_url'], v)
|
|
414
|
-
except Exception as e:
|
|
415
|
-
raise ValueError(f"Error validating test_branch: {str(e)}")
|
|
416
|
-
|
|
417
|
-
r = git.Repo(values.get("repo_path"))
|
|
418
|
-
try:
|
|
419
|
-
if r.active_branch.name != v:
|
|
420
|
-
print(f"We are trying to test {v} but the current active branch is {r.active_branch}")
|
|
421
|
-
print(f"Checking out {v}")
|
|
422
|
-
r.git.checkout(v)
|
|
423
|
-
except Exception as e:
|
|
424
|
-
raise ValueError(f"Error checking out test_branch '{v}': {str(e)}")
|
|
425
|
-
return v
|
|
426
|
-
|
|
427
|
-
@validator('commit_hash')
|
|
428
|
-
def validate_commit_hash(cls, v, values):
|
|
429
|
-
try:
|
|
430
|
-
#We can a hash with this function too
|
|
431
|
-
Utils.validate_git_hash(values['repo_path'],values['repo_url'], v, values['test_branch'])
|
|
432
|
-
except Exception as e:
|
|
433
|
-
raise ValueError(f"Error validating commit_hash '{v}': {str(e)}")
|
|
434
|
-
return v
|
|
435
|
-
|
|
436
|
-
@validator('pr_number')
|
|
437
|
-
def validate_pr_number(cls, v, values):
|
|
438
|
-
if v == None:
|
|
439
|
-
return v
|
|
440
|
-
|
|
441
|
-
hash = Utils.validate_git_pull_request(values['repo_path'], v)
|
|
442
|
-
|
|
443
|
-
#Ensure that the hash is equal to the one in the config file, if it exists.
|
|
444
|
-
if values['commit_hash'] is None:
|
|
445
|
-
values['commit_hash'] = hash
|
|
446
|
-
else:
|
|
447
|
-
if values['commit_hash'] != hash:
|
|
448
|
-
raise(ValueError(f"commit_hash specified in configuration was {values['commit_hash']}, but commit_hash"\
|
|
449
|
-
f" from pr_number {v} was {hash}. These must match. If you're testing"\
|
|
450
|
-
" a PR, you probably do NOT want to provide the commit_hash in the configuration file "\
|
|
451
|
-
"and always want to test the head of the PR. This will be done automatically if you do "\
|
|
452
|
-
"not provide the commit_hash."))
|
|
453
|
-
|
|
454
|
-
return v
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
class TestConfig(BaseModel, extra=Extra.forbid, validate_assignment=True):
|
|
458
|
-
|
|
459
|
-
version_control_config: Union[VersionControlConfig,None] = VersionControlConfig()
|
|
460
|
-
|
|
461
|
-
infrastructure_config: InfrastructureConfig = Field(
|
|
462
|
-
default=InfrastructureConfig(),
|
|
463
|
-
title=f"The infrastructure for testing to be run on",
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
post_test_behavior: PostTestBehavior = Field(
|
|
468
|
-
default=PostTestBehavior.pause_on_failure,
|
|
469
|
-
title=f"What to do after a test has completed. Choose one of {PostTestBehavior._member_names_}",
|
|
470
|
-
)
|
|
471
|
-
mode: DetectionTestingMode = Field(
|
|
472
|
-
default=DetectionTestingMode.all,
|
|
473
|
-
title=f"Control which detections should be tested. Choose one of {DetectionTestingMode._member_names_}",
|
|
474
|
-
)
|
|
475
|
-
detections_list: Union[list[str], None] = Field(
|
|
476
|
-
default=None, title="List of paths to detections which should be tested"
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
splunkbase_username: Union[str, None] = Field(
|
|
481
|
-
default=None,
|
|
482
|
-
title="The username for logging into Splunkbase in case apps must be downloaded",
|
|
483
|
-
)
|
|
484
|
-
splunkbase_password: Union[str, None] = Field(
|
|
485
|
-
default=None,
|
|
486
|
-
title="The password for logging into Splunkbase in case apps must be downloaded",
|
|
487
|
-
)
|
|
488
|
-
apps: list[App] = Field(
|
|
489
|
-
default=App.get_default_apps(),
|
|
490
|
-
title="A list of all the apps to be installed on each container",
|
|
491
|
-
)
|
|
492
|
-
enable_integration_testing: bool = Field(
|
|
493
|
-
default=False,
|
|
494
|
-
title="Whether integration testing should be enabled, in addition to unit testing (requires a configured Splunk"
|
|
495
|
-
" instance with ES installed)"
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
# Ensure that at least 1 of test_branch, commit_hash, and/or pr_number were passed.
|
|
507
|
-
# Otherwise, what are we testing??
|
|
508
|
-
# @root_validator(pre=False)
|
|
509
|
-
def ensure_there_is_something_to_test(cls, values):
|
|
510
|
-
if 'test_branch' not in values and 'commit_hash' not in values and'pr_number' not in values:
|
|
511
|
-
if 'mode' in values and values['mode'] == DetectionTestingMode.changes:
|
|
512
|
-
raise(ValueError(f"Under mode [{DetectionTestingMode.changes}], 'test_branch', 'commit_hash', and/or 'pr_number' must be defined so that we know what to test."))
|
|
513
|
-
|
|
514
|
-
return values
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
# presumably the post test behavior is validated by the enum?
|
|
519
|
-
# presumably the mode is validated by the enum?
|
|
520
|
-
|
|
521
|
-
@validator("detections_list", always=True)
|
|
522
|
-
def validate_detections_list(cls, v, values):
|
|
523
|
-
# A detections list can only be provided if the mode is selected
|
|
524
|
-
# otherwise, we must throw an error
|
|
525
|
-
|
|
526
|
-
# First check the mode
|
|
527
|
-
if values["mode"] != DetectionTestingMode.selected:
|
|
528
|
-
if v is not None:
|
|
529
|
-
# We intentionally raise an error even if the list is an empty list
|
|
530
|
-
raise (
|
|
531
|
-
ValueError(
|
|
532
|
-
f"For Detection Testing Mode '{values['mode']}', "
|
|
533
|
-
f"'detections_list' MUST be none. Instead, it was a list containing {len(v)} detections."
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
return v
|
|
537
|
-
|
|
538
|
-
# Mode is DetectionTestingMode.selected - verify the paths of all the detections
|
|
539
|
-
all_errors = []
|
|
540
|
-
if v == None:
|
|
541
|
-
raise (
|
|
542
|
-
ValueError(
|
|
543
|
-
f"mode is '{DetectionTestingMode.selected}', but detections_list was not provided."
|
|
544
|
-
)
|
|
545
|
-
)
|
|
546
|
-
for detection in v:
|
|
547
|
-
try:
|
|
548
|
-
if not pathlib.Path(detection).exists():
|
|
549
|
-
all_errors.append(detection)
|
|
550
|
-
except Exception as e:
|
|
551
|
-
all_errors.append(
|
|
552
|
-
f"Unexpected error validating path '{detection}': {str(e)}"
|
|
553
|
-
)
|
|
554
|
-
if len(all_errors):
|
|
555
|
-
joined_errors = "\n\t".join(all_errors)
|
|
556
|
-
raise (
|
|
557
|
-
ValueError(
|
|
558
|
-
f"Paths to the following detections in 'detections_list' "
|
|
559
|
-
f"were invalid: \n\t{joined_errors}"
|
|
560
|
-
)
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
return v
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
@validator("splunkbase_username")
|
|
572
|
-
def validate_splunkbase_username(cls, v):
|
|
573
|
-
return v
|
|
574
|
-
|
|
575
|
-
@validator("splunkbase_password")
|
|
576
|
-
def validate_splunkbase_password(cls, v, values):
|
|
577
|
-
if values["splunkbase_username"] == None:
|
|
578
|
-
return v
|
|
579
|
-
elif (v == None and values["splunkbase_username"] != None) or (
|
|
580
|
-
v != None and values["splunkbase_username"] == None
|
|
581
|
-
):
|
|
582
|
-
raise (
|
|
583
|
-
ValueError(
|
|
584
|
-
"splunkbase_username OR splunkbase_password "
|
|
585
|
-
"was provided, but not both. You must provide"
|
|
586
|
-
" neither of these value or both, but not just "
|
|
587
|
-
"1 of them"
|
|
588
|
-
)
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
else:
|
|
592
|
-
return v
|
|
593
|
-
|
|
594
|
-
@validator("apps",)
|
|
595
|
-
def validate_apps(cls, v, values):
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
app_errors = []
|
|
599
|
-
|
|
600
|
-
# ensure that the splunkbase username and password are provided
|
|
601
|
-
username = values["splunkbase_username"]
|
|
602
|
-
password = values["splunkbase_password"]
|
|
603
|
-
app_directory = LOCAL_APP_DIR
|
|
604
|
-
try:
|
|
605
|
-
os.makedirs(LOCAL_APP_DIR, exist_ok=True)
|
|
606
|
-
except Exception as e:
|
|
607
|
-
raise (
|
|
608
|
-
Exception(f"Error: When trying to create {CONTAINER_APP_DIR}: {str(e)}")
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
for app in v:
|
|
612
|
-
if app.environment_path != ENVIRONMENT_PATH_NOT_SET:
|
|
613
|
-
#Avoid re-configuring the apps that have already been configured.
|
|
614
|
-
continue
|
|
615
|
-
|
|
616
|
-
try:
|
|
617
|
-
app.configure_app_source_for_container(
|
|
618
|
-
username, password, app_directory, CONTAINER_APP_DIR
|
|
619
|
-
)
|
|
620
|
-
except Exception as e:
|
|
621
|
-
error_string = f"Unable to prepare app '{app.title}': {str(e)}"
|
|
622
|
-
app_errors.append(error_string)
|
|
623
|
-
|
|
624
|
-
if len(app_errors) != 0:
|
|
625
|
-
error_string = "\n\t".join(app_errors)
|
|
626
|
-
raise (ValueError(f"Error preparing apps to install:\n\t{error_string}"))
|
|
627
|
-
|
|
628
|
-
return v
|
|
629
|
-
|
|
630
|
-
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
{% for detection in objects %}
|
|
3
|
-
[{{ detection.name | replace(' ', '_') | replace('-', '_') | replace('.', '_') | replace('/', '_') | lower + '_filter' }}]
|
|
4
|
-
definition = search *
|
|
5
|
-
description = Update this macro to limit the output results to filter out false positives.
|
|
6
|
-
|
|
7
|
-
{% endfor %}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Splunk ES Content Update
|
|
2
|
-
|
|
3
|
-
This subscription service delivers pre-packaged Security Content for use with Splunk Enterprise Security. Subscribers get regular updates to help security practitioners more quickly address ongoing and time-sensitive customer problems and threats.
|
|
4
|
-
|
|
5
|
-
Requires Splunk Enterprise Security version 4.5 or greater.
|
|
6
|
-
|
|
7
|
-
For more information please visit the [Splunk ES Content Update user documentation](https://docs.splunk.com/Documentation/ESSOC).
|