Skip to content

Commit 50704d2

Browse files
committed
Throw much better and descriptive exception when triyng to replay to a custom_index that does not exist on the target server. list out the attempted index and all indexes on the server for documentation purposes.
1 parent bf72575 commit 50704d2

File tree

1 file changed

+42
-36
lines changed

1 file changed

+42
-36
lines changed

contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,23 @@ class CannotRunBaselineException(Exception):
6868
# exception
6969
pass
7070

71+
class ReplayIndexDoesNotExistOnServer(Exception):
72+
'''
73+
In order to replay data files into the Splunk Server
74+
for testing, they must be replayed into an index that
75+
exists. If that index does not exist, this error will
76+
be generated and raised before we try to do anything else
77+
with that Data File.
78+
'''
79+
pass
7180

7281
@dataclasses.dataclass(frozen=False)
7382
class DetectionTestingManagerOutputDto():
7483
inputQueue: list[Detection] = Field(default_factory=list)
7584
outputQueue: list[Detection] = Field(default_factory=list)
7685
currentTestingQueue: dict[str, Union[Detection, None]] = Field(default_factory=dict)
7786
start_time: Union[datetime.datetime, None] = None
78-
replay_index: str = "CONTENTCTL_TESTING_INDEX"
87+
replay_index: str = "contentctl_testing_index"
7988
replay_host: str = "CONTENTCTL_HOST"
8089
timeout_seconds: int = 60
8190
terminate: bool = False
@@ -88,6 +97,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
8897
sync_obj: DetectionTestingManagerOutputDto
8998
hec_token: str = ""
9099
hec_channel: str = ""
100+
all_indexes_on_server: list[str] = []
91101
_conn: client.Service = PrivateAttr()
92102
pbar: tqdm.tqdm = None
93103
start_time: Optional[float] = None
@@ -131,6 +141,7 @@ def setup(self):
131141
(self.get_conn, "Waiting for App Installation"),
132142
(self.configure_conf_file_datamodels, "Configuring Datamodels"),
133143
(self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
144+
(self.get_all_indexes, "Getting all indexes from server"),
134145
(self.configure_imported_roles, "Configuring Roles"),
135146
(self.configure_delete_indexes, "Configuring Indexes"),
136147
(self.configure_hec, "Configuring HEC"),
@@ -169,14 +180,11 @@ def configure_hec(self):
169180
pass
170181

171182
try:
172-
# Retrieve all available indexes on the splunk instance
173-
all_indexes = self.get_all_indexes()
174-
175183
res = self.get_conn().inputs.create(
176184
name="DETECTION_TESTING_HEC",
177185
kind="http",
178186
index=self.sync_obj.replay_index,
179-
indexes=",".join(all_indexes), # This allows the HEC to write to all indexes
187+
indexes=",".join(self.all_indexes_on_server), # This allows the HEC to write to all indexes
180188
useACK=True,
181189
)
182190
self.hec_token = str(res.token)
@@ -185,17 +193,20 @@ def configure_hec(self):
185193
except Exception as e:
186194
raise (Exception(f"Failure creating HEC Endpoint: {str(e)}"))
187195

188-
def get_all_indexes(self) -> list[str]:
196+
def get_all_indexes(self) -> None:
189197
"""
190198
Retrieve a list of all indexes in the Splunk instance
191199
"""
192200
try:
193-
# Always include the special, default replay index here
194-
indexes = [self.sync_obj.replay_index]
201+
# We do not include the replay index because by
202+
# the time we get to this function, it has already
203+
# been created on the server.
204+
indexes = []
195205
res = self.get_conn().indexes
196206
for index in res.list():
197207
indexes.append(index.name)
198-
return indexes
208+
# Retrieve all available indexes on the splunk instance
209+
self.all_indexes_on_server = indexes
199210
except Exception as e:
200211
raise (Exception(f"Failure getting indexes: {str(e)}"))
201212

@@ -281,11 +292,7 @@ def configure_imported_roles(
281292
self,
282293
imported_roles: list[str] = ["user", "power", "can_delete"],
283294
enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
284-
indexes: list[str] = ["_*", "*"],
285-
):
286-
indexes.append(self.sync_obj.replay_index)
287-
indexes_encoded = ";".join(indexes)
288-
295+
):
289296
try:
290297
# Set which roles should be configured. For Enterprise Security/Integration Testing,
291298
# we must add some extra foles.
@@ -297,7 +304,7 @@ def configure_imported_roles(
297304
self.get_conn().roles.post(
298305
self.infrastructure.splunk_app_username,
299306
imported_roles=roles,
300-
srchIndexesAllowed=indexes_encoded,
307+
srchIndexesAllowed=";".join(self.all_indexes_on_server),
301308
srchIndexesDefault=self.sync_obj.replay_index,
302309
)
303310
return
@@ -309,19 +316,17 @@ def configure_imported_roles(
309316
self.get_conn().roles.post(
310317
self.infrastructure.splunk_app_username,
311318
imported_roles=imported_roles,
312-
srchIndexesAllowed=indexes_encoded,
319+
srchIndexesAllowed=";".join(self.all_indexes_on_server),
313320
srchIndexesDefault=self.sync_obj.replay_index,
314321
)
315322

316-
def configure_delete_indexes(self, indexes: list[str] = ["_*", "*"]):
317-
indexes.append(self.sync_obj.replay_index)
323+
def configure_delete_indexes(self):
318324
endpoint = "/services/properties/authorize/default/deleteIndexesAllowed"
319-
indexes_encoded = ";".join(indexes)
320325
try:
321-
self.get_conn().post(endpoint, value=indexes_encoded)
326+
self.get_conn().post(endpoint, value=";".join(self.all_indexes_on_server))
322327
except Exception as e:
323328
self.pbar.write(
324-
f"Error configuring deleteIndexesAllowed with '{indexes_encoded}': [{str(e)}]"
329+
f"Error configuring deleteIndexesAllowed with '{self.all_indexes_on_server}': [{str(e)}]"
325330
)
326331

327332
def wait_for_conf_file(self, app_name: str, conf_file_name: str):
@@ -670,8 +675,6 @@ def execute_unit_test(
670675
# Set the mode and timeframe, if required
671676
kwargs = {"exec_mode": "blocking"}
672677

673-
674-
675678
# Set earliest_time and latest_time appropriately if FORCE_ALL_TIME is False
676679
if not FORCE_ALL_TIME:
677680
if test.earliest_time is not None:
@@ -1051,8 +1054,8 @@ def retry_search_until_timeout(
10511054
# Get the start time and compute the timeout
10521055
search_start_time = time.time()
10531056
search_stop_time = time.time() + self.sync_obj.timeout_seconds
1054-
1055-
# Make a copy of the search string since we may
1057+
1058+
# Make a copy of the search string since we may
10561059
# need to make some small changes to it below
10571060
search = detection.search
10581061

@@ -1104,8 +1107,6 @@ def retry_search_until_timeout(
11041107
# Initialize the collection of fields that are empty that shouldn't be
11051108
present_threat_objects: set[str] = set()
11061109
empty_fields: set[str] = set()
1107-
1108-
11091110

11101111
# Filter out any messages in the results
11111112
for result in results:
@@ -1135,7 +1136,7 @@ def retry_search_until_timeout(
11351136
# not populated and we should throw an error. This can happen if there is a typo
11361137
# on a field. In this case, the field will appear but will not contain any values
11371138
current_empty_fields: set[str] = set()
1138-
1139+
11391140
for field in observable_fields_set:
11401141
if result.get(field, 'null') == 'null':
11411142
if field in risk_object_fields_set:
@@ -1155,9 +1156,7 @@ def retry_search_until_timeout(
11551156
if field in threat_object_fields_set:
11561157
present_threat_objects.add(field)
11571158
continue
1158-
11591159

1160-
11611160
# If everything succeeded up until now, and no empty fields are found in the
11621161
# current result, then the search was a success
11631162
if len(current_empty_fields) == 0:
@@ -1171,8 +1170,7 @@ def retry_search_until_timeout(
11711170

11721171
else:
11731172
empty_fields = empty_fields.union(current_empty_fields)
1174-
1175-
1173+
11761174
missing_threat_objects = threat_object_fields_set - present_threat_objects
11771175
# Report a failure if there were empty fields in a threat object in all results
11781176
if len(missing_threat_objects) > 0:
@@ -1188,7 +1186,6 @@ def retry_search_until_timeout(
11881186
duration=time.time() - search_start_time,
11891187
)
11901188
return
1191-
11921189

11931190
test.result.set_job_content(
11941191
job.content,
@@ -1249,9 +1246,19 @@ def replay_attack_data_file(
12491246
test_group: TestGroup,
12501247
test_group_start_time: float,
12511248
):
1252-
tempfile = mktemp(dir=tmp_dir)
1253-
1249+
# Before attempting to replay the file, ensure that the index we want
1250+
# to replay into actuall exists. If not, we should throw a detailed
1251+
# exception that can easily be interpreted by the user.
1252+
if attack_data_file.custom_index is not None and \
1253+
attack_data_file.custom_index not in self.all_indexes_on_server:
1254+
raise ReplayIndexDoesNotExistOnServer(
1255+
f"Unable to replay data file {attack_data_file.data} "
1256+
f"into index '{attack_data_file.custom_index}'. "
1257+
"The index does not exist on the Splunk Server. "
1258+
f"The only valid indexes on the server are {self.all_indexes_on_server}"
1259+
)
12541260

1261+
tempfile = mktemp(dir=tmp_dir)
12551262
if not (str(attack_data_file.data).startswith("http://") or
12561263
str(attack_data_file.data).startswith("https://")) :
12571264
if pathlib.Path(str(attack_data_file.data)).is_file():
@@ -1296,7 +1303,6 @@ def replay_attack_data_file(
12961303
)
12971304
)
12981305

1299-
13001306
# Upload the data
13011307
self.format_pbar_string(
13021308
TestReportingType.GROUP,

0 commit comments

Comments
 (0)