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.

"""Unit tests for the Kibana integration."""

import base64
import importlib
import sys
from datetime import datetime, timedelta, timezone
from types import ModuleType
from unittest.mock import MagicMock

import demistomock as demisto
import pytest

UTC = timezone.utc  # noqa: UP017

DEFAULT_PARAMS = {
    "url": "https://example.com",
    "elastic_port": "9200",
    "kibana_port": "443",
    "auth_type": "Basic auth",
    "credentials": {"identifier": "user", "password": "pass"},
    "api_key_auth_credentials": {"identifier": "key_id", "password": "key_secret"},
    "client_type": "Elasticsearch",
    "insecure": False,
    "timeout": "60",
    "proxy": False,
}


def _stub_elasticsearch_modules():
    """Stub the optional elasticsearch/opensearch clients so the module imports without them installed."""
    for name in ("elasticsearch", "elasticsearch7", "opensearchpy", "elastic_transport"):
        module = ModuleType(name)
        # Provide the symbols imported by Kibana.py at module load time.
        module.Elasticsearch = MagicMock()  # type: ignore[attr-defined]
        module.OpenSearch = MagicMock()  # type: ignore[attr-defined]
        module.RequestsHttpConnection = MagicMock()  # type: ignore[attr-defined]
        module.RequestsHttpNode = object  # type: ignore[attr-defined]
        sys.modules.setdefault(name, module)


@pytest.fixture()
def kibana(mocker):
    """Import the Kibana module with mocked params and stubbed clients."""
    _stub_elasticsearch_modules()
    mocker.patch.object(demisto, "params", return_value=dict(DEFAULT_PARAMS))
    if "Kibana" in sys.modules:
        module = importlib.reload(sys.modules["Kibana"])
    else:
        module = importlib.import_module("Kibana")
    return module


def test_port_fallback_defaults(mocker):
    """
    Given: params without explicit ports.
    When: the module is imported.
    Then: ELASTIC_SERVER/KIBANA_SERVER fall back to 9200/443 (no TypeError).
    """
    _stub_elasticsearch_modules()
    params = dict(DEFAULT_PARAMS)
    params["elastic_port"] = ""
    params["kibana_port"] = ""
    mocker.patch.object(demisto, "params", return_value=params)
    module = importlib.reload(sys.modules["Kibana"]) if "Kibana" in sys.modules else importlib.import_module("Kibana")

    assert module.ELASTIC_SERVER.endswith(":9200")
    assert module.KIBANA_SERVER.endswith(":443")


def test_get_api_key_header_val_with_tuple(kibana):
    """
    Given: an API key as a (id, secret) tuple.
    When: building the ApiKey header value.
    Then: it returns a base64-encoded "ApiKey ..." string.
    """
    result = kibana.get_api_key_header_val(("my_id", "my_secret"))
    expected = "ApiKey " + base64.b64encode(b"my_id:my_secret").decode()
    assert result == expected


def test_get_api_key_header_val_with_string(kibana):
    """
    Given: an API key already encoded as a string.
    When: building the ApiKey header value.
    Then: it is returned as-is with the ApiKey prefix.
    """
    assert kibana.get_api_key_header_val("encoded_key") == "ApiKey encoded_key"


def test_is_access_token_expired_valid(kibana):
    """
    Given: an expiration time well in the future.
    When: checking whether the token is expired.
    Then: it returns False.
    """
    future = (datetime.now(UTC) + timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
    assert kibana.is_access_token_expired(future) is False


def test_is_access_token_expired_past(kibana):
    """
    Given: an expiration time in the past.
    When: checking whether the token is expired.
    Then: it returns True.
    """
    past = (datetime.now(UTC) - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
    assert kibana.is_access_token_expired(past) is True


def test_is_access_token_expired_invalid(kibana):
    """
    Given: a malformed expiration string.
    When: checking whether the token is expired.
    Then: it is treated as expired (returns True).
    """
    assert kibana.is_access_token_expired("not-a-date") is True


def test_verify_es_server_version_v8_with_v7_client_raises(kibana):
    """
    Given: an ES v8 server while the configured client is the legacy v7 'Elasticsearch'.
    When: verifying the server version.
    Then: a configuration ValueError is raised.
    """
    with pytest.raises(ValueError, match="Configuration Error"):
        kibana.verify_es_server_version({"version": {"number": "8.4.1"}})


def test_verify_es_server_version_v7_ok(kibana):
    """
    Given: an ES v7 server with the default 'Elasticsearch' client.
    When: verifying the server version.
    Then: no exception is raised.
    """
    kibana.verify_es_server_version({"version": {"number": "7.3.0"}})


def test_kibana_find_cases_uses_arg_to_datetime_and_raw_response(kibana, mocker):
    """
    Given: a from_time argument and a successful API response.
    When: kibana_find_cases is called.
    Then: from_time is normalized to ISO format, and the CommandResults includes outputs and raw_response.
    """
    api_response = {"cases": [{"id": "1", "status": "open"}]}
    http_mock = mocker.patch.object(kibana, "http_request", return_value=api_response)

    result = kibana.kibana_find_cases({"status": "open", "from_time": "2025-10-02T00:00:00Z"}, proxies=None)

    assert result.outputs == api_response["cases"]
    assert result.raw_response == api_response
    assert result.outputs_prefix == "Kibana.Cases"
    # arg_to_datetime should have converted from_time into the API "from" param.
    _, kwargs = http_mock.call_args
    assert kwargs["params"]["from"] is not None


def test_kibana_update_alert_status_returns_command_results(kibana, mocker):
    """
    Given: a successful update call.
    When: kibana_update_alert_status is called.
    Then: it returns a CommandResults (not a plain string) with the expected readable output.
    """
    mocker.patch.object(kibana, "http_request", return_value={})

    result = kibana.kibana_update_alert_status({"alert_id": "a1", "status": "closed"}, proxies=None)

    assert isinstance(result, kibana.CommandResults)
    assert result.readable_output == "Updated alert ID a1 to status of closed"


def test_kibana_delete_case_serializes_ids_as_json(kibana, mocker):
    """
    Given: a case_id to delete.
    When: kibana_delete_case is called.
    Then: the ids query param is a proper JSON-encoded list, and a CommandResults is returned.
    """
    http_mock = mocker.patch.object(kibana, "http_request", return_value={})

    result = kibana.kibana_delete_case({"case_id": "case-123"}, proxies=None)

    _, kwargs = http_mock.call_args
    assert kwargs["params"]["ids"] == '["case-123"]'
    assert isinstance(result, kibana.CommandResults)


def test_kibana_get_user_list_handles_v8_body(kibana, mocker):
    """
    Given: an ES v8 client response exposing a .body attribute.
    When: kibana_get_user_list is called.
    Then: the users are read from .body and returned in CommandResults.
    """
    es_response = MagicMock()
    es_response.body = {"users": [{"username": "alice"}]}
    es = MagicMock()
    es.security.query_user.return_value = es_response
    mocker.patch.object(kibana, "elasticsearch_builder", return_value=es)

    result = kibana.kibana_get_user_list({}, proxies=None)

    assert result.outputs == [{"username": "alice"}]


def test_kibana_get_user_list_handles_v7_dict(kibana, mocker):
    """
    Given: an ES v7/OpenSearch client returning a plain dict (no .body).
    When: kibana_get_user_list is called.
    Then: the users are read directly from the dict (no AttributeError).
    """
    es = MagicMock()
    es.security.query_user.return_value = {"users": [{"username": "bob"}]}
    mocker.patch.object(kibana, "elasticsearch_builder", return_value=es)

    result = kibana.kibana_get_user_list({}, proxies=None)

    assert result.outputs == [{"username": "bob"}]


def test_test_func_success(kibana, mocker):
    """
    Given: a successful connectivity/auth check.
    When: test_func is called.
    Then: it returns "ok".
    """
    mocker.patch.object(kibana, "test_connectivity_auth", return_value=(True, "Connectivity test successful"))
    assert kibana.test_func(proxies=None) == "ok"


def test_http_request_returns_json_on_success(kibana, mocker):
    """
    Given: a server responding 200 with a JSON body.
    When: http_request is called with parse_json=True (default).
    Then: the parsed JSON is returned.
    """
    response = MagicMock()
    response.status_code = 200
    response.json.return_value = {"ok": True}
    mocker.patch.object(kibana.requests, "request", return_value=response)

    result = kibana.http_request(method="GET", url_suffix="/api/status", headers={})

    assert result == {"ok": True}


def test_http_request_returns_none_on_204(kibana, mocker):
    """
    Given: a server responding 204 No Content.
    When: http_request is called.
    Then: None is returned (no JSON parsing attempted).
    """
    response = MagicMock()
    response.status_code = 204
    mocker.patch.object(kibana.requests, "request", return_value=response)

    assert kibana.http_request(method="DELETE", url_suffix="/api/lists", headers={}) is None


def test_http_request_calls_return_error_on_failure(kibana, mocker):
    """
    Given: a server responding 500 with a JSON error body.
    When: http_request is called.
    Then: return_error is invoked with the status code and reason.
    """
    response = MagicMock()
    response.status_code = 500
    response.json.return_value = {"message": "boom"}
    mocker.patch.object(kibana.requests, "request", return_value=response)
    return_error_mock = mocker.patch.object(kibana, "return_error")

    kibana.http_request(method="GET", url_suffix="/api/status", headers={})

    return_error_mock.assert_called_once()
    assert "500" in return_error_mock.call_args[0][0]


def test_test_connectivity_auth_basic_success(kibana, mocker):
    """
    Given: Basic auth configured and a healthy v7 server.
    When: test_connectivity_auth is called.
    Then: it returns (True, "Connectivity test successful").
    """
    response = MagicMock()
    response.status_code = 200
    response.json.return_value = {"version": {"number": "7.10.0"}}
    mocker.patch.object(kibana, "AUTH_TYPE", kibana.BASIC_AUTH)
    mocker.patch.object(kibana.requests, "get", return_value=response)

    success, message = kibana.test_connectivity_auth(proxies=None)

    assert success is True
    assert message == "Connectivity test successful"


def test_test_connectivity_auth_failure_status(kibana, mocker):
    """
    Given: Basic auth configured and a server returning 403.
    When: test_connectivity_auth is called.
    Then: it returns (False, message) describing the failure.
    """
    response = MagicMock()
    response.status_code = 403
    response.reason = "Forbidden"
    mocker.patch.object(kibana, "AUTH_TYPE", kibana.BASIC_AUTH)
    mocker.patch.object(kibana.requests, "get", return_value=response)

    success, message = kibana.test_connectivity_auth(proxies=None)

    assert success is False
    assert "Failed to connect" in message


def test_test_func_failure(kibana, mocker):
    """
    Given: a failed connectivity/auth check.
    When: test_func is called.
    Then: it returns the failure message.
    """
    mocker.patch.object(kibana, "test_connectivity_auth", return_value=(False, "Failed to connect."))
    assert kibana.test_func(proxies=None) == "Failed to connect."


# Command functions that wrap http_request and return a CommandResults with a fixed readable_output.
# Each entry: (function_name, args, http_response, expected_readable_output)
SIMPLE_WRITE_COMMANDS = [
    ("kibana_assign_alert_user", {"alert_id": "a1", "user_id": "u1"}, {}, "Assigned user ID u1 to alert a1"),
    ("kibana_add_alert_note", {"alert_id": "e1", "note": "hello"}, {}, "Added note hello to alert e1"),
    ("kibana_delete_rule", {"rule_id": "r1"}, {}, "Successfully deleted rule with ID of r1"),
    ("kibana_disable_alert_rule", {"rule_id": "r1"}, {}, "Successfully disabled rule with ID of r1"),
    ("kibana_enable_alert_rule", {"rule_id": "r1"}, {}, "Successfully enabled rule with ID of r1"),
    (
        "kibana_create_value_list",
        {"description": "d", "list_id": "l1", "name": "n1", "data_type": "keyword"},
        {},
        "Successfully created value list with name of n1",
    ),
    (
        "kibana_create_value_list_item",
        {"list_id": "l1", "new_value_list_item": "v1"},
        {},
        "Successfully added v1 to value list with ID of l1",
    ),
    (
        "kibana_import_value_list_items",
        {"list_id": "l1", "file_content": "c1"},
        {},
        "Successfully imported c1 to value list with ID of l1",
    ),
    (
        "kibana_delete_value_list_item",
        {"item_id": "i1", "list_id": "l1"},
        {},
        "Successfully deleted i1 from value list with ID of l1",
    ),
    ("kibana_delete_value_list", {"list_id": "l1"}, {}, "Successfully deleted value list with ID of l1"),
    (
        "kibana_delete_case_comment",
        {"case_id": "c1", "comment_id": "cm1"},
        {},
        "Deleted comment with ID cm1 from case c1",
    ),
    (
        "kibana_add_case_comment",
        {"case_id": "c1", "case_owner": "o", "comment": "txt"},
        {"updated_at": "2025-01-01"},
        "Case comment updated at 2025-01-01",
    ),
]


@pytest.mark.parametrize("func_name, args, response, expected", SIMPLE_WRITE_COMMANDS)
def test_simple_write_commands(kibana, mocker, func_name, args, response, expected):
    """
    Given: a write-style command and a successful API response.
    When: the command function is called.
    Then: it returns a CommandResults with the expected human-readable output.
    """
    mocker.patch.object(kibana, "http_request", return_value=response)

    result = getattr(kibana, func_name)(args, proxies=None)

    assert isinstance(result, kibana.CommandResults)
    assert result.readable_output == expected


# Command functions that wrap http_request and surface (a slice of) the response as outputs.
# Each entry: (function_name, args, http_response, expected_outputs, outputs_prefix)
READ_COMMANDS = [
    (
        "kibana_find_alerts_for_case",
        {"case_id": "c1"},
        {"alerts": [{"id": "x"}]},
        {"alerts": [{"id": "x"}]},
        "Kibana.Alerts.For.Case",
    ),
    (
        "kibana_update_case_status",
        {"case_id": "c1", "status": "closed", "version_id": "v1"},
        [{"id": "c1", "status": "closed"}],
        [{"id": "c1", "status": "closed"}],
        "Kibana.Updated.Case.Status",
    ),
    (
        "kibana_find_user_spaces",
        {},
        [{"id": "default"}],
        [{"id": "default"}],
        "Kibana.User.Spaces",
    ),
    (
        "kibana_find_case_comments",
        {"case_id": "c1"},
        {"comments": [{"id": "cm1"}]},
        [{"id": "cm1"}],
        "Kibana.Case.Comments",
    ),
    (
        "kibana_search_rule_details",
        {"kql_query": "name:*"},
        {"data": [{"id": "r1"}]},
        [{"id": "r1"}],
        "Kibana.Rule.Details",
    ),
    (
        "kibana_get_alerting_health",
        {},
        {"status": "ok"},
        {"status": "ok"},
        "Alerting.Framework.Health",
    ),
    (
        "kibana_get_exception_lists",
        {},
        {"data": [{"id": "el1"}]},
        [{"id": "el1"}],
        "Kibana.Exception.Lists",
    ),
    (
        "kibana_get_value_lists",
        {},
        {"data": [{"id": "vl1"}]},
        [{"id": "vl1"}],
        "Alerting.Value.Lists",
    ),
    (
        "kibana_get_value_list_items",
        {"list_id": "l1", "result_size": "10"},
        {"data": [{"value": "v1"}]},
        [{"value": "v1"}],
        "Value.List.Items",
    ),
    (
        "kibana_get_status",
        {},
        {"status": {"overall": {"level": "available"}}},
        {"overall": {"level": "available"}},
        "Kibana.Operational.Status",
    ),
    (
        "kibana_get_task_manager_health",
        {},
        {"stats": {"runtime": {}}},
        {"runtime": {}},
        "Kibana.Task.Manager.Health",
    ),
    (
        "kibana_get_upgrade_readiness_status",
        {},
        {"readyForUpgrade": True},
        {"readyForUpgrade": True},
        "Kibana.Upgrade.Readiness.Status",
    ),
    (
        "kibana_get_case_information",
        {"case_id": "c1"},
        {"id": "c1", "title": "t"},
        {"id": "c1", "title": "t"},
        "Kibana.Case.Info",
    ),
]


@pytest.mark.parametrize("func_name, args, response, expected_outputs, prefix", READ_COMMANDS)
def test_read_commands(kibana, mocker, func_name, args, response, expected_outputs, prefix):
    """
    Given: a read-style command and a successful API response.
    When: the command function is called.
    Then: it returns a CommandResults with the expected outputs and outputs_prefix.
    """
    mocker.patch.object(kibana, "http_request", return_value=response)

    result = getattr(kibana, func_name)(args, proxies=None)

    assert isinstance(result, kibana.CommandResults)
    assert result.outputs == expected_outputs
    assert result.outputs_prefix == prefix


def test_kibana_list_detection_alerts_flattens_sources(kibana, mocker):
    """
    Given: a detection alerts search response with nested hits.
    When: kibana_list_detection_alerts is called.
    Then: each hit's _source is flattened into the outputs list.
    """
    response = {"hits": {"hits": [{"_source": {"id": "1"}}, {"_source": {"id": "2"}}]}}
    mocker.patch.object(kibana, "http_request", return_value=response)

    result = kibana.kibana_list_detection_alerts({"alert_status": "open"}, proxies=None)

    assert result.outputs == [{"id": "1"}, {"id": "2"}]
    assert result.outputs_prefix == "Kibana.Detection.Alerts"


def test_kibana_get_user_by_email_returns_users(kibana, mocker):
    """
    Given: an email wildcard and an ES client returning matching users.
    When: kibana_get_user_by_email is called.
    Then: the users list is returned in CommandResults.
    """
    es = MagicMock()
    es.security.query_user.return_value = {"users": [{"username": "carol"}]}
    mocker.patch.object(kibana, "elasticsearch_builder", return_value=es)

    result = kibana.kibana_get_user_by_email({"email_wildcard": "carol@*"}, proxies=None)

    assert result.outputs == [{"username": "carol"}]
    assert result.outputs_prefix == "Kibana.User.Data"


def test_kibana_add_file_to_case_uploads_and_reports(kibana, mocker, tmp_path):
    """
    Given: a file registered in the war room and a successful upload.
    When: kibana_add_file_to_case is called.
    Then: it uploads the file and returns a success message including the file name.
    """
    file_path = tmp_path / "evidence.txt"
    file_path.write_text("data")
    mocker.patch.object(kibana.demisto, "getFilePath", return_value={"path": str(file_path), "name": "evidence.txt"})
    http_mock = mocker.patch.object(kibana, "http_request", return_value={})

    result = kibana.kibana_add_file_to_case({"case_id": "c1", "file_id": "f1"}, proxies=None)

    assert result == "Successfully added file evidence.txt to case c1"
    _, kwargs = http_mock.call_args
    assert kwargs["url_suffix"] == "/api/cases/c1/files"