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
@@ -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).