Skip to content

Commit 05113c3

Browse files
authored
Merge branch 'main' into dependabot/pip/attackcti-gte-0.3.7-and-lt-0.5.0
2 parents f42e3e8 + 2820c29 commit 05113c3

36 files changed

+1723
-817
lines changed

.github/workflows/testEndToEnd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
python_version: ["3.11", "3.12"]
15-
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14"]
15+
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14", "windows-2022"]
1616
#operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest"]
1717

1818

.github/workflows/test_against_escu.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
python_version: ["3.11", "3.12"]
21+
2122
operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14"]
22-
#operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest"]
23+
# Do not test against ESCU until known character encoding issue is resolved
24+
# operating_system: ["ubuntu-20.04", "ubuntu-22.04", "macos-latest", "macos-14", "windows-2022"]
2325

2426

2527
runs-on: ${{ matrix.operating_system }}

.vscode/launch.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"configurations": [
3+
{
4+
"name":"contentctl (pick args)",
5+
"type":"debugpy",
6+
"request":"launch",
7+
"program":"${workspaceFolder}/.venv/bin/contentctl",
8+
"console":"integratedTerminal",
9+
"cwd":"${env:SECURITY_CONTENT_PATH}",
10+
"args":"${command:pickArgs}"},
311
{
412
"name": "contentctl init",
513
"type": "debugpy",

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ Testing is run using [GitHub Hosted Runners](https://docs.github.com/en/actions/
6565

6666
| Requirement | Supported | Description | Passing Integration Tests |
6767
| --------------------- | ----- | ---- | ------ |
68-
| Python <3.9 | No | No support planned. contentctl tool uses modern language constructs not supported ion Python3.8 and below | N/A |
69-
| Python 3.9 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
70-
| Python 3.10 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
71-
| Python 3.11 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
68+
| Python 3.11+ | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
7269
| Docker (local) | Yes | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | (locally + GitHub Actions) |
7370
| Docker (remote) | Planned | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | No |
7471

@@ -80,7 +77,7 @@ It is typically recommended to install poetry to the Global Python Environment.*
8077

8178
#### Install via pip (recommended):
8279
```
83-
python3.9 -m venv .venv
80+
python3.11 -m venv .venv
8481
source .venv/bin/activate
8582
pip install contentctl
8683
```
@@ -89,7 +86,7 @@ pip install contentctl
8986
```
9087
git clone https://github.com/splunk/contentctl
9188
cd contentctl
92-
python3.9 -m pip install poetry
89+
python3.11 -m pip install poetry
9390
poetry install
9491
poetry shell
9592
contentctl --help

contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
8585
hec_channel: str = ""
8686
_conn: client.Service = PrivateAttr()
8787
pbar: tqdm.tqdm = None
88-
start_time: float = None
88+
start_time: Optional[float] = None
8989

9090
class Config:
9191
arbitrary_types_allowed = True
@@ -136,7 +136,6 @@ def setup(self):
136136
TestReportingType.SETUP,
137137
self.get_name(),
138138
msg,
139-
self.start_time,
140139
update_sync_status=True,
141140
)
142141
func()
@@ -147,7 +146,7 @@ def setup(self):
147146
self.finish()
148147
return
149148

150-
self.format_pbar_string(TestReportingType.SETUP, self.get_name(), "Finished Setup!", self.start_time)
149+
self.format_pbar_string(TestReportingType.SETUP, self.get_name(), "Finished Setup!")
151150

152151
def wait_for_ui_ready(self):
153152
self.get_conn()
@@ -216,7 +215,6 @@ def connect_to_api(self, sleep_seconds: int = 5):
216215
TestReportingType.SETUP,
217216
self.get_name(),
218217
"Waiting for reboot",
219-
self.start_time,
220218
update_sync_status=True,
221219
)
222220
else:
@@ -236,18 +234,12 @@ def connect_to_api(self, sleep_seconds: int = 5):
236234
self.pbar.write(
237235
f"Error getting API connection (not quitting) '{type(e).__name__}': {str(e)}"
238236
)
239-
print("wow")
240-
# self.pbar.write(
241-
# f"Unhandled exception getting connection to splunk server: {str(e)}"
242-
# )
243-
# self.sync_obj.terminate = True
244237

245238
for _ in range(sleep_seconds):
246239
self.format_pbar_string(
247240
TestReportingType.SETUP,
248241
self.get_name(),
249242
"Getting API Connection",
250-
self.start_time,
251243
update_sync_status=True,
252244
)
253245
time.sleep(1)
@@ -318,7 +310,6 @@ def wait_for_conf_file(self, app_name: str, conf_file_name: str):
318310
TestReportingType.SETUP,
319311
self.get_name(),
320312
"Configuring Datamodels",
321-
self.start_time,
322313
)
323314

324315
def configure_conf_file_datamodels(self, APP_NAME: str = "Splunk_SA_CIM"):
@@ -424,7 +415,7 @@ def test_detection(self, detection: Detection) -> None:
424415
TestReportingType.GROUP,
425416
test_group.name,
426417
FinalTestingStates.SKIP.value,
427-
time.time(),
418+
start_time=time.time(),
428419
set_pbar=False,
429420
)
430421
)
@@ -465,7 +456,7 @@ def test_detection(self, detection: Detection) -> None:
465456
TestReportingType.GROUP,
466457
test_group.name,
467458
TestingStates.DONE_GROUP.value,
468-
setup_results.start_time,
459+
start_time=setup_results.start_time,
469460
set_pbar=False,
470461
)
471462
)
@@ -486,7 +477,7 @@ def setup_test_group(self, test_group: TestGroup) -> SetupTestGroupResults:
486477
TestReportingType.GROUP,
487478
test_group.name,
488479
TestingStates.BEGINNING_GROUP.value,
489-
setup_start_time
480+
start_time=setup_start_time
490481
)
491482
# https://github.com/WoLpH/python-progressbar/issues/164
492483
# Use NullBar if there is more than 1 container or we are running
@@ -526,7 +517,7 @@ def cleanup_test_group(
526517
TestReportingType.GROUP,
527518
test_group.name,
528519
TestingStates.DELETING.value,
529-
test_group_start_time,
520+
start_time=test_group_start_time,
530521
)
531522

532523
# TODO: do we want to clean up even if replay failed? Could have been partial failure?
@@ -544,7 +535,7 @@ def format_pbar_string(
544535
test_reporting_type: TestReportingType,
545536
test_name: str,
546537
state: str,
547-
start_time: Union[float, None],
538+
start_time: Optional[float] = None,
548539
set_pbar: bool = True,
549540
update_sync_status: bool = False,
550541
) -> str:
@@ -559,8 +550,13 @@ def format_pbar_string(
559550
:param update_sync_status: bool indicating whether a sync status update should be queued
560551
:returns: a formatted string for use w/ pbar
561552
"""
562-
# set start time id not provided
553+
# set start time if not provided
563554
if start_time is None:
555+
# if self.start_time is still None, something went wrong
556+
if self.start_time is None:
557+
raise ValueError(
558+
"self.start_time is still None; a function may have been called before self.setup()"
559+
)
564560
start_time = self.start_time
565561

566562
# invoke the helper method
@@ -575,7 +571,7 @@ def format_pbar_string(
575571

576572
# update sync status if needed
577573
if update_sync_status:
578-
self.sync_obj.currentTestingQueue[self.get_name()] = {
574+
self.sync_obj.currentTestingQueue[self.get_name()] = { # type: ignore
579575
"name": state,
580576
"search": "N/A",
581577
}
@@ -621,7 +617,7 @@ def execute_unit_test(
621617
TestReportingType.UNIT,
622618
f"{detection.name}:{test.name}",
623619
TestingStates.BEGINNING_TEST,
624-
test_start_time,
620+
start_time=test_start_time,
625621
)
626622

627623
# if the replay failed, record the test failure and return
@@ -690,14 +686,14 @@ def execute_unit_test(
690686
res = "ERROR"
691687
link = detection.search
692688
else:
693-
res = test.result.status.value.upper()
689+
res = test.result.status.value.upper() # type: ignore
694690
link = test.result.get_summary_dict()["sid_link"]
695691

696692
self.format_pbar_string(
697693
TestReportingType.UNIT,
698694
f"{detection.name}:{test.name}",
699695
f"{res} - {link} (CTRL+D to continue)",
700-
test_start_time,
696+
start_time=test_start_time,
701697
)
702698

703699
# Wait for user input
@@ -722,7 +718,7 @@ def execute_unit_test(
722718
TestReportingType.UNIT,
723719
f"{detection.name}:{test.name}",
724720
FinalTestingStates.PASS.value,
725-
test_start_time,
721+
start_time=test_start_time,
726722
set_pbar=False,
727723
)
728724
)
@@ -744,7 +740,7 @@ def execute_unit_test(
744740
TestReportingType.UNIT,
745741
f"{detection.name}:{test.name}",
746742
FinalTestingStates.FAIL.value,
747-
test_start_time,
743+
start_time=test_start_time,
748744
set_pbar=False,
749745
)
750746
)
@@ -755,7 +751,7 @@ def execute_unit_test(
755751
TestReportingType.UNIT,
756752
f"{detection.name}:{test.name}",
757753
FinalTestingStates.ERROR.value,
758-
test_start_time,
754+
start_time=test_start_time,
759755
set_pbar=False,
760756
)
761757
)
@@ -770,7 +766,7 @@ def execute_unit_test(
770766
stdout.flush()
771767
test.result.duration = round(time.time() - test_start_time, 2)
772768

773-
# TODO (cmcginley): break up the execute routines for integration/unit tests some more to remove
769+
# TODO (#227): break up the execute routines for integration/unit tests some more to remove
774770
# code w/ similar structure
775771
def execute_integration_test(
776772
self,
@@ -837,7 +833,7 @@ def execute_integration_test(
837833
TestReportingType.INTEGRATION,
838834
f"{detection.name}:{test.name}",
839835
TestingStates.BEGINNING_TEST,
840-
test_start_time,
836+
start_time=test_start_time,
841837
)
842838

843839
# if the replay failed, record the test failure and return
@@ -874,15 +870,10 @@ def execute_integration_test(
874870
start_time=test_start_time
875871
)
876872

877-
# TODO (cmcginley): right now, we are creating one CorrelationSearch instance for each
878-
# test case; typically, there is only one unit test, and thus one integration test,
879-
# per detection, so this is not an issue. However, if we start having many test cases
880-
# per detection, we will be duplicating some effort & network calls that we don't need
881-
# to. Consider refactoring in order to re-use CorrelationSearch objects across tests
882-
# in such a case
873+
# TODO (#228): consider reusing CorrelationSearch instances across test cases
883874
# Instantiate the CorrelationSearch
884875
correlation_search = CorrelationSearch(
885-
detection_name=detection.name,
876+
detection=detection,
886877
service=self.get_conn(),
887878
pbar_data=pbar_data,
888879
)
@@ -892,14 +883,12 @@ def execute_integration_test(
892883
except Exception as e:
893884
# Catch and report and unhandled exceptions in integration testing
894885
test.result = IntegrationTestResult(
895-
message="TEST FAILED: unhandled exception in CorrelationSearch",
886+
message="TEST ERROR: unhandled exception in CorrelationSearch",
896887
exception=e,
897888
status=TestResultStatus.ERROR
898889
)
899890

900-
# TODO (cmcginley): when in interactive mode, consider maybe making the cleanup routine in
901-
# correlation_search happen after the user breaks the interactivity; currently
902-
# risk/notable indexes are dumped before the user can inspect
891+
# TODO (#229): when in interactive mode, cleanup should happen after user interaction
903892
# Pause here if the terminate flag has NOT been set AND either of the below are true:
904893
# 1. the behavior is always_pause
905894
# 2. the behavior is pause_on_failure and the test failed
@@ -908,7 +897,7 @@ def execute_integration_test(
908897
if test.result is None:
909898
res = "ERROR"
910899
else:
911-
res = test.result.status.value.upper()
900+
res = test.result.status.value.upper() # type: ignore
912901

913902
# Get the link to the saved search in this specific instance
914903
link = f"https://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
@@ -917,7 +906,7 @@ def execute_integration_test(
917906
TestReportingType.INTEGRATION,
918907
f"{detection.name}:{test.name}",
919908
f"{res} - {link} (CTRL+D to continue)",
920-
test_start_time,
909+
start_time=test_start_time,
921910
)
922911

923912
# Wait for user input
@@ -1036,8 +1025,8 @@ def retry_search_until_timeout(
10361025
search = f"{detection.search} {test.pass_condition}"
10371026

10381027
# Ensure searches that do not begin with '|' must begin with 'search '
1039-
if not search.strip().startswith("|"):
1040-
if not search.strip().startswith("search "):
1028+
if not search.strip().startswith("|"): # type: ignore
1029+
if not search.strip().startswith("search "): # type: ignore
10411030
search = f"search {search}"
10421031

10431032
# exponential backoff for wait time
@@ -1054,7 +1043,7 @@ def retry_search_until_timeout(
10541043
TestReportingType.UNIT,
10551044
f"{detection.name}:{test.name}",
10561045
TestingStates.PROCESSING.value,
1057-
start_time
1046+
start_time=start_time
10581047
)
10591048

10601049
time.sleep(1)
@@ -1063,7 +1052,7 @@ def retry_search_until_timeout(
10631052
TestReportingType.UNIT,
10641053
f"{detection.name}:{test.name}",
10651054
TestingStates.SEARCHING.value,
1066-
start_time,
1055+
start_time=start_time,
10671056
)
10681057

10691058
# Execute the search and read the results
@@ -1079,7 +1068,7 @@ def retry_search_until_timeout(
10791068
test.result = UnitTestResult()
10801069

10811070
# Initialize the collection of fields that are empty that shouldn't be
1082-
empty_fields = set()
1071+
empty_fields: set[str] = set()
10831072

10841073
# Filter out any messages in the results
10851074
for result in results:
@@ -1194,10 +1183,15 @@ def replay_attack_data_file(
11941183
):
11951184
tempfile = mktemp(dir=tmp_dir)
11961185

1186+
11971187
if not (str(attack_data_file.data).startswith("http://") or
11981188
str(attack_data_file.data).startswith("https://")) :
11991189
if pathlib.Path(str(attack_data_file.data)).is_file():
1200-
self.format_pbar_string(TestReportingType.GROUP, test_group.name, "Copying Data", test_group_start_time)
1190+
self.format_pbar_string(TestReportingType.GROUP,
1191+
test_group.name,
1192+
"Copying Data",
1193+
test_group_start_time)
1194+
12011195
try:
12021196
copyfile(str(attack_data_file.data), tempfile)
12031197
except Exception as e:
@@ -1221,7 +1215,7 @@ def replay_attack_data_file(
12211215
TestReportingType.GROUP,
12221216
test_group.name,
12231217
TestingStates.DOWNLOADING.value,
1224-
test_group_start_time
1218+
start_time=test_group_start_time
12251219
)
12261220

12271221
Utils.download_file_from_http(
@@ -1240,7 +1234,7 @@ def replay_attack_data_file(
12401234
TestReportingType.GROUP,
12411235
test_group.name,
12421236
TestingStates.REPLAYING.value,
1243-
test_group_start_time
1237+
start_time=test_group_start_time
12441238
)
12451239

12461240
self.hec_raw_replay(tempfile, attack_data_file)

0 commit comments

Comments
 (0)