StixCreator

Gets a list of indicators from the indicators argument, and generates a JSON file in STIX 2.1 format.

python · Common Scripts

Source

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

""" IMPORTS """
import json
from collections.abc import Callable
from typing import Any

from stix2 import (
    AttackPattern,
    Bundle,
    Campaign,
    CourseOfAction,
    ExternalReference,
    KillChainPhase,
    Indicator,
    Infrastructure,
    IntrusionSet,
    Malware,
    Report,
    ThreatActor,
    Tool,
    Vulnerability,
)
from stix2.exceptions import InvalidValueError, MissingPropertiesError

SCOs: dict[str, str] = {  # pragma: no cover
    "md5": "file:hashes.md5",
    "sha1": "file:hashes.sha1",
    "sha256": "file:hashes.sha256",
    "ssdeep": "file:hashes.ssdeep",
    "ip": "ipv4-addr:value",
    "cidr": "ipv4-addr:value",
    "ipv6": "ipv6-addr:value",
    "ipv6cidr": "ipv6-addr:value",
    "url": "url:value",
    "email": "email-message:sender_ref.value",
    "account": "user-account:account_login",
    "domain": "domain-name:value",
    "host": "domain-name:value",
    "registry key": "windows-registry-key:key",
    "asn": "autonomous-system:name",
    "software": "software:name",
}

SDOs: dict[str, Callable] = {  # pragma: no cover
    "malware": Malware,
    "attack pattern": AttackPattern,
    "campaign": Campaign,
    "infrastructure": Infrastructure,
    "tool": Tool,
    "intrusion set": IntrusionSet,
    "report": Report,
    "threat actor": ThreatActor,
    "cve": Vulnerability,
    "course of action": CourseOfAction,
}


def search_related_indicators(value: str) -> list[dict]:  # pragma: no cover
    relationships = demisto.searchRelationships({"entities": [value]}).get("data", [])
    demisto.debug(f"found {len(relationships)} relationships")
    query = ""
    for rel in relationships:
        entity_a = rel.get("entityA", "").lower()
        entity_b = rel.get("entityB", "").lower()
        if entity_a == value.lower():
            query += f'"{entity_b}"'
        elif entity_b == value.lower():
            query += f'"{entity_a}"'
        else:
            demisto.info(f"Relationship: {rel} is not relevant for indicator: {value}")
            continue
        query += " or "
    if not query:
        demisto.info(f"No relevant relationship found for indicator: {value}")
        return []
    query = query[:-4]
    demisto.debug(f"using query: {query}")
    demisto_indicators = demisto.searchIndicators(query=query).get("iocs", [])
    demisto.debug(f"found {len(demisto_indicators)} related indicators")
    return demisto_indicators


def get_indicators_stix_ids(value: str, indicator_type: str, indicators: list[dict]) -> list[str]:
    stix_ids = []
    for indicator in indicators:
        if stix_id := indicator.get("stixid"):
            demisto.debug(f"Found stix id: {stix_id} for indicator: {indicator}")
            stix_ids.append(stix_id)
        else:
            if indicator_type in SDOs:
                stix_type = XSOAR_TYPES_TO_STIX_SDO.get(indicator.get("indicator_type", "indicator"))
                stix_id = XSOAR2STIXParser.create_sdo_stix_uuid(indicator, stix_type, PAWN_UUID, indicator.get("value", ""))
            elif indicator_type in SCOs:
                stix_type = XSOAR_TYPES_TO_STIX_SCO.get(indicator.get("indicator_type", "indicator"), "indicator")
                stix_id = XSOAR2STIXParser.create_sco_stix_uuid(indicator, stix_type, indicator.get("value", ""))
            else:
                demisto.info(f"Indicator type: {indicator_type}, with the value: {value} is unknown.")
                continue
            demisto.debug(f"Created stix id: {stix_id} for indicator: {indicator}")
        stix_ids.append(stix_id)
    return stix_ids


def hash_type(value: str) -> str:  # pragma: no cover
    length = len(value)
    if length == 32:
        return "md5"
    if length == 40:
        return "sha1"
    if length == 64 and ":" in value:
        return "ssdeep"
    elif length == 64:
        return "sha256"
    if length == 128:
        return "sha512"
    return ""


def guess_indicator_type(type_: str, val: str) -> str:
    # try to guess by key
    for sco in SCOs:
        if sco in type_:
            return sco

    # try to auto_detect by value
    return (auto_detect_indicator_type(val) or type_).lower()


def add_file_fields_to_indicator(xsoar_indicator: Dict, value: str) -> Dict:
    """
    Create the hashes dictionary for the indicator object.
    Args:
        xsoar_indicator: Dict - The XSOAR representation of the indicator.
        value: str - The value of the indicator.
    Returns:
        The dictionary with the file hashes.
    """
    hashes_dict = {}
    for hash_kind in ["md5", "sha1", "sha256", "sha512"]:
        if get_hash_type(value) == hash_kind:
            hashes_dict[HASH_TYPE_TO_STIX_HASH_TYPE.get(hash_kind)] = value
        elif hash_kind in xsoar_indicator:
            hashes_dict[HASH_TYPE_TO_STIX_HASH_TYPE.get(hash_kind)] = xsoar_indicator.get(hash_kind, "")
    return hashes_dict


def create_stix_sco_indicator(stix_id: Optional[str], stix_type: Optional[str], value: str, xsoar_indicator: Dict) -> Dict:
    """
    Create stix sco indicator object.
    Args:
        stix_id: Optional[str] - The stix id of the indicator.
        stix_type: str - the stix type of the indicator.
        xsoar_indicator: Dict - The XSOAR representation of the indicator.
        value: str - The value of the indicator.
    Returns:
        The Dictionary representing the stix indicator.
    """
    stix_indicator: Dict[str, Any] = {"type": stix_type, "spec_version": "2.1", "id": stix_id}
    if stix_type == "file":
        stix_indicator["hashes"] = add_file_fields_to_indicator(xsoar_indicator, value)
    elif stix_type == "autonomous-system":
        stix_indicator["number"] = value
        stix_indicator["name"] = xsoar_indicator.get("name", "")
    elif stix_type == "software":
        # Software type requires 'name' as the primary field
        stix_indicator["name"] = value
        # Add optional software fields if available in CustomFields
        custom_fields = xsoar_indicator.get("CustomFields") or {}
        if vendor := custom_fields.get("vendor"):
            stix_indicator["vendor"] = vendor
        if version := custom_fields.get("version"):
            stix_indicator["version"] = version
        if cpe := custom_fields.get("cpe"):
            stix_indicator["cpe"] = cpe
    else:
        stix_indicator["value"] = value
    return stix_indicator


def main():
    user_args = demisto.args().get("indicators", "Unknown")
    doubleBackslash = demisto.args().get("doubleBackslash", True)
    is_sco = argToBoolean(demisto.args().get("sco_flag", False))
    all_args = {}

    if isinstance(user_args, dict):
        all_args = json.loads(json.dumps(user_args))

    else:
        try:
            all_args = json.loads(demisto.args().get("indicators", "Unknown"))
        except:  # noqa: E722
            return_error("indicators argument is invalid json object")

    indicators = []

    for indicator_fields in all_args:
        kwargs: dict[str, Any] = {"allow_custom": True}

        xsoar_indicator = all_args[indicator_fields]
        demisto_indicator_type = xsoar_indicator.get("indicator_type", "Unknown")

        value = xsoar_indicator.get("value", "").replace("\\", "\\\\") if doubleBackslash else xsoar_indicator.get("value", "")

        if demisto_indicator_type in XSOAR_TYPES_TO_STIX_SCO and is_sco:
            stix_type = XSOAR_TYPES_TO_STIX_SCO.get(demisto_indicator_type)
            stix_id = XSOAR2STIXParser.create_sco_stix_uuid(xsoar_indicator, stix_type, value)
            stix_indicator = create_stix_sco_indicator(stix_id, stix_type, value, xsoar_indicator)
            indicators.append(stix_indicator)

        else:
            demisto_score = xsoar_indicator.get("score", "").lower()

            if demisto_score in ["bad", "malicious"]:
                kwargs["score"] = "High"

            elif demisto_score == "suspicious":
                kwargs["score"] = "Medium"

            elif demisto_score in ["good", "benign"]:
                kwargs["score"] = "None"

            else:
                kwargs["score"] = "Not Specified"

            stix_type = XSOAR_TYPES_TO_STIX_SDO.get(demisto_indicator_type, "indicator")
            stix_id = XSOAR2STIXParser.create_sdo_stix_uuid(xsoar_indicator, stix_type, PAWN_UUID, value)
            kwargs["id"] = stix_id

            kwargs["created"] = dateparser.parse(xsoar_indicator.get("timestamp", ""))
            kwargs["modified"] = dateparser.parse(xsoar_indicator.get("lastSeen", f'{kwargs["created"]}'))
            kwargs["labels"] = [demisto_indicator_type.lower()]
            kwargs["description"] = xsoar_indicator.get("description", "")

            kwargs = {k: v for k, v in kwargs.items() if v}  # Removing keys with empty strings

            try:
                indicator_type = demisto_indicator_type.lower().replace("-", "")
                if indicator_type == "file":
                    indicator_type = hash_type(value)
                if indicator_type not in SCOs and indicator_type not in SDOs:
                    indicator_type = guess_indicator_type(indicator_type, value)
                demisto.debug(f"Creating an indicator with the following pattern: [{SCOs[indicator_type]} = '{value}']")

                indicator = Indicator(pattern=f"[{SCOs[indicator_type]} = '{value}']", pattern_type="stix", **kwargs)
                indicators.append(indicator)

            except KeyError:
                demisto.debug(f"{demisto_indicator_type} isn't a SCO, checking other IOC types")

                try:
                    indicator_type = demisto_indicator_type.lower()

                    if indicator_type == "cve":
                        kwargs["external_references"] = [ExternalReference(source_name="cve", external_id=value)]

                    elif indicator_type == "attack pattern":
                        try:
                            mitreid = xsoar_indicator.get("mitreid", "")
                            if mitreid:
                                kwargs["external_references"] = [ExternalReference(source_name="mitre", external_id=mitreid)]

                            # Add kill-chain phases if available
                            kill_chain_phases = xsoar_indicator.get("killchainphases")
                            if kill_chain_phases:
                                # Convert XSOAR kill chain phase names to STIX format
                                # XSOAR stores human-readable names like "Defense Evasion"
                                # STIX requires objects with kill_chain_name and phase_name (lowercase with hyphens)
                                # Docs for this can be found here: https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html#_i4tjv75ce50h
                                stix_kill_chain_phases = []

                                # Handle both list and comma-separated string formats
                                if isinstance(kill_chain_phases, str):
                                    kill_chain_phases = [phase.strip() for phase in kill_chain_phases.split(",")]

                                for phase in kill_chain_phases:
                                    # Strip whitespace and convert human-readable name to lowercase with hyphens
                                    # e.g., " Defense Evasion" -> "defense-evasion"
                                    phase_name = phase.strip().lower().replace(" ", "-")
                                    stix_kill_chain_phases.append(
                                        KillChainPhase(kill_chain_name="mitre-attack", phase_name=phase_name)
                                    )
                                kwargs["kill_chain_phases"] = stix_kill_chain_phases
                        except KeyError:
                            pass

                    elif indicator_type == "malware":
                        kwargs["is_family"] = argToBoolean(xsoar_indicator.get("ismalwarefamily", "False").lower())

                    if indicator_type == "report":
                        kwargs["published"] = dateparser.parse(xsoar_indicator.get("timestamp", ""))

                        related_indicators = search_related_indicators(value)
                        stix_ids = get_indicators_stix_ids(value, indicator_type, related_indicators)
                        kwargs["object_refs"] = stix_ids
                    demisto.debug(f"Creating {indicator_type} indicator: {value}, with the following kwargs: {kwargs}")
                    indicator = SDOs[indicator_type](name=value, **kwargs)

                    indicators.append(indicator)

                except (KeyError, TypeError):
                    demisto.info(f"Indicator type: {demisto_indicator_type}, with the value: {value} is not STIX compatible")
                    demisto.info(f"Export failure exception: {traceback.format_exc()}")
                    continue

                except (InvalidValueError, MissingPropertiesError):
                    demisto.info(
                        f"Indicator type: {demisto_indicator_type}, with the value: {value} is not STIX compatible. Skipping."
                    )
                    demisto.info(f"Export failure exception: {traceback.format_exc()}")
                    continue

            except (InvalidValueError, MissingPropertiesError):
                demisto.info(
                    f"Indicator type: {demisto_indicator_type}, with the value: {value} is not STIX compatible. Skipping."
                )
                demisto.info(f"Export failure exception: {traceback.format_exc()}")
                continue
    if len(indicators) > 1:
        bundle = Bundle(indicators, allow_custom=True, spec_version="2.1")
        context = {"StixExportedIndicators(val.pattern && val.pattern == obj.pattern)": json.loads(str(bundle))}
        res = CommandResults(readable_output="", outputs=context, raw_response=str(bundle))

    elif len(indicators) == 1:
        bundle = Bundle(indicators, allow_custom=True)
        bundle_obj = bundle.get("objects", [])[0]
        context = {"StixExportedIndicators(val.pattern && val.pattern == obj.pattern)": json.loads(str(bundle_obj))}
        res = CommandResults(readable_output="", outputs=context, raw_response=str(bundle_obj))
    else:
        context = {"StixExportedIndicators": {}}
        res = CommandResults(readable_output="", outputs=context, raw_response={})

    return_results(res)


from TAXII2ApiModule import *  # noqa: E402

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

README

Gets a list of indicators from the indicators argument, and generates a JSON file in STIX 2.1 format.

Script Data


Name Description
Script Type python3
Tags -

Inputs


Argument Name Description
indicators A JSON object of all indicators and their fields, indicator index mapped to XSOAR common indicator fields. Indicator keys that don’t match the XSOAR common indicator names are also supported, if their key contains a common indicator name (e.g. “special-ip” will be mapped to ip), or their value matches the expected indicator value (e.g. 8.8.8.8 for ip).
doubleBackslash Adds a second backslash to all existing backslashes in the value field.

Outputs


Path Description Type
StixExportedIndicators.created The date/time that the indicator was created. date
StixExportedIndicators.firstSeen The date/time that the indicator was first seen. date
StixExportedIndicators.source The source system for this indicator. string
StixExportedIndicators.type The STIX type (always exported as “indicator”). string
StixExportedIndicators.pattern The type and value of indicators. For example, “URL”, “IPv4”, “domain”, “email”, and so on. string
StixExportedIndicators.score The STIX impact score. Can be, “High”, “Medium”, “None”, or “Not Specified”. string
StixExportedIndicators.modified The date/time that the indicator was last seen. date