GetIndicatorsByQuery

Gets a list of indicator objects and the associated indicator outputs that match the specified query and filters. The results are returned in a structured data file.

python · Base

Source

import hashlib

from CommonServerPython import *

PAGE_SIZE = 500

RANDOM_UUID = str(demisto.args().get("addRandomSalt", "").encode("utf8"))
# Memo for key matching
CACHE = {}  # type: ignore


def hash_value(simple_value):
    if not isinstance(simple_value, str):
        simple_value = str(simple_value)
    if simple_value.lower() in ["none", "null"]:
        return None
    return hashlib.md5(simple_value.encode("utf8") + RANDOM_UUID.encode("utf8")).hexdigest()  # nosec


def pattern_match(pattern, s):
    regex = re.compile(pattern.replace("*", ".*"))
    return re.match(regex, s) is not None


def is_key_match_fields_to_hash(key, fields_to_hash):
    if key is None:
        return False

    if key in CACHE:
        return CACHE[key]
    for field in fields_to_hash:
        if pattern_match(field, key):
            CACHE[key] = True
            return True
    return False


def hash_multiple(value, fields_to_hash, to_hash=False):
    if isinstance(value, list):
        return [hash_multiple(x, fields_to_hash, to_hash) for x in value]
    if isinstance(value, dict):
        for k, v in value.items():
            _hash = to_hash or is_key_match_fields_to_hash(k, fields_to_hash)
            value[k] = hash_multiple(v, fields_to_hash, _hash)
        return value
    else:
        try:
            if isinstance(value, int | float | bool):
                to_hash = False
            if not isinstance(value, str):
                value = str(value)
        except Exception:
            value = ""
        if to_hash and value:
            return hash_value(value)
        else:
            return value


def parse_ioc(ioc):
    global fields_to_hash, unpopulate_fields, populate_fields
    # flat
    cf = ioc.pop("CustomFields", {}) or {}
    ioc.update(cf)
    new_ioc = {}
    for k, v in ioc.items():
        if v in ["0001-01-01T00:00:00Z"]:
            continue
        if populate_fields:
            if k in populate_fields:
                new_ioc[k] = v
        else:
            if unpopulate_fields:
                if k not in unpopulate_fields:
                    new_ioc[k] = v
            else:
                new_ioc[k] = v

    ioc = new_ioc
    if fields_to_hash and is_key_match_fields_to_hash(k, fields_to_hash):
        ioc = hash_multiple(ioc, fields_to_hash)

    return ioc


def find_indicators_with_limit_loop(indicator_query: str, limit: int):
    """
    Finds indicators using while loop with demisto.searchIndicators, and returns result and last page
    """
    iocs: List[dict] = []
    demisto.debug(f"Searching indicators with {indicator_query=} and {populate_fields=}.")
    search_indicators = IndicatorsSearcher(
        query=indicator_query,
        limit=limit,
        size=PAGE_SIZE,
        filter_fields=",".join(populate_fields) if populate_fields else None,
    )

    for ioc_res in search_indicators:
        fetched_iocs = ioc_res.get("iocs") or []
        iocs.extend(fetched_iocs)
    demisto.debug(f"Received {len(iocs)} results from server. Parsing.")

    return [parse_ioc(x) for x in iocs]


def get_parsed_populated_fields(fields_to_parse: list[str]) -> frozenset | None:
    """Gets a list of fields to populate for an indicator and parse it according to specific requirements."""
    # Due to using the always populated field and no ability to provide None value in the UI, we allow an explicit override.

    if "ALL" in fields_to_parse:
        demisto.debug("All fields are requested. populated_field argument is set to None.")

        return None

    new_fields = [field for field in fields_to_parse if field]

    # Due to a server bug where API request an result non-matching names, we allow this "non existing" field.
    if "RelatedIncCount" in new_fields:
        new_fields.append("investigationsCount")

    demisto.debug(f"User's fields to populate: {new_fields}.")
    return frozenset(new_fields)


fields_to_hash, unpopulate_fields, populate_fields = [], [], []  # type: ignore


def main():
    global fields_to_hash, unpopulate_fields, populate_fields
    args = demisto.args()
    fields_to_hash = frozenset([x for x in argToList(args.get("fieldsToHash", "")) if x])  # type: ignore
    unpopulate_fields = frozenset([x for x in argToList(args.get("dontPopulateFields", "")) if x])  # type: ignore
    populate_fields = get_parsed_populated_fields(argToList(args.get("populateFields", "")))  # type: ignore

    limit = int(args.get("limit", PAGE_SIZE))
    query = args.get("query", "")
    offset = int(args.get("offset", 0))
    indicators = find_indicators_with_limit_loop(query, limit + offset)[offset : offset + limit]

    entry = fileResult("indicators.json", json.dumps(indicators).encode("utf8"))
    entry["Contents"] = indicators
    entry["ContentsFormat"] = formats["json"]
    entry["HumanReadable"] = f"Fetched {len(indicators)} indicators successfully by the query: {query}"

    return entry


if __name__ in ["__main__", "__builtin__", "builtins"]:
    demisto.results(main())

README

Gets a list of indicator objects and the associated indicator outputs that match the specified query and filters. The results are returned in a structured data file.

Script Data


Name Description
Script Type python3
Tags ml
Cortex XSOAR Version 5.5.0

Inputs


Argument Name Description
query The indicators query.
dontPopulateFields A comma-separated list of fields in the object to ignore.
limit The maximum number of indicators to fetch.
offset The results offset page. Only change when the number of the results exceed the limit.
addRandomSalt Salt for the hash function.
fieldsToHash A comma-separated list of fields to hash. Supports wildcard “*”.
populateFields A comma-separated list of fields in the object to poplulate. Defaults are id, score, and investigationIDs.

Outputs


There are no outputs for this script.