GIBIncidentUpdateAllTypes

This script prevents duplication of existing incidents. This automation runs using the default Limited User role, unless you explicitly change the permissions. For more information, see the section about permissions here: - For Cortex XSOAR 6 see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations - For Cortex XSOAR 8 Cloud see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script - For Cortex XSOAR 8.7 On-prem see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script

python · Group-IB Threat Intelligence

Source

"""
GIBIncidentUpdateAllTypes
=========================

Pre-Processing Rule script for Group-IB TI integrations.

For each incoming Group-IB incident the script:

1. Extracts the Group-IB business identifier (`gibid`) from the incoming
   incident in a defensive way (CustomFields -> top-level -> labels -> rawJSON).
2. Streams already-open XSOAR incidents that share the same `gibid` page by
   page, never holding more than one page in memory at a time.
3. Copies all fields from the incoming incident onto every existing duplicate
   via `setIncident` (with the minimal technical guards required for the call
   to be well-formed: see `_RESERVED_UPDATE_KEYS`).
4. Tells XSOAR to drop the incoming incident if at least one real duplicate
   was found, otherwise keeps it.

Memory & blast-radius are bounded by:
    * `PAGE_SIZE`     - upper bound on RAM per `getIncidents` call.
    * `MAX_INCIDENTS` - hard ceiling on how many duplicates one pre-processing
      call is allowed to touch (circuit-breaker against gibid collisions or
      misconfigured queries).
"""

import json
from collections.abc import Iterator
from typing import Any

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


# `EntryType.ERROR` resolves to 4 in the production XSOAR runtime. The local
# `CommonServerPython` stub used by `pytest-in-docker` does not expose
# `EntryType`, so we depend on the numeric literal here. Keep this in sync
# with `EntryType.ERROR` in CommonServerPython.
_ENTRY_TYPE_ERROR: int = 4

# `id` is the target identifier for `setIncident`; if it leaked into the
# update payload, Python's kwargs override would silently redirect the call
# to the incoming incident instead of the duplicate, breaking deduplication.
# `CustomFields` is intentionally flattened into top-level kwargs, so we do
# not pass the container itself a second time.
_RESERVED_UPDATE_KEYS: frozenset[str] = frozenset({"id", "CustomFields"})

# Hard ceiling: how many duplicate incidents one pre-processing call is
# allowed to update. Circuit-breaker for `gibid` collisions or misconfigured
# queries that would otherwise flood the worker with thousands of setIncidents.
MAX_INCIDENTS: int = 1000

# Page size for `getIncidents`. Streaming RAM cost is O(PAGE_SIZE),
# independent of MAX_INCIDENTS.
PAGE_SIZE: int = 200


def _normalize(value: Any) -> str | None:
    """Return a stripped non-empty string representation of `value`, else None."""
    if value is None:
        return None
    text = str(value).strip()
    return text or None


def get_gibid(incident: dict) -> str | None:
    """Extract the Group-IB business identifier from the incoming incident.

    Looked up in priority order: CustomFields.gibid -> top-level gibid ->
    labels[type in {gibid, id}] -> rawJSON.id. Returns None if absent.
    """
    cf = incident.get("CustomFields") or {}
    if isinstance(cf, dict) and cf.get("gibid"):
        return _normalize(cf["gibid"])

    if incident.get("gibid"):
        return _normalize(incident["gibid"])

    for label in incident.get("labels") or []:
        if isinstance(label, dict) and label.get("type") in ("gibid", "id") and label.get("value"):
            return _normalize(label["value"])

    raw = incident.get("rawJSON")
    if isinstance(raw, str) and raw:
        try:
            parsed = json.loads(raw)
            if isinstance(parsed, dict) and parsed.get("id"):
                return _normalize(parsed["id"])
        except Exception:
            # rawJSON may legitimately be missing/malformed; ignore silently.
            pass

    return None


def build_update_fields(incident: dict[str, Any]) -> dict[str, Any]:
    """Build the kwargs payload for `setIncident` on a duplicate.

    Propagates every field from the incoming incident with only two
    technical guards:

    * Keys in `_RESERVED_UPDATE_KEYS` are dropped to keep the `setIncident`
      call well-formed.
    * `None` values are filtered out so a missing field on the incoming
      side never wipes a populated value on the duplicate.
    """
    base: dict[str, Any] = {k: v for k, v in incident.items() if k not in _RESERVED_UPDATE_KEYS}

    cf = incident.get("CustomFields") or {}
    if isinstance(cf, dict):
        # CustomFields are passed as flat named arguments, not as a nested
        # container; only `id` collisions are stripped.
        base.update({k: v for k, v in cf.items() if k != "id"})

    return {k: v for k, v in base.items() if v is not None}


def iter_existing_incidents(
    gibid: str,
    max_total: int = MAX_INCIDENTS,
    page_size: int = PAGE_SIZE,
) -> Iterator[dict[str, Any]]:
    """Stream open XSOAR incidents matching `gibid`, page by page.

    Memory stays at O(page_size) regardless of the total number of duplicates.
    Iteration stops cleanly on: empty page, partial page (no further pages),
    XSOAR error response, or `max_total` reached.
    """
    if max_total <= 0 or page_size <= 0:
        return

    query = f"gibid: {gibid} and -status:Closed"
    yielded = 0
    page = 0

    while yielded < max_total:
        res = demisto.executeCommand(
            "getIncidents",
            {"query": query, "sort": "created.desc", "size": page_size, "page": page},
        )
        if not res or (isinstance(res[0], dict) and res[0].get("Type") == _ENTRY_TYPE_ERROR):
            demisto.debug(f"[GIB-dedup] getIncidents error or empty on page={page}: {res!r}")
            return

        data = (res[0].get("Contents") or {}).get("data") or []
        if not data:
            return

        for existing in data:
            yield existing
            yielded += 1
            if yielded >= max_total:
                demisto.debug(f"[GIB-dedup] reached max_total={max_total} for gibid={gibid}; " "remaining duplicates skipped")
                return

        if len(data) < page_size:
            return
        page += 1


def _set_incident(incident_id: str, fields: dict[str, Any]) -> bool:
    """Invoke `setIncident` for one duplicate; return True on success."""
    res = demisto.executeCommand("setIncident", {"id": incident_id, **fields})
    if isinstance(res, list) and res and isinstance(res[0], dict) and res[0].get("Type") == _ENTRY_TYPE_ERROR:
        demisto.debug(f"[GIB-dedup] setIncident failed for {incident_id}: {res[0].get('Contents')!r}")
        return False
    return True


def main() -> None:
    try:
        incident = demisto.incident() or {}
        if not isinstance(incident, dict):
            raise Exception("Incoming incident is missing from the pre-processing context.")

        gibid = get_gibid(incident)
        demisto.debug(f"[GIB-dedup] gibid={gibid}")

        if not gibid:
            return_results(True)
            return

        update_fields = build_update_fields(incident)

        considered = 0
        updated = 0
        for existing in iter_existing_incidents(gibid):
            existing_id = _normalize(existing.get("id"))
            if not existing_id:
                continue
            considered += 1
            if update_fields and _set_incident(existing_id, update_fields):
                updated += 1

        if considered == 0:
            # No real duplicates -> let XSOAR create the incoming incident.
            return_results(True)
            return

        demisto.debug(f"[GIB-dedup] updated {updated}/{considered} existing duplicates")
        # Real duplicates exist -> drop the incoming incident.
        return_results(False)
    except Exception as exc:  # noqa: BLE001 - top-level XSOAR script handler
        demisto.error(f"[GIB-dedup] failed: {exc}")
        return_error(f"GIBIncidentUpdateAllTypes failed: {exc}")


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

README

This script prevents duplication of existing incidents.

Permissions


This automation runs using the default Limited User role, unless you explicitly change the permissions.
For more information, see the section about permissions here: For Cortex XSOAR 6, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations for Cortex XSOAR 8 Cloud, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script for Cortex XSOAR 8 On-prem, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script.

Script Data


Name Description
Script Type python3
Tags preProcessing
Cortex XSOAR Version 6.0.0

Inputs


There are no inputs for this script.

Outputs


There are no outputs for this script.