Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
Sahithya1357 marked this conversation as resolved.
Resolved
"""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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since siemplify.extract_action_param(...) returns Any, let's add an explicit : str type annotation to case_id and collapse this call onto a single line (which also keeps main() under 50 lines):

Suggested change
)
case_id: str = siemplify.extract_action_param(param_name="Case Id", is_mandatory=True, print_value=True)

assign_to = siemplify.extract_action_param(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please annotate assign_to with an explicit : str type annotation here as well since extract_action_param returns Any:

Suggested change
assign_to = siemplify.extract_action_param(
assign_to: str = siemplify.extract_action_param(

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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add an explicit : str annotation for alert_id and place this call on a single line (110 characters):

Suggested change
)
alert_id: str = siemplify.extract_action_param(param_name="Alert Id", default_value="id", print_value=True)


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] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use SingleJson (from TIPCommon.types) here instead of dict[str, Any]:

Suggested change
json_result: dict[str, Any] = {
json_result: SingleJson = {

"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__":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ creator: Rob
default_result_value: ''
description: 'This action will assign a user to a case. '
dynamic_results_metadata:
- result_example_path: null
- result_example_path: resources/AssignCaseToUser_JsonResult_example.json
result_name: JsonResult
show_result: true
integration_identifier: Tools
Expand All @@ -19,8 +19,8 @@ parameters:
name: Assign To
type: string
- default_value: id
description: Alert Id
is_mandatory: true
description: Alert Id (applicable to legacy SOAR only; ignored on Unified SecOps 1P where assignment applies to the entire case).
is_mandatory: false
name: Alert Id
type: string
script_result_name: ScriptResult
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,24 +23,42 @@


@output_handler
def main():
def main() -> None:
Comment thread
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add an explicit : bool type annotation for hide_disabled since extract_action_param returns Any:

Suggested change
hide_disabled = siemplify.extract_action_param(
hide_disabled: bool = siemplify.extract_action_param(

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]},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this add_result_json call onto a single line since it is 110 characters:

Suggested change
)
siemplify.result.add_result_json({"siemplifyUsers": [s_user.to_json() for s_user in siemplify_users]})

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__":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "Tools"
version = "85.0"
version = "86.0"
description = "A set of utility actions for data manipulation and common platform tasks to power up playbook capabilities."
requires-python = ">=3.11,<3.12"
dependencies = [
Expand Down Expand Up @@ -28,9 +28,9 @@ dev = [

[tool.uv.sources]
environmentcommon = { path = "../../../../packages/envcommon/whls/EnvironmentCommon-1.0.3-py3-none-any.whl" }
integration-testing = { path = "../../../../packages/integration_testing_whls/integration_testing-2.3.9-py3-none-any.whl" }
integration-testing = { path = "../../../../packages/integration_testing_whls/integration_testing-2.4.9-py3-none-any.whl" }
soar-sdk = { git = "https://github.com/chronicle/soar-sdk.git" }
tipcommon = { path = "../../../../packages/tipcommon/whls/TIPCommon-2.4.8-py3-none-any.whl" }
tipcommon = { path = "../../../../packages/tipcommon/whls/TIPCommon-2.4.9-py3-none-any.whl" }

[[tool.uv.index]]
url = "https://pypi.org/simple"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,15 @@
item_type: Action
publish_time: '2026-10-28'
ticket_number: '572634903'
- description: Get Siemplify Users - Updated error handling logic.
version: 86.0
item_name: Get Siemplify Users
item_type: Action
publish_time: '2026-11-11'
ticket_number: '574548101'
- description: Assign Case to User - Updated case assignment processing logic.
version: 86.0
item_name: Assign Case to User
item_type: Action
publish_time: '2026-11-11'
ticket_number: '574548101'
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
) -> None:
def test_assign_case_to_user_success_with_api_payload(mock_assign: MagicMock, action_output: MockActionOutput) -> None:

"""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
Loading
Loading