From 7535bf45cc8b939bc6f511fa9b5009efd016e200 Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 17:38:24 +0530 Subject: [PATCH 1/8] feat(mTLS): auto fetch client certificate for mTLS --- .gitignore | 2 ++ solnlib/server_info.py | 26 +++++++++++++++++++++++++- solnlib/splunk_rest_client.py | 7 +++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index df66d9d2..396ff909 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ __pycache__ *.pyo .coverage +*.log +events.pickle diff --git a/solnlib/server_info.py b/solnlib/server_info.py index 7bb24618..78a31cc3 100644 --- a/solnlib/server_info.py +++ b/solnlib/server_info.py @@ -16,13 +16,15 @@ """This module contains Splunk server info related functionalities.""" +import os import json from typing import Any, Dict, Optional +from splunk.rest import getWebCertFile, getWebKeyFile from splunklib import binding - from solnlib import splunk_rest_client as rest_client from solnlib import utils +from .splunkenv import get_splunkd_access_info __all__ = ["ServerInfo", "ServerInfoException"] @@ -56,6 +58,28 @@ def __init__( port: The port number, default is None. context: Other configurations for Splunk rest client. """ + is_localhost: bool = False + if not all([scheme, host, port]) and os.environ.get("SPLUNK_HOME"): + scheme, host, port = get_splunkd_access_info() + is_localhost = ( + host == "localhost" or host == "127.0.0.1" or host in ("::1", "[::1]") + ) + + if getWebCertFile() and getWebKeyFile(): + context["cert_file"] = getWebCertFile() + context["key_file"] = getWebKeyFile() + + if all([is_localhost, context.get("verify") is None]): + # NOTE: this is specifically for mTLS communication + # ONLY if scheme, host, port aren't provided AND user hasn't provided server certificate + # we set verify to off (similar to 'rest.simpleRequest' implementation) + context["verify"] = False + + elif getWebCertFile() is not None: + context["cert_file"] = getWebCertFile() + if all([is_localhost, context.get("verify") is None]): + context["verify"] = False + self._rest_client = rest_client.SplunkRestClient( session_key, "-", scheme=scheme, host=host, port=port, **context ) diff --git a/solnlib/splunk_rest_client.py b/solnlib/splunk_rest_client.py index 42e93118..c83c2545 100644 --- a/solnlib/splunk_rest_client.py +++ b/solnlib/splunk_rest_client.py @@ -88,10 +88,13 @@ def _request_handler(context): verify = context.get("verify", False) if context.get("key_file") and context.get("cert_file"): - # cert = ('/path/client.cert', '/path/client.key') - cert = context["key_file"], context["cert_file"] + # cert: if tuple, ('cert', 'key') pair as per requests library + cert = context["cert_file"], context["key_file"] elif context.get("cert_file"): cert = context["cert_file"] + elif context.get("cert"): + # as the solnlib uses requests, we need to have a check for 'cert' key as well + cert = context["cert"] else: cert = None From bcf96489e8c2d3f1aa29300c8d2c17223d8bf349 Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 17:38:57 +0530 Subject: [PATCH 2/8] test(unit): update test case for mTLS implementation --- tests/unit/conftest.py | 6 ++ tests/unit/test_server_info.py | 103 ++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/unit/conftest.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000..1dbefb3d --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,6 @@ +import sys +from unittest.mock import MagicMock + +# mock modules of 'splunk' library added 'splunk_rest_client' +sys.modules["splunk"] = MagicMock() +sys.modules["splunk.rest"] = MagicMock() diff --git a/tests/unit/test_server_info.py b/tests/unit/test_server_info.py index f312baad..f0d4a4a9 100644 --- a/tests/unit/test_server_info.py +++ b/tests/unit/test_server_info.py @@ -13,8 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import common import pytest +import common +from unittest.mock import patch, MagicMock from splunklib import binding from solnlib import server_info @@ -72,3 +73,103 @@ def _mock_get(self, path_segment, owner=None, app=None, sharing=None, **query): si = server_info.ServerInfo(common.SESSION_KEY) assert si.is_captain_ready() + + @patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME") + @patch( + "solnlib.server_info.get_splunkd_access_info", + autospec=True, + return_value=("https", "127.0.0.1", "8089"), + ) + @patch("solnlib.server_info.rest_client", autospec=True) + @patch("solnlib.server_info.getWebCertFile", autospec=True, return_value=None) + @patch("solnlib.server_info.getWebKeyFile", autospec=True, return_value=None) + def test_server_info_object_with_no_certs( + self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env + ): + mock_rest_client.SplunkRestClient = MagicMock() + + server_info.ServerInfo(common.SESSION_KEY) + + for call_arg in mock_rest_client.SplunkRestClient.call_args_list: + _, kwargs = call_arg + assert kwargs.get("cert_file") is None + assert kwargs.get("key_file") is None + assert kwargs.get("verify") is None + + @patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME") + @patch( + "solnlib.server_info.get_splunkd_access_info", + autospec=True, + return_value=("https", "127.0.0.1", "8089"), + ) + @patch("solnlib.server_info.rest_client", autospec=True) + @patch( + "solnlib.server_info.getWebCertFile", + autospec=True, + return_value="/path/cert/pem", + ) + @patch( + "solnlib.server_info.getWebKeyFile", autospec=True, return_value="/path/key/pem" + ) + def test_server_info_object_with_both_certs( + self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env + ): + mock_rest_client.SplunkRestClient = MagicMock() + + server_info.ServerInfo(common.SESSION_KEY) + + for call_arg in mock_rest_client.SplunkRestClient.call_args_list: + _, kwargs = call_arg + assert kwargs.get("cert_file") == "/path/cert/pem" + assert kwargs.get("key_file") == "/path/key/pem" + assert kwargs.get("verify") is False + + @patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME") + @patch( + "solnlib.server_info.get_splunkd_access_info", + autospec=True, + return_value=("https", "127.0.0.1", "8089"), + ) + @patch("solnlib.server_info.rest_client", autospec=True) + @patch( + "solnlib.server_info.getWebCertFile", + autospec=True, + return_value="/path/cert/pem", + ) + @patch("solnlib.server_info.getWebKeyFile", autospec=True, return_value=None) + def test_server_info_object_with_cert_file( + self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env + ): + mock_rest_client.SplunkRestClient = MagicMock() + + server_info.ServerInfo(common.SESSION_KEY) + + for call_arg in mock_rest_client.SplunkRestClient.call_args_list: + _, kwargs = call_arg + assert kwargs.get("cert_file") == "/path/cert/pem" + assert kwargs.get("key_file") is None + assert kwargs.get("verify") is False + + @patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME") + @patch( + "solnlib.server_info.get_splunkd_access_info", + autospec=True, + return_value=("https", "127.0.0.1", "8089"), + ) + @patch("solnlib.server_info.rest_client", autospec=True) + @patch("solnlib.server_info.getWebCertFile", autospec=True, return_value=None) + @patch( + "solnlib.server_info.getWebKeyFile", autospec=True, return_value="/path/key/pem" + ) + def test_server_info_object_with_key_file( + self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env + ): + mock_rest_client.SplunkRestClient = MagicMock() + + server_info.ServerInfo(common.SESSION_KEY) + + for call_arg in mock_rest_client.SplunkRestClient.call_args_list: + _, kwargs = call_arg + assert kwargs.get("cert_file") is None + assert kwargs.get("key_file") is None + assert kwargs.get("verify") is None From be3ad3e21dfa9a98fb303f3ab6854874538f5836 Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 18:21:20 +0530 Subject: [PATCH 3/8] test(unit): fix test cases for py 3.10 --- .gitignore | 3 ++- tests/unit/test_server_info.py | 28 ++++++++-------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 396ff909..8954eebf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # IDE related files *.idea *.DS_Store* -.venv/ +# ignore all virtual environments +.venv* # Compiled files __pycache__ diff --git a/tests/unit/test_server_info.py b/tests/unit/test_server_info.py index f0d4a4a9..f7f6e5a6 100644 --- a/tests/unit/test_server_info.py +++ b/tests/unit/test_server_info.py @@ -81,8 +81,8 @@ def _mock_get(self, path_segment, owner=None, app=None, sharing=None, **query): return_value=("https", "127.0.0.1", "8089"), ) @patch("solnlib.server_info.rest_client", autospec=True) - @patch("solnlib.server_info.getWebCertFile", autospec=True, return_value=None) - @patch("solnlib.server_info.getWebKeyFile", autospec=True, return_value=None) + @patch("solnlib.server_info.getWebCertFile", return_value=None) + @patch("solnlib.server_info.getWebKeyFile", return_value=None) def test_server_info_object_with_no_certs( self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env ): @@ -103,14 +103,8 @@ def test_server_info_object_with_no_certs( return_value=("https", "127.0.0.1", "8089"), ) @patch("solnlib.server_info.rest_client", autospec=True) - @patch( - "solnlib.server_info.getWebCertFile", - autospec=True, - return_value="/path/cert/pem", - ) - @patch( - "solnlib.server_info.getWebKeyFile", autospec=True, return_value="/path/key/pem" - ) + @patch("solnlib.server_info.getWebCertFile", return_value="/path/cert/pem") + @patch("solnlib.server_info.getWebKeyFile", return_value="/path/key/pem") def test_server_info_object_with_both_certs( self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env ): @@ -131,12 +125,8 @@ def test_server_info_object_with_both_certs( return_value=("https", "127.0.0.1", "8089"), ) @patch("solnlib.server_info.rest_client", autospec=True) - @patch( - "solnlib.server_info.getWebCertFile", - autospec=True, - return_value="/path/cert/pem", - ) - @patch("solnlib.server_info.getWebKeyFile", autospec=True, return_value=None) + @patch("solnlib.server_info.getWebCertFile", return_value="/path/cert/pem") + @patch("solnlib.server_info.getWebKeyFile", return_value=None) def test_server_info_object_with_cert_file( self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env ): @@ -157,10 +147,8 @@ def test_server_info_object_with_cert_file( return_value=("https", "127.0.0.1", "8089"), ) @patch("solnlib.server_info.rest_client", autospec=True) - @patch("solnlib.server_info.getWebCertFile", autospec=True, return_value=None) - @patch( - "solnlib.server_info.getWebKeyFile", autospec=True, return_value="/path/key/pem" - ) + @patch("solnlib.server_info.getWebCertFile", return_value=None) + @patch("solnlib.server_info.getWebKeyFile", return_value="/path/key/pem") def test_server_info_object_with_key_file( self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env ): From 258969b3b1a490d0aa9e21b31198a17466cfc7c6 Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 18:48:45 +0530 Subject: [PATCH 4/8] test(integration): add path for splunk library --- tests/integration/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/integration/conftest.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..0db43b93 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,8 @@ +import os +import sys + +# path manipulation get the 'splunk' library for the imports while running on GH Actions +sys.path.append( + os.path.sep.join([os.environ["SPLUNK_HOME"], "lib", "python3.7", "site-packages"]) +) +# TODO: 'python3.7' needs to be updated as and when Splunk has new folder for Python. From 503b40267e747c7a969453f837068822503324aa Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 19:06:26 +0530 Subject: [PATCH 5/8] ci(env): add splunk_db environment variable --- .github/workflows/build-test-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index 5d5bf276..86bd11d3 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -117,6 +117,7 @@ jobs: export SPLUNK_BUILD_URL=https://download.splunk.com/products/${SPLUNK_PRODUCT}/releases/${SPLUNK_VERSION}/linux/${SPLUNK_LINUX_FILENAME} echo "$SPLUNK_BUILD_URL" export SPLUNK_HOME=/opt/splunk + export SPLUNK_DB=$SPLUNK_HOME/var/lib/splunk wget -qO /tmp/splunk.tgz "${SPLUNK_BUILD_URL}" sudo tar -C /opt -zxf /tmp/splunk.tgz sudo cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps From a2daa70711d16ac8d5124813cb34d9613cd84f8c Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 19:29:54 +0530 Subject: [PATCH 6/8] ci(env): rectify adding splunk_db environment variable --- .github/workflows/build-test-release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index 86bd11d3..dad264a3 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -117,7 +117,6 @@ jobs: export SPLUNK_BUILD_URL=https://download.splunk.com/products/${SPLUNK_PRODUCT}/releases/${SPLUNK_VERSION}/linux/${SPLUNK_LINUX_FILENAME} echo "$SPLUNK_BUILD_URL" export SPLUNK_HOME=/opt/splunk - export SPLUNK_DB=$SPLUNK_HOME/var/lib/splunk wget -qO /tmp/splunk.tgz "${SPLUNK_BUILD_URL}" sudo tar -C /opt -zxf /tmp/splunk.tgz sudo cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps @@ -136,7 +135,7 @@ jobs: - name: Run tests run: | poetry install - SPLUNK_HOME=/opt/splunk/ poetry run pytest --junitxml=test-results/results.xml -v tests/integration + SPLUNK_HOME=/opt/splunk SPLUNK_DB=$SPLUNK_HOME/var/lib/splunk poetry run pytest --junitxml=test-results/results.xml -v tests/integration - uses: actions/upload-artifact@v4 with: name: test-splunk-${{ matrix.splunk.version }} From 4824f95c237d173efbd86c1b381e52f554c0541c Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Thu, 20 Jun 2024 22:54:22 +0530 Subject: [PATCH 7/8] ci: rectify the usage of sudo for app install --- .github/workflows/build-test-release.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-test-release.yml b/.github/workflows/build-test-release.yml index dad264a3..9caf4913 100644 --- a/.github/workflows/build-test-release.yml +++ b/.github/workflows/build-test-release.yml @@ -119,17 +119,17 @@ jobs: export SPLUNK_HOME=/opt/splunk wget -qO /tmp/splunk.tgz "${SPLUNK_BUILD_URL}" sudo tar -C /opt -zxf /tmp/splunk.tgz - sudo cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps - sudo cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/ - sudo mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/ - sudo chown -R "$USER":"$USER" /opt/splunk + sudo chown -R "$USER":"$USER" $SPLUNK_HOME + cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps + cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/ + mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/ ls $SPLUNK_HOME/etc/apps/solnlib_demo/bin/ - echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | sudo tee -a /opt/splunk/etc/system/local/user-seed.conf - echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | sudo tee -a /opt/splunk/etc/splunk-launch.conf - sudo /opt/splunk/bin/splunk start --accept-license - sudo /opt/splunk/bin/splunk cmd python -m pip install solnlib - sudo /opt/splunk/bin/splunk set servername custom-servername -auth admin:Chang3d! - sudo /opt/splunk/bin/splunk restart + echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | tee -a $SPLUNK_HOME/etc/system/local/user-seed.conf + echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | tee -a $SPLUNK_HOME/etc/splunk-launch.conf + $SPLUNK_HOME/bin/splunk start --accept-license + $SPLUNK_HOME/bin/splunk cmd python -m pip install solnlib + $SPLUNK_HOME/bin/splunk set servername custom-servername -auth admin:Chang3d! + $SPLUNK_HOME/bin/splunk restart until curl -k -s -u admin:Chang3d! https://localhost:8089/services/server/info\?output_mode\=json | jq '.entry[0].content.kvStoreStatus' | grep -o "ready" ; do echo -n "Waiting for KVStore to become ready-" && sleep 5 ; done timeout-minutes: 5 - name: Run tests From 1ec9b3b2d421598a2665d204085e533f95431830 Mon Sep 17 00:00:00 2001 From: hetangmodi-crest Date: Mon, 24 Jun 2024 10:03:07 +0530 Subject: [PATCH 8/8] chore: resolve review comments --- solnlib/server_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solnlib/server_info.py b/solnlib/server_info.py index 78a31cc3..01643067 100644 --- a/solnlib/server_info.py +++ b/solnlib/server_info.py @@ -24,7 +24,7 @@ from splunklib import binding from solnlib import splunk_rest_client as rest_client from solnlib import utils -from .splunkenv import get_splunkd_access_info +from solnlib.splunkenv import get_splunkd_access_info __all__ = ["ServerInfo", "ServerInfoException"] @@ -58,7 +58,7 @@ def __init__( port: The port number, default is None. context: Other configurations for Splunk rest client. """ - is_localhost: bool = False + is_localhost = False if not all([scheme, host, port]) and os.environ.get("SPLUNK_HOME"): scheme, host, port = get_splunkd_access_info() is_localhost = (