diff --git a/packages/integration_testing/pyproject.toml b/packages/integration_testing/pyproject.toml index 26f073456d..e962f2fec6 100644 --- a/packages/integration_testing/pyproject.toml +++ b/packages/integration_testing/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "integration-testing" -version = "2.4.3" +version = "2.4.4" description = "Blackbox testing infrastructure to test and run marketplace integration scripts locally using mocks." readme = "README.md" authors = [ @@ -47,7 +47,7 @@ testpaths = [ ] [tool.uv.sources] -tipcommon = { path = "../tipcommon/whls/TIPCommon-2.4.3-py3-none-any.whl" } +tipcommon = { path = "../tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl" } soar-sdk = { git = "https://github.com/chronicle/soar-sdk.git" } [[tool.uv.index]] diff --git a/packages/integration_testing/uv.lock b/packages/integration_testing/uv.lock index 78a437b42a..b13a609120 100644 --- a/packages/integration_testing/uv.lock +++ b/packages/integration_testing/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = "==3.11.*" [[package]] @@ -393,7 +393,7 @@ wheels = [ [[package]] name = "integration-testing" -version = "2.4.3" +version = "2.4.4" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -420,7 +420,7 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.2" }, { name = "requests", specifier = ">=2.32.3" }, { name = "soar-sdk", git = "https://github.com/chronicle/soar-sdk.git" }, - { name = "tipcommon", path = "../tipcommon/whls/TIPCommon-2.4.3-py3-none-any.whl" }, + { name = "tipcommon", path = "../tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl" }, { name = "yarl", specifier = ">=1.20.1" }, ] @@ -769,8 +769,8 @@ dependencies = [ [[package]] name = "tipcommon" -version = "2.4.3" -source = { path = "../tipcommon/whls/TIPCommon-2.4.3-py3-none-any.whl" } +version = "2.4.4" +source = { path = "../tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl" } dependencies = [ { name = "google-api-python-client" }, { name = "google-auth" }, @@ -782,7 +782,7 @@ dependencies = [ { name = "requests-toolbelt" }, ] wheels = [ - { filename = "tipcommon-2.4.3-py3-none-any.whl", hash = "sha256:314fb40b97ea836b5b5b127bedc900028a4508492cfa11b98957c09e5cb83ddf" }, + { filename = "tipcommon-2.4.4-py3-none-any.whl", hash = "sha256:6c48e3d9c077911127dda65a7fceecf2bc99663ee2701637ebe2c8db0bc748d7" }, ] [package.metadata] diff --git a/packages/integration_testing_whls/integration_testing-2.4.4-py3-none-any.whl b/packages/integration_testing_whls/integration_testing-2.4.4-py3-none-any.whl new file mode 100644 index 0000000000..397b485670 Binary files /dev/null and b/packages/integration_testing_whls/integration_testing-2.4.4-py3-none-any.whl differ diff --git a/packages/tipcommon/TIPCommon/pyproject.toml b/packages/tipcommon/TIPCommon/pyproject.toml index 7c5f53cbba..33042d0e7b 100644 --- a/packages/tipcommon/TIPCommon/pyproject.toml +++ b/packages/tipcommon/TIPCommon/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "TIPCommon" -version = "2.4.3" +version = "2.4.4" description = "General Purpose CLI tool for Google SecOps Marketplace" readme = "README.md" authors = [ diff --git a/packages/tipcommon/TIPCommon/src/TIPCommon/rest/soar_platform_clients/one_platform_soar_api.py b/packages/tipcommon/TIPCommon/src/TIPCommon/rest/soar_platform_clients/one_platform_soar_api.py index a6a0d56b11..d561b91a9f 100644 --- a/packages/tipcommon/TIPCommon/src/TIPCommon/rest/soar_platform_clients/one_platform_soar_api.py +++ b/packages/tipcommon/TIPCommon/src/TIPCommon/rest/soar_platform_clients/one_platform_soar_api.py @@ -16,6 +16,7 @@ from __future__ import annotations +import urllib.parse from typing import TYPE_CHECKING from TIPCommon.consts import DATAPLANE_1P_HEADER, DEFAULT_1P_PAGE_SIZE @@ -98,7 +99,8 @@ def get_case_insights(self) -> list[SingleJson]: def get_installed_integrations_of_environment(self) -> list[SingleJson]: """Get installed integrations of environment using legacy API.""" name = "*" if self.params.environment == "Shared Instances" else self.params.environment - query_string = f"$filter=environment eq '{escape_odata_literal(name)}'&pageSize={_PAGE_SIZE}" + filter_str = f"environment eq '{escape_odata_literal(name)}'" + query_string = f"$filter={urllib.parse.quote(filter_str)}&pageSize={_PAGE_SIZE}" endpoint = f"/integrations/{self.params.integration_identifier}/integrationInstances?{query_string}" return self._paginate_results(endpoint, "integrationInstances") @@ -390,7 +392,7 @@ def get_traking_list_record(self) -> SingleJson: base_endpoint = "/system/settings/customLists" if filter_string: - initial_endpoint = f"{base_endpoint}?$filter={filter_string}&pageSize={_PAGE_SIZE}" + initial_endpoint = f"{base_endpoint}?$filter={urllib.parse.quote(filter_string)}&pageSize={_PAGE_SIZE}" else: initial_endpoint = f"{base_endpoint}?pageSize={_PAGE_SIZE}" @@ -411,7 +413,7 @@ def get_traking_list_records_filtered(self) -> SingleJson: base_endpoint = "/system/settings/customLists" if filter_string: - initial_endpoint = f"{base_endpoint}?$filter={filter_string}&pageSize={_PAGE_SIZE}" + initial_endpoint = f"{base_endpoint}?$filter={urllib.parse.quote(filter_string)}&pageSize={_PAGE_SIZE}" else: initial_endpoint = f"{base_endpoint}?pageSize={_PAGE_SIZE}" @@ -542,15 +544,8 @@ def save_or_update_job(self) -> requests.Response: Example ``job_data``:: { - "name": "projects/my-proj/locations/us/" - "instances/abc/integrations/MyInt/" - "jobs/j1/jobInstances/ji1", - "parameters": [ - { - "displayName": "API Key", - "value": "new-value" - } - ] + "name": "projects/my-proj/locations/us/instances/abc/integrations/MyInt/jobs/j1/jobInstances/ji1", + "parameters": [{"displayName": "API Key", "value": "new-value"}], } Raises: @@ -574,9 +569,7 @@ def save_or_update_job(self) -> requests.Response: parameters = job_data.get("parameters") if parameters is None: - raise EmptyMandatoryValues( - "Job data is missing 'parameters' field, Nothing to update." - ) + raise EmptyMandatoryValues("Job data is missing 'parameters' field, Nothing to update.") # The resource path is a full GCP resource name, e.g.: # projects/X/locations/Y/instances/Z/integrations/.../jobInstances/{id} # The API base URL already includes up to instances/Z, so we need @@ -586,10 +579,7 @@ def save_or_update_job(self) -> requests.Response: raise ParameterValidationError( param_name="name", value=resource_path, - message=( - "Cannot parse resource path. " - "Expected path to contain 'integrations/'" - ), + message=("Cannot parse resource path. Expected path to contain 'integrations/'"), ) endpoint = f"/{resource_path[segment_pos:]}" @@ -845,7 +835,7 @@ def get_cases_by_timestamp_filter(self) -> list[SingleJson]: base_endpoint = "/cases" initial_endpoint = ( - f"{base_endpoint}?$filter={filter_string}" + f"{base_endpoint}?$filter={urllib.parse.quote(filter_string)}" "&$select=id, updateTime" "&$expand=tags" f"&pageSize={_PAGE_SIZE}" diff --git a/packages/tipcommon/TIPCommon/tests/test_rest/test_soar_platform_clients/test_one_platform_soar_api.py b/packages/tipcommon/TIPCommon/tests/test_rest/test_soar_platform_clients/test_one_platform_soar_api.py index 8d3f853315..8fe975b540 100644 --- a/packages/tipcommon/TIPCommon/tests/test_rest/test_soar_platform_clients/test_one_platform_soar_api.py +++ b/packages/tipcommon/TIPCommon/tests/test_rest/test_soar_platform_clients/test_one_platform_soar_api.py @@ -13,6 +13,7 @@ # limitations under the License. import copy +import urllib.parse from typing import Any from unittest.mock import MagicMock, call @@ -103,9 +104,11 @@ def test_get_installed_integrations_of_environment_shared_instances( instances = client.get_installed_integrations_of_environment() assert instances == [{"id": "inst_1"}] + expected_filter = "environment eq '*'" + expected_url = f"https://mock-soar-api.com/integrations/intel_1/integrationInstances?$filter={urllib.parse.quote(expected_filter)}&pageSize=1000" mock_chronicle_soar.session.request.assert_called_once_with( "GET", - "https://mock-soar-api.com/integrations/intel_1/integrationInstances?$filter=environment eq '*'&pageSize=1000", + expected_url, params=None, json=None, ) @@ -128,9 +131,11 @@ def test_get_installed_integrations_of_environment_specific_env( instances = client.get_installed_integrations_of_environment() assert instances == [{"id": "inst_1"}] + expected_filter = "environment eq 'Production Environment'" + expected_url = f"https://mock-soar-api.com/integrations/intel_1/integrationInstances?$filter={urllib.parse.quote(expected_filter)}&pageSize=1000" mock_chronicle_soar.session.request.assert_called_once_with( "GET", - "https://mock-soar-api.com/integrations/intel_1/integrationInstances?$filter=environment eq 'Production Environment'&pageSize=1000", + expected_url, params=None, json=None, ) @@ -321,9 +326,7 @@ def test_save_or_update_job_success( ) -def test_save_or_update_job_missing_name( - mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock -) -> None: +def test_save_or_update_job_missing_name(mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock) -> None: """Test save_or_update_job raises EmptyMandatoryValues if 'name' is missing.""" client = OnePlatformSoarApi(mock_chronicle_soar) params: Any = client.params @@ -337,9 +340,7 @@ def test_save_or_update_job_missing_name( assert "Job data must include a 'name' field" in str(exc_info.value) -def test_save_or_update_job_missing_parameters( - mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock -) -> None: +def test_save_or_update_job_missing_parameters(mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock) -> None: """Test save_or_update_job raises EmptyMandatoryValues if 'parameters' is missing.""" client = OnePlatformSoarApi(mock_chronicle_soar) params: Any = client.params @@ -369,3 +370,61 @@ def test_save_or_update_job_invalid_resource_path( assert "Cannot parse resource path" in str(exc_info.value) assert 'Invalid parameter "name"' in str(exc_info.value) + + +def test_get_traking_list_records_filtered_url_encoded( + mocker: MockerFixture, mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock +) -> None: + + client = OnePlatformSoarApi(mock_chronicle_soar) + params: Any = client.params + params.environment = "Indevis GmbH & Co. KG" + params.category_name = "Indevis_Test_&_München" + params.entity_id = "192.168.1.100" + + mock_response = mocker.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"customLists": [{"id": "list_1"}], "nextPageToken": None} + mock_chronicle_soar.session.request.return_value = mock_response + + records = client.get_traking_list_records_filtered() + + assert records == [{"id": "list_1"}] + expected_filter = client._build_tracking_list_filter_string( + params.category_name, params.entity_id, environment=params.environment + ) + expected_url = f"https://mock-soar-api.com/system/settings/customLists?$filter={urllib.parse.quote(expected_filter)}&pageSize=1000" + mock_chronicle_soar.session.request.assert_called_once_with( + "GET", + expected_url, + params=None, + json=None, + ) + + +def test_get_traking_list_record_url_encoded( + mocker: MockerFixture, mock_chronicle_soar: MagicMock, mock_get_sdk_api_uri: MagicMock +) -> None: + """Test get_traking_list_record URL-encodes special characters in OData filter.""" + + client = OnePlatformSoarApi(mock_chronicle_soar) + params: Any = client.params + params.category_name = "Indevis_Test_&_München" + params.entity_id = "192.168.1.100" + + mock_response = mocker.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"customLists": [{"id": "list_2"}], "nextPageToken": None} + mock_chronicle_soar.session.request.return_value = mock_response + + records = client.get_traking_list_record() + + assert records == [{"id": "list_2"}] + expected_filter = client._build_tracking_list_filter_string(params.category_name, params.entity_id) + expected_url = f"https://mock-soar-api.com/system/settings/customLists?$filter={urllib.parse.quote(expected_filter)}&pageSize=1000" + mock_chronicle_soar.session.request.assert_called_once_with( + "GET", + expected_url, + params=None, + json=None, + ) diff --git a/packages/tipcommon/TIPCommon/uv.lock b/packages/tipcommon/TIPCommon/uv.lock index 929908cb69..447ed00d06 100644 --- a/packages/tipcommon/TIPCommon/uv.lock +++ b/packages/tipcommon/TIPCommon/uv.lock @@ -558,7 +558,7 @@ dependencies = [ [[package]] name = "tipcommon" -version = "2.4.3" +version = "2.4.4" source = { editable = "." } dependencies = [ { name = "google-api-python-client" }, diff --git a/packages/tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl b/packages/tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl new file mode 100644 index 0000000000..013a0aaeb4 Binary files /dev/null and b/packages/tipcommon/whls/TIPCommon-2.4.4-py3-none-any.whl differ