spl2-testing-framework 1.7.0__tar.gz → 1.7.1__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.
Files changed (35) hide show
  1. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/PKG-INFO +3 -3
  2. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/README.md +2 -2
  3. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/pyproject.toml +1 -1
  4. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/spl2test_runner.py +15 -3
  5. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/splunk_integration/splunk_handler.py +68 -19
  6. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/utils.py +9 -4
  7. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/LICENSE +0 -0
  8. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/__init__.py +0 -0
  9. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/conftest.py +0 -0
  10. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/launch_remotely_spl2.py +0 -0
  11. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/logger_manager.py +0 -0
  12. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/pytest.ini.sample +0 -0
  13. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/single_spl2_file_runner.py +0 -0
  14. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_test_config.json +0 -0
  15. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_utils/assertions.spl2 +0 -0
  16. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/spl2_utils/logs_to_metrics.spl2 +0 -0
  17. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/test_runner.py +0 -0
  18. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/__init__.py +0 -0
  19. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/create_diff_sheet.py +0 -0
  20. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/__init__.py +0 -0
  21. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/cli_job.py +0 -0
  22. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/cloud_job.py +0 -0
  23. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/job.py +0 -0
  24. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/splunk_job.py +0 -0
  25. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/jobs/test_cloud_job.py +0 -0
  26. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/performance.py +0 -0
  27. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/results.py +0 -0
  28. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/__init__.py +0 -0
  29. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/cli_search_client.py +0 -0
  30. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/cloud_search_client.py +0 -0
  31. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/search_client.py +0 -0
  32. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/search_clients/splunk_search_client.py +0 -0
  33. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/splunk_integration/__init__.py +0 -0
  34. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/spl2_testing_framework/tools/test_discovery.py +0 -0
  35. {spl2_testing_framework-1.7.0 → spl2_testing_framework-1.7.1}/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.0
3
+ Version: 1.7.1
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, push to Splunk, query by host, then validate that the returned event has the expected CIM fields (from `expected_cim_fields.cim_fields` in the box test). Uses `validate_compatibility()`. Skips the test if the box test has no `cim_fields`. |
264
- | **TA** | For each output event, push to Splunk, query by host, then assert that the returned event equals the expected output event for that index. |
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, push to Splunk, query by host, then validate that the returned event has the expected CIM fields (from `expected_cim_fields.cim_fields` in the box test). Uses `validate_compatibility()`. Skips the test if the box test has no `cim_fields`. |
231
- | **TA** | For each output event, push to Splunk, query by host, then assert that the returned event equals the expected output event for that index. |
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
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  [tool.poetry]
17
17
  name = "spl2-testing-framework"
18
- version = "1.7.0"
18
+ version = "1.7.1"
19
19
  description = "SPL2 Testing Framework"
20
20
  authors = ["mgazda-splunk <mgazda@splunk.com>"]
21
21
  readme = "README.md"
@@ -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.splunk_handler.process_event(event)
238
- validate_compatibility(box_test.cim_fields, processed_event)
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.splunk_handler.process_event(event)
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
- host = f"test_{uuid.uuid4()}"
40
-
41
- self.push_to_hec(event, host)
42
- res = self.get_events_by_host_and_index(host)
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 Exception("No event found.")
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
- return res[0]
49
-
50
- def push_to_hec(self, data, host):
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
- params = {
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
- params["source"] = source
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
- data=event_data,
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
- def get_events_by_host_and_index(self, host):
80
- SPL_TEMPLATE = "| search index={index} host={host} | fields *"
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(index=self.splunk_config["index"], host=host)
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
@@ -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(expected, parsed_output):
71
- """Validate if all fields from parsed_output are present in expected and have the same value."""
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 parsed_output:
76
- if parsed_output[field] == value:
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: