diff --git a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.py b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.py index 314351b3fd9f..1e731b737897 100644 --- a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.py +++ b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.py @@ -1271,6 +1271,23 @@ def create_co_from_template(self, template: str): """ return self.send_request(f"change/standard/{template}", "POST", body={}, cr_api=True) + def create_event(self, events: list[dict]) -> dict: + """Create one or more events in ServiceNow Event Management. + + Sends events to the /api/global/em/jsonv2 webhook endpoint which writes + to the em_events table. + + Args: + events: List of event dictionaries. Each event may contain fields + such as source, node, type, resource, severity, description, + time_of_event, and additional_info. + + Returns: + Response dict from ServiceNow. + """ + body = {"records": events} + return self.send_request(path="", method="POST", body=body, custom_api="/api/global/em/jsonv2") + def get_co_tasks(self, sys_id: str) -> dict: """Get item details from service catalog by sending a GET request to the Change Request REST API. @@ -3748,6 +3765,63 @@ def get_co_human_readable(ticket: dict, ticket_type: str, additional_fields: Ite return item +def create_event_command(client: Client, args: dict) -> CommandResults: + """Create an event in ServiceNow Event Management via the /api/global/em/jsonv2 webhook. + + Builds the event from named arguments. ``source`` defaults to ``"Cortex"``. + ``node``, ``type``, and ``time_of_event`` are required. + + Args: + client: Authenticated ServiceNow client. + args: Command arguments, usually ``demisto.args()``. + + Returns: + ``CommandResults`` with a human-readable success message and the raw ServiceNow response. + + Raises: + DemistoException: If required fields are missing. + """ + severity_map: dict[str, str] = { + "critical": "1", + "high": "2", + "medium": "3", + "low": "4", + "info": "5", + } + resolution_state_map: dict[str, str] = { + "new": "open", + "in progress": "open", + "resolved": "closed", + } + + node = args.get("node") + event_type = args.get("type") + time_of_event = args.get("time_of_event") + + event: dict = { + "source": args.get("source") or "Cortex", + "node": node, + "type": event_type, + "time_of_event": time_of_event, + } + + for field in ("resource", "description", "additional_info", "message_key"): + if (value := args.get(field)) is not None: + event[field] = value + + if (severity := args.get("severity")) is not None: + event["severity"] = severity_map.get(severity.lower(), severity) + + if (resolution_state := args.get("resolution_state")) is not None: + event["resolution_state"] = resolution_state_map.get(resolution_state.lower(), resolution_state) + + client.create_event([event]) + + return CommandResults( + readable_output="Event successfully sent to ServiceNow Event Management.", + ) + + def generic_api_call_command(client: Client, args: dict) -> Union[str, CommandResults]: """make a call to ServiceNow api Args: @@ -3969,6 +4043,10 @@ def main(): demisto.results(get_ticket_command(client, args)) elif command == "servicenow-delete-ticket": return_results(delete_ticket_command(client, args)) + elif command == "servicenow-create-event": + return_results(create_event_command(client, args)) + elif command == "servicenow-create-event-quick-action": + return_results(create_event_command(client, args)) elif command == "servicenow-generic-api-call": return_results(generic_api_call_command(client, args)) elif command == "get-remote-data": diff --git a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.yml b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.yml index 9fb66d6a872b..826c495e7ecc 100644 --- a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.yml +++ b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.yml @@ -1757,6 +1757,45 @@ script: - contextPath: ServiceNow.Tasks.State description: state of task. type: Unknown + - arguments: + - defaultValue: Cortex + description: Source of the event. + name: source + - description: Node where the event occurred. + name: node + required: true + - description: Type of the event. + name: type + required: true + - description: 'Time the event occurred, in the format: YYYY-MM-DD HH:MM:SS (UTC).' + name: time_of_event + required: true + - description: Resource on the node that triggered the event. + name: resource + - auto: PREDEFINED + description: Severity of the event. + name: severity + predefined: + - critical + - high + - medium + - low + - info + - description: Description of the event. + name: description + - description: 'Additional key-value pairs to attach to the event, in the format: {"key": "value"}.' + name: additional_info + - description: Unique key used to deduplicate events. + name: message_key + - auto: PREDEFINED + description: Resolution state of the event. + name: resolution_state + predefined: + - new + - in progress + - resolved + description: Creates an event in ServiceNow Event Management. + name: servicenow-create-event - arguments: - auto: PREDEFINED defaultValue: 'GET' @@ -2009,7 +2048,67 @@ script: - contextPath: ServiceNow.Ticket.CloseCode description: Ticket close code. type: string - + - arguments: + - defaultValue: Cortex + description: Source of the event. Defaults to "Cortex". + name: source + prettyname: Source + required: true + - description: Node (hostname or asset name) where the event occurred. + name: node + prettyname: Node + required: true + prettypredefined: + Issue Name: ${issue.name} + - description: Event type. + name: type + prettyname: Type + required: true + prettypredefined: + Category: ${issue.categoryname} + - description: Time the event occurred, in the format "YYYY-MM-DD HH:MM:SS" (UTC). + name: time_of_event + prettyname: Time of Event + required: true + prettypredefined: + Created: ${issue.created} + - description: Resource on the node that triggered the event. + name: resource + prettyname: Resource + prettypredefined: + Issue ID: ${issue.id} + - auto: PREDEFINED + description: Severity of the event. Accepts Cortex labels (critical, high, medium, low, info) or raw ServiceNow integers (0–5). + name: severity + prettyname: Severity + prettypredefined: + Severity: ${issue.severityStr} + - description: Human-readable description of the event. + name: description + prettyname: Description + prettypredefined: + Details: ${issue.details} + - description: Unique key used to deduplicate events. + name: message_key + prettyname: Message Key + prettypredefined: + Issue ID: ${issue.id} + - description: Resolution state. Accepts Cortex labels (new, in progress, resolved) or raw ServiceNow values. + name: resolution_state + prettyname: Resolution State + prettypredefined: + Resolution Status: ${issue.resolution_status} + - description: Additional key-value pairs as a JSON string. Includes the full issue as serialized JSON. + name: additional_info + prettyname: Additional Info + prettypredefined: + Full Issue: ${issue} + description: Creates an event in ServiceNow Event Management. + name: servicenow-create-event-quick-action + prettyname: Create ServiceNow Event + hidden: true + quickaction: true + outputs: [] dockerimage: demisto/auth-utils:1.0.0.13053055 isfetch: true ismappable: true @@ -2018,7 +2117,6 @@ script: script: '' subtype: python3 type: python - supportsquickactions: true fromversion: 5.0.0 tests: diff --git a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2_test.py b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2_test.py index c90322f5e873..87eadda1c611 100644 --- a/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2_test.py +++ b/Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2_test.py @@ -23,6 +23,7 @@ Client, ServiceNowClient, add_comment_command, + create_event_command, add_link_command, add_tag_command, check_assigned_to_field, @@ -3105,6 +3106,105 @@ def test_generic_api_call_command(command, args, response, mocker): assert result.outputs == response +def _make_client() -> Client: + """Return a minimal Client instance suitable for unit tests.""" + return Client( + "server_url", + "sc_server_url", + "cr_server_url", + "username", + "password", + "verify", + "fetch_time", + "sysparm_query", + "sysparm_limit", + "timestamp_field", + "ticket_type", + "get_attachments", + "incident_name", + ) + + +_REQUIRED_FIELDS = {"node": "web-server-01", "type": "CPU Utilization", "time_of_event": "2026-01-01 00:00:00"} + + +@pytest.mark.parametrize( + "args, expected_events", + [ + # Individual fields — all required + optional resource/severity/description. + ( + { + "source": "SCOM", + "node": "web-server-01", + "type": "CPU Utilization", + "time_of_event": "2026-01-01 00:00:00", + "resource": "CPU", + "severity": "2", + "description": "CPU usage exceeded 90%", + }, + [ + { + "source": "SCOM", + "node": "web-server-01", + "type": "CPU Utilization", + "time_of_event": "2026-01-01 00:00:00", + "resource": "CPU", + "severity": "2", + "description": "CPU usage exceeded 90%", + } + ], + ), + # source defaults to "Cortex" when omitted. + ( + {**_REQUIRED_FIELDS}, + [{"source": "Cortex", **_REQUIRED_FIELDS}], + ), + # Cortex severity label "high" → ServiceNow "2". + ( + {**_REQUIRED_FIELDS, "severity": "high"}, + [{"source": "Cortex", **_REQUIRED_FIELDS, "severity": "2"}], + ), + # Cortex severity label "critical" → ServiceNow "1". + ( + {**_REQUIRED_FIELDS, "severity": "critical"}, + [{"source": "Cortex", **_REQUIRED_FIELDS, "severity": "1"}], + ), + # Cortex resolution_state "resolved" → ServiceNow "closed". + ( + {**_REQUIRED_FIELDS, "resolution_state": "resolved"}, + [{"source": "Cortex", **_REQUIRED_FIELDS, "resolution_state": "closed"}], + ), + # Cortex resolution_state "in progress" → ServiceNow "open". + ( + {**_REQUIRED_FIELDS, "resolution_state": "in progress"}, + [{"source": "Cortex", **_REQUIRED_FIELDS, "resolution_state": "open"}], + ), + ], +) +def test_create_event_command_sends_correct_payload(args, expected_events, mocker): + """ + Given: + - Case 1: Individual field arguments with all required fields and optional ones. + - Case 2: source defaults to "Cortex" when omitted. + - Case 3-4: Cortex severity labels are mapped to ServiceNow integers. + - Case 5-6: Cortex resolution_state labels are mapped to ServiceNow values. + When: + - Running create_event_command. + Then: + - The client's create_event method is called with the correct list of event dicts. + - CommandResults are returned with the expected readable output. + """ + client = _make_client() + snow_response = {"result": {"status": "inserted", "message": ""}} + mock_create_event = mocker.patch.object(client, "create_event", return_value=snow_response) + + result = create_event_command(client, args) + + mock_create_event.assert_called_once_with(expected_events) + assert result.outputs is None + assert "Event successfully sent to ServiceNow Event Management." in result.readable_output + + @pytest.mark.parametrize( "file_type , expected", [(EntryType.FILE, True), (3, True), (EntryType.IMAGE, True), (EntryType.NOTE, False), (15, False)] ) diff --git a/Packs/ServiceNow/ReleaseNotes/2_10_13.md b/Packs/ServiceNow/ReleaseNotes/2_10_13.md new file mode 100644 index 000000000000..5a9992fa8a84 --- /dev/null +++ b/Packs/ServiceNow/ReleaseNotes/2_10_13.md @@ -0,0 +1,5 @@ +#### Integrations + +##### ServiceNow v2 + +- Added the new **servicenow-create-event** command, which creates an event in ServiceNow Event Management. diff --git a/Packs/ServiceNow/pack_metadata.json b/Packs/ServiceNow/pack_metadata.json index e0c1a852d875..86af530ae177 100644 --- a/Packs/ServiceNow/pack_metadata.json +++ b/Packs/ServiceNow/pack_metadata.json @@ -2,7 +2,7 @@ "name": "ServiceNow", "description": "Use The ServiceNow IT Service Management (ITSM) solution to modernize the way you manage and deliver services to your users.", "support": "xsoar", - "currentVersion": "2.10.12", + "currentVersion": "2.10.13", "author": "Cortex XSOAR", "url": "https://www.paloaltonetworks.com/cortex", "email": "",