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
78 changes: 78 additions & 0 deletions Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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")

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.

The docstring specifies that node, type, and time_of_event are required fields and that a DemistoException is raised if any are missing, but the function does not validate their presence before constructing the event. If omitted, None values are sent to ServiceNow.

Consider adding explicit validation:

Suggested change
node = args.get("node")
node = args.get("node")
event_type = args.get("type")
time_of_event = args.get("time_of_event")
missing = [name for name, val in (("node", node), ("type", event_type), ("time_of_event", time_of_event)) if not val]
if missing:
raise DemistoException(f"Missing required argument(s): {', '.join(missing)}")

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)

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.

If severity is provided as a non-string type (such as an integer from playbook context, e.g., severity=1), calling .lower() will raise an AttributeError.

Consider casting to str before calling .lower():

Suggested change
event["severity"] = severity_map.get(severity.lower(), severity)
event["severity"] = severity_map.get(str(severity).lower(), str(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])

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.

The docstring states that CommandResults returns the raw ServiceNow response, but the return value of client.create_event([event]) is currently ignored and raw_response is not passed to CommandResults.

Consider capturing the response and including it in CommandResults:

Suggested change
client.create_event([event])
response = client.create_event([event])
return CommandResults(
readable_output="Event successfully sent to ServiceNow Event Management.",
raw_response=response,
)


return CommandResults(

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 assign the response from client.create_event([event]) and pass it to CommandResults as raw_response.

    response = client.create_event([event])

    return CommandResults(
        readable_output="Event successfully sent to ServiceNow Event Management.",
        raw_response=response,
    )

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:
Expand Down Expand Up @@ -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":
Expand Down
102 changes: 100 additions & 2 deletions Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,45 @@ script:
- contextPath: ServiceNow.Tasks.State
description: state of task.
type: Unknown
- arguments:
- defaultValue: Cortex

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.

Suggested change
- defaultValue: Cortex
- defaultvalue: Cortex

According to Cortex XSOAR YAML formatting standards, schema keys are case-sensitive and should use lowercase (e.g., defaultvalue instead of defaultValue). Please update this key here and in the quick-action arguments on line 2052.

description: Source of the event.

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.

Suggested change
description: Source of the event.
description: The source of the event.

Argument descriptions should follow the descriptive form starting with "The " and ending with a period (e.g., "The source of the event."). Please apply this convention across the other newly added argument descriptions as well.

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'
Expand Down Expand Up @@ -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".

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.

Suggested change
description: Source of the event. Defaults to "Cortex".
description: The source of the event.

Since a default value is defined in the argument's defaultvalue field, remove "Defaults to "Cortex"." from the description. The defaultvalue field serves as the single source of truth. Additionally, start the description with "The " per description guidelines.

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
Expand All @@ -2018,7 +2117,6 @@ script:
script: ''
subtype: python3
type: python

supportsquickactions: true
fromversion: 5.0.0
tests:
Expand Down
100 changes: 100 additions & 0 deletions Packs/ServiceNow/Integrations/ServiceNowv2/ServiceNowv2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Client,
ServiceNowClient,
add_comment_command,
create_event_command,
add_link_command,
add_tag_command,
check_assigned_to_field,
Expand Down Expand Up @@ -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)]
)
Expand Down
5 changes: 5 additions & 0 deletions Packs/ServiceNow/ReleaseNotes/2_10_13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#### Integrations

##### ServiceNow v2

- Added the new **servicenow-create-event** command, which creates an event in ServiceNow Event Management.

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.

Command names should be formatted in bold-italics (***command-name***) rather than bold per the Entity Styling guidelines.

Suggested change
- Added the new **servicenow-create-event** command, which creates an event in ServiceNow Event Management.
- Added the new ***servicenow-create-event*** command, which creates an event in ServiceNow Event Management.

2 changes: 1 addition & 1 deletion Packs/ServiceNow/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down
Loading