spl2-testing-framework 1.7.0__tar.gz → 1.7.2__tar.gz
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.
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/PKG-INFO +3 -3
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/README.md +2 -2
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/pyproject.toml +1 -1
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/spl2test_runner.py +15 -3
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/splunk_integration/splunk_handler.py +68 -19
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/utils.py +31 -13
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/LICENSE +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/__init__.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/conftest.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/launch_remotely_spl2.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/logger_manager.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/pytest.ini.sample +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/single_spl2_file_runner.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/spl2_test_config.json +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/spl2_utils/assertions.spl2 +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/test_runner.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/__init__.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/create_diff_sheet.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/__init__.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/cli_job.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/cloud_job.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/job.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/splunk_job.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/jobs/test_cloud_job.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/performance.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/results.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/search_clients/__init__.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/search_clients/cli_search_client.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/search_clients/cloud_search_client.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/search_clients/search_client.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/search_clients/splunk_search_client.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/splunk_integration/__init__.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/test_discovery.py +0 -0
- {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: spl2-testing-framework
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.2
|
|
4
4
|
Summary: SPL2 Testing Framework
|
|
5
5
|
Home-page: https://github.com/splunk/spl2-testing-framework
|
|
6
6
|
Keywords: SPL2 testing framework
|
|
@@ -260,8 +260,8 @@ When set (`--check_splunk_results=<option>`), box tests (after running the SPL2
|
|
|
260
260
|
|
|
261
261
|
| Value | Behavior |
|
|
262
262
|
|-------|----------|
|
|
263
|
-
| **CIM** | For each output event,
|
|
264
|
-
| **TA** | For each output event,
|
|
263
|
+
| **CIM** | For each output event, ingest via HEC, load back from Splunk, then check CIM fields from `expected_cim_fields.cim_fields` with `validate_compatibility(actual, expected)`. Skips if the box test has no `cim_fields`. |
|
|
264
|
+
| **TA** | For each output event, ingest via HEC, load back from Splunk, then assert equality with the expected row for that index. |
|
|
265
265
|
|
|
266
266
|
For **TA** only, you can name top-level fields to drop on **both** the queried event and each `expected_destination_result` row by adding **`ignore_fields_in_splunk_check`**: an object mapping **field name → short reason**. Use the same key under `test` in each entry in `*.test.json` (same file as `expected_destination_result`):
|
|
267
267
|
|
|
@@ -227,8 +227,8 @@ When set (`--check_splunk_results=<option>`), box tests (after running the SPL2
|
|
|
227
227
|
|
|
228
228
|
| Value | Behavior |
|
|
229
229
|
|-------|----------|
|
|
230
|
-
| **CIM** | For each output event,
|
|
231
|
-
| **TA** | For each output event,
|
|
230
|
+
| **CIM** | For each output event, ingest via HEC, load back from Splunk, then check CIM fields from `expected_cim_fields.cim_fields` with `validate_compatibility(actual, expected)`. Skips if the box test has no `cim_fields`. |
|
|
231
|
+
| **TA** | For each output event, ingest via HEC, load back from Splunk, then assert equality with the expected row for that index. |
|
|
232
232
|
|
|
233
233
|
For **TA** only, you can name top-level fields to drop on **both** the queried event and each `expected_destination_result` row by adding **`ignore_fields_in_splunk_check`**: an object mapping **field name → short reason**. Use the same key under `test` in each entry in `*.test.json` (same file as `expected_destination_result`):
|
|
234
234
|
|
|
@@ -23,6 +23,9 @@ from spl2_testing_framework.tools.performance import PerformanceCheck
|
|
|
23
23
|
from spl2_testing_framework.tools.search_clients.cli_search_client import (
|
|
24
24
|
CLISearchClient,
|
|
25
25
|
)
|
|
26
|
+
from spl2_testing_framework.tools.splunk_integration.splunk_handler import (
|
|
27
|
+
SplunkEventNotFoundError,
|
|
28
|
+
)
|
|
26
29
|
from spl2_testing_framework.tools.test_types import UnitTest, BoxTest, SingleSPL2
|
|
27
30
|
from spl2_testing_framework.tools.utils import (
|
|
28
31
|
_make_functions_visible_for_testing,
|
|
@@ -227,6 +230,15 @@ class SPL2TestRunner:
|
|
|
227
230
|
return row
|
|
228
231
|
return {k: v for k, v in row.items() if k not in ignore_keys}
|
|
229
232
|
|
|
233
|
+
def _splunk_process_event(self, box_test: BoxTest, event):
|
|
234
|
+
try:
|
|
235
|
+
return self.splunk_handler.process_event(event)
|
|
236
|
+
except SplunkEventNotFoundError as e:
|
|
237
|
+
raise SplunkEventNotFoundError(
|
|
238
|
+
e.event_uuid,
|
|
239
|
+
context=f"box_test={box_test.name!r}",
|
|
240
|
+
) from e
|
|
241
|
+
|
|
230
242
|
def _check_splunk_results(self, box_test, job):
|
|
231
243
|
output_result = job.result.get("destination", [])
|
|
232
244
|
if self.check_splunk_results == "CIM":
|
|
@@ -234,13 +246,13 @@ class SPL2TestRunner:
|
|
|
234
246
|
pytest.skip("CIM output not available for this test")
|
|
235
247
|
|
|
236
248
|
for event in output_result:
|
|
237
|
-
processed_event = self.
|
|
238
|
-
validate_compatibility(box_test.cim_fields
|
|
249
|
+
processed_event = self._splunk_process_event(box_test, event)
|
|
250
|
+
validate_compatibility(processed_event, box_test.cim_fields)
|
|
239
251
|
elif self.check_splunk_results == "TA":
|
|
240
252
|
ignore_spec = box_test.ignore_fields_in_splunk_check
|
|
241
253
|
ignore_keys = frozenset(ignore_spec.keys())
|
|
242
254
|
for event, expected in zip(output_result, box_test.output):
|
|
243
|
-
processed_event = self.
|
|
255
|
+
processed_event = self._splunk_process_event(box_test, event)
|
|
244
256
|
actual = self._strip_ignored_fields_for_splunk_compare(
|
|
245
257
|
processed_event, ignore_keys
|
|
246
258
|
)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import uuid
|
|
17
|
+
from typing import Optional
|
|
17
18
|
|
|
18
19
|
import requests
|
|
19
20
|
import splunklib.client as client
|
|
@@ -26,6 +27,21 @@ from requests.auth import HTTPBasicAuth
|
|
|
26
27
|
_LOGGER = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
SPL2_TF_EVENT_UUID_FIELD = "spl2_testing_framework_event_uuid"
|
|
31
|
+
SPL2_TF_DEFAULT_HEC_HOST = "spl2-testing-framework"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SplunkEventNotFoundError(Exception):
|
|
35
|
+
"""Raised when HEC ingest succeeded but search returned no matching event."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, event_uuid: str, *, context: Optional[str] = None):
|
|
38
|
+
self.event_uuid = event_uuid
|
|
39
|
+
msg = f"No event found after HEC submit (event_uuid={event_uuid!r})"
|
|
40
|
+
if context:
|
|
41
|
+
msg = f"{context}: {msg}"
|
|
42
|
+
super().__init__(msg)
|
|
43
|
+
|
|
44
|
+
|
|
29
45
|
class SplunkHandler:
|
|
30
46
|
def __init__(self, config, auth):
|
|
31
47
|
self._config = config
|
|
@@ -36,18 +52,45 @@ class SplunkHandler:
|
|
|
36
52
|
self._create_index()
|
|
37
53
|
|
|
38
54
|
def process_event(self, event):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
event_uuid = str(uuid.uuid4())
|
|
56
|
+
metadata_host = event.get("host")
|
|
57
|
+
if metadata_host is not None and str(metadata_host).strip() != "":
|
|
58
|
+
hec_host = str(metadata_host).strip()
|
|
59
|
+
else:
|
|
60
|
+
hec_host = SPL2_TF_DEFAULT_HEC_HOST
|
|
61
|
+
|
|
62
|
+
self.push_to_hec(event, hec_host, event_uuid)
|
|
63
|
+
res = self.get_events_by_event_uuid(event_uuid)
|
|
43
64
|
if not res:
|
|
44
|
-
raise
|
|
65
|
+
raise SplunkEventNotFoundError(event_uuid)
|
|
45
66
|
|
|
46
67
|
if len(res) > 1:
|
|
47
68
|
_LOGGER.error("Found more than one event, the result may not be correct")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
out = dict(res[0])
|
|
70
|
+
out.pop(SPL2_TF_EVENT_UUID_FIELD, None)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _hec_event_url(raw_url: str) -> str:
|
|
75
|
+
if "/services/collector/raw" in raw_url:
|
|
76
|
+
return raw_url.replace(
|
|
77
|
+
"/services/collector/raw", "/services/collector/event"
|
|
78
|
+
)
|
|
79
|
+
return raw_url
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _raw_as_event_string(raw):
|
|
83
|
+
"""Normalize pipeline `_raw` for HEC JSON `event` (must be a string).
|
|
84
|
+
|
|
85
|
+
``str`` is passed through ``str()``; ``bytes`` are decoded as UTF-8 with
|
|
86
|
+
replacement on invalid sequences (avoids :class:`UnicodeDecodeError` for
|
|
87
|
+
odd binary payloads; prefer valid UTF-8 log text in normal use).
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(raw, bytes):
|
|
90
|
+
return raw.decode("utf-8", errors="replace")
|
|
91
|
+
return str(raw)
|
|
92
|
+
|
|
93
|
+
def push_to_hec(self, data, host, event_uuid):
|
|
51
94
|
sourcetype = data.get("sourcetype", "UNKNOWN")
|
|
52
95
|
|
|
53
96
|
if sourcetype == "UNKNOWN":
|
|
@@ -55,29 +98,31 @@ class SplunkHandler:
|
|
|
55
98
|
|
|
56
99
|
source = data.get("source", None)
|
|
57
100
|
|
|
58
|
-
|
|
101
|
+
payload = {
|
|
102
|
+
"event": SplunkHandler._raw_as_event_string(data["_raw"]),
|
|
59
103
|
"sourcetype": sourcetype,
|
|
60
104
|
"index": self.splunk_config["index"],
|
|
61
105
|
"host": host,
|
|
106
|
+
"fields": {SPL2_TF_EVENT_UUID_FIELD: event_uuid},
|
|
62
107
|
}
|
|
63
|
-
|
|
64
108
|
if source is not None:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
event_data = data["_raw"].encode("utf-8")
|
|
109
|
+
payload["source"] = source
|
|
68
110
|
|
|
69
111
|
response = requests.post(
|
|
70
|
-
url=self.splunk_config["url"],
|
|
71
|
-
|
|
72
|
-
params=params,
|
|
112
|
+
url=self._hec_event_url(self.splunk_config["url"]),
|
|
113
|
+
json=payload,
|
|
73
114
|
auth=self._auth,
|
|
74
115
|
verify=False,
|
|
75
116
|
)
|
|
76
117
|
|
|
77
118
|
response.raise_for_status()
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _splunk_search_term(value: str) -> str:
|
|
122
|
+
return '"' + str(value).replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
123
|
+
|
|
124
|
+
def get_events_by_event_uuid(self, event_uuid):
|
|
125
|
+
SPL_TEMPLATE = "| search index={index} {uuid_field}={uuid} | fields *"
|
|
81
126
|
# Connect to Splunk
|
|
82
127
|
service = client.connect(
|
|
83
128
|
host=self.splunk_config["ip"],
|
|
@@ -86,7 +131,11 @@ class SplunkHandler:
|
|
|
86
131
|
password=self.splunk_config["password"],
|
|
87
132
|
app="search",
|
|
88
133
|
)
|
|
89
|
-
SPL = SPL_TEMPLATE.format(
|
|
134
|
+
SPL = SPL_TEMPLATE.format(
|
|
135
|
+
index=self.splunk_config["index"],
|
|
136
|
+
uuid_field=SPL2_TF_EVENT_UUID_FIELD,
|
|
137
|
+
uuid=self._splunk_search_term(event_uuid),
|
|
138
|
+
)
|
|
90
139
|
|
|
91
140
|
results_all = []
|
|
92
141
|
retry = 0
|
{spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/tools/utils.py
RENAMED
|
@@ -67,13 +67,18 @@ def calculate_reduction_rate(original_event, reduced_event):
|
|
|
67
67
|
return reduction_rates
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def validate_compatibility(
|
|
71
|
-
"""
|
|
70
|
+
def validate_compatibility(actual, expected):
|
|
71
|
+
"""Check that every field in ``expected`` exists in ``actual`` with the same value.
|
|
72
|
+
|
|
73
|
+
Order matches the usual test convention ``assert actual == expected`` (observed
|
|
74
|
+
value first, reference second). For CIM checks, ``actual`` is the Splunk-processed
|
|
75
|
+
event and ``expected`` is ``box_test.cim_fields``.
|
|
76
|
+
"""
|
|
72
77
|
extracted_but_wrong = {}
|
|
73
78
|
not_extracted = {}
|
|
74
79
|
for field, value in expected.items():
|
|
75
|
-
if field in
|
|
76
|
-
if
|
|
80
|
+
if field in actual:
|
|
81
|
+
if actual[field] == value:
|
|
77
82
|
_LOGGER.info(f"FIELD {field} in reduced event")
|
|
78
83
|
continue
|
|
79
84
|
else:
|
|
@@ -90,19 +95,32 @@ def validate_compatibility(expected, parsed_output):
|
|
|
90
95
|
assert not_extracted == {}
|
|
91
96
|
|
|
92
97
|
|
|
98
|
+
_QUOTED_STRING_OR_BARE_KEY = re.compile(
|
|
99
|
+
r'"(?:[^"\\]|\\.)*"' r"|" r"([{,])\s*([A-Za-z_]+)\s*(:)"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _quote_unquoted_keys(input_str):
|
|
104
|
+
"""Wrap bare (unquoted) object keys in double quotes to produce valid JSON,
|
|
105
|
+
without corrupting content inside JSON string values.
|
|
106
|
+
|
|
107
|
+
Uses regex alternation: the first branch consumes quoted strings (so their
|
|
108
|
+
content is never touched), the second branch captures bare keys to quote.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def _replacer(m):
|
|
112
|
+
if m.group(1) is None:
|
|
113
|
+
return m.group(0)
|
|
114
|
+
return f'{m.group(1)}"{m.group(2)}":'
|
|
115
|
+
|
|
116
|
+
return _QUOTED_STRING_OR_BARE_KEY.sub(_replacer, input_str)
|
|
117
|
+
|
|
118
|
+
|
|
93
119
|
def parse_raw_from_input(input_str):
|
|
94
120
|
"""Parse SPL2-style or JSON array of events and return list of _raw field values.
|
|
95
121
|
Handles unquoted keys (e.g. {_raw: "x"}) by converting them to JSON-quoted keys.
|
|
96
122
|
"""
|
|
97
|
-
|
|
98
|
-
key_pattern = re.compile(
|
|
99
|
-
r"(\{|,)\s*(_raw|source|sourcetype|host)\s*:", re.MULTILINE
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
def quote_key(match):
|
|
103
|
-
return f'{match.group(1)}"{match.group(2)}":'
|
|
104
|
-
|
|
105
|
-
normalized = key_pattern.sub(quote_key, input_str)
|
|
123
|
+
normalized = _quote_unquoted_keys(input_str)
|
|
106
124
|
try:
|
|
107
125
|
parsed = json.loads(normalized)
|
|
108
126
|
except json.JSONDecodeError:
|
|
File without changes
|
{spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/__init__.py
RENAMED
|
File without changes
|
{spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.2}/spl2_testing_framework/test_runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|