Kibana

This integration enables using Elastic Security for SIEM for security operations management and searching Elastic logs. This pack is to be used in combination with the Elasticsearch v2 integration.

Analytics & SIEM · Kibana

Details

IDKibana
ProviderElastic
CategoryAnalytics & SIEM
From Version6.0.0
Docker Imagedemisto/elasticsearch:1.0.0.10133006

README

Use the Kibana integration to manage Elastic Security cases, detection alerts, rules, and value lists for security operations.
This integration was tested with Elasticsearch versions 6.6.2, 7.3, 8.4.1, and 9.3.1.

Configure Kibana in Cortex

Parameter Description Required
Server URL The Elasticsearch server to which the integration connects. Ensure that the URL includes the correct Elasticsearch port. The default port for Elasticsearch v7 and below is 9200. Use the Server URL for on-premises deployments. True
Elastic API Port The port for the Elastic API. False
Kibana API Port The port for the Kibana API. False
Authorization type The authentication type and credentials to use: Basic Auth (Username and Password), Bearer Auth (Username and Password), or API Key Auth (API Key ID and API Key). False
API key ID   False
API Key   False
Username The username and password to use instead of API key and API ID. False
Password   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Client type In some hosted ElasticSearch environments, the standard ElasticSearch client is not supported. If you encounter any related client issues, please consider using the OpenSearch client type. False
Request timeout (in seconds).   False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

kibana-cases-find


Lists cases in Kibana.

Base Command

kibana-cases-find

Input

Argument Name Description Required
status The status of the cases to retrieve. Possible values are: open, in-progress, closed. Default is open. Optional
severity The severity of the cases to retrieve. Possible values are: critical, high, medium, low. Optional
from_time The earliest time to search from (for example, 2025-10-02T00:27:58.162Z). Optional

Context Output

Path Type Description
Kibana.Cases.Status unknown The status of the case in Kibana.
Kibana.Cases.Version unknown The version number of the case in Kibana.
Kibana.Cases.ID unknown The ID number of the case in Kibana.

kibana-case-alerts-find


Returns information on the alerts of the input case in Kibana.

Base Command

kibana-case-alerts-find

Input

Argument Name Description Required
case_id The ID of the case in Kibana. Locate it with the “kibana-cases-find” command. Required

Context Output

Path Type Description
Kibana.CaseAlerts.ID unknown The ID of alerts tied to the case in Kibana.

kibana-alert-status-update


Updates the status of an input alert.

Base Command

kibana-alert-status-update

Input

Argument Name Description Required
alert_id The alert ID to update. Find it with the “kibana-detection-alerts-list” command. Required
status The status to set the alert to. Possible values are: open, closed. Required

Context Output

There is no context output for this command.

kibana-case-status-update


Updates the status of an input case.

Base Command

kibana-case-status-update

Input

Argument Name Description Required
status The status of the case to update. Possible values are: open, in-progress, closed. Required
case_id The ID of the case in Kibana. Locate it with the “kibana-cases-find” command. Required
version_id The version ID of the case. Find it with the “kibana-cases-find” command. This ID changes after each case update. Required

Context Output

There is no context output for this command.

kibana-user-spaces-find


Gets the list of user spaces in Kibana.

Base Command

kibana-user-spaces-find

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.UserSpaces.description unknown The default user space description.
Kibana.UserSpaces.disabledFeatures unknown The list of disabled Kibana features.

kibana-case-comments-find


Finds comments for an input case ID.

Base Command

kibana-case-comments-find

Input

Argument Name Description Required
case_id The case ID to find comments for. Locate it with the “kibana-cases-find” command. Required

Context Output

Path Type Description
Kibana.CaseComments.version unknown The version number of the case comment in Kibana.
Kibana.CaseComments.id unknown The ID number of the case comment in Kibana.

kibana-case-delete


Deletes a case in Kibana based on case ID.

Base Command

kibana-case-delete

Input

Argument Name Description Required
case_id The case ID to delete. Locate it with the “kibana-cases-find” command. Required

Context Output

There is no context output for this command.

kibana-rule-delete


Deletes a rule in Kibana based on the input rule ID.

Base Command

kibana-rule-delete

Input

Argument Name Description Required
rule_id The rule ID to delete. Find it with the “kibana-rule-details-search” command. Required

Context Output

There is no context output for this command.

kibana-rule-details-search


Retrieves details about a detection rule in Kibana based on the input KQL filter.

Base Command

kibana-rule-details-search

Input

Argument Name Description Required
kql_query The KQL filter to search rules with. For example: “alert.attributes.name: Smith”. Optional

Context Output

Path Type Description
Kibana.RuleDetails.enabled unknown Whether the rule is enabled in Kibana.
Kibana.RuleDetails.name unknown The name of the rule in Kibana.
Kibana.RuleDetails.id unknown The ID of the rule in Kibana.

kibana-case-comment-add


Adds a comment to a case in Kibana. The case ID and owner can be obtained from the “kibana-cases-find” command.

Base Command

kibana-case-comment-add

Input

Argument Name Description Required
case_id The case ID to add the comment to. Locate it with the “kibana-cases-find” command. Required
case_owner The owner of the case, as listed in the “kibana-cases-find” command output. Possible values are: cases, observability, securitySolution. Required
comment The comment to add to the case in Kibana. Required

Context Output

There is no context output for this command.

kibana-user-list-get


Searches for the list of users in Kibana and returns the users’ UIDs.

Base Command

kibana-user-list-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.UserList.username unknown The username of the user in Kibana.
Kibana.UserList.roles unknown The associated roles of the user in Kibana.

kibana-alert-assign


Assigns an alert in Kibana to a user via user ID input.

Base Command

kibana-alert-assign

Input

Argument Name Description Required
user_id The UID of the user to be assigned. Locate it with the “kibana-user-list-get” command. Required
alert_id The alert ID to assign the user to. Find it with the “kibana-detection-alerts-list” command. Required

Context Output

There is no context output for this command.

kibana-detection-alerts-list


Searches for detection alerts in Kibana.

Base Command

kibana-detection-alerts-list

Input

Argument Name Description Required
alert_status The status of the detection alert to search for. Possible values are: open, closed. Required

Context Output

Path Type Description
Kibana.DetectionAlerts.bhe.windows.security_id unknown The username associated with the detection alert.
Kibana.DetectionAlerts.kibana.alert.original_data_stream.dataset unknown The dataset associated with the detection alert.
Kibana.DetectionAlerts.message unknown The raw log message of the detection alert.
Kibana.DetectionAlerts.kibana.alert.uuid unknown The ID of the detection alert.
Kibana.DetectionAlerts.kibana.alert.rule.name unknown The rule name associated with the detection alert.

kibana-alert-note-add


Adds a note to an alert in Kibana.

Base Command

kibana-alert-note-add

Input

Argument Name Description Required
alert_id The alert ID to update the note on. Find it with the “kibana-detection-alerts-list” command. Required
note The note text to add to the alert. Required

Context Output

There is no context output for this command.

kibana-alerting-health-get


Retrieves the health status of the Kibana alerting framework.

Base Command

kibana-alerting-health-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.AlertingFrameworkHealth.alerting_framework_health.decryption_health.status unknown Whether Kibana can successfully decrypt encrypted alert data.
Kibana.AlertingFrameworkHealth.alerting_framework_health.execution_health.status unknown Whether rules are running on time or failing.
Kibana.AlertingFrameworkHealth.alerting_framework_health.read_health.status unknown Whether rule configurations can be successfully retrieved from internal Kibana indices.

kibana-alert-rule-disable


Disables a detection alerting rule. Clears associated alerts from the active alerts page.

Base Command

kibana-alert-rule-disable

Input

Argument Name Description Required
rule_id The rule ID to disable. Find it with the “kibana-rule-details-search” command. Required

Context Output

There is no context output for this command.

kibana-alert-rule-enable


Enables a rule used for detection alerting.

Base Command

kibana-alert-rule-enable

Input

Argument Name Description Required
rule_id The rule ID to enable. Find it with the “kibana-rule-details-search” command. Required

Context Output

There is no context output for this command.

kibana-exception-lists-get


Retrieves a list of all exception list containers.

Base Command

kibana-exception-lists-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.ExceptionLists.name unknown The name of the exception list.
Kibana.ExceptionLists.list_id unknown The list ID of the exception list.
Kibana.ExceptionLists.description unknown The description of the exception list.

kibana-value-list-create


Creates a value list in Kibana.

Base Command

kibana-value-list-create

Input

Argument Name Description Required
description The description of the value list. Required
name The name of the value list. Required
data_type The Elasticsearch data type the list container holds. Possible values are: keyword, ip, ip_range, text. Required
list_id The identifier of the value list. Required

Context Output

There is no context output for this command.

kibana-value-lists-get


Finds all value lists in the Kibana Detection Rules menu.

Base Command

kibana-value-lists-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.ValueLists.name unknown The name of the value list.
Kibana.ValueLists.id unknown The ID of the value list.
Kibana.ValueLists.description unknown The description of the value list.

kibana-value-list-items-import


Imports value list items from a TXT or CSV file.

Base Command

kibana-value-list-items-import

Input

Argument Name Description Required
list_id The value list ID to import values to. Find it with the “kibana-value-lists-get” command. Required
file_content The IOC file entries to import to Kibana in Python string format. Required

Context Output

There is no context output for this command.

kibana-value-list-item-create


Creates a value list item and associates it with the specified value list.

Base Command

kibana-value-list-item-create

Input

Argument Name Description Required
list_id The value list ID to update. Find it with the “kibana-value-lists-get” command. Required
new_value_list_item The item to add to the specified value list. Required

Context Output

There is no context output for this command.

kibana-value-list-items-get


Displays entries in an input value list.

Base Command

kibana-value-list-items-get

Input

Argument Name Description Required
list_id The value list ID to retrieve values for. Find it with the “kibana-value-lists-get” command. Required
result_size The size of results to return. Default is 100. Optional

Context Output

Path Type Description
Kibana.ValueListItems.value unknown The value of the value list item.
Kibana.ValueListItems.id unknown The ID of the value list item.
Kibana.ValueListItems.list_id unknown The list ID of the value list.

kibana-value-list-item-delete


Deletes a value list item, given the item ID and list ID as input.

Base Command

kibana-value-list-item-delete

Input

Argument Name Description Required
item_id The value list entry ID to delete. Find it with the “kibana-value-list-items-get” command. Required
list_id The value list ID to delete the value from. Find it with the “kibana-value-lists-get” command. Required

Context Output

There is no context output for this command.

kibana-value-list-delete


Deletes a value list given the list ID as input.

Base Command

kibana-value-list-delete

Input

Argument Name Description Required
list_id The value list ID to delete. Find it with the “kibana-value-lists-get” command. Required

Context Output

There is no context output for this command.

kibana-status-get


Checks the Kibana operational status.

Base Command

kibana-status-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.OperationalStatus.core.elasticsearch.level unknown The connection health between Kibana and Elasticsearch.
Kibana.OperationalStatus.overall.level unknown The aggregated health status of the Kibana instance.
Kibana.OperationalStatus.core.savedObjects.level unknown The health status of the Saved Objects repository.

kibana-task-manager-health-get


Retrieves the health status of the Kibana task manager.

Base Command

kibana-task-manager-health-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.TaskManagerHealth.capacity_estimation.status unknown The ability to handle scheduled tasks in Kibana.
Kibana.TaskManagerHealth.configuration.status unknown The configuration status of the Kibana task manager.
Kibana.TaskManagerHealth.runtime.status unknown The performance, drift, and load of Kibana task execution.
Kibana.TaskManagerHealth.workload.status unknown The status of tasks running, to identify potential overload.

kibana-upgrade-readiness-status-get


Checks the status of the cluster.

Base Command

kibana-upgrade-readiness-status-get

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
Kibana.UpgradeReadinessStatus.details unknown The details for what is needed prior to Kibana upgrades.
Kibana.UpgradeReadinessStatus.readyForUpgrade unknown Whether Kibana is ready for upgrade.

kibana-case-comment-delete


Deletes a case comment.

Base Command

kibana-case-comment-delete

Input

Argument Name Description Required
case_id The case ID to delete the comment on. Retrieve case IDs with the “kibana-cases-find” command. Required
comment_id The identifier for the comment. Find comment IDs with the “kibana-case-comments-find” command. Required

Context Output

There is no context output for this command.

kibana-case-file-add


Attaches a file to a case.

Base Command

kibana-case-file-add

Input

Argument Name Description Required
case_id The case ID to attach the file to. Locate it with the “kibana-cases-find” command. Required
file_id The file entry ID from Cortex XSOAR context data to add to the case. Required

Context Output

There is no context output for this command.

kibana-user-by-email-get


Searches for a single user’s UID in Kibana by email address filter.

Base Command

kibana-user-by-email-get

Input

Argument Name Description Required
email_wildcard The full or partial email address to search for the user with (for example, william.smith@*). Required

Context Output

Path Type Description
Kibana.UserData.profile_uid unknown The user ID for tracking user activity and checking privileges.
Kibana.UserData.roles unknown The roles tied to the user account.

kibana-case-information-get


Retrieves information for a specific case in Kibana.

Base Command

kibana-case-information-get

Input

Argument Name Description Required
case_id The case ID to retrieve information for. View available case IDs with the “kibana-cases-find” command. Required

Context Output

Path Type Description
Kibana.CaseInfo.status unknown Whether the case is open, in-progress, or closed.
Kibana.CaseInfo.owner unknown The application that created the case.
Kibana.CaseInfo.version unknown The version of the case being updated.
Kibana.CaseInfo.id unknown The unique identifier for a case.

Configuration parameters

  • url — Server URL (required)
  • elastic_port — Elastic API Port
  • kibana_port — Kibana API Port
  • auth_type — Authorization type
  • api_key_auth_credentials — API key ID
  • credentials — Username
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • client_type — Client type
  • timeout — Request timeout (in seconds).

Commands (32)

  • kibana-alert-assign

    Assigns an alert in Kibana to a user via user ID input.

  • kibana-alert-note-add

    Adds a note to an alert in Kibana.

  • kibana-alert-rule-disable

    Disables a detection alerting rule. Clears associated alerts from the active alerts page.

  • kibana-alert-rule-enable

    Enables a rule used for detection alerting.

  • kibana-alert-status-update

    Updates the status of an input alert.

  • kibana-alerting-health-get

    Retrieves the health status of the Kibana alerting framework.

  • kibana-case-alerts-find

    Returns information on the alerts of the input case in Kibana.

  • kibana-case-comment-add

    Adds a comment to a case in Kibana. The case ID and owner can be obtained from the "kibana-cases-find" command.

  • kibana-case-comment-delete

    Deletes a case comment.

  • kibana-case-comments-find

    Finds comments for an input case ID.

  • kibana-case-delete

    Deletes a case in Kibana based on case ID.

  • kibana-case-file-add

    Attaches a file to a case.

  • kibana-case-information-get

    Retrieves information for a specific case in Kibana.

  • kibana-case-status-update

    Updates the status of an input case.

  • kibana-cases-find

    Lists cases in Kibana.

  • kibana-detection-alerts-list

    Searches for detection alerts in Kibana.

  • kibana-exception-lists-get

    Retrieves a list of all exception list containers.

  • kibana-rule-delete

    Deletes a rule in Kibana based on the input rule ID.

  • kibana-rule-details-search

    Retrieves details about a detection rule in Kibana based on the input KQL filter.

  • kibana-status-get

    Checks the Kibana operational status.

  • kibana-task-manager-health-get

    Retrieves the health status of the Kibana task manager.

  • kibana-upgrade-readiness-status-get

    Checks the status of the cluster.

  • kibana-user-by-email-get

    Searches for a single user's UID in Kibana by email address filter.

  • kibana-user-list-get

    Searches for the list of users in Kibana and returns the users' UIDs.

  • kibana-user-spaces-find

    Gets the list of user spaces in Kibana.

  • kibana-value-list-create

    Creates a value list in Kibana.

  • kibana-value-list-delete

    Deletes a value list given the list ID as input.

  • kibana-value-list-item-create

    Creates a value list item and associates it with the specified value list.

  • kibana-value-list-item-delete

    Deletes a value list item, given the item ID and list ID as input.

  • kibana-value-list-items-get

    Displays entries in an input value list.

  • kibana-value-list-items-import

    Imports value list items from a TXT or CSV file.

  • kibana-value-lists-get

    Finds all value lists in the Kibana Detection Rules menu.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

from CommonServerUserPython import *


"""IMPORTS"""
import requests
import json
from datetime import datetime, timezone

UTC = timezone.utc  # noqa: UP017

"""Parameters"""
PARAMS = demisto.params()
AUTH_TYPE = PARAMS.get("auth_type", "Basic auth")
USERNAME: str = PARAMS.get("credentials", {}).get("identifier")
PASSWORD: str = PARAMS.get("credentials", {}).get("password")
AUTH = (USERNAME, PASSWORD)
API_KEY_ID: str = PARAMS.get("api_key_auth_credentials", {}).get("identifier")
API_KEY_SECRET: str = PARAMS.get("api_key_auth_credentials", {}).get("password")
API_KEY = None
PROXY = PARAMS.get("proxy")
ELASTIC_SERVER = PARAMS.get("url", "").rstrip("/") + ":" + (PARAMS.get("elastic_port") or "9200")
KIBANA_SERVER = PARAMS.get("url", "").rstrip("/") + ":" + (PARAMS.get("kibana_port") or "443")
INSECURE = not PARAMS.get("insecure", False)
TIMEOUT = int(PARAMS.get("timeout") or 60)

# .ymla values
BASIC_AUTH = "Basic auth"
BEARER_AUTH = "Bearer auth"
API_KEY_AUTH = "API key auth"
API_KEY_PREFIX = "_api_key_id:"

# Using API key auth by username and password fields for backward compatibility.
if AUTH_TYPE == BASIC_AUTH:
    if USERNAME and USERNAME.startswith(API_KEY_PREFIX):
        AUTH_TYPE = API_KEY_AUTH
        API_KEY_ID = USERNAME[len(API_KEY_PREFIX) :]
        API_KEY = (API_KEY_ID, PASSWORD)

elif AUTH_TYPE == API_KEY_AUTH:
    API_KEY = (API_KEY_ID, API_KEY_SECRET)

ELASTICSEARCH_V8 = "Elasticsearch_v8"
ELASTICSEARCH_V9 = "Elasticsearch_v9"
OPEN_SEARCH = "OpenSearch"
ELASTIC_SEARCH_CLIENT = PARAMS.get("client_type") or "Elasticsearch"
if ELASTIC_SEARCH_CLIENT == OPEN_SEARCH:
    from opensearchpy import RequestsHttpConnection
    from opensearchpy import OpenSearch as Elasticsearch
elif ELASTIC_SEARCH_CLIENT in [ELASTICSEARCH_V8, ELASTICSEARCH_V9]:
    from elastic_transport import RequestsHttpNode
    from elasticsearch import Elasticsearch  # type: ignore[assignment]
else:  # Elasticsearch (<= v7)
    from elasticsearch7 import Elasticsearch, RequestsHttpConnection  # type: ignore[assignment,misc]


def get_api_key_header_val(api_key):
    """
    Check the type of the passed api_key and return the correct header value
    for the `API Key authentication
    <https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html>`
    :arg api_key, either a tuple or a base64 encoded string
    """
    if isinstance(api_key, tuple | list):
        s = f"{api_key[0]}:{api_key[1]}".encode()
        return "ApiKey " + base64.b64encode(s).decode("utf-8")
    return "ApiKey " + api_key


def is_access_token_expired(expires_in: str) -> bool:
    """Check if access token is expired.

    Args:
        expires_in: ISO format datetime string representing when the token expires (UTC)

    Returns:
        bool: True if token is expired or will expire within 1 minute, False otherwise
    """
    try:
        # Parse the expires_in string to a UTC datetime object
        expiration_time = datetime.strptime(expires_in, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)

        # Subtract 1 min to refresh slightly early and avoid expiration issues.
        current_time_with_buffer = datetime.now(UTC) + timedelta(minutes=1)

        is_not_expired = expiration_time > current_time_with_buffer
        if is_not_expired:
            demisto.debug(
                f"is_access_token_expired - using existing Access token from integration context (expires in {expires_in})."
            )
            return False
        else:
            demisto.debug("is_access_token_expired - Access token expired.")
            return True
    except (ValueError, TypeError) as e:
        demisto.debug(f"is_access_token_expired - Error parsing expiration time: {e}. Treating as expired.")
        return True


def get_elastic_token(proxies):
    """
    Authenticates and retrieves an OAuth 2.0 access token from Elasticsearch.

    Returns an access token either by refreshing an existing token or performing a new token request.
        1. Check if existing access token is valid (with 1min buffer).
        2. If not, try to use refresh token if it exists and is valid.
        3. If not, perform a full password grant authentication for receiving initial access token.
    """
    try:
        url = urljoin(ELASTIC_SERVER, "_security/oauth2/token")
        headers = {"Content-Type": "application/json"}

        integration_context = get_integration_context()
        access_token = integration_context.get("access_token", "")
        access_token_expires_in = integration_context.get("access_token_expires_in", "")
        refresh_token = integration_context.get("refresh_token", "")
        refresh_token_expires_in = integration_context.get("refresh_token_expires_in", "")

        # 1. Check if token exists and if it is still valid
        if access_token and not is_access_token_expired(access_token_expires_in):
            demisto.debug("get_elastic_token - Using existing access token from integration context.")
            return access_token

        if not USERNAME or not PASSWORD:
            demisto.debug("get_elastic_token - username or password fields are missing.")
            raise DemistoException("username or password fields are missing.")

        # 2. Token exists but expired, and refresh token is valid
        if refresh_token and not is_access_token_expired(refresh_token_expires_in):
            demisto.debug(
                "get_elastic_token - Access token expired, but Refresh token valid. Attempting to get token using refresh token"
            )

            payload = {"grant_type": "refresh_token", "refresh_token": refresh_token}
            response = requests.post(
                url, headers=headers, json=payload, verify=INSECURE, auth=(USERNAME, PASSWORD), proxies=proxies
            )

            if response.status_code == 200:
                now = datetime.now(UTC)
                token_data = response.json()
                access_token_expires_in = (now + timedelta(seconds=token_data.get("expires_in"))).strftime("%Y-%m-%dT%H:%M:%SZ")
                refresh_token_expires_in = (now + timedelta(hours=24)).strftime(
                    "%Y-%m-%dT%H:%M:%SZ"
                )  # refresh token has a lifetime of 24 hours

                integration_context.update(
                    {
                        "access_token": token_data.get("access_token"),
                        "refresh_token": token_data.get("refresh_token"),
                        "access_token_expires_in": access_token_expires_in,
                        "refresh_token_expires_in": refresh_token_expires_in,
                    }
                )
                set_integration_context(integration_context)
                demisto.debug(
                    "get_elastic_token - Access token received successfully by refresh token and set to integration context."
                )
                return integration_context["access_token"]

            # If refresh fails, clear the refresh token to force generating of new token
            demisto.debug("get_elastic_token - refresh fails, a new token will be generated via password grant.")
            integration_context.update({"refresh_token": None, "refresh_token_expires_in": None})
            set_integration_context(integration_context)

        # Generate a new access vi password grant
        demisto.debug("get_elastic_token - Attempting to get token using grant_type:password")

        payload = {"grant_type": "password", "username": USERNAME, "password": PASSWORD}
        response = requests.post(url, headers=headers, auth=(USERNAME, PASSWORD), json=payload, verify=INSECURE, proxies=proxies)
        if response.status_code == 200:
            now = datetime.now(UTC)
            token_data = response.json()
            access_token_expires_in = (now + timedelta(seconds=token_data.get("expires_in"))).strftime("%Y-%m-%dT%H:%M:%SZ")
            refresh_token_expires_in = (now + timedelta(hours=24)).strftime(
                "%Y-%m-%dT%H:%M:%SZ"
            )  # refresh token has a lifetime of 24 hours

            integration_context.update(
                {
                    "access_token": token_data.get("access_token"),
                    "refresh_token": token_data.get("refresh_token"),
                    "access_token_expires_in": access_token_expires_in,
                    "refresh_token_expires_in": refresh_token_expires_in,
                }
            )
            set_integration_context(integration_context)
            demisto.debug(
                "get_elastic_token - Access token received successfully via password grant and set to integration context."
            )
            return integration_context["access_token"]

        demisto.debug(f"Failed to authenticate: {response.status_code}\n{response.text}")
        try:
            reason = json.loads(response.text).get("error", {}).get("reason")
        except Exception:
            reason = response.reason or response.text
        raise DemistoException(f"{response.status_code}, {reason}")

    except Exception as e:
        demisto.debug(f"get_elastic_token error: \n{str(e)}")
        raise DemistoException(f"{str(e)}")


def elasticsearch_builder(proxies):
    """Builds an Elasticsearch obj with the necessary credentials, proxy settings and secure connection."""

    connection_args: Dict[str, Union[bool, int, str, list, tuple[str, str], RequestsHttpConnection]] = {
        "hosts": [ELASTIC_SERVER],
        "verify_certs": INSECURE,
        "timeout": TIMEOUT,
    }
    if ELASTIC_SEARCH_CLIENT not in [ELASTICSEARCH_V9, ELASTICSEARCH_V8]:
        # Adding the proxy related parameters to the Elasticsearch client v7 and below or OpenSearch (BC)
        connection_args["connection_class"] = RequestsHttpConnection  # type: ignore[assignment]
        connection_args["proxies"] = proxies

    else:
        # Adding the proxy related parameter to the Elasticsearch client v8
        # Reference- https://github.com/elastic/elastic-transport-python/issues/53#issuecomment-1447903214
        class CustomHttpNode(RequestsHttpNode):  # pylint: disable=E0601
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.session.proxies = proxies

        connection_args["node_class"] = CustomHttpNode  # type: ignore[assignment]

    if AUTH_TYPE == API_KEY_AUTH and API_KEY:
        connection_args["api_key"] = API_KEY

    elif AUTH_TYPE == BASIC_AUTH and USERNAME and PASSWORD:
        if ELASTIC_SEARCH_CLIENT in [ELASTICSEARCH_V9, ELASTICSEARCH_V8]:
            connection_args["basic_auth"] = (USERNAME, PASSWORD)
        else:  # Elasticsearch version v7 and below or OpenSearch (BC)
            connection_args["http_auth"] = (USERNAME, PASSWORD)

    elif AUTH_TYPE == BEARER_AUTH:
        connection_args["bearer_auth"] = get_elastic_token(proxies)

    es = Elasticsearch(**connection_args)  # type: ignore[arg-type]

    # Ensuring api_key will be set correctly in case the authentication type is API key auth.
    # this should be passed as api_key via Elasticsearch init, but this code ensures it'll be set correctly
    # In some versions of the ES library, the transport object does not have a get_session func
    if AUTH_TYPE == API_KEY_AUTH and hasattr(es, "transport") and hasattr(es.transport, "get_connection"):
        es.transport.get_connection().session.headers["authorization"] = get_api_key_header_val(  # type: ignore[attr-defined]
            API_KEY
        )

    return es


def http_request(
    method, url_suffix, headers, auth=AUTH, params=None, data=None, files=None, safe=False, parse_json=True, proxies=None
):
    """
    A wrapper for requests lib to send our requests and handle requests and responses better.

    :type method: ``str``
    :param method: HTTP method for the request.

    :type url_suffix: ``str``
    :param url_suffix: The suffix of the URL (endpoint)

    :type params: ``dict``
    :param params: The URL params to be passed.

    :type data: ``dict``
    :param data: The body data of the request.

    :type headers: ``dict``
    :param headers: Request headers

    :type safe: ``bool``
    :param safe: If set to true will return None in case of error

    :return: Returns the http request response json
    :rtype: ``dict`` or ``str``
    """
    url = KIBANA_SERVER + url_suffix
    try:
        res = requests.request(
            method,
            url,
            auth=auth,
            verify=INSECURE,
            params=params,
            json=data,
            files=files,
            headers=headers,
            proxies=proxies,
        )
    except requests.exceptions.RequestException as e:
        demisto.debug(str(e))
        return_error("Error in connection to the server. Please make sure you entered the URL correctly.")
    # Handle error responses gracefully

    if res.status_code not in {200, 201}:
        if safe:
            return None
        elif res.status_code == 401:
            reason = "Unauthorized. Please check your API token"
        elif res.status_code == 204:
            return None
        else:
            try:
                reason = res.json()
            except ValueError:
                reason = res.reason
        return_error(f"Error in API call status code: {res.status_code}, reason: {reason}")
    if parse_json:
        return res.json()
    return res.content


def test_func(proxies):
    """
    Tests API connectivity to the Elasticsearch server.
    Tests the existence of all necessary fields for fetch.

    Due to load considerations, the test module doesn't check the validity of the fetch-incident - to test that the fetch works
    as excepted the user should run the es-integration-health-check command.

    """
    success, message = test_connectivity_auth(proxies)
    if not success:
        return message
    return "ok"


def test_connectivity_auth(proxies) -> tuple[bool, str]:
    """
    Test connectivity and authentication with Elasticsearch server
    Args:
        proxies (dict): Dictionary of proxy settings

    Returns:
        tuple[bool, str]: (success status, message)
    """

    demisto.debug("test_connectivity_auth started")
    headers = {"Content-Type": "application/json"}
    res = None

    try:
        if AUTH_TYPE == BASIC_AUTH:
            demisto.debug("test_connectivity_auth - Basic auth setting authorization header and sending request")
            res = requests.get(ELASTIC_SERVER, auth=(USERNAME, PASSWORD), verify=INSECURE, headers=headers, proxies=proxies)

        elif AUTH_TYPE == API_KEY_AUTH:
            demisto.debug("test_connectivity_auth - API key auth setting authorization header and sending request")
            headers["authorization"] = get_api_key_header_val(API_KEY)
            res = requests.get(ELASTIC_SERVER, verify=INSECURE, headers=headers, proxies=proxies)

        elif AUTH_TYPE == BEARER_AUTH:
            demisto.debug("test_connectivity_auth - Bearer auth setting authorization header and sending request")
            headers["Authorization"] = f"Bearer {get_elastic_token(proxies)}"
            res = requests.get(ELASTIC_SERVER, verify=INSECURE, headers=headers, proxies=proxies)

        if res is not None:
            if res.status_code >= 400:
                demisto.debug(f"test_connectivity_auth - Failed to connect.\n{res.status_code=}, {res.text=}")
                return False, f"Failed to connect.\nStatus:{res.status_code}, {res.reason}"

            elif res.status_code == 200:
                demisto.debug("test_connectivity_auth - Connectivity test successful")
                verify_es_server_version(res.json())
                return True, "Connectivity test successful"

        return False, "No response received from server"

    except Exception as e:
        demisto.debug(f"test_connectivity_auth - Failed to connect.\nError message: {e}")
        return False, f"Failed to connect.\n{e}"


def verify_es_server_version(res):
    """
    Gets the requests.get raw response, extracts the elasticsearch server version,
    and verifies that the client type parameter is configured accordingly.
    Raises exceptions for server version miss configuration issues.

    Args:
        res(dict): requests.models.Response object including information regarding the elasticsearch server.
    """
    es_server_version = res.get("version", {}).get("number", "")
    demisto.debug(f"Elasticsearch server version is: {es_server_version}")
    if es_server_version:
        major_version = es_server_version.split(".")[0]
        if major_version:
            if int(major_version) >= 8 and ELASTIC_SEARCH_CLIENT not in [ELASTICSEARCH_V9, ELASTICSEARCH_V8, OPEN_SEARCH]:
                raise ValueError(
                    f"Configuration Error: Your Elasticsearch server is version {es_server_version}. "
                    f"Please ensure that the client type is set to {ELASTICSEARCH_V9}, {ELASTICSEARCH_V8} or {OPEN_SEARCH}. "
                    f"For more information please see the integration documentation."
                )
            elif int(major_version) <= 7 and ELASTIC_SEARCH_CLIENT not in [OPEN_SEARCH, "Elasticsearch"]:
                raise ValueError(
                    f"Configuration Error: Your Elasticsearch server is version {es_server_version}. "
                    f"Please ensure that the client type is set to Elasticsearch or {OPEN_SEARCH}. "
                    f"For more information please see the integration documentation."
                )


def kibana_find_cases(args, proxies):
    """
    Returns information on the cases in Kibana.
    API reference: https://www.elastic.co/docs/api/doc/kibana/operation/operation-findcasesdefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    status = args.get("status")
    severity = args.get("severity")
    from_time = arg_to_datetime(args.get("from_time"))
    from_time_str = from_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ") if from_time else None

    query_params = {"status": status, "severity": severity, "from": from_time_str}

    response = http_request(method="GET", url_suffix="/api/cases/_find", params=query_params, headers=headers, proxies=proxies)
    json_data = response["cases"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Cases", json_data, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Cases", outputs=json_data, raw_response=response)

    return result


def kibana_find_alerts_for_case(args, proxies):
    """
    Returns information on the alerts of a case in Kibana.
    API reference: https://www.elastic.co/docs/api/doc/kibana/operation/operation-getcasealertsdefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")

    response = http_request(method="GET", url_suffix=f"/api/cases/{case_id}/alerts", headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Kibana Alerts For Case", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Alerts.For.Case", outputs=response)

    return result


def kibana_update_alert_status(args, proxies):
    """
    Update status of input alert in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-setalertsstatus
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    alert_id = args.get("alert_id")
    status = args.get("status")

    data = {
        "status": status,
        "signal_ids": [
            alert_id,
        ],
    }

    http_request(method="POST", url_suffix="/api/detection_engine/signals/status", data=data, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Updated alert ID {alert_id} to status of {status}")


def kibana_update_case_status(args, proxies):
    """
    Update status of input case in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-updatecasedefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")
    status = args.get("status")
    version = args.get("version_id")

    data = {"cases": [{"id": case_id, "status": status, "version": version}]}

    response = http_request(method="PATCH", url_suffix="/api/cases", data=data, headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Kibana Updated Case Status", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Updated.Case.Status", outputs=response)

    return result


def kibana_find_user_spaces(args, proxies):
    """
    Get list of user spaces in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-spaces-space
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/spaces/space", headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Kibana User Spaces", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.User.Spaces", outputs=response)

    return result


def kibana_find_case_comments(args, proxies):
    """
    Get list of comments for a case in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-findcasecommentsdefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")

    response = http_request(method="GET", url_suffix=f"/api/cases/{case_id}/comments/_find", headers=headers, proxies=proxies)
    response = response["comments"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Case Comments", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Case.Comments", outputs=response)

    return result


def kibana_delete_case(args, proxies):
    """
    Delete case in Kibana based on input case ID.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-deletecasedefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")

    params = {"ids": json.dumps([case_id])}

    http_request(method="DELETE", url_suffix="/api/cases", params=params, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully deleted case with ID of {case_id}")


def kibana_delete_rule(args, proxies):
    """
    Delete rule in Kibana based on input rule ID.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-delete-alerting-rule-id
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    rule_id = args.get("rule_id")

    http_request(method="DELETE", url_suffix=f"/api/alerting/rule/{rule_id}", headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully deleted rule with ID of {rule_id}")


def kibana_search_rule_details(args, proxies):
    """
    Retrieve details about detection rule in Kibana based on input KQL filter.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-alerting-rules-find
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    kql_query = args.get("kql_query")

    params = {"filter": kql_query}

    response = http_request(method="GET", url_suffix="/api/alerting/rules/_find", params=params, headers=headers, proxies=proxies)
    response = response["data"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Rule Details", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Rule.Details", outputs=response)

    return result


def kibana_add_case_comment(args, proxies):
    """
    Add comment to case in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-addcasecommentdefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")
    case_owner = args.get("case_owner")
    comment = args.get("comment")

    json_data = {
        "type": "user",
        "owner": case_owner,
        "comment": comment,
    }

    response = http_request(
        method="POST", url_suffix=f"/api/cases/{case_id}/comments", data=json_data, headers=headers, proxies=proxies
    )
    updated_at = response["updated_at"]

    return CommandResults(readable_output=f"Case comment updated at {updated_at}")


def kibana_get_user_list(args, proxies):
    """
    Search for a list of all users and UIDs in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-query-user
    """
    es = elasticsearch_builder(proxies)

    try:
        all_users = es.security.query_user(with_profile_uid=True, size=100)
        # The v8/v9 client wraps the response in an ObjectApiResponse exposing `.body`,
        # while the v7 and OpenSearch clients return a plain dict.
        all_users = getattr(all_users, "body", all_users)["users"]

        # output results to markdown table
        md = tableToMarkdown("Kibana User List", all_users, headers=[])

        result = CommandResults(readable_output=md, outputs_prefix="Kibana.User.List", outputs=all_users)

        return result

    except Exception as e:  # pragma: no cover
        raise DemistoException(f"Error querying all users: {e}")


def kibana_assign_alert_user(args, proxies):
    """
    Assign user to input alert in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-setalertassignees
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    alert_id = args.get("alert_id")
    user_id = args.get("user_id")

    json_data = {
        "ids": [
            alert_id,
        ],
        "assignees": {
            "add": [
                user_id,
            ],
            "remove": [],
        },
    }

    http_request(
        method="POST", url_suffix="/api/detection_engine/signals/assignees", data=json_data, headers=headers, proxies=proxies
    )

    return CommandResults(readable_output=f"Assigned user ID {user_id} to alert {alert_id}")


def kibana_list_detection_alerts(args, proxies):
    """
    List detection alerts in Kibana matching a status filter.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-searchalerts
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    alert_status = args.get("alert_status")

    json_data = {
        "query": {
            "bool": {
                "filter": [
                    {
                        "bool": {
                            "must": [],
                            "filter": [
                                {
                                    "match_phrase": {
                                        "kibana.alert.workflow_status": alert_status,
                                    },
                                },
                            ],
                        },
                    },
                ],
            },
        },
        "runtime_mappings": {},
    }

    response = http_request(
        method="POST", url_suffix="/api/detection_engine/signals/search", data=json_data, headers=headers, proxies=proxies
    )

    result_json = response["hits"]
    result_list = result_json.get("hits")
    result_list_final = []

    # append each _source dict in list of dicts to a final results list
    for item in result_list:
        result_list_final.append(item.get("_source"))

    # output results to markdown table
    md = tableToMarkdown("Kibana Detection Alerts", result_list_final, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Detection.Alerts", outputs=result_list_final)

    return result


def kibana_add_alert_note(args, proxies):
    """
    Add note to detection alerts in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-persistnoteroute
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    event_id = args.get("alert_id")
    note = args.get("note")

    json_data = {
        "note": {
            "eventId": event_id,
            "note": note,
            "timelineId": "",
        },
    }

    http_request(method="PATCH", url_suffix="/api/note", data=json_data, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Added note {note} to alert {event_id}")


def kibana_get_alerting_health(args, proxies):
    """
    Get alerting framework health in Kibana.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-getalertinghealth
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/alerting/_health", headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Alerting Framework Health", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Alerting.Framework.Health", outputs=response)

    return result


def kibana_disable_alert_rule(args, proxies):
    """
    Used to disable a rule used for detection alerting. Clears all associated alerts from active alerts page.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-alerting-rule-id-disable
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    rule_id = args.get("rule_id")

    json_data = {
        "untrack": True,
    }

    http_request(
        method="POST", url_suffix=f"/api/alerting/rule/{rule_id}/_disable", data=json_data, headers=headers, proxies=proxies
    )

    return CommandResults(readable_output=f"Successfully disabled rule with ID of {rule_id}")


def kibana_enable_alert_rule(args, proxies):
    """
    Used to enable a rule used for detection alerting.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-alerting-rule-id-enable
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    rule_id = args.get("rule_id")

    http_request(method="POST", url_suffix=f"/api/alerting/rule/{rule_id}/_enable", headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully enabled rule with ID of {rule_id}")


def kibana_get_exception_lists(args, proxies):
    """
    Used to get a list of all exception list containers.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-findexceptionlists
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/exception_lists/_find", headers=headers, proxies=proxies)
    response = response["data"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Exception Lists", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Exception.Lists", outputs=response)

    return result


def kibana_create_value_list(args, proxies):
    """
    Used to create a value list in Kibana Detection Rules menu.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-createlist
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    description = args.get("description")
    list_id = args.get("list_id")
    name = args.get("name")
    data_type = args.get("data_type")

    json_data = {
        "id": list_id,
        "name": name,
        "type": data_type,
        "description": description,
    }

    http_request(method="POST", url_suffix="/api/lists", data=json_data, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully created value list with name of {name}")


def kibana_get_value_lists(args, proxies):
    """
    Used to find all value lists in Kibana Detection Rules menu.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-findlists
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/lists/_find", headers=headers, proxies=proxies)
    result_json = response["data"]

    # output results to markdown table
    md = tableToMarkdown("Alerting Value Lists", result_json, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Alerting.Value.Lists", outputs=result_json)

    return result


def kibana_import_value_list_items(args, proxies):
    """
    Used to import value list items from a TXT file.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-importlistitems
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    list_id = args.get("list_id")
    file_content = args.get("file_content")

    json_data = {
        "list_id": list_id,
    }

    files = {"file": ("value_list.txt", file_content, "text/plain")}

    http_request(
        method="POST", url_suffix="/api/lists/items/_import", params=json_data, files=files, headers=headers, proxies=proxies
    )

    return CommandResults(readable_output=f"Successfully imported {file_content} to value list with ID of {list_id}")


def kibana_create_value_list_item(args, proxies):
    """
    Used to create a value list item and associate it with the specified value list.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-createlistitem
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    list_id = args.get("list_id")
    new_item = args.get("new_value_list_item")

    json_data = {"value": new_item, "list_id": list_id}

    http_request(method="POST", url_suffix="/api/lists/items", data=json_data, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully added {new_item} to value list with ID of {list_id}")


def kibana_get_value_list_items(args, proxies):
    """
    Used to display entries in an input value list.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-findlistitems
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    list_id = args.get("list_id")
    result_size = args.get("result_size")

    params = {"list_id": list_id, "sort_field": "created_at", "sort_order": "asc", "per_page": result_size}

    response = http_request(method="GET", url_suffix="/api/lists/items/_find", params=params, headers=headers, proxies=proxies)
    result_output = response["data"]

    # output results to markdown table
    md = tableToMarkdown("Value List Items", result_output, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Value.List.Items", outputs=result_output)

    return result


def kibana_delete_value_list_item(args, proxies):
    """
    Used to delete a value list item given the item ID as input.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-deletelistitem
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
        "Content-Type": "application/json",
    }

    item_id = args.get("item_id")
    list_id = args.get("list_id")

    json_data = {"id": item_id, "list_id": list_id}

    http_request(method="DELETE", url_suffix="/api/lists/items", params=json_data, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully deleted {item_id} from value list with ID of {list_id}")


def kibana_delete_value_list(args, proxies):
    """
    Used to delete a value list given the list ID as input.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-deletelist
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
        "Content-Type": "application/json",
    }

    list_id = args.get("list_id")

    params = {"id": list_id}

    http_request(method="DELETE", url_suffix="/api/lists", params=params, headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Successfully deleted value list with ID of {list_id}")


def kibana_get_status(args, proxies):
    """
    Used to check Kibana's operational status.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-status
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/status", headers=headers, proxies=proxies)
    response = response["status"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Operational Status", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Operational.Status", outputs=response)

    return result


def kibana_get_task_manager_health(args, proxies):
    """
    Get the health status of the Kibana task manager.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-task-manager-health
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/task_manager/_health", headers=headers, proxies=proxies)
    response = response["stats"]

    # output results to markdown table
    md = tableToMarkdown("Kibana Task Manager Health", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Task.Manager.Health", outputs=response)

    return result


def kibana_get_upgrade_readiness_status(args, proxies):
    """
    Check the upgrade readiness status of your cluster.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-upgrade-status
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    response = http_request(method="GET", url_suffix="/api/upgrade_assistant/status", headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Kibana Upgrade Readiness Status", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Upgrade.Readiness.Status", outputs=response)

    return result


def kibana_delete_case_comment(args, proxies):
    """
    Delete a case comment.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-deletecasecommentdefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")
    comment_id = args.get("comment_id")

    http_request(method="DELETE", url_suffix=f"/api/cases/{case_id}/comments/{comment_id}", headers=headers, proxies=proxies)

    return CommandResults(readable_output=f"Deleted comment with ID {comment_id} from case {case_id}")


def kibana_add_file_to_case(args, proxies):
    """
    Attach a file to a case.
    Reference - https://www.elastic.co/docs/api/doc/kibana/operation/operation-addcasefiledefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")
    file_id = args.get("file_id")

    file_path_dict = demisto.getFilePath(file_id)
    file_path = file_path_dict["path"]
    file_name = file_path_dict["name"]

    with open(file_path, "rb") as f:
        files = {"file": (file_name, f)}

        http_request(method="POST", url_suffix=f"/api/cases/{case_id}/files", files=files, headers=headers, proxies=proxies)
        return f"Successfully added file {file_name} to case {case_id}"


def kibana_get_user_by_email(args, proxies):
    """
    Search for a single user's UID in Kibana by email address filter.
    Reference - https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-query-user
    """

    email_wildcard = args.get("email_wildcard")

    es = elasticsearch_builder(proxies)

    query_body = {"query": {"wildcard": {"email": {"value": email_wildcard, "case_insensitive": True}}}}

    try:
        user_data = es.security.query_user(with_profile_uid=True, body=query_body)
        user_data = user_data["users"]

        # output results to markdown table
        md = tableToMarkdown("Kibana User Data", user_data, headers=[])

        result = CommandResults(readable_output=md, outputs_prefix="Kibana.User.Data", outputs=user_data)

        return result

    except Exception as e:  # pragma: no cover
        raise DemistoException(f"Error querying users by email: {e}")


def kibana_get_case_information(args, proxies):
    """
    Retrieve information for a specific case in Kibana.
    API reference: https://www.elastic.co/docs/api/doc/kibana/operation/operation-getcasedefaultspace
    """
    headers = {
        "kbn-xsrf": "true",  # Required for Kibana API requests
    }

    case_id = args.get("case_id")

    response = http_request(method="GET", url_suffix=f"/api/cases/{case_id}", headers=headers, proxies=proxies)

    # output results to markdown table
    md = tableToMarkdown("Kibana Case Info", response, headers=[])

    result = CommandResults(readable_output=md, outputs_prefix="Kibana.Case.Info", outputs=response)

    return result


def main():  # pragma: no cover
    proxies = handle_proxy()
    proxies = proxies if proxies else None
    args = demisto.args()
    try:
        demisto.debug(f"command is {demisto.command()}")
        if demisto.command() == "test-module":
            return_results(test_func(proxies))
        elif demisto.command() == "kibana-cases-find":
            return_results(kibana_find_cases(args, proxies))
        elif demisto.command() == "kibana-case-alerts-find":
            return_results(kibana_find_alerts_for_case(args, proxies))
        elif demisto.command() == "kibana-alert-status-update":
            return_results(kibana_update_alert_status(args, proxies))
        elif demisto.command() == "kibana-case-status-update":
            return_results(kibana_update_case_status(args, proxies))
        elif demisto.command() == "kibana-user-spaces-find":
            return_results(kibana_find_user_spaces(args, proxies))
        elif demisto.command() == "kibana-case-comments-find":
            return_results(kibana_find_case_comments(args, proxies))
        elif demisto.command() == "kibana-case-delete":
            return_results(kibana_delete_case(args, proxies))
        elif demisto.command() == "kibana-rule-delete":
            return_results(kibana_delete_rule(args, proxies))
        elif demisto.command() == "kibana-rule-details-search":
            return_results(kibana_search_rule_details(args, proxies))
        elif demisto.command() == "kibana-case-comment-add":
            return_results(kibana_add_case_comment(args, proxies))
        elif demisto.command() == "kibana-user-list-get":
            return_results(kibana_get_user_list(args, proxies))
        elif demisto.command() == "kibana-alert-assign":
            return_results(kibana_assign_alert_user(args, proxies))
        elif demisto.command() == "kibana-detection-alerts-list":
            return_results(kibana_list_detection_alerts(args, proxies))
        elif demisto.command() == "kibana-alert-note-add":
            return_results(kibana_add_alert_note(args, proxies))
        elif demisto.command() == "kibana-alerting-health-get":
            return_results(kibana_get_alerting_health(args, proxies))
        elif demisto.command() == "kibana-alert-rule-disable":
            return_results(kibana_disable_alert_rule(args, proxies))
        elif demisto.command() == "kibana-alert-rule-enable":
            return_results(kibana_enable_alert_rule(args, proxies))
        elif demisto.command() == "kibana-exception-lists-get":
            return_results(kibana_get_exception_lists(args, proxies))
        elif demisto.command() == "kibana-value-list-create":
            return_results(kibana_create_value_list(args, proxies))
        elif demisto.command() == "kibana-value-lists-get":
            return_results(kibana_get_value_lists(args, proxies))
        elif demisto.command() == "kibana-value-list-items-import":
            return_results(kibana_import_value_list_items(args, proxies))
        elif demisto.command() == "kibana-value-list-item-create":
            return_results(kibana_create_value_list_item(args, proxies))
        elif demisto.command() == "kibana-value-list-items-get":
            return_results(kibana_get_value_list_items(args, proxies))
        elif demisto.command() == "kibana-value-list-item-delete":
            return_results(kibana_delete_value_list_item(args, proxies))
        elif demisto.command() == "kibana-value-list-delete":
            return_results(kibana_delete_value_list(args, proxies))
        elif demisto.command() == "kibana-status-get":
            return_results(kibana_get_status(args, proxies))
        elif demisto.command() == "kibana-task-manager-health-get":
            return_results(kibana_get_task_manager_health(args, proxies))
        elif demisto.command() == "kibana-upgrade-readiness-status-get":
            return_results(kibana_get_upgrade_readiness_status(args, proxies))
        elif demisto.command() == "kibana-case-comment-delete":
            return_results(kibana_delete_case_comment(args, proxies))
        elif demisto.command() == "kibana-case-file-add":
            return_results(kibana_add_file_to_case(args, proxies))
        elif demisto.command() == "kibana-user-by-email-get":
            return_results(kibana_get_user_by_email(args, proxies))
        elif demisto.command() == "kibana-case-information-get":
            return_results(kibana_get_case_information(args, proxies))
        else:
            raise NotImplementedError(f"Command {demisto.command()} is not implemented.")

    except Exception as e:
        demisto.debug(f"An unexpected error occurred:\n{traceback.format_exc()}")
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e}")


if __name__ in ("__main__", "builtin", "builtins"):
    main()