contentctl 5.2.0__py3-none-any.whl → 5.3.0__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 +5 -43
- contentctl/actions/detection_testing/DetectionTestingManager.py +64 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +146 -42
- contentctl/actions/detection_testing/views/DetectionTestingView.py +5 -6
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +2 -0
- contentctl/actions/initialize.py +35 -9
- contentctl/actions/release_notes.py +14 -12
- contentctl/actions/test.py +16 -20
- contentctl/actions/validate.py +8 -15
- contentctl/helper/utils.py +69 -20
- contentctl/input/director.py +147 -119
- contentctl/input/yml_reader.py +39 -27
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +94 -20
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +548 -8
- contentctl/objects/baseline.py +24 -6
- contentctl/objects/config.py +32 -8
- contentctl/objects/content_versioning_service.py +508 -0
- contentctl/objects/correlation_search.py +53 -63
- contentctl/objects/dashboard.py +15 -1
- contentctl/objects/data_source.py +13 -1
- contentctl/objects/deployment.py +23 -9
- contentctl/objects/detection.py +2 -0
- contentctl/objects/enums.py +28 -18
- contentctl/objects/investigation.py +40 -20
- contentctl/objects/lookup.py +61 -5
- contentctl/objects/macro.py +19 -4
- contentctl/objects/playbook.py +16 -2
- contentctl/objects/rba.py +1 -33
- contentctl/objects/removed_security_content_object.py +50 -0
- contentctl/objects/security_content_object.py +1 -0
- contentctl/objects/story.py +37 -5
- contentctl/output/api_json_output.py +5 -3
- contentctl/output/conf_output.py +9 -1
- contentctl/output/runtime_csv_writer.py +111 -0
- contentctl/output/svg_output.py +4 -5
- contentctl/output/templates/savedsearches_detections.j2 +2 -6
- {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/METADATA +4 -3
- {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/RECORD +41 -39
- {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/WHEEL +1 -1
- contentctl/output/data_source_writer.py +0 -52
- {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/LICENSE.md +0 -0
- {contentctl-5.2.0.dist-info → contentctl-5.3.0.dist-info}/entry_points.txt +0 -0
contentctl/actions/build.py
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import pathlib
|
|
1
4
|
import shutil
|
|
2
|
-
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
|
|
5
7
|
from contentctl.input.director import DirectorOutputDto
|
|
8
|
+
from contentctl.objects.config import build
|
|
9
|
+
from contentctl.output.api_json_output import ApiJsonOutput
|
|
6
10
|
from contentctl.output.conf_output import ConfOutput
|
|
7
11
|
from contentctl.output.conf_writer import ConfWriter
|
|
8
|
-
from contentctl.output.api_json_output import ApiJsonOutput
|
|
9
|
-
from contentctl.output.data_source_writer import DataSourceWriter
|
|
10
|
-
from contentctl.objects.lookup import CSVLookup, Lookup_Type
|
|
11
|
-
import pathlib
|
|
12
|
-
import json
|
|
13
|
-
import datetime
|
|
14
|
-
import uuid
|
|
15
|
-
|
|
16
|
-
from contentctl.objects.config import build
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
@dataclass(frozen=True)
|
|
@@ -28,39 +23,6 @@ class Build:
|
|
|
28
23
|
updated_conf_files: set[pathlib.Path] = set()
|
|
29
24
|
conf_output = ConfOutput(input_dto.config)
|
|
30
25
|
|
|
31
|
-
# Construct a path to a YML that does not actually exist.
|
|
32
|
-
# We mock this "fake" path since the YML does not exist.
|
|
33
|
-
# This ensures the checking for the existence of the CSV is correct
|
|
34
|
-
data_sources_fake_yml_path = (
|
|
35
|
-
input_dto.config.getPackageDirectoryPath()
|
|
36
|
-
/ "lookups"
|
|
37
|
-
/ "data_sources.yml"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
# Construct a special lookup whose CSV is created at runtime and
|
|
41
|
-
# written directly into the lookups folder. We will delete this after a build,
|
|
42
|
-
# assuming that it is successful.
|
|
43
|
-
data_sources_lookup_csv_path = (
|
|
44
|
-
input_dto.config.getPackageDirectoryPath()
|
|
45
|
-
/ "lookups"
|
|
46
|
-
/ "data_sources.csv"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
DataSourceWriter.writeDataSourceCsv(
|
|
50
|
-
input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path
|
|
51
|
-
)
|
|
52
|
-
input_dto.director_output_dto.addContentToDictMappings(
|
|
53
|
-
CSVLookup.model_construct(
|
|
54
|
-
name="data_sources",
|
|
55
|
-
id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
|
|
56
|
-
version=1,
|
|
57
|
-
author=input_dto.config.app.author_name,
|
|
58
|
-
date=datetime.date.today(),
|
|
59
|
-
description="A lookup file that will contain the data source objects for detections.",
|
|
60
|
-
lookup_type=Lookup_Type.csv,
|
|
61
|
-
file_path=data_sources_fake_yml_path,
|
|
62
|
-
)
|
|
63
|
-
)
|
|
64
26
|
updated_conf_files.update(conf_output.writeHeaders())
|
|
65
27
|
updated_conf_files.update(
|
|
66
28
|
conf_output.writeLookups(input_dto.director_output_dto.lookups)
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import datetime
|
|
3
|
+
import signal
|
|
4
|
+
import traceback
|
|
5
|
+
from dataclasses import dataclass
|
|
1
6
|
from typing import List, Union
|
|
2
|
-
|
|
7
|
+
|
|
8
|
+
import docker
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
3
11
|
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
|
|
4
12
|
DetectionTestingInfrastructure,
|
|
13
|
+
DetectionTestingManagerOutputDto,
|
|
5
14
|
)
|
|
6
15
|
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureContainer import (
|
|
7
16
|
DetectionTestingInfrastructureContainer,
|
|
@@ -9,24 +18,12 @@ from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfras
|
|
|
9
18
|
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructureServer import (
|
|
10
19
|
DetectionTestingInfrastructureServer,
|
|
11
20
|
)
|
|
12
|
-
import signal
|
|
13
|
-
import datetime
|
|
14
|
-
|
|
15
|
-
# from queue import Queue
|
|
16
|
-
from dataclasses import dataclass
|
|
17
|
-
|
|
18
|
-
# import threading
|
|
19
|
-
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
|
|
20
|
-
DetectionTestingManagerOutputDto,
|
|
21
|
-
)
|
|
22
21
|
from contentctl.actions.detection_testing.views.DetectionTestingView import (
|
|
23
22
|
DetectionTestingView,
|
|
24
23
|
)
|
|
25
|
-
from contentctl.objects.
|
|
26
|
-
from pydantic import BaseModel
|
|
24
|
+
from contentctl.objects.config import Container, Infrastructure, test, test_servers
|
|
27
25
|
from contentctl.objects.detection import Detection
|
|
28
|
-
import
|
|
29
|
-
import docker
|
|
26
|
+
from contentctl.objects.enums import PostTestBehavior
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
@dataclass(frozen=False)
|
|
@@ -63,12 +60,14 @@ class DetectionTestingManager(BaseModel):
|
|
|
63
60
|
# a newline '\r\n' which will cause that wait to stop
|
|
64
61
|
print("*******************************")
|
|
65
62
|
print(
|
|
66
|
-
"If testing is paused and you are debugging a detection, you MUST hit CTRL-D
|
|
63
|
+
"If testing is paused and you are debugging a detection, you MUST hit CTRL-D "
|
|
64
|
+
"at the prompt to complete shutdown."
|
|
67
65
|
)
|
|
68
66
|
print("*******************************")
|
|
69
67
|
|
|
70
68
|
signal.signal(signal.SIGINT, sigint_handler)
|
|
71
69
|
|
|
70
|
+
# TODO (#337): futures can be hard to maintain/debug; let's consider alternatives
|
|
72
71
|
with (
|
|
73
72
|
concurrent.futures.ThreadPoolExecutor(
|
|
74
73
|
max_workers=len(self.input_dto.config.test_instances),
|
|
@@ -80,10 +79,19 @@ class DetectionTestingManager(BaseModel):
|
|
|
80
79
|
max_workers=len(self.input_dto.config.test_instances),
|
|
81
80
|
) as view_shutdowner,
|
|
82
81
|
):
|
|
82
|
+
# Capture any errors for reporting at the end after all threads have been gathered
|
|
83
|
+
errors: dict[str, list[Exception]] = {
|
|
84
|
+
"INSTANCE SETUP ERRORS": [],
|
|
85
|
+
"TESTING ERRORS": [],
|
|
86
|
+
"ERRORS DURING VIEW SHUTDOWN": [],
|
|
87
|
+
"ERRORS DURING VIEW EXECUTION": [],
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
# Start all the views
|
|
84
91
|
future_views = {
|
|
85
92
|
view_runner.submit(view.setup): view for view in self.input_dto.views
|
|
86
93
|
}
|
|
94
|
+
|
|
87
95
|
# Configure all the instances
|
|
88
96
|
future_instances_setup = {
|
|
89
97
|
instance_pool.submit(instance.setup): instance
|
|
@@ -96,7 +104,11 @@ class DetectionTestingManager(BaseModel):
|
|
|
96
104
|
future.result()
|
|
97
105
|
except Exception as e:
|
|
98
106
|
self.output_dto.terminate = True
|
|
99
|
-
|
|
107
|
+
# Output the traceback if we encounter errors in verbose mode
|
|
108
|
+
if self.input_dto.config.verbose:
|
|
109
|
+
tb = traceback.format_exc()
|
|
110
|
+
print(tb)
|
|
111
|
+
errors["INSTANCE SETUP ERRORS"].append(e)
|
|
100
112
|
|
|
101
113
|
# Start and wait for all tests to run
|
|
102
114
|
if not self.output_dto.terminate:
|
|
@@ -111,7 +123,11 @@ class DetectionTestingManager(BaseModel):
|
|
|
111
123
|
future.result()
|
|
112
124
|
except Exception as e:
|
|
113
125
|
self.output_dto.terminate = True
|
|
114
|
-
|
|
126
|
+
# Output the traceback if we encounter errors in verbose mode
|
|
127
|
+
if self.input_dto.config.verbose:
|
|
128
|
+
tb = traceback.format_exc()
|
|
129
|
+
print(tb)
|
|
130
|
+
errors["TESTING ERRORS"].append(e)
|
|
115
131
|
|
|
116
132
|
self.output_dto.terminate = True
|
|
117
133
|
|
|
@@ -123,14 +139,34 @@ class DetectionTestingManager(BaseModel):
|
|
|
123
139
|
try:
|
|
124
140
|
future.result()
|
|
125
141
|
except Exception as e:
|
|
126
|
-
|
|
142
|
+
# Output the traceback if we encounter errors in verbose mode
|
|
143
|
+
if self.input_dto.config.verbose:
|
|
144
|
+
tb = traceback.format_exc()
|
|
145
|
+
print(tb)
|
|
146
|
+
errors["ERRORS DURING VIEW SHUTDOWN"].append(e)
|
|
127
147
|
|
|
128
148
|
# Wait for original view-related threads to complete
|
|
129
149
|
for future in concurrent.futures.as_completed(future_views):
|
|
130
150
|
try:
|
|
131
151
|
future.result()
|
|
132
152
|
except Exception as e:
|
|
133
|
-
|
|
153
|
+
# Output the traceback if we encounter errors in verbose mode
|
|
154
|
+
if self.input_dto.config.verbose:
|
|
155
|
+
tb = traceback.format_exc()
|
|
156
|
+
print(tb)
|
|
157
|
+
errors["ERRORS DURING VIEW EXECUTION"].append(e)
|
|
158
|
+
|
|
159
|
+
# Log any errors
|
|
160
|
+
for error_type in errors:
|
|
161
|
+
if len(errors[error_type]) > 0:
|
|
162
|
+
print()
|
|
163
|
+
print(f"[{error_type}]:")
|
|
164
|
+
for error in errors[error_type]:
|
|
165
|
+
print(f"\t❌ {str(error)}")
|
|
166
|
+
if isinstance(error, ExceptionGroup):
|
|
167
|
+
for suberror in error.exceptions: # type: ignore
|
|
168
|
+
print(f"\t\t❌ {str(suberror)}") # type: ignore
|
|
169
|
+
print()
|
|
134
170
|
|
|
135
171
|
return self.output_dto
|
|
136
172
|
|
|
@@ -154,12 +190,15 @@ class DetectionTestingManager(BaseModel):
|
|
|
154
190
|
)
|
|
155
191
|
if len(parts) != 2:
|
|
156
192
|
raise Exception(
|
|
157
|
-
|
|
158
|
-
f"
|
|
193
|
+
"Expected to find a name:tag in "
|
|
194
|
+
f"{self.input_dto.config.container_settings.full_image_path}, "
|
|
195
|
+
f"but instead found {parts}. Note that this path MUST include the "
|
|
196
|
+
"tag, which is separated by ':'"
|
|
159
197
|
)
|
|
160
198
|
|
|
161
199
|
print(
|
|
162
|
-
|
|
200
|
+
"Getting the latest version of the container image "
|
|
201
|
+
f"[{self.input_dto.config.container_settings.full_image_path}]...",
|
|
163
202
|
end="",
|
|
164
203
|
flush=True,
|
|
165
204
|
)
|
|
@@ -168,7 +207,8 @@ class DetectionTestingManager(BaseModel):
|
|
|
168
207
|
break
|
|
169
208
|
except Exception as e:
|
|
170
209
|
raise Exception(
|
|
171
|
-
|
|
210
|
+
"Failed to pull docker container image "
|
|
211
|
+
f"[{self.input_dto.config.container_settings.full_image_path}]: {str(e)}"
|
|
172
212
|
)
|
|
173
213
|
|
|
174
214
|
already_staged_container_files = False
|
|
@@ -11,13 +11,20 @@ from shutil import copyfile
|
|
|
11
11
|
from ssl import SSLEOFError, SSLZeroReturnError
|
|
12
12
|
from sys import stdout
|
|
13
13
|
from tempfile import TemporaryDirectory, mktemp
|
|
14
|
-
from typing import Optional, Union
|
|
14
|
+
from typing import Callable, Optional, Union
|
|
15
15
|
|
|
16
16
|
import requests # type: ignore
|
|
17
17
|
import splunklib.client as client # type: ignore
|
|
18
|
-
import splunklib.results
|
|
19
18
|
import tqdm # type: ignore
|
|
20
|
-
from pydantic import
|
|
19
|
+
from pydantic import (
|
|
20
|
+
BaseModel,
|
|
21
|
+
ConfigDict,
|
|
22
|
+
Field,
|
|
23
|
+
PrivateAttr,
|
|
24
|
+
computed_field,
|
|
25
|
+
dataclasses,
|
|
26
|
+
)
|
|
27
|
+
from semantic_version import Version
|
|
21
28
|
from splunklib.binding import HTTPError # type: ignore
|
|
22
29
|
from splunklib.results import JSONResultsReader, Message # type: ignore
|
|
23
30
|
from urllib3 import disable_warnings
|
|
@@ -31,7 +38,8 @@ from contentctl.actions.detection_testing.progress_bar import (
|
|
|
31
38
|
from contentctl.helper.utils import Utils
|
|
32
39
|
from contentctl.objects.base_test import BaseTest
|
|
33
40
|
from contentctl.objects.base_test_result import TestResultStatus
|
|
34
|
-
from contentctl.objects.config import Infrastructure, test_common
|
|
41
|
+
from contentctl.objects.config import All, Infrastructure, test_common
|
|
42
|
+
from contentctl.objects.content_versioning_service import ContentVersioningService
|
|
35
43
|
from contentctl.objects.correlation_search import CorrelationSearch, PbarData
|
|
36
44
|
from contentctl.objects.detection import Detection
|
|
37
45
|
from contentctl.objects.enums import AnalyticsType, PostTestBehavior
|
|
@@ -42,6 +50,9 @@ from contentctl.objects.test_group import TestGroup
|
|
|
42
50
|
from contentctl.objects.unit_test import UnitTest
|
|
43
51
|
from contentctl.objects.unit_test_result import UnitTestResult
|
|
44
52
|
|
|
53
|
+
# The app name of ES; needed to check ES version
|
|
54
|
+
ES_APP_NAME = "SplunkEnterpriseSecuritySuite"
|
|
55
|
+
|
|
45
56
|
|
|
46
57
|
class SetupTestGroupResults(BaseModel):
|
|
47
58
|
exception: Union[Exception, None] = None
|
|
@@ -136,21 +147,27 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
136
147
|
)
|
|
137
148
|
|
|
138
149
|
self.start_time = time.time()
|
|
150
|
+
|
|
151
|
+
# Init the list of setup functions we always need
|
|
152
|
+
primary_setup_functions: list[
|
|
153
|
+
tuple[Callable[[], None | client.Service], str]
|
|
154
|
+
] = [
|
|
155
|
+
(self.start, "Starting"),
|
|
156
|
+
(self.get_conn, "Waiting for App Installation"),
|
|
157
|
+
(self.configure_conf_file_datamodels, "Configuring Datamodels"),
|
|
158
|
+
(self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
|
|
159
|
+
(self.get_all_indexes, "Getting all indexes from server"),
|
|
160
|
+
(self.check_for_es_install, "Checking for ES Install"),
|
|
161
|
+
(self.configure_imported_roles, "Configuring Roles"),
|
|
162
|
+
(self.configure_delete_indexes, "Configuring Indexes"),
|
|
163
|
+
(self.configure_hec, "Configuring HEC"),
|
|
164
|
+
(self.wait_for_ui_ready, "Finishing Primary Setup"),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# Execute and report on each setup function
|
|
139
168
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
(self.get_conn, "Waiting for App Installation"),
|
|
143
|
-
(self.configure_conf_file_datamodels, "Configuring Datamodels"),
|
|
144
|
-
(
|
|
145
|
-
self.create_replay_index,
|
|
146
|
-
f"Create index '{self.sync_obj.replay_index}'",
|
|
147
|
-
),
|
|
148
|
-
(self.get_all_indexes, "Getting all indexes from server"),
|
|
149
|
-
(self.configure_imported_roles, "Configuring Roles"),
|
|
150
|
-
(self.configure_delete_indexes, "Configuring Indexes"),
|
|
151
|
-
(self.configure_hec, "Configuring HEC"),
|
|
152
|
-
(self.wait_for_ui_ready, "Finishing Setup"),
|
|
153
|
-
]:
|
|
169
|
+
# Run the primary setup functions
|
|
170
|
+
for func, msg in primary_setup_functions:
|
|
154
171
|
self.format_pbar_string(
|
|
155
172
|
TestReportingType.SETUP,
|
|
156
173
|
self.get_name(),
|
|
@@ -160,18 +177,114 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
160
177
|
func()
|
|
161
178
|
self.check_for_teardown()
|
|
162
179
|
|
|
180
|
+
# Run any setup functions only applicable to content versioning validation
|
|
181
|
+
if self.should_test_content_versioning:
|
|
182
|
+
self.pbar.write(
|
|
183
|
+
self.format_pbar_string(
|
|
184
|
+
TestReportingType.SETUP,
|
|
185
|
+
self.get_name(),
|
|
186
|
+
"Beginning Content Versioning Validation...",
|
|
187
|
+
set_pbar=False,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
for func, msg in self.content_versioning_service.setup_functions:
|
|
191
|
+
self.format_pbar_string(
|
|
192
|
+
TestReportingType.SETUP,
|
|
193
|
+
self.get_name(),
|
|
194
|
+
msg,
|
|
195
|
+
update_sync_status=True,
|
|
196
|
+
)
|
|
197
|
+
func()
|
|
198
|
+
self.check_for_teardown()
|
|
199
|
+
|
|
163
200
|
except Exception as e:
|
|
164
|
-
self.
|
|
201
|
+
msg = f"[{self.get_name()}]: {str(e)}"
|
|
165
202
|
self.finish()
|
|
166
|
-
|
|
203
|
+
if isinstance(e, ExceptionGroup):
|
|
204
|
+
raise ExceptionGroup(msg, e.exceptions) from e # type: ignore
|
|
205
|
+
raise Exception(msg) from e
|
|
167
206
|
|
|
168
|
-
self.
|
|
169
|
-
|
|
207
|
+
self.pbar.write(
|
|
208
|
+
self.format_pbar_string(
|
|
209
|
+
TestReportingType.SETUP,
|
|
210
|
+
self.get_name(),
|
|
211
|
+
"Finished Setup!",
|
|
212
|
+
set_pbar=False,
|
|
213
|
+
)
|
|
170
214
|
)
|
|
171
215
|
|
|
172
216
|
def wait_for_ui_ready(self):
|
|
173
217
|
self.get_conn()
|
|
174
218
|
|
|
219
|
+
@computed_field
|
|
220
|
+
@property
|
|
221
|
+
def content_versioning_service(self) -> ContentVersioningService:
|
|
222
|
+
"""
|
|
223
|
+
A computed field returning a handle to the content versioning service, used by ES to
|
|
224
|
+
version detections. We use this model to validate that all detections have been installed
|
|
225
|
+
compatibly with ES versioning.
|
|
226
|
+
|
|
227
|
+
:return: a handle to the content versioning service on the instance
|
|
228
|
+
:rtype: :class:`contentctl.objects.content_versioning_service.ContentVersioningService`
|
|
229
|
+
"""
|
|
230
|
+
return ContentVersioningService(
|
|
231
|
+
global_config=self.global_config,
|
|
232
|
+
infrastructure=self.infrastructure,
|
|
233
|
+
service=self.get_conn(),
|
|
234
|
+
detections=self.sync_obj.inputQueue,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def should_test_content_versioning(self) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Indicates whether we should test content versioning. Content versioning
|
|
241
|
+
should be tested when integration testing is enabled, the mode is all, and ES is at least
|
|
242
|
+
version 8.0.0.
|
|
243
|
+
|
|
244
|
+
:return: a bool indicating whether we should test content versioning
|
|
245
|
+
:rtype: bool
|
|
246
|
+
"""
|
|
247
|
+
es_version = self.es_version
|
|
248
|
+
return (
|
|
249
|
+
self.global_config.enable_integration_testing
|
|
250
|
+
and isinstance(self.global_config.mode, All)
|
|
251
|
+
and es_version is not None
|
|
252
|
+
and es_version >= Version("8.0.0")
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def es_version(self) -> Version | None:
|
|
257
|
+
"""
|
|
258
|
+
Returns the version of Enterprise Security installed on the instance; None if not installed.
|
|
259
|
+
|
|
260
|
+
:return: the version of ES, as a semver aware object
|
|
261
|
+
:rtype: :class:`semantic_version.Version`
|
|
262
|
+
"""
|
|
263
|
+
if not self.es_installed:
|
|
264
|
+
return None
|
|
265
|
+
return Version(self.get_conn().apps[ES_APP_NAME]["version"]) # type: ignore
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def es_installed(self) -> bool:
|
|
269
|
+
"""
|
|
270
|
+
Indicates whether ES is installed on the instance.
|
|
271
|
+
|
|
272
|
+
:return: a bool indicating whether ES is installed or not
|
|
273
|
+
:rtype: bool
|
|
274
|
+
"""
|
|
275
|
+
return ES_APP_NAME in self.get_conn().apps
|
|
276
|
+
|
|
277
|
+
def check_for_es_install(self) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Validating function which raises an error if Enterprise Security is not installed and
|
|
280
|
+
integration testing is enabled.
|
|
281
|
+
"""
|
|
282
|
+
if not self.es_installed and self.global_config.enable_integration_testing:
|
|
283
|
+
raise Exception(
|
|
284
|
+
"Enterprise Security does not appear to be installed on this instance and "
|
|
285
|
+
"integration testing is enabled."
|
|
286
|
+
)
|
|
287
|
+
|
|
175
288
|
def configure_hec(self):
|
|
176
289
|
self.hec_channel = str(uuid.uuid4())
|
|
177
290
|
try:
|
|
@@ -298,14 +411,14 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
298
411
|
imported_roles: list[str] = ["user", "power", "can_delete"],
|
|
299
412
|
enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
|
|
300
413
|
):
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
roles = imported_roles
|
|
414
|
+
# Set which roles should be configured. For Enterprise Security/Integration Testing,
|
|
415
|
+
# we must add some extra foles.
|
|
416
|
+
if self.global_config.enable_integration_testing:
|
|
417
|
+
roles = imported_roles + enterprise_security_roles
|
|
418
|
+
else:
|
|
419
|
+
roles = imported_roles
|
|
308
420
|
|
|
421
|
+
try:
|
|
309
422
|
self.get_conn().roles.post(
|
|
310
423
|
self.infrastructure.splunk_app_username,
|
|
311
424
|
imported_roles=roles,
|
|
@@ -314,16 +427,9 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
314
427
|
)
|
|
315
428
|
return
|
|
316
429
|
except Exception as e:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
self.get_conn().roles.post(
|
|
322
|
-
self.infrastructure.splunk_app_username,
|
|
323
|
-
imported_roles=imported_roles,
|
|
324
|
-
srchIndexesAllowed=";".join(self.all_indexes_on_server),
|
|
325
|
-
srchIndexesDefault=self.sync_obj.replay_index,
|
|
326
|
-
)
|
|
430
|
+
msg = f"Error configuring roles: {str(e)}"
|
|
431
|
+
self.pbar.write(msg)
|
|
432
|
+
raise Exception(msg) from e
|
|
327
433
|
|
|
328
434
|
def configure_delete_indexes(self):
|
|
329
435
|
endpoint = "/services/properties/authorize/default/deleteIndexesAllowed"
|
|
@@ -1170,8 +1276,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1170
1276
|
# on a field. In this case, the field will appear but will not contain any values
|
|
1171
1277
|
current_empty_fields: set[str] = set()
|
|
1172
1278
|
|
|
1173
|
-
# TODO (cmcginley): @ljstella is this something we're keeping for testing as
|
|
1174
|
-
# well?
|
|
1175
1279
|
for field in full_rba_field_set:
|
|
1176
1280
|
if result.get(field, "null") == "null":
|
|
1177
1281
|
if field in risk_object_fields_set:
|
|
@@ -1257,7 +1361,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1257
1361
|
job = self.get_conn().jobs.create(splunk_search, **kwargs)
|
|
1258
1362
|
results_stream = job.results(output_mode="json")
|
|
1259
1363
|
# TODO: should we be doing something w/ this reader?
|
|
1260
|
-
_ =
|
|
1364
|
+
_ = JSONResultsReader(results_stream)
|
|
1261
1365
|
|
|
1262
1366
|
except Exception as e:
|
|
1263
1367
|
raise (
|
|
@@ -4,14 +4,13 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
|
-
from contentctl.objects.config import test_common
|
|
8
|
-
|
|
9
7
|
from contentctl.actions.detection_testing.infrastructures.DetectionTestingInfrastructure import (
|
|
10
8
|
DetectionTestingManagerOutputDto,
|
|
11
9
|
)
|
|
12
10
|
from contentctl.helper.utils import Utils
|
|
13
|
-
from contentctl.objects.enums import DetectionStatus
|
|
14
11
|
from contentctl.objects.base_test_result import TestResultStatus
|
|
12
|
+
from contentctl.objects.config import test_common
|
|
13
|
+
from contentctl.objects.enums import ContentStatus
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class DetectionTestingView(BaseModel, abc.ABC):
|
|
@@ -117,11 +116,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
117
116
|
total_skipped += 1
|
|
118
117
|
|
|
119
118
|
# Aggregate production status metrics
|
|
120
|
-
if detection.status ==
|
|
119
|
+
if detection.status == ContentStatus.production:
|
|
121
120
|
total_production += 1
|
|
122
|
-
elif detection.status ==
|
|
121
|
+
elif detection.status == ContentStatus.experimental:
|
|
123
122
|
total_experimental += 1
|
|
124
|
-
elif detection.status ==
|
|
123
|
+
elif detection.status == ContentStatus.deprecated:
|
|
125
124
|
total_deprecated += 1
|
|
126
125
|
|
|
127
126
|
# Check if the detection is manual_test
|
|
@@ -47,6 +47,8 @@ class DetectionTestingViewCLI(DetectionTestingView, arbitrary_types_allowed=True
|
|
|
47
47
|
while True:
|
|
48
48
|
summary = self.getSummaryObject()
|
|
49
49
|
|
|
50
|
+
# TODO (#338): there's a 1-off error here I think (we show one more than we
|
|
51
|
+
# actually have during testing)
|
|
50
52
|
total = len(
|
|
51
53
|
summary.get("tested_detections", [])
|
|
52
54
|
+ summary.get("untested_detections", [])
|
contentctl/actions/initialize.py
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import shutil
|
|
2
1
|
import os
|
|
3
2
|
import pathlib
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
from contentctl.objects.baseline import Baseline
|
|
4
6
|
from contentctl.objects.config import test
|
|
7
|
+
from contentctl.objects.dashboard import Dashboard
|
|
8
|
+
from contentctl.objects.data_source import DataSource
|
|
9
|
+
from contentctl.objects.deployment import Deployment
|
|
10
|
+
from contentctl.objects.detection import Detection
|
|
11
|
+
from contentctl.objects.investigation import Investigation
|
|
12
|
+
from contentctl.objects.lookup import Lookup
|
|
13
|
+
from contentctl.objects.macro import Macro
|
|
14
|
+
from contentctl.objects.playbook import Playbook
|
|
15
|
+
from contentctl.objects.removed_security_content_object import (
|
|
16
|
+
RemovedSecurityContentObject,
|
|
17
|
+
)
|
|
18
|
+
from contentctl.objects.story import Story
|
|
5
19
|
from contentctl.output.yml_writer import YmlWriter
|
|
6
20
|
|
|
7
21
|
|
|
@@ -13,21 +27,33 @@ class Initialize:
|
|
|
13
27
|
|
|
14
28
|
YmlWriter.writeYmlFile(str(config.path / "contentctl.yml"), config.model_dump())
|
|
15
29
|
|
|
16
|
-
# Create the following empty directories
|
|
30
|
+
# Create the following empty directories. Each type of content,
|
|
31
|
+
# even if you don't have any of that type of content, need its own directory to exist.
|
|
32
|
+
for contentType in [
|
|
33
|
+
Detection,
|
|
34
|
+
Playbook,
|
|
35
|
+
Story,
|
|
36
|
+
DataSource,
|
|
37
|
+
Investigation,
|
|
38
|
+
Macro,
|
|
39
|
+
Lookup,
|
|
40
|
+
Dashboard,
|
|
41
|
+
Baseline,
|
|
42
|
+
Deployment,
|
|
43
|
+
RemovedSecurityContentObject,
|
|
44
|
+
]:
|
|
45
|
+
contentType.containing_folder().mkdir(exist_ok=False, parents=True)
|
|
46
|
+
|
|
47
|
+
# Some other directories that do not map directly to a piece of content also must exist
|
|
48
|
+
|
|
17
49
|
for emptyDir in [
|
|
18
|
-
"lookups",
|
|
19
|
-
"baselines",
|
|
20
|
-
"data_sources",
|
|
21
50
|
"docs",
|
|
22
51
|
"reporting",
|
|
23
|
-
"investigations",
|
|
24
52
|
"detections/application",
|
|
25
53
|
"detections/cloud",
|
|
26
54
|
"detections/endpoint",
|
|
27
55
|
"detections/network",
|
|
28
56
|
"detections/web",
|
|
29
|
-
"macros",
|
|
30
|
-
"stories",
|
|
31
57
|
]:
|
|
32
58
|
# Throw an error if this directory already exists
|
|
33
59
|
(config.path / emptyDir).mkdir(exist_ok=False, parents=True)
|
|
@@ -60,7 +86,7 @@ class Initialize:
|
|
|
60
86
|
source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
|
|
61
87
|
target_directory = config.path / targetDir
|
|
62
88
|
# Throw an exception if the target exists
|
|
63
|
-
shutil.copytree(source_directory, target_directory, dirs_exist_ok=
|
|
89
|
+
shutil.copytree(source_directory, target_directory, dirs_exist_ok=True)
|
|
64
90
|
|
|
65
91
|
# Create a README.md file. Note that this is the README.md for the repository, not the
|
|
66
92
|
# one which will actually be packaged into the app. That is located in the app_template folder.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
from contentctl.objects.config import release_notes
|
|
2
|
-
from git import Repo
|
|
3
|
-
import re
|
|
4
|
-
import yaml
|
|
5
1
|
import pathlib
|
|
2
|
+
import re
|
|
6
3
|
from typing import List, Union
|
|
7
4
|
|
|
5
|
+
import yaml
|
|
6
|
+
from git import Repo
|
|
7
|
+
|
|
8
|
+
from contentctl.objects.config import release_notes
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class ReleaseNotes:
|
|
10
12
|
def create_notes(
|
|
@@ -171,13 +173,13 @@ class ReleaseNotes:
|
|
|
171
173
|
|
|
172
174
|
def release_notes(self, config: release_notes) -> None:
|
|
173
175
|
### Remove hard coded path
|
|
174
|
-
directories = [
|
|
175
|
-
"detections
|
|
176
|
-
"stories
|
|
177
|
-
"macros
|
|
178
|
-
"lookups
|
|
179
|
-
"playbooks
|
|
180
|
-
"ssa_detections
|
|
176
|
+
directories: list[pathlib.Path] = [
|
|
177
|
+
config.path / "detections",
|
|
178
|
+
config.path / "stories",
|
|
179
|
+
config.path / "macros",
|
|
180
|
+
config.path / "lookups",
|
|
181
|
+
config.path / "playbooks",
|
|
182
|
+
config.path / "ssa_detections",
|
|
181
183
|
]
|
|
182
184
|
|
|
183
185
|
repo = Repo(config.path)
|
|
@@ -229,7 +231,7 @@ class ReleaseNotes:
|
|
|
229
231
|
file_path = pathlib.Path(diff.a_path)
|
|
230
232
|
|
|
231
233
|
# Check if the file is in the specified directories
|
|
232
|
-
if any(
|
|
234
|
+
if any(file_path.is_relative_to(directory) for directory in directories):
|
|
233
235
|
# Check if a file is Modified
|
|
234
236
|
if diff.change_type == "M":
|
|
235
237
|
modified_files.append(file_path)
|