SplunkShowDrilldown

Automation to display drilldown search results from Splunk.

python · Splunk

Source

import re

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


def escape_backslashes_in_field_filters(search: str) -> str:
    """Re-escapes backslashes inside the value of `field="value"` filters in an SPL search.

    The drilldown enrichment data is persisted on the incident as a JSON string. When this script
    reads it back with ``json.loads``, backslashes inside a double-quoted field filter value are
    collapsed to a single backslash, which is not valid SPL. We re-escape (double) them so the
    query shown to the user can be copied straight into the Splunk UI.

    Only genuine ``field="value"`` filters are touched - the quoted value must be directly preceded
    by a field-name token and ``=``. Regex/string literals that follow ``(`` or ``,`` inside SPL
    functions (e.g. ``eval x=replace(field,"(\\)","\\\\")``) and free-text / ``rex`` quoted strings
    are left untouched. The operation is idempotent.

    Args:
        search (str): The SPL drilldown search.

    Returns:
        str: The SPL search with backslashes escaped inside field filter values.
    """
    if not isinstance(search, str):
        return search

    def _escape(match: re.Match) -> str:
        prefix = match.group(1)  # the field name, '=' and any surrounding whitespace
        value = match.group(2)  # the value between the double quotes
        normalized = value.replace("\\\\", "\\")  # normalize already-doubled backslashes
        escaped = normalized.replace("\\", "\\\\")  # then double every backslash (idempotent)
        return f'{prefix}"{escaped}"'

    return re.sub(r'([\w.]+\s*=\s*)"([^"]*)"', _escape, search)


QUERY_KEYS = {"search", "query_search"}


def escape_drilldown_data(data):
    """Recursively re-escape backslashes in SPL query fields of parsed drilldown data.

    Applies escape_backslashes_in_field_filters to the values of the 'search' and 'query_search'
    keys so any displayed query is valid for the Splunk UI, regardless of nesting (list/dict).
    """
    if isinstance(data, list):
        return [escape_drilldown_data(item) for item in data]
    if isinstance(data, dict):
        return {
            key: escape_backslashes_in_field_filters(value)
            if key in QUERY_KEYS and isinstance(value, str)
            else escape_drilldown_data(value)
            for key, value in data.items()
        }
    return data


def format_drilldown_config(parsed_json) -> str:
    """Render parsed drilldown configuration as Markdown, showing each SPL query in a fenced code block.

    A fenced code block is used (instead of a Markdown table cell) because tableToMarkdown escapes
    backticks and Markdown collapses backslashes in plain cells, which would corrupt the displayed
    query. Inside a fenced code block the query - including its escaped backslashes - is shown
    verbatim, so the user can copy a Splunk-valid query. Assumes values were re-escaped by
    escape_drilldown_data.
    """
    items = parsed_json if isinstance(parsed_json, list) else [parsed_json]
    sections = []
    for item in items:
        if not isinstance(item, dict):
            sections.append(f"```\n{item}\n```")
            continue
        lines = []
        name = item.get("name") or item.get("query_name")
        if name:
            lines.append(f"**Name:** {name}\n")
        for key in QUERY_KEYS:
            if isinstance(item.get(key), str):
                lines.append(f"**Search:**\n```\n{item[key]}\n```\n")
        # Show any remaining metadata fields (e.g. earliest_offset, disabled) for context.
        for key, value in item.items():
            if key in QUERY_KEYS or key in ("name", "query_name"):
                continue
            lines.append(f"**{key}:** {value}\n")
        sections.append("".join(lines))
    return "\n".join(sections)


def format_raw_data(data: str) -> str:
    """Format raw JSON-like string for better readability."""
    if isinstance(data, str):
        return data.replace("},{", "},\n{").replace("[{", "[\n{").replace("}]", "}\n]")
    return str(data)


def display_error_with_raw_data(title: str, error_msg: str, raw_data: str) -> dict:
    """Display error message with formatted raw data."""
    markdown = f"#### {title}\n\n"
    markdown += f"**Error:** {error_msg}\n\n"

    if raw_data:
        markdown += "**Raw Drilldown Searches Data:**\n```\n"
        markdown += format_raw_data(raw_data) + "\n```"
    else:
        markdown += "*No drilldown searches data found.*"

    return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}


def display_json_parsing_error(title: str, error_msg: str, raw_data: str) -> dict:
    """Display detailed error message for JSON parsing issues from Splunk."""
    markdown = f"#### {title}\n\n"
    markdown += "⚠️ **Note:** The drilldown_searches data received from Splunk contains invalid JSON formatting.\n\n"
    markdown += "The data from Splunk has JSON syntax issues (such as unescaped quotes or malformed structure).\n\n"
    markdown += f"**Error Details:** {error_msg}\n\n"

    if raw_data:
        markdown += "**Raw Data from Splunk:**\n```\n"
        markdown += format_raw_data(raw_data) + "\n```\n\n"
        markdown += "**Recommendation:** Check the drilldown configuration in Splunk to ensure it generates valid JSON."
    else:
        markdown += "*No drilldown searches data found.*"

    return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}


def main():
    drilldown_results = []
    incident = demisto.incident()
    if not incident:
        raise ValueError("Error - demisto.incident() expected to return current incident from context but returned None")

    labels = incident.get("labels", [])
    custom_fields = incident.get("CustomFields", {})
    drilldown_searches = custom_fields.get("splunkdrilldown", "")

    # If drilldown_searches not found in custom fields, search in labels
    if not drilldown_searches:
        for label in labels:
            if label.get("type") == "drilldown_searches":
                drilldown_searches = label.get("value", "")
                break

    # Check if enrichment is configured
    has_enrichment_status = False
    for label in labels:
        if label.get("type") == "successful_drilldown_enrichment":
            has_enrichment_status = True
            is_successful = label.get("value")
            if is_successful == "false":
                return display_error_with_raw_data(
                    "Drilldown Enrichment Not Successful",
                    (
                        "The drilldown enrichment did not complete successfully. "
                        "This could be due to query parsing issues, no results found, or other errors."
                    ),
                    drilldown_searches,
                )
        if label.get("type") == "Drilldown":
            try:
                # json.loads collapses doubled backslashes in field filter values; re-escape them so the
                # displayed query is valid for the Splunk UI (XSUP-70829).
                drilldown_results = escape_drilldown_data(json.loads(label.get("value", [])))
            except Exception as e:
                return display_json_parsing_error(
                    "Drilldown Searches (Invalid JSON)", f"JSON Parsing Error: {str(e)}", label.get("value", "")
                )

    if not drilldown_results:
        # No enrichment results found in labels

        # Case 1: No enrichment status label AND no configuration data -> enrichment is not configured.
        if not has_enrichment_status and not drilldown_searches:
            markdown = "#### Drilldown Configuration Status\n\n"
            markdown += (
                "⚠️ **Drilldown enrichment is not configured for this integration instance.**\n\n"
                "Enrichment is not enabled, so drilldown results are not available.\n\n"
                "**To enable drilldown enrichment:**\n"
                "1. Go to the integration instance settings\n"
                "2. In the 'Enrichment Types' parameter, select 'Drilldown'\n"
                "3. Save the configuration and fetch new incidents\n\n"
            )
            return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}

        # Case 1b: A drilldown configuration exists but produced no results - it is configured, but the
        # search likely failed or returned nothing on the Splunk side (e.g. an invalid query).
        if not has_enrichment_status:
            markdown = "#### Drilldown Configuration Status\n\n"
            markdown += (
                "⚠️ **The drilldown is configured, but no results were returned.**\n\n"
                "The drilldown search was not run successfully on the Splunk side, or it returned no "
                "results. Review the search query below for errors (for example, unbalanced quotes or "
                "invalid syntax) and validate it directly in the Splunk UI.\n\n"
            )

            # Show the configuration so the user can review/fix the query.
            try:
                parsed_json = escape_drilldown_data(json.loads(drilldown_searches))
                markdown += "**Raw Drilldown Searches Configuration (from Splunk):**\n\n"
                markdown += format_drilldown_config(parsed_json)
            except Exception as e:
                # Failed to parse raw configuration
                markdown += "**Raw Drilldown Searches Data (Failed to Parse):**\n\n"
                markdown += f"⚠️ Failed to parse the raw drilldown configuration. Error: {str(e)}\n\n"
                markdown += "**Raw Data:**\n```\n"
                markdown += format_raw_data(drilldown_searches) + "\n```"

            return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}

        # Case 2: Enrichment IS configured but no results found
        else:
            markdown = "#### Drilldown Enrichment Results\n\n"
            markdown += (
                "⚠️ **Drilldown enrichment results not found.**\n\n"
                "Enrichment is configured, but the results could not be retrieved from the incident context.\n\n"
            )

            # Try to show raw configuration if available
            if drilldown_searches:
                try:
                    parsed_json = escape_drilldown_data(json.loads(drilldown_searches))
                    markdown += "**Drilldown Searches Configuration:**\n\n"
                    markdown += format_drilldown_config(parsed_json)
                except Exception as e:
                    # Failed to parse raw configuration
                    markdown += "**Raw Drilldown Searches Data (Failed to Parse):**\n\n"
                    markdown += f"⚠️ Failed to parse the drilldown configuration. Error: {str(e)}\n\n"
                    markdown += "**Raw Data:**\n```\n"
                    markdown += format_raw_data(drilldown_searches) + "\n```"
            else:
                markdown += "*No drilldown searches configuration data found.*"

            return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}

    if isinstance(drilldown_results, list):
        if "query_name" in drilldown_results[0]:
            # Get drilldown results of multiple drilldown searches
            markdown = "#### Drilldown Searches Results\n"

            for drilldown in drilldown_results:
                # Render the query inside a fenced code block so markdown does not collapse the
                # escaped backslashes - the user can copy a Splunk-valid query directly (XSUP-70829).
                markdown += (
                    f"**Query Name:** {drilldown.get('query_name','')}\n\n"
                    f"**Query Search:**\n```\n{drilldown.get('query_search','')}\n```\n\n"
                    f"**Results:**\n"
                )

                if drilldown.get("enrichment_status") == "Enrichment failed":
                    markdown += "\nDrilldown enrichment failed."

                elif results := drilldown.get("query_results", []):
                    markdown += tableToMarkdown("", results, headers=results[0].keys())

                else:
                    markdown += "\nNo results found for drilldown search."

                markdown += "\n\n"

        else:
            # Drilldown results of a single drilldown search
            markdown = tableToMarkdown("", drilldown_results, headers=drilldown_results[0].keys())

    else:
        markdown = tableToMarkdown("", drilldown_results)

    return {"ContentsFormat": formats["markdown"], "Type": entryTypes["note"], "Contents": markdown}


if __name__ in ("__main__", "__builtin__", "builtins"):
    try:
        return_results(main())
    except Exception as e:
        return_error(f"Got an error while parsing Splunk events: {e}")

README

Automation to display drilldown search results from Splunk.

Script Data


Name Description
Script Type python3
Tags dynamic-section
Cortex XSOAR Version 6.0.0

Inputs


There are no inputs for this script.

Outputs


There are no outputs for this script.