Generic Webhook

The Generic Webhook integration is used to create incidents on event triggers. The trigger can be any query posted to the integration.

Utilities · Generic Webhook

Details

IDGeneric Webhook
ProviderOpen Source
CategoryUtilities
From Version5.5.0
Docker Imagedemisto/fastapi:0.125.0.10158186
Supported ModulesAgentix XSIAM

README

The Generic Webhook integration enables you to push events to Cortex XSOAR or Cortex XSIAM. The Generic Webhook integration can be used when there is no relevant integration, when the integration does not match your organization’s needs, when there are restrictions on pulling information from the third-party solution, or when the triggering source is not an event that can be fetched by Cortex XSOAR or Cortex XSIAM, such as a slack message or a completed form.

Example: Your organization’s employees fill out a Google form to report security incidents. Employees specify the type of incident in a drop-down field with predefined options, the Generic Webhook integration pushes the event into Cortex XSOAR or Cortex XSIAM, and a playbook runs. The incident type and the specific playbook is determined by the incident type field in the drop-down.

The Generic Webhook integration creates incidents in Cortex XSOAR, alerts in Cortex XSIAM 2.x or issues in Cortex XSIAM 3.x. The trigger can be any query posted to the integration.

The Generic Webhook integration is a long-running integration. For more information about long-running integrations, see the Cortex XSOAR 8 Cloud, Cortex XSOAR 8 On-prem or Cortex XSIAM documentation.

To use the Generic Webhook integration, you need to complete the following steps:

  1. Configure Generic Webhook in Cortex XSOAR or Cortex XSIAM.
  2. Set up authentication.
  3. Determine the webhook URL.
  4. Trigger the webhook to generate incidents, alerts, or issues.

Configure Generic Webhook on Cortex XSOAR or Cortex XSIAM

  1. In the Cortex XSOAR or Cortex XSIAM integrations page, search for Generic Webhook and click Add instance to create and configure a new integration instance.
Parameter Description Required
Listen Port

Note: This field only appears in Cortex XSOAR 8 and Cortex XSIAM if you are using an engine. It always appears in Cortex 6.x.
Runs the service on this port. Requires a unique port for each long-running integration instance. Do not use the same port for multiple instances.
Note: If you click the test button more than once, a failure may occur, mistakenly indicating that the port is already in use.
True
username Username (see Security for more details) For Cortex XSOAR 6.x - False

For Cortex XSOAR 8 and Cortex XSIAM if the integration is running on an engine - False

For Cortex XSOAR 8 and Cortex XSIAM if the integration is not running on an engine - True
password Password (see Security for more details) For Cortex XSOAR 6.x - False

For Cortex XSOAR 8 and Cortex XSIAM if the integration is running on an engine - False

For Cortex XSOAR 8 and Cortex XSIAM if the integration is not running on an engine - True
certificate For use with HTTPS - the certificate that the service should use.
Supported for Cortex XSOAR On-prem (6.x or 8). Supported for Cortex XSOAR 8 Cloud and Cortex XSIAM only when using an engine.

Cortex XSOAR 8 Cloud tenants and Cortex XSIAM tenants do not support custom certificates.
False
Private Key For use with HTTPS - the private key that the service should use.
Supported for Cortex XSOAR On-prem (6.x or 8). Supported for Cortex XSOAR 8 Cloud and Cortex XSIAM only when using an engine.

Cortex XSOAR 8 Cloud tenants and Cortex XSIAM tenants do not support private keys.
False
Result Automatically generated webhook trigger link (based on user configuration). Auto-populated.

Note: This field does not appear for Cortex XSOAR 6.x or Cortex XSOAR 8.9 On-prem
incidentType Incident, issue, or alert type False
store_samples Store sample events for mapping (Because this is a push-based integration, it cannot fetch sample events in the mapping wizard). False
  1. Click Done.
  2. For Cortex XSOAR 6.x:
    1. Navigate to Settings > About > Troubleshooting.
    2. In the Server Configuration section, verify that the value for the instance.execute.external.<INTEGRATION-INSTANCE-NAME> key is set to true. If this key does not exist, click + Add Server Configuration and add instance.execute.external.<INTEGRATION-INSTANCE-NAME> and set the value to true. See the following reference article for further information.

Set up Authentication

Authentication options depend on the product and version.

Product Authentication required Authentication options
Cortex XSOAR 8 Cloud tenant Yes - Basic authentication (username and password)
Cortex XSIAM tenant Yes - Basic authentication (username and password)
Cortex XSOAR 8 On-prem No - Basic authentication (username and password)

- Custom certificate
NOTE: For more information about setting up custom certificates for Cortex XSOAR 8 On-prem, see HTTPS with a signed certificate.
Cortex XSOAR 6.x No - Basic authentication (username and password)

- Header-based authentication using _header:<HEADER-NAME> syntax

- Custom certificate
NOTE: For more information about setting up custom certificates for Cortex XSOAR 6.x, see HTTPS with a signed certificate.
Engines (Cortex XSOAR 8 Cloud, Cortex XSOAR 8 On-Prem, Cortex XSIAM, Cortex 6.x) No - Basic authentication (username and password)

- Header-based authentication using _header:<HEADER-NAME> syntax (Cortex XSOAR 6.x only)

- Custom certificate
NOTE: For more information about setting up custom certificates for engines, see Configure an engine to use custom certificates.

Determine the webhook URL

For Cortex XSIAM and Cortex XSOAR 8 Cloud, the Results section of the integration configuration provides the webhook URL. We recommend you verify the URL using the instructions below. For on-prem Cortex XSOAR 6.x and Cortex XSOAR 8, you must use the URL template below to determine the webhook URL.

Prerequisite:
For Cortex XSOAR 8 On-prem, you need to add the ext- FQDN DNS record to map the Cortex XSOAR DNS name to the external IP address.
For example, ext-xsoar.mycompany.com.

Product URL Example
Cortex XSOAR 8 Cloud and On-prem <ext-<CORTEX-TENANT-URL>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME> https://ext-mytenant.crtx.us.paloaltonetworks.com/xsoar/instance/execute/my_instance_01
Cortex XSIAM <ext-<CORTEX-TENANT-URL>/xsoar/instance/execute/<INTEGRATION-INSTANCE-NAME> https://ext-mytenant.crtx.us.paloaltonetworks.com/xsoar/instance/execute/my_instance_01
Cortex XSOAR 6.x <CORTEX-XSOAR-URL>/instance/execute/<INTEGRATION-INSTANCE-NAME> https://my.xsoar.live/instance/execute/webhook

Notes:

  • For Cortex XSIAM, you must replace xdr in the tenant URL with crtx. For example, if your tenant URL is https://companyname.xdr.eu.paloaltonetworks.com the webhook URL is https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01.
  • The string instance does not refer to the name of your Cortex instance, it is part of the URL.
  • The name of the instance cannot include special characters.
  • For Cortex XSOAR 6.x or if you are using an engine, if you are not invoking the integration via the server HTTPS endpoint, you can trigger the webhook URL as follows: <CORTEX-XSOAR-URL>:<LISTEN_PORT>/ For example, https://my.xsoar.live:8000/.

Usage

The Generic Webhook integration accepts POST HTTP queries, with the following optional fields in the request body:

Field Type Description
name string Name of the incident, alert, or issue to be created.
type string Type of the incident, alert, or issue to be created. If not provided, the value of the integration parameter incidentType is used.
occurred string Date the incident occurred in ISO-8601 format. If not provided, the trigger time is used.
rawJson object Details of the incident, alert, or issue to be created. Headers can be found in a seperate key. For example, {"field1":"value1","field2":"value2","headers": {"header_field3": "header_value3"}}

Note: The cURL examples below are formatted for macOS. For Windows machines, modify as needed.

Examples

Basic authentication can be used in three ways with the same username/password configured in the integration:

Using -u flag:

curl -X POST https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -u "<Username:Password>" -H "Content-Type: application/json" -d '{"name":"incident created via generic webhook","rawJson":{"some_field":"some_value"}}'

Using Authorization header (where the header value is base64 encoded username:password):

curl -X POST https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -H "Authorization: Basic MTIzOjEyMw==" -H "Content-Type: application/json" -d '{"name":"incident created via generic webhook","rawJson":{"some_field":"some_value"}}'
Note: MTIzOjEyMw== is Base64 encoded username:password example.

Or embedding credentials directly in the URL:

curl -X POST https://username:password@ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -H "Content-Type: application/json" -d '{"name":"incident created via generic webhook","rawJson":{"some_field":"some_value"}}'

The request payload does not have to contain the fields mentioned above, and may include any JSON fields and values:

curl -X POST https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -u "<Username:Password>" -H "Content-Type: application/json" -d '{"string_field":"string_field_value","array_field":["item1","item2"]}'

Multiple incidents, alerts, or issues can be created in one request by sending an array as the request body:

curl -X POST https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -u "<Username:Password>" -H "Content-Type: application/json" -d '[{"name":"incident1","rawJson":{"some_field":"some_value"}}, {"name":"incident2","rawJson":{"some_field":"some_value"}}]'

Using custom header authentication (Cortex XSOAR 6.x only). In this example, the username in the integration instance is set to _header:Authorization and the password in the integration instance is set to Basic MYIvOkEyMw== :

curl -X POST https://ext-companyname.crtx.eu.paloaltonetworks.com/xsoar/instance/execute/my_instance_01 -H "Authorization: Basic MYIvOkEyMw==" -H "Content-Type: application/json" -d '{"name":"incident created via generic webhook","rawJson":{"some_field":"some_value"}}' -v

The response is an array containing an object with the created incident metadata, such as the incident ID.

The payload can then be mapped. For more information see:

Note: To use the mapping wizard, the Store sample events for mapping parameter must be set. Because this is a push-based integration, it cannot fetch sample events in the mapping wizard. After you finish mapping, we recommend turning off the sample events storage to reduce performance overhead.

Authorization headers

For Cortex XSOAR 6.x users, you can use the special _header:<HEADER-NAME> syntax to authenticate requests using custom headers from your third-party service. This helps prevent unauthorized creation of incidents. Set the username field in the integration to _header:<HEADER-NAME> and provide the header value in the password field.
Example: If the request included in the Authorization header the value Bearer XXX, then the username should be set to _header:Authorization and the password should be set to Bearer XXX.

Troubleshooting authorization headers

  • Header Size Limit: Each server or framework may impose a limit on the total size of the headers received in a request. For example, servers such as Nginx or Apache have their own default values that can be configured. FastAPI itself doesn’t specifically limit the header size, but underlying ASGI servers like Uvicorn or Hypercorn that run FastAPI do have default limits (For example, Uvicorn has a default of 1MB for the total size of request headers).
  • Allowed Characters: Headers should only use ASCII characters. Non-ASCII characters must be encoded.
  • Header Names and Values: Certain characters are restricted in header names and values. Typically, names cannot include characters such as : or newlines, and values are restricted from including newlines to protect against header injection attacks.
  • Case Sensitivity: Header keys are case-insensitive as per HTTP standards, but it is good practice to keep a consistent casing convention for ease of maintenance and readability.

Configuration parameters

  • engine_placeholder
  • longRunning — Long running instance
  • credentials — Username
  • longRunningPort — Listen Port (required)
  • certificate — Certificate (Required for HTTPS)
  • key — Private Key (Required for HTTPS)
  • incidentType — Incident type
  • store_samples — Store sample events for mapping
  • resultLink — After adding the Generic Webhook settings, a unique link will be generated below.

Commands (0)

This integration defines no commands.

import asyncio
from http import HTTPStatus
from unittest.mock import MagicMock

import pytest
from CommonServerPython import *

# import demistomock as demisto
from fastapi import Request
from fastapi.testclient import TestClient
from GenericWebhook import app, main, parse_incidents


@pytest.fixture
def client():
    return TestClient(app)


def test_handle_post_single_incident(mocker, client):
    """
    Given: A bodu that is one incident
    When: Sending a post request
    Then: The body is parsed properly
    """
    incident_data = {
        "name": "Test Incident",
        "type": "Test Type",
        "occurred": "2024-03-17T12:00:00Z",
        "rawJson": {"key": "value"},
    }
    return_incidents = [{"name": "something"}]

    create_incidents = mocker.patch.object(demisto, "createIncidents", return_value=return_incidents)
    response = client.post("/", json=incident_data)

    called_arg = create_incidents.call_args_list[0].args[0]
    assert isinstance(called_arg, list)
    assert len(called_arg) == 1
    assert response.status_code == HTTPStatus.OK
    assert response.json() == return_incidents


def test_handle_post_multiple_incident(mocker, client):
    """
    Given: A bodu that is an array of incidents
    When: Sending a post request
    Then: The body is parsed properly
    """
    incident_data = [
        {"name": "Test Incident", "type": "Test Type", "occurred": "2024-03-17T12:00:00Z", "raw_json": {"key": "value"}},
        {"name": "Test Incident2", "type": "Test Type", "occurred": "2024-03-17T12:00:00Z", "raw_json": {"key": "value"}},
    ]
    return_incidents = [{"name": "something"}]

    create_incidents = mocker.patch.object(demisto, "createIncidents", return_value=return_incidents)
    response = client.post("/", json=incident_data)

    called_arg = create_incidents.call_args_list[0].args[0]
    assert isinstance(called_arg, list)
    assert len(called_arg) == 2
    assert response.status_code == HTTPStatus.OK
    assert response.json() == return_incidents


def test_handle_post_with_invalid_credentials(mocker, client):
    """
    Given: a server that expects a user and password
    When: Calling post with bad credentials
    Then:a 401 response code is recieved
    """
    mocker.patch.object(demisto, "params", return_value={"credentials": {"identifier": "user", "password": "pass"}})
    response = client.post("/", json=[{"name": "Test Incident"}], auth=("invalid_username", "invalid_password"))
    assert response.status_code == HTTPStatus.UNAUTHORIZED
    assert response.text == "Authorization failed."


def test_handle_post_with_valid_credentials(mocker, client):
    """
    Given: a server that expects a user and password
    When: Calling post with proper credentials
    Then:a 200 response code is recieved
    """
    mocker.patch.object(demisto, "params", return_value={"credentials": {"identifier": "user", "password": "pass"}})
    response = client.post("/", json=[{"name": "Test Incident"}], auth=("user", "pass"))
    assert response.status_code == HTTPStatus.OK
    assert response.text == "[]"


def test_handle_post_with_missing_data(mocker, client):
    """
    Given: A request with no body
    When: Post is called
    Then: A readable error message is returned
    """
    mocker.patch.object(demisto, "error")
    response = client.post("/")
    assert response.status_code == HTTPStatus.BAD_REQUEST
    assert "Request, and rawJson field if exists must be in JSON format" in response.text


def test_handle_post_with_invalid_json(mocker, client):
    """
    Given: A request with a bad body
    When: Post is called
    Then: A readable error message is returned
    """
    mocker.patch.object(demisto, "error")
    response = client.post("/", data="invalid_json")
    assert response.status_code == HTTPStatus.BAD_REQUEST
    assert "Request, and rawJson field if exists must be in JSON format" in response.text


@pytest.mark.parametrize(
    "body",
    [
        {"name": "Test Incident 1", "type": "Test Type 1", "occurred": "2024-03-17T12:00:00Z", "rawJson": {"key": "value"}},
        {"name": "Test Incident 1", "type": "Test Type 1", "occurred": "2024-03-17T12:00:00Z", "key": "value"},
        {"name": "Test Incident 1", "type": "Test Type 1", "occurred": "2024-03-17T12:00:00Z", "raw_json": {"key": "value"}},
    ],
)
def test_parse_request(body):
    """
    Given: two inputs, either with raw_json being real json or a string representation of json
    When: calling parse_body
    Then: The body is parsed the same
    """
    # Prepare a mock Request object with JSON data
    mock_request = MagicMock(spec=Request)

    async def mockbody():
        return body

    mock_request.json = mockbody

    # Call the parse_incidents function with the mock Request
    result = asyncio.run(parse_incidents(mock_request))

    # Check if the function returns a list of dictionaries with the parsed incidents
    assert isinstance(result, list)
    assert len(result) == 1
    assert result[0]["name"] == "Test Incident 1"
    assert result[0]["type"] == "Test Type 1"
    assert result[0]["occurred"] == "2024-03-17T12:00:00Z"
    assert result[0]["rawJson"]["key"] == "value"


def test_main_test_module(mocker):
    mocker.patch.object(demisto, "command", return_value="test-module")
    mocker.patch.object(demisto, "params", return_value={"longRunningPort": "444"})
    results = mocker.patch.object(demisto, "results")
    main()
    assert results.call_args_list[0].args[0] == "ok"


def test_main_long_running(mocker):
    """
    We have an autorecovery mechanism here that when the app fails with an exception it should be restarted five seconds later
    """
    mocker.patch.object(demisto, "error")

    mocker.patch.object(demisto, "command", return_value="long-running-execution")
    mocker.patch.object(
        demisto, "params", return_value={"longRunningPort": "444", "certificate": "something", "key": "something"}
    )
    mocker.patch.object(demisto, "results")
    mocker.patch("time.sleep")
    uvicornmock = MagicMock(side_effect=[Exception("restart once"), Exception("Twice"), BaseException("Hack to get out")])
    mocker.patch("uvicorn.run", uvicornmock)
    try:
        main()
        raise AssertionError
    except BaseException:
        """ This is kind of a hack to stop the while true loop"""

    assert len(uvicornmock.call_args_list) == 3