Repository navigation
[b/574548101] Tools: Fix 1P error handling in Get Siemplify Users and Assign Case to User actions #1426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[b/574548101] Tools: Fix 1P error handling in Get Siemplify Users and Assign Case to User actions #1426
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,33 +14,69 @@ | |||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| from typing import Any | ||||||
|
|
||||||
| from soar_sdk.ScriptResult import EXECUTION_STATE_COMPLETED, EXECUTION_STATE_FAILED | ||||||
| from soar_sdk.SiemplifyAction import SiemplifyAction | ||||||
| from soar_sdk.SiemplifyUtils import output_handler | ||||||
| from TIPCommon.rest.soar_api import assign_case_to_user | ||||||
|
|
||||||
| ACTION_NAME = "AssignCaseToUser" | ||||||
| TRUE = "true" | ||||||
|
|
||||||
|
|
||||||
| @output_handler | ||||||
| def main(): | ||||||
| def main() -> None: | ||||||
| """Main execution flow for assigning a case to a user.""" | ||||||
| siemplify = SiemplifyAction() | ||||||
| siemplify.script_name = ACTION_NAME | ||||||
|
|
||||||
| case_id = siemplify.parameters["Case Id"] | ||||||
| assign_to = siemplify.parameters["Assign To"] | ||||||
| alert_id = siemplify.parameters["Alert Id"] | ||||||
| status = EXECUTION_STATE_COMPLETED | ||||||
| result_value = True | ||||||
|
|
||||||
| try: | ||||||
| case_id = siemplify.extract_action_param( | ||||||
| param_name="Case Id", | ||||||
| is_mandatory=True, | ||||||
| print_value=True, | ||||||
| ) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since
Suggested change
|
||||||
| assign_to = siemplify.extract_action_param( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please annotate
Suggested change
|
||||||
| param_name="Assign To", | ||||||
| default_value="@Admin", | ||||||
| is_mandatory=True, | ||||||
| print_value=True, | ||||||
| ) | ||||||
| alert_id = siemplify.extract_action_param( | ||||||
| param_name="Alert Id", | ||||||
| default_value="id", | ||||||
| print_value=True, | ||||||
| ) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add an explicit
Suggested change
|
||||||
|
|
||||||
| response = assign_case_to_user( | ||||||
| chronicle_soar=siemplify, | ||||||
| case_id=case_id, | ||||||
| assign_to=assign_to, | ||||||
| alert_identifier=alert_id, | ||||||
| ) | ||||||
| siemplify.LOGGER.info(f"Assign case response: {response}") | ||||||
|
|
||||||
| json_result: dict[str, Any] = { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use
Suggested change
|
||||||
| "caseId": case_id, | ||||||
| "assignedTo": assign_to, | ||||||
| } | ||||||
| if response: | ||||||
| json_result["response"] = response | ||||||
|
|
||||||
| add_task = assign_case_to_user( | ||||||
| chronicle_soar=siemplify, | ||||||
| case_id=case_id, | ||||||
| assign_to=assign_to, | ||||||
| alert_identifier=alert_id, | ||||||
| ) | ||||||
| siemplify.result.add_result_json(json_result) | ||||||
| output_message = f"Successfully assigned case {case_id} to {assign_to}." | ||||||
|
|
||||||
| siemplify.LOGGER.info(add_task) | ||||||
| except Exception as e: | ||||||
| output_message = f"Error executing action '{ACTION_NAME}'. Reason: {e}" | ||||||
| siemplify.LOGGER.error(output_message) | ||||||
| siemplify.LOGGER.exception(e) | ||||||
| result_value = False | ||||||
| status = EXECUTION_STATE_FAILED | ||||||
|
|
||||||
| siemplify.end(TRUE, True) | ||||||
| siemplify.end(output_message, result_value, status) | ||||||
|
|
||||||
|
|
||||||
| if __name__ == "__main__": | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| from soar_sdk.ScriptResult import EXECUTION_STATE_COMPLETED, EXECUTION_STATE_FAILED | ||||||
| from soar_sdk.SiemplifyAction import SiemplifyAction | ||||||
| from soar_sdk.SiemplifyUtils import output_handler | ||||||
| from TIPCommon.rest.soar_api import get_siemplify_user_details | ||||||
|
|
@@ -22,24 +23,42 @@ | |||||
|
|
||||||
|
|
||||||
| @output_handler | ||||||
| def main(): | ||||||
| def main() -> None: | ||||||
|
Sahithya1357 marked this conversation as resolved.
Resolved
|
||||||
| """Main execution flow for retrieving Siemplify users.""" | ||||||
| siemplify = SiemplifyAction() | ||||||
| siemplify.script_name = ACTION_NAME | ||||||
|
|
||||||
| hide_disabled = siemplify.parameters.get("Hide Disabled Users").lower() == "true" | ||||||
| siemplify_users = get_siemplify_user_details( | ||||||
| chronicle_soar=siemplify, | ||||||
| search_term="", | ||||||
| filter_by_role=False, | ||||||
| requested_page=0, | ||||||
| page_size=1000, | ||||||
| should_hide_disabled_users=hide_disabled, | ||||||
| ) | ||||||
| siemplify.result.add_result_json( | ||||||
| {"siemplifyUsers": [s_user.to_json() for s_user in siemplify_users]}, | ||||||
| ) | ||||||
| output_message = "Returned Siemplify Users." | ||||||
| siemplify.end(output_message, True) | ||||||
| status = EXECUTION_STATE_COMPLETED | ||||||
| result_value = True | ||||||
|
|
||||||
| try: | ||||||
| hide_disabled = siemplify.extract_action_param( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add an explicit
Suggested change
|
||||||
| param_name="Hide Disabled Users", | ||||||
| default_value=True, | ||||||
| input_type=bool, | ||||||
| print_value=True, | ||||||
| ) | ||||||
| siemplify_users = get_siemplify_user_details( | ||||||
| chronicle_soar=siemplify, | ||||||
| search_term="", | ||||||
| filter_by_role=False, | ||||||
| requested_page=0, | ||||||
| page_size=1000, | ||||||
| should_hide_disabled_users=hide_disabled, | ||||||
| ) | ||||||
| siemplify.result.add_result_json( | ||||||
| {"siemplifyUsers": [s_user.to_json() for s_user in siemplify_users]}, | ||||||
| ) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can simplify this
Suggested change
|
||||||
| output_message = "Returned Siemplify Users." | ||||||
|
|
||||||
| except Exception as e: | ||||||
| output_message = f"Error executing action '{ACTION_NAME}'. Reason: {e}" | ||||||
| siemplify.LOGGER.error(output_message) | ||||||
| siemplify.LOGGER.exception(e) | ||||||
| result_value = False | ||||||
| status = EXECUTION_STATE_FAILED | ||||||
|
|
||||||
| siemplify.end(output_message, result_value, status) | ||||||
|
|
||||||
|
|
||||||
| if __name__ == "__main__": | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "caseId": "1", | ||
| "assignedTo": "@Admin" | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,150 @@ | ||||||
| # Copyright 2026 Google LLC | ||||||
| # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | ||||||
| # You may obtain a copy of the License at | ||||||
| # | ||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| # | ||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | ||||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| from unittest.mock import ANY, MagicMock, patch | ||||||
|
|
||||||
| import pytest | ||||||
| import requests | ||||||
| from integration_testing.platform.script_output import MockActionOutput | ||||||
| from integration_testing.set_meta import set_metadata | ||||||
| from TIPCommon.base.action import ExecutionState | ||||||
|
|
||||||
| from ...actions import AssignCaseToUser | ||||||
|
|
||||||
|
|
||||||
| @set_metadata( | ||||||
| integration_config={}, | ||||||
| parameters={ | ||||||
| "Case Id": "8", | ||||||
| "Assign To": "@Admin", | ||||||
| "Alert Id": "alert_1", | ||||||
| }, | ||||||
| ) | ||||||
| @patch.object(AssignCaseToUser, "assign_case_to_user") | ||||||
| def test_assign_case_to_user_success_empty_204_response( | ||||||
| mock_assign: MagicMock, | ||||||
| action_output: MockActionOutput, | ||||||
| ) -> None: | ||||||
| """Verify successful assignment sets clear output message and populates JsonResult.""" | ||||||
| mock_assign.return_value = [] | ||||||
|
|
||||||
| AssignCaseToUser.main() | ||||||
|
|
||||||
| mock_assign.assert_called_once_with( | ||||||
| chronicle_soar=ANY, | ||||||
| case_id="8", | ||||||
| assign_to="@Admin", | ||||||
| alert_identifier="alert_1", | ||||||
| ) | ||||||
| assert action_output.results.execution_state == ExecutionState.COMPLETED | ||||||
| assert action_output.results.result_value is True | ||||||
| assert action_output.results.output_message == "Successfully assigned case 8 to @Admin." | ||||||
| assert action_output.results.json_output is not None | ||||||
| assert action_output.results.json_output.json_result == { | ||||||
| "caseId": "8", | ||||||
| "assignedTo": "@Admin", | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| @set_metadata( | ||||||
| integration_config={}, | ||||||
| parameters={ | ||||||
| "Case Id": "42", | ||||||
| "Assign To": "user-uuid-123", | ||||||
| "Alert Id": "alert_1", | ||||||
| }, | ||||||
| ) | ||||||
| @patch.object(AssignCaseToUser, "assign_case_to_user") | ||||||
| def test_assign_case_to_user_success_with_api_payload( | ||||||
| mock_assign: MagicMock, | ||||||
| action_output: MockActionOutput, | ||||||
| ) -> None: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test function signature fits within 120 characters (119 chars), so we can collapse it onto a single line:
Suggested change
|
||||||
| """Verify non-empty API response data is included in JsonResult.""" | ||||||
| mock_assign.return_value = {"updatedCases": [42]} | ||||||
|
|
||||||
| AssignCaseToUser.main() | ||||||
|
|
||||||
| assert action_output.results.execution_state == ExecutionState.COMPLETED | ||||||
| assert action_output.results.result_value is True | ||||||
| assert action_output.results.output_message == "Successfully assigned case 42 to user-uuid-123." | ||||||
| assert action_output.results.json_output is not None | ||||||
| assert action_output.results.json_output.json_result == { | ||||||
| "caseId": "42", | ||||||
| "assignedTo": "user-uuid-123", | ||||||
| "response": {"updatedCases": [42]}, | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| @pytest.mark.parametrize( | ||||||
| ("status_code", "error_body", "expected_error_message"), | ||||||
| [ | ||||||
| ( | ||||||
| 400, | ||||||
| '{"errorCode":2000,"errorMessage":"Cannot assign case to disabled user account."}', | ||||||
| "Cannot assign case to disabled user account.", | ||||||
| ), | ||||||
| ( | ||||||
| 403, | ||||||
| '{"error":{"code":403,"message":"Permission chronicle.cases.update denied"}}', | ||||||
| "Permission chronicle.cases.update denied", | ||||||
| ), | ||||||
| ( | ||||||
| 500, | ||||||
| '{"error":{"code":500,"message":"Internal error"}}', | ||||||
| "Internal error", | ||||||
| ), | ||||||
| ], | ||||||
| ) | ||||||
| @set_metadata( | ||||||
| integration_config={}, | ||||||
| parameters={ | ||||||
| "Case Id": "8", | ||||||
| "Assign To": "tjones@example.com", | ||||||
| "Alert Id": "id", | ||||||
| }, | ||||||
| ) | ||||||
| @patch("TIPCommon.rest.soar_platform_clients.api_client_factory.platform_supports_1p_api", return_value=True) | ||||||
| @patch("TIPCommon.utils.platform_supports_1p_api", return_value=True) | ||||||
| def test_assign_case_to_user_1p_http_error_fails_cleanly_with_body( | ||||||
| _mock_utils_1p: MagicMock, | ||||||
| _mock_factory_1p: MagicMock, | ||||||
| status_code: int, | ||||||
| error_body: str, | ||||||
| expected_error_message: str, | ||||||
| action_output: MockActionOutput, | ||||||
| ) -> None: | ||||||
| """Verify 1P HTTP errors set FAILED state, False result, and include the API error message.""" | ||||||
| err_resp = requests.Response() | ||||||
| err_resp.status_code = status_code | ||||||
| err_resp.reason = "Error" | ||||||
| err_resp.url = ( | ||||||
| "https://soar.example.com/v1alpha/projects/p/locations/us/instances/i/" | ||||||
| "cases:executeBulkAssign" | ||||||
| ) | ||||||
| err_resp._content = error_body.encode() | ||||||
|
|
||||||
| with patch( | ||||||
| "TIPCommon.rest.soar_platform_clients.base_soar_api.BaseSoarApi._make_request", | ||||||
| return_value=err_resp, | ||||||
| ): | ||||||
| AssignCaseToUser.main() | ||||||
|
|
||||||
| assert action_output.results.execution_state == ExecutionState.FAILED | ||||||
| assert action_output.results.result_value is False | ||||||
| assert "Error executing action 'AssignCaseToUser'." in action_output.results.output_message | ||||||
| assert str(status_code) in action_output.results.output_message | ||||||
| assert expected_error_message in action_output.results.output_message | ||||||
| assert action_output.results.json_output is None | ||||||
Uh oh!
There was an error while loading. Please reload this page.