StixParser

Parse STIX files to Cortex XSOAR indicators by clicking the **Upload STIX File** button.

python · Base

Source

import collections
import copy
import tempfile

import dateutil.parser
import demistomock as demisto  # noqa: F401
import pytz
from bs4 import BeautifulSoup
from CommonServerPython import *  # noqa: F401
from lxml import etree
from netaddr import IPNetwork
from six import string_types

EPOCH = datetime.utcfromtimestamp(0).replace(tzinfo=pytz.UTC)
SCRIPT_NAME = "STIXParser"

# CONSTANTS
TAXII_VER_2_0 = "2.0"
TAXII_VER_2_1 = "2.1"

DFLT_LIMIT_PER_REQUEST = 100
API_USERNAME = "_api_token_key"
HEADER_USERNAME = "_header:"
XSOAR_TAXII2_SERVER_SCHEMA = (
    "https://github.com/demisto/content/blob/4265bd5c71913cd9d9ed47d9c37d0d4d3141c3eb/"
    "Packs/TAXIIServer/doc_files/XSOAR_indicator_schema.json"
)
SYSTEM_FIELDS = [
    "id",
    "version",
    "modified",
    "sortValues",
    "timestamp",
    "indicator_type",
    "value",
    "sourceInstances",
    "sourceBrands",
    "investigationIDs",
    "lastSeen",
    "firstSeen",
    "firstSeenEntryID",
    "score",
    "insightCache",
    "moduleToFeedMap",
    "expirationStatus",
    "expirationSource",
    "calculatedTime",
    "lastReputationRun",
    "modifiedTime",
    "aggregatedReliability",
]
ERR_NO_COLL = "No collection is available for this user, please make sure you entered the configuration correctly"

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"

# Pattern Regexes - used to extract indicator type and value, spaces are removed before matching the following regexes
INDICATOR_OPERATOR_VAL_FORMAT_PATTERN = r"(\w.*?{value}{operator})'(.*?)'"
INDICATOR_IN_VAL_PATTERN = r"(\w.*?valueIN)\(+('.*?')\)"
INDICATOR_EQUALS_VAL_PATTERN = INDICATOR_OPERATOR_VAL_FORMAT_PATTERN.format(value="value", operator="=")
CIDR_ISSUBSET_VAL_PATTERN = INDICATOR_OPERATOR_VAL_FORMAT_PATTERN.format(value="value", operator="ISSUBSET")
CIDR_ISUPPERSET_VAL_PATTERN = INDICATOR_OPERATOR_VAL_FORMAT_PATTERN.format(value="value", operator="ISSUPPERSET")
HASHES_EQUALS_VAL_PATTERN = INDICATOR_OPERATOR_VAL_FORMAT_PATTERN.format(value=r"hashes\..*?", operator="=")
REGISTRY_EQUALS_VAL_PATTERN = INDICATOR_OPERATOR_VAL_FORMAT_PATTERN.format(value="key", operator="=")

TAXII_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"

STIX_2_TYPES_TO_CORTEX_TYPES = {
    "mutex": FeedIndicatorType.MUTEX,
    "windows-registry-key": FeedIndicatorType.Registry,
    "user-account": FeedIndicatorType.Account,
    "email-addr": FeedIndicatorType.Email,
    "autonomous-system": FeedIndicatorType.AS,
    "ipv4-addr": FeedIndicatorType.IP,
    "ipv6-addr": FeedIndicatorType.IPv6,
    "domain": FeedIndicatorType.Domain,
    "domain-name": FeedIndicatorType.Domain,
    "url": FeedIndicatorType.URL,
    "file": FeedIndicatorType.File,
    "md5": FeedIndicatorType.File,
    "sha-1": FeedIndicatorType.File,
    "sha-256": FeedIndicatorType.File,
    "file:hashes": FeedIndicatorType.File,
    "attack-pattern": ThreatIntel.ObjectsNames.ATTACK_PATTERN,
    "malware": ThreatIntel.ObjectsNames.MALWARE,
    "tool": ThreatIntel.ObjectsNames.TOOL,
    "report": ThreatIntel.ObjectsNames.REPORT,
    "threat-actor": ThreatIntel.ObjectsNames.THREAT_ACTOR,
    "course-of-action": ThreatIntel.ObjectsNames.COURSE_OF_ACTION,
    "campaign": ThreatIntel.ObjectsNames.CAMPAIGN,
    "infrastructure": ThreatIntel.ObjectsNames.INFRASTRUCTURE,
    "intrusion-set": ThreatIntel.ObjectsNames.INTRUSION_SET,
}

MITRE_CHAIN_PHASES_TO_DEMISTO_FIELDS = {
    "build-capabilities": ThreatIntel.KillChainPhases.BUILD_CAPABILITIES,
    "privilege-escalation": ThreatIntel.KillChainPhases.PRIVILEGE_ESCALATION,
    "adversary-opsec": ThreatIntel.KillChainPhases.ADVERSARY_OPSEC,
    "credential-access": ThreatIntel.KillChainPhases.CREDENTIAL_ACCESS,
    "exfiltration": ThreatIntel.KillChainPhases.EXFILTRATION,
    "lateral-movement": ThreatIntel.KillChainPhases.LATERAL_MOVEMENT,
    "defense-evasion": ThreatIntel.KillChainPhases.DEFENSE_EVASION,
    "persistence": ThreatIntel.KillChainPhases.PERSISTENCE,
    "collection": ThreatIntel.KillChainPhases.COLLECTION,
    "impact": ThreatIntel.KillChainPhases.IMPACT,
    "initial-access": ThreatIntel.KillChainPhases.INITIAL_ACCESS,
    "discovery": ThreatIntel.KillChainPhases.DISCOVERY,
    "execution": ThreatIntel.KillChainPhases.EXECUTION,
    "installation": ThreatIntel.KillChainPhases.INSTALLATION,
    "delivery": ThreatIntel.KillChainPhases.DELIVERY,
    "weaponization": ThreatIntel.KillChainPhases.WEAPONIZATION,
    "act-on-objectives": ThreatIntel.KillChainPhases.ACT_ON_OBJECTIVES,
    "command-and-control": ThreatIntel.KillChainPhases.COMMAND_AND_CONTROL,
}

STIX_2_TYPES_TO_CORTEX_CIDR_TYPES = {
    "ipv4-addr": FeedIndicatorType.CIDR,
    "ipv6-addr": FeedIndicatorType.IPv6CIDR,
}

THREAT_INTEL_TYPE_TO_DEMISTO_TYPES = {
    "campaign": ThreatIntel.ObjectsNames.CAMPAIGN,
    "attack-pattern": ThreatIntel.ObjectsNames.ATTACK_PATTERN,
    "report": ThreatIntel.ObjectsNames.REPORT,
    "malware": ThreatIntel.ObjectsNames.MALWARE,
    "course-of-action": ThreatIntel.ObjectsNames.COURSE_OF_ACTION,
    "intrusion-set": ThreatIntel.ObjectsNames.INTRUSION_SET,
    "tool": ThreatIntel.ObjectsNames.TOOL,
    "threat-actor": ThreatIntel.ObjectsNames.THREAT_ACTOR,
    "infrastructure": ThreatIntel.ObjectsNames.INFRASTRUCTURE,
}


def convert_to_json(string):
    """Will try to convert given string to json.

    Args:
        string: str of stix/json file. may be xml, then function will fail

    Returns:
        json object if succeed
        False if failed
    """
    try:
        js = json.loads(string)
        return js
    except ValueError:
        return None


class STIX2Parser:
    OBJECTS_TO_PARSE = [
        "indicator",
        "report",
        "malware",
        "campaign",
        "attack-pattern",
        "course-of-action",
        "intrusion-set",
        "tool",
        "threat-actor",
        "infrastructure",
        "autonomous-system",
        "domain-name",
        "email-addr",
        "file",
        "ipv4-addr",
        "ipv6-addr",
        "mutex",
        "url",
        "user-account",
        "windows-registry-key",
        "relationship",
        "extension-definition",
    ]

    def __init__(self):
        """
        TAXII 2 Client used to poll and parse indicators in XSOAR formar
        """
        self.indicator_regexes = [
            re.compile(INDICATOR_EQUALS_VAL_PATTERN),
            re.compile(INDICATOR_IN_VAL_PATTERN),
            re.compile(HASHES_EQUALS_VAL_PATTERN),
            re.compile(REGISTRY_EQUALS_VAL_PATTERN),
        ]
        self.cidr_regexes = [
            re.compile(CIDR_ISSUBSET_VAL_PATTERN),
            re.compile(CIDR_ISUPPERSET_VAL_PATTERN),
        ]
        self.id_to_object: Dict[str, Any] = {}
        self.parsed_object_id_to_object: Dict[str, Any] = {}

    @staticmethod
    def get_indicator_publication(indicator: Dict[str, Any]):
        """
        Build publications grid field from the indicator external_references field

        Args:
            indicator: The indicator with publication field

        Returns:
            list. publications grid field
        """
        publications = []
        for external_reference in indicator.get("external_references", []):
            url = external_reference.get("url", "")
            description = external_reference.get("description", "")
            source_name = external_reference.get("source_name", "")
            publications.append({"link": url, "title": description, "source": source_name})
        return publications

    @staticmethod
    def change_attack_pattern_to_stix_attack_pattern(indicator: Dict[str, Any]):
        indicator["indicator_type"] = f'STIX {indicator["indicator_type"]}'
        indicator["customFields"]["stixkillchainphases"] = indicator["customFields"].pop("killchainphases", None)
        indicator["customFields"]["stixdescription"] = indicator["customFields"].pop("description", None)

        return indicator

    @staticmethod
    def get_ioc_type(indicator: str, id_to_object: Dict[str, Dict[str, Any]]) -> str:
        """
        Get IOC type by extracting it from the pattern field.

        Args:
            indicator: the indicator to get information on.
            id_to_object: a dict in the form of - id: stix_object.

        Returns:
            str. the IOC type.
        """
        ioc_type = ""
        indicator_obj = id_to_object.get(indicator, {})
        pattern = indicator_obj.get("pattern", "")
        for stix_type in STIX_2_TYPES_TO_CORTEX_TYPES:
            if pattern.startswith(f"[{stix_type}"):
                ioc_type = STIX_2_TYPES_TO_CORTEX_TYPES.get(stix_type)  # type: ignore
                break
        return ioc_type

    @staticmethod
    def change_ip_to_cidr(indicators):
        """
        Iterates over indicators list and changes IP to CIDR type if needed.
        :param indicators: list of parsed indicators.
        :return: changes indicators list in-place.
        """
        for indicator in indicators:
            if indicator.get("indicator_type") == FeedIndicatorType.IP:
                value = indicator.get("value")
                if value.endswith("/32"):
                    pass
                elif "/" in value:
                    indicator["indicator_type"] = FeedIndicatorType.CIDR

    """ PARSING FUNCTIONS"""

    def parse_indicator(self, indicator_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single indicator object
        :param indicator_obj: indicator object
        :return: indicators extracted from the indicator object in cortex format
        """
        field_map: dict = {}
        pattern = indicator_obj.get("pattern")
        indicators = []
        if pattern:
            # this is done in case the server doesn't properly space the operator,
            # supported indicators have no spaces, so this action shouldn't affect extracted values
            trimmed_pattern = pattern.replace(" ", "")

            indicator_groups = self.extract_indicator_groups_from_pattern(trimmed_pattern, self.indicator_regexes)

            indicators.extend(
                self.get_indicators_from_indicator_groups(
                    indicator_groups,
                    indicator_obj,
                    STIX_2_TYPES_TO_CORTEX_TYPES,
                    field_map,
                )
            )

            cidr_groups = self.extract_indicator_groups_from_pattern(trimmed_pattern, self.cidr_regexes)
            indicators.extend(
                self.get_indicators_from_indicator_groups(
                    cidr_groups,
                    indicator_obj,
                    STIX_2_TYPES_TO_CORTEX_CIDR_TYPES,
                    field_map,
                )
            )
            self.change_ip_to_cidr(indicators)

        return indicators

    @staticmethod
    def parse_attack_pattern(attack_pattern_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single attack pattern object
        :param attack_pattern_obj: attack pattern object
        :return: attack pattern extracted from the attack pattern object in cortex format
        """
        publications = STIX2Parser.get_indicator_publication(attack_pattern_obj)

        kill_chain_mitre = [chain.get("phase_name", "") for chain in attack_pattern_obj.get("kill_chain_phases", [])]
        kill_chain_phases = [MITRE_CHAIN_PHASES_TO_DEMISTO_FIELDS.get(phase) for phase in kill_chain_mitre]

        # Extract MitreID from external_references
        mitre_id = ""
        for external_ref in attack_pattern_obj.get("external_references", []):
            if external_ref.get("source_name") == "mitre":
                mitre_id = external_ref.get("external_id", "")
                break

        attack_pattern = {
            "value": attack_pattern_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.ATTACK_PATTERN,
            "score": ThreatIntel.ObjectsScore.ATTACK_PATTERN,
            "rawJSON": attack_pattern_obj,
        }
        fields = {
            "mitreid": mitre_id,
            "stixid": attack_pattern_obj.get("id"),
            "killchainphases": kill_chain_phases,
            "firstseenbysource": attack_pattern_obj.get("created"),
            "modified": attack_pattern_obj.get("modified"),
            "description": attack_pattern_obj.get("description", ""),
            "operatingsystemrefs": attack_pattern_obj.get("x_mitre_platforms"),
            "publications": publications,
            "tags": list(set(attack_pattern_obj.get("labels", []))),
        }

        attack_pattern["customFields"] = fields

        if not is_demisto_version_ge("6.2.0"):
            # For versions less than 6.2 - that only support STIX and not the newer types - Malware, Tool, etc.
            attack_pattern = STIX2Parser.change_attack_pattern_to_stix_attack_pattern(attack_pattern)

        return [attack_pattern]

    @staticmethod
    def parse_report(report_obj: Dict[str, Any]):
        """
        Parses a single report object
        :param report_obj: report object
        :return: report extracted from the report object in cortex format
        """
        object_refs = report_obj.get("object_refs", [])
        new_relationships = []
        for obj_id in object_refs:
            new_relationships.append(
                {
                    "type": "relationship",
                    "id": "relationship--fakeid",
                    "created": report_obj.get("created"),
                    "modified": report_obj.get("modified"),
                    "relationship_type": "contains",
                    "source_ref": report_obj.get("id"),
                    "target_ref": obj_id,
                }
            )

        report = {
            "indicator_type": ThreatIntel.ObjectsNames.REPORT,
            "value": report_obj.get("name"),
            "score": ThreatIntel.ObjectsScore.REPORT,
            "rawJSON": report_obj,
        }
        fields = {
            "stixid": report_obj.get("id"),
            "firstseenbysource": report_obj.get("created"),
            "published": report_obj.get("published"),
            "description": report_obj.get("description", ""),
            "report_types": report_obj.get("report_types", []),
            "tags": list(set(report_obj.get("labels", []))),
        }

        report["customFields"] = fields

        return [report], new_relationships

    @staticmethod
    def parse_threat_actor(threat_actor_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single threat actor object
        :param threat_actor_obj: report object
        :return: threat actor extracted from the threat actor object in cortex format
        """

        threat_actor = {
            "value": threat_actor_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.THREAT_ACTOR,
            "score": ThreatIntel.ObjectsScore.THREAT_ACTOR,
            "rawJSON": threat_actor_obj,
        }
        fields = {
            "stixid": threat_actor_obj.get("id"),
            "firstseenbysource": threat_actor_obj.get("created"),
            "modified": threat_actor_obj.get("modified"),
            "description": threat_actor_obj.get("description", ""),
            "aliases": threat_actor_obj.get("aliases", []),
            "threat_actor_types": threat_actor_obj.get("threat_actor_types", []),
            "roles": threat_actor_obj.get("roles", []),
            "goals": threat_actor_obj.get("goals", []),
            "sophistication": threat_actor_obj.get("sophistication", ""),
            "resource_level": threat_actor_obj.get("resource_level", ""),
            "primary_motivation": threat_actor_obj.get("primary_motivation", ""),
            "secondary_motivations": threat_actor_obj.get("secondary_motivations", []),
            "tags": list(set(threat_actor_obj.get("labels", []))),
        }

        threat_actor["customFields"] = fields

        return [threat_actor]

    @staticmethod
    def parse_infrastructure(infrastructure_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single infrastructure object
        :param infrastructure_obj: infrastructure object
        :return: infrastructure extracted from the infrastructure object in cortex format
        """
        kill_chain_mitre = [chain.get("phase_name", "") for chain in infrastructure_obj.get("kill_chain_phases", [])]
        kill_chain_phases = [MITRE_CHAIN_PHASES_TO_DEMISTO_FIELDS.get(phase) for phase in kill_chain_mitre]

        infrastructure = {
            "value": infrastructure_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.INFRASTRUCTURE,
            "score": ThreatIntel.ObjectsScore.INFRASTRUCTURE,
            "rawJSON": infrastructure_obj,
        }
        fields = {
            "stixid": infrastructure_obj.get("id"),
            "description": infrastructure_obj.get("description", ""),
            "infrastructure_types": infrastructure_obj.get("infrastructure_types", []),
            "aliases": infrastructure_obj.get("aliases", []),
            "kill_chain_phases": kill_chain_phases,
            "firstseenbysource": infrastructure_obj.get("created"),
            "modified": infrastructure_obj.get("modified"),
            "tags": list(set(infrastructure_obj.get("labels", []))),
        }

        infrastructure["customFields"] = fields
        return [infrastructure]

    @staticmethod
    def parse_malware(malware_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single malware object
        :param malware_obj: malware object
        :return: malware extracted from the malware object in cortex format
        """

        kill_chain_mitre = [chain.get("phase_name", "") for chain in malware_obj.get("kill_chain_phases", [])]
        kill_chain_phases = [MITRE_CHAIN_PHASES_TO_DEMISTO_FIELDS.get(phase) for phase in kill_chain_mitre]

        malware = {
            "value": malware_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.MALWARE,
            "score": ThreatIntel.ObjectsScore.MALWARE,
            "rawJSON": malware_obj,
        }
        fields = {
            "stixid": malware_obj.get("id"),
            "firstseenbysource": malware_obj.get("created"),
            "modified": malware_obj.get("modified"),
            "description": malware_obj.get("description", ""),
            "malware_types": malware_obj.get("malware_types", []),
            "is_family": malware_obj.get("is_family", False),
            "aliases": malware_obj.get("aliases", []),
            "kill_chain_phases": kill_chain_phases,
            "os_execution_envs": malware_obj.get("os_execution_envs", []),
            "architecture_execution_envs": malware_obj.get("architecture_execution_envs", []),
            "capabilities": malware_obj.get("capabilities", []),
            "sample_refs": malware_obj.get("sample_refs", []),
            "tags": list(set(malware_obj.get("labels", []))),
        }

        malware["customFields"] = fields
        return [malware]

    @staticmethod
    def parse_tool(tool_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single tool object
        :param tool_obj: tool object
        :return: tool extracted from the tool object in cortex format
        """
        kill_chain_mitre = [chain.get("phase_name", "") for chain in tool_obj.get("kill_chain_phases", [])]
        kill_chain_phases = [MITRE_CHAIN_PHASES_TO_DEMISTO_FIELDS.get(phase) for phase in kill_chain_mitre]

        tool = {
            "value": tool_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.TOOL,
            "score": ThreatIntel.ObjectsScore.TOOL,
            "rawJSON": tool_obj,
        }
        fields = {
            "stixid": tool_obj.get("id"),
            "killchainphases": kill_chain_phases,
            "firstseenbysource": tool_obj.get("created"),
            "modified": tool_obj.get("modified"),
            "tool_types": tool_obj.get("tool_types", []),
            "description": tool_obj.get("description", ""),
            "aliases": tool_obj.get("aliases", []),
            "tool_version": tool_obj.get("tool_version", ""),
            "tags": list(set(tool_obj.get("labels", []))),
        }

        tool["customFields"] = fields
        return [tool]

    @staticmethod
    def parse_course_of_action(coa_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single course of action object
        :param coa_obj: course of action object
        :return: course of action extracted from the course of action object in cortex format
        """
        publications = STIX2Parser.get_indicator_publication(coa_obj)

        course_of_action = {
            "value": coa_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.COURSE_OF_ACTION,
            "score": ThreatIntel.ObjectsScore.COURSE_OF_ACTION,
            "rawJSON": coa_obj,
        }
        fields = {
            "stixid": coa_obj.get("id"),
            "firstseenbysource": coa_obj.get("created"),
            "modified": coa_obj.get("modified"),
            "description": coa_obj.get("description", ""),
            "action_type": coa_obj.get("action_type", ""),
            "publications": publications,
            "tags": list(set(coa_obj.get("labels", []))),
        }

        course_of_action["customFields"] = fields
        return [course_of_action]

    @staticmethod
    def parse_campaign(campaign_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single campaign object
        :param campaign_obj: campaign object
        :return: campaign extracted from the campaign object in cortex format
        """
        campaign = {
            "value": campaign_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.CAMPAIGN,
            "score": ThreatIntel.ObjectsScore.CAMPAIGN,
            "rawJSON": campaign_obj,
        }
        fields = {
            "stixid": campaign_obj.get("id"),
            "firstseenbysource": campaign_obj.get("created"),
            "modified": campaign_obj.get("modified"),
            "description": campaign_obj.get("description", ""),
            "aliases": campaign_obj.get("aliases", []),
            "objective": campaign_obj.get("objective", ""),
            "tags": list(set(campaign_obj.get("labels", []))),
        }

        campaign["customFields"] = fields
        return [campaign]

    @staticmethod
    def parse_intrusion_set(intrusion_set_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses a single intrusion set object
        :param intrusion_set_obj: intrusion set object
        :return: intrusion set extracted from the intrusion set object in cortex format
        """
        publications = STIX2Parser.get_indicator_publication(intrusion_set_obj)

        intrusion_set = {
            "value": intrusion_set_obj.get("name"),
            "indicator_type": ThreatIntel.ObjectsNames.INTRUSION_SET,
            "score": ThreatIntel.ObjectsScore.INTRUSION_SET,
            "rawJSON": intrusion_set_obj,
        }
        fields = {
            "stixid": intrusion_set_obj.get("id"),
            "firstseenbysource": intrusion_set_obj.get("created"),
            "modified": intrusion_set_obj.get("modified"),
            "description": intrusion_set_obj.get("description", ""),
            "aliases": intrusion_set_obj.get("aliases", []),
            "goals": intrusion_set_obj.get("goals", []),
            "resource_level": intrusion_set_obj.get("resource_level", ""),
            "primary_motivation": intrusion_set_obj.get("primary_motivation", ""),
            "secondary_motivations": intrusion_set_obj.get("secondary_motivations", []),
            "publications": publications,
            "tags": list(set(intrusion_set_obj.get("labels", []))),
        }
        intrusion_set["customFields"] = fields
        return [intrusion_set]

    @staticmethod
    def parse_general_sco_indicator(sco_object: Dict[str, Any], value_mapping: str = "value") -> List[Dict[str, Any]]:
        """
        Parses a single SCO indicator.

        Args:
            sco_object (dict): indicator as an observable object.
            value_mapping (str): the key that extracts the value from the indicator response.
        """
        sco_indicator = {
            "value": sco_object.get(value_mapping),
            "score": Common.DBotScore.NONE,
            "rawJSON": sco_object,
            "indicator_type": STIX_2_TYPES_TO_CORTEX_TYPES.get(sco_object.get("type")),  # type: ignore[arg-type]
        }

        fields = {"stixid": sco_object.get("id")}

        sco_indicator["customFields"] = fields
        return [sco_indicator]

    @staticmethod
    def parse_sco_autonomous_system_indicator(autonomous_system_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses autonomous_system indicator type to cortex format.

        Args:
            autonomous_system_obj (dict): indicator as an observable object of type autonomous-system.
        """
        autonomous_system_indicator = STIX2Parser.parse_general_sco_indicator(autonomous_system_obj, value_mapping="number")
        autonomous_system_indicator[0]["customFields"]["name"] = autonomous_system_obj.get("name")

        return autonomous_system_indicator

    @staticmethod
    def parse_sco_file_indicator(file_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses file indicator type to cortex format.

        Args:
            file_obj (dict): indicator as an observable object of file type.
        """
        file_hashes = file_obj.get("hashes", {})
        value = file_hashes.get("SHA-256") or file_hashes.get("SHA-1") or file_hashes.get("MD5")
        if not value:
            return []

        file_obj["value"] = value

        file_indicator = STIX2Parser.parse_general_sco_indicator(file_obj)
        file_indicator[0]["customFields"].update(
            {
                "associatedfilenames": file_obj.get("name"),
                "size": file_obj.get("size"),
                "path": file_obj.get("parent_directory_ref"),
                "md5": file_hashes.get("MD5"),
                "sha1": file_hashes.get("SHA-1"),
                "sha256": file_hashes.get("SHA-256"),
            }
        )

        return file_indicator

    @staticmethod
    def parse_sco_mutex_indicator(mutex_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses mutex indicator type to cortex format.

        Args:
            mutex_obj (dict): indicator as an observable object of mutex type.
        """
        return STIX2Parser.parse_general_sco_indicator(sco_object=mutex_obj, value_mapping="name")

    @staticmethod
    def parse_sco_account_indicator(account_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses account indicator type to cortex format.

        Args:
            account_obj (dict): indicator as an observable object of account type.
        """
        account_indicator = STIX2Parser.parse_general_sco_indicator(account_obj, value_mapping="user_id")
        account_indicator[0]["customFields"].update(
            {"displayname": account_obj.get("user_id"), "accounttype": account_obj.get("account_type")}
        )
        return account_indicator

    @staticmethod
    def parse_sco_windows_registry_key_indicator(registry_key_obj: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Parses registry_key indicator type to cortex format.

        Args:
            registry_key_obj (dict): indicator as an observable object of registry_key type.
        """
        registry_key_indicator = STIX2Parser.parse_general_sco_indicator(registry_key_obj, value_mapping="key")
        registry_key_indicator[0]["customFields"].update(
            {
                "registryvalue": registry_key_obj.get("values"),
                "modified_time": registry_key_obj.get("modified_time"),
                "number_of_subkeys": registry_key_obj.get("number_of_subkeys"),
            }
        )
        return registry_key_indicator

    def parse_relationships(self, relationships_lst: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Parse the Relationships objects retrieved from the feed.

        Returns:
            A dict of relationship value to processed relationships as indicator object.
        """
        a_value_to_relationship: Dict[str, Any] = {}
        for relationships_object in relationships_lst:
            relationship_type = relationships_object.get("relationship_type")
            if relationship_type not in EntityRelationship.Relationships.RELATIONSHIPS_NAMES:
                if relationship_type == "indicates":
                    relationship_type = "indicated-by"
                else:
                    demisto.debug(f"Invalid relation type: {relationship_type}")
                    continue

            a_stixid = relationships_object.get("source_ref", "")
            a_object = self.parsed_object_id_to_object.get(a_stixid, {})
            b_stixid = relationships_object.get("target_ref", "")
            b_object = self.parsed_object_id_to_object.get(b_stixid, {})

            if not a_object or not b_object:
                demisto.debug(f"Cant find {a_object=} or {b_object=}.")
                continue

            a_value, a_type = a_object.get("value"), a_object.get("indicator_type")
            b_value, b_type = b_object.get("value"), b_object.get("indicator_type")

            if not (a_value and a_type and b_value and b_type):
                continue

            mapping_fields = {
                "lastseenbysource": relationships_object.get("modified"),
                "firstseenbysource": relationships_object.get("created"),
            }

            entity_relation = EntityRelationship(
                name=relationship_type,
                entity_a=a_value,
                entity_a_type=a_type,
                entity_b=b_value,
                entity_b_type=b_type,
                fields=mapping_fields,
            )
            indicator_relationship = entity_relation.to_indicator()
            if a_value_to_relationship.get(a_value):
                a_value_to_relationship[a_value].append(indicator_relationship)
            else:
                a_value_to_relationship[a_value] = [indicator_relationship]

        return a_value_to_relationship

    def parse_stix2(self, js_content) -> List[Dict[str, str]]:
        """
        Polls the taxii server and builds a list of cortex indicators objects from the result
        :return: Cortex indicators list
        """
        if js_content.get("objects"):
            envelopes = STIX2Parser.create_envelopes_by_type(js_content["objects"])
        else:
            envelopes = STIX2Parser.create_envelopes_by_type([js_content])
        indicators = self.load_stix_objects_from_envelope(envelopes)

        return indicators

    def load_stix_objects_from_envelope(self, envelopes: Dict[str, Any]):
        parse_stix_2_objects = {
            "indicator": self.parse_indicator,
            "attack-pattern": self.parse_attack_pattern,
            "malware": self.parse_malware,
            "report": self.parse_report,
            "course-of-action": self.parse_course_of_action,
            "campaign": self.parse_campaign,
            "intrusion-set": self.parse_intrusion_set,
            "tool": self.parse_tool,
            "threat-actor": self.parse_threat_actor,
            "infrastructure": self.parse_infrastructure,
            "domain-name": self.parse_general_sco_indicator,
            "ipv4-addr": self.parse_general_sco_indicator,
            "ipv6-addr": self.parse_general_sco_indicator,
            "email-addr": self.parse_general_sco_indicator,
            "url": self.parse_general_sco_indicator,
            "autonomous-system": self.parse_sco_autonomous_system_indicator,
            "file": self.parse_sco_file_indicator,
            "mutex": self.parse_sco_mutex_indicator,
            "user-account": self.parse_sco_account_indicator,
            "windows-registry-key": self.parse_sco_windows_registry_key_indicator,
        }
        indicators = self.parse_dict_envelope(envelopes, parse_stix_2_objects)
        demisto.debug(f"{SCRIPT_NAME} has extracted {len(indicators)} indicators")
        return indicators

    def parse_dict_envelope(self, envelopes: Dict[str, Any], parse_objects_func):
        indicators = []
        relationships_list: List[Dict[str, Any]] = []

        xsoar_taxii_server_extensions = self.get_taxii2_extensions_from_envelope(envelopes.get("extension-definition", []))

        for obj_type, stix_objects in envelopes.items():
            if obj_type == "relationship":
                relationships_list.extend(stix_objects)
            else:
                for obj in stix_objects:
                    # handled separately
                    if obj.get("type") == "extension-definition":
                        continue
                    self.id_to_object[obj.get("id")] = obj
                    if obj.get("type") == "report":
                        result, relationships = self.parse_report(obj)
                        relationships_list.extend(relationships)
                    else:
                        result = parse_objects_func[obj_type](obj)
                    if not result:
                        continue
                    self.update_obj_if_extensions(xsoar_taxii_server_extensions, obj, result)
                    self.parsed_object_id_to_object[obj.get("id")] = result[0]
                    indicators.extend(result)

        if relationships_list:
            relationships_mapping = self.parse_relationships(relationships_list)
            STIX2Parser.add_relationship_to_indicator(relationships_mapping, indicators)
        return indicators

    @staticmethod
    def create_envelopes_by_type(objects) -> dict:
        """
        Creates objects envelops by type
        """
        types_envelopes: dict = {}
        index = 0
        for obj in objects:
            obj_type = obj.get("type")
            if obj_type not in STIX2Parser.OBJECTS_TO_PARSE:
                demisto.debug(f"Cannot parse object of type {obj_type}, skipping.")
                index += 1
                continue
            if obj_type not in types_envelopes:
                types_envelopes[obj_type] = []
            types_envelopes[obj_type].append(obj)

        return types_envelopes

    @staticmethod
    def get_indicators_from_indicator_groups(
        indicator_groups: List[tuple[str, str]],
        indicator_obj: Dict[str, str],
        indicator_types: Dict[str, str],
        field_map: Dict[str, str],
    ) -> List[Dict[str, str]]:
        """
        Get indicators from indicator regex groups
        :param indicator_groups: caught regex group in pattern of: [`type`, `indicator`]
        :param indicator_obj: taxii indicator object
        :param indicator_types: supported indicator types -> cortex types
        :param field_map: map used to create fields entry ({field_name: field_value})
        :return: Indicators list
        """
        indicators = []
        if indicator_groups:
            for term in indicator_groups:
                for taxii_type in indicator_types:
                    # term should be list with 2 argument parsed with regex - [`type`, `indicator`]
                    if len(term) == 2 and taxii_type in term[0]:
                        type_ = indicator_types[taxii_type]
                        value = term[1]

                        # support added for cases as 'value1','value2','value3' for 3 different indicators
                        for indicator_value in value.split(","):
                            indicator_value = indicator_value.strip("'")
                            indicator = STIX2Parser.create_indicator(indicator_obj, type_, indicator_value.strip("'"), field_map)
                            indicators.append(indicator)
                        break
        return indicators

    @staticmethod
    def create_indicator(indicator_obj, type_, value, field_map):
        """
        Create a cortex indicator from a stix indicator
        :param indicator_obj: rawJSON value of the indicator
        :param type_: cortex type of the indicator
        :param value: indicator value
        :param field_map: field map used for mapping fields ({field_name: field_value})
        :return: Cortex indicator
        """
        ioc_obj_copy = copy.deepcopy(indicator_obj)
        ioc_obj_copy["value"] = value
        ioc_obj_copy["type"] = type_
        indicator = {
            "value": value,
            "indicator_type": type_,
            "rawJSON": ioc_obj_copy,
        }
        fields = {}
        tags = []
        # create tags from labels:
        for label in ioc_obj_copy.get("labels", []):
            tags.append(label)

        # add description if able
        if "description" in ioc_obj_copy:
            fields["description"] = ioc_obj_copy["description"]

        # add field_map fields
        for field_name, field_path in field_map.items():
            if field_path in ioc_obj_copy:
                fields[field_name] = ioc_obj_copy.get(field_path)

        # union of tags and labels
        if "tags" in fields:
            field_tag = fields.get("tags")
            if isinstance(field_tag, list):
                tags.extend(field_tag)
            else:
                tags.append(field_tag)

        fields["tags"] = tags

        indicator["customFields"] = fields
        return indicator

    @staticmethod
    def extract_indicator_groups_from_pattern(pattern: str, regexes: List) -> List[tuple[str, str]]:
        """
        Extracts indicator [`type`, `indicator`] groups from pattern
        :param pattern: stix pattern
        :param regexes: regexes to run to pattern
        :return: extracted indicators list from pattern
        """
        groups: List[tuple[str, str]] = []
        for regex in regexes:
            find_result = regex.findall(pattern)
            if find_result:
                groups.extend(find_result)
        return groups

    @staticmethod
    def add_relationship_to_indicator(relationships_mapping, indicators):
        """
        Adds relationship to right indicator
        :param relationships_mapping: maps a_value to relationship object
        :param indicators: all indicators that were fetched from file.
        """
        for indicator in indicators:
            if (a_value := indicator.get("value")) and (relationships := relationships_mapping.get(a_value)):
                indicator["relationships"] = relationships

    @staticmethod
    def update_obj_if_extensions(xsoar_taxii_server_extensions, obj, result):
        """
        If stix object has extension, check if it xsoar taxii2 server extension, if yes parse it to XSOAR.
        :param xsoar_taxii_server_extensions: ids of all XSOAR extentions in current bundle.
        :param obj: stix object
        :param result: parsed xsoar indicator
        :return: updated xsoar indicator
        """
        parsed_result = result[0]
        if extensions := obj.get("extensions"):
            custom_fields = parsed_result.get("customFields", {})
            for ext_id, extension in extensions.items():
                if ext_id in xsoar_taxii_server_extensions:
                    extension.pop("extension_type")
                    for field, value in extension.items():
                        if field in SYSTEM_FIELDS:
                            parsed_result[field] = value
                        elif field.lower() == "customfields":
                            custom_fields.update(value)
                        else:
                            custom_fields[field] = value
                    parsed_result["customFields"] = custom_fields

    @staticmethod
    def get_taxii2_extensions_from_envelope(stix_objects):
        """
        :param stix_objects: list of all extension objects.
        :return: list of xsoar extensions ids.
        """
        xsoar_taxii_server_extensions = []
        for obj in stix_objects:
            if obj.get("schema") == XSOAR_TAXII2_SERVER_SCHEMA:
                xsoar_taxii_server_extensions.append(obj.get("id"))
        return xsoar_taxii_server_extensions


# STIX 1 Parsing


def package_extract_properties(package):
    """Extracts properties from the STIX package"""
    result: Dict[str, str] = {}

    header = package.find_all("STIX_Header")
    if len(header) == 0:
        return result

    # share level
    mstructures = header[0].find_all("Marking_Structure")
    for ms in mstructures:
        type_ = ms.get("xsi:type")
        if type_ is result:
            continue

        color = ms.get("color")
        if color is result:
            continue

        type_ = type_.lower()
        if "tlpmarkingstructuretype" not in type_:
            continue

        result["share_level"] = color.lower()  # To keep backward compatibility
        result["TLP"] = color.upper()  # https://www.us-cert.gov/tlp
        break

    # decode title
    title = next((c for c in header[0] if c.name == "Title"), None)
    if title is not None:
        result["stix_package_title"] = title.text

    # decode description
    description = next((c for c in header[0] if c.name == "Description"), None)
    if description is not None:
        result["stix_package_description"] = description.text

    # decode description
    sdescription = next((c for c in header[0] if c.name == "Short_Description"), None)
    if sdescription is not None:
        result["stix_package_short_description"] = sdescription.text

    # decode identity name from information_source
    information_source = next((c for c in header[0] if c.name == "Information_Source"), None)
    if information_source is not None:
        identity = next((c for c in information_source if c.name == "Identity"), None)
        if identity is not None:
            name = next(c for c in identity if c.name == "Name")
            if name is not None:
                result["stix_package_information_source"] = name.text

    return result


def observable_extract_properties(observable):
    """Extracts properties from observable"""
    result = {}

    if id_ref := observable.get("id"):
        result["indicator_ref"] = id_ref

    title = next((c for c in observable if c.name == "Title"), None)
    if title is not None:
        title = title.text
        result["stix_title"] = title

    description = next((c for c in observable if c.name == "Description"), None)
    if description is not None:
        description = description.text
        result["stix_description"] = description

    return result


def indicator_extract_properties(indicator) -> Dict[str, Any]:
    """Extracts the Indicator properties

    Args:
        indicator (bs4.element.Tag): The Indicator content in xml.

    Returns:
        dict: The ttp properties in a dict {'property': 'value'}. (The value can be a list)

    """

    result: Dict[str, Any] = {}

    title = next((c for c in indicator if c.name == "Title"), None)
    if title is not None:
        title = title.text
        result["stix_indicator_name"] = title

    description = next((c for c in indicator if c.name == "Description"), None)
    if description is not None:
        description = description.text
        result["stix_indicator_description"] = description

    confidence = next((c for c in indicator if c.name == "Confidence"), None)
    if confidence is not None:
        value = next((c for c in confidence if c.name == "Value"), None)
        if value is not None:
            value = value.text
            result["confidence"] = value

    if indicated_ttp := indicator.find_all("Indicated_TTP"):
        result["ttp_ref"] = []
        # Each indicator can be related to few ttps
        for ttp_value in indicated_ttp:
            ttp = next((c for c in ttp_value if c.name == "TTP"), None)
            if ttp is not None:
                value = ttp.get("idref")
                result["ttp_ref"].append(value)

    return result


def ttp_extract_properties(ttp, behavior) -> Dict[str, str]:
    """Extracts the TTP properties

    Args:
        ttp (bs4.element.Tag): The TTP content in xml.
        behavior (str): The TTP behavior ['Malware', 'Attack Pattern'].

    Returns:
        dict: The ttp properties in a dict {'property': 'value'}.

    """

    result = {"type": behavior}

    if behavior == "Malware":
        type_ = next((c for c in ttp if c.name == "Type"), None)
        if type_ is not None:
            type_ = type_.text
            result["malware_type"] = type_

        name = next((c for c in ttp if c.name == "Name"), None)
        if name is not None:
            name = name.text
            result["indicator"] = name

        title = next((c for c in ttp if c.name == "Title"), None)
        if title is not None:
            title = title.text
            result["title"] = title

    if behavior == "Attack Pattern":
        id_ref = next((c for c in ttp if c.name == "idref"), None)
        if id_ref is not None:
            id_ref = id_ref.text
            result["stix_id_ref"] = id_ref

        title = next((c for c in ttp if c.name == "Title"), None)
        if title is not None:
            title = title.text
            result["indicator"] = title

    description = next((c for c in ttp if c.name == "Description"), None)
    if description is not None:
        description = description.text
        result["description"] = description

    short_description = next((c for c in ttp if c.name == "Short_Description"), None)
    if short_description is not None:
        short_description = short_description.text
        result["short_description"] = short_description

    return result


def create_relationships(indicator):
    results = []

    for relationship in indicator.get("relationships", {}):
        if relationship.get("type") == "Malware":
            name = "indicator-of"
            relationship_type = "Malware"
        else:
            name = "related-to"
            relationship_type = "Attack Pattern"

        entity_relationship = EntityRelationship(
            name=name,
            entity_a=indicator.get("value"),
            entity_a_type=indicator.get("type"),
            entity_b=relationship.get("indicator"),
            entity_b_type=relationship_type,
        )
        results.append(entity_relationship.to_indicator())

    return results


class AddressObject:
    """
    Implements address object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/AddressObj/AddressObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        result: List[Dict[str, str]] = []

        indicator = props.find("Address_Value")
        if indicator is None:
            return result

        indicator = indicator.string.encode("ascii", "replace").decode()
        category = props.get("category", None)
        address_list = indicator.split("##comma##")

        if category == "e-mail":
            return [{"indicator": address, "type": "Email"} for address in address_list]

        try:
            for address in address_list:
                ip = IPNetwork(address)
                if ip.version == 4:
                    if len(address.split("/")) > 1:
                        type_ = "CIDR"
                    else:
                        type_ = "IP"
                elif ip.version == 6:
                    if len(address.split("/")) > 1:
                        type_ = "IPv6CIDR"
                    else:
                        type_ = "IPv6"
                else:
                    LOG(f"Unknown ip version: {ip.version!r}")
                    return []

                result.append({"indicator": address, "type": type_})

        except Exception:
            return result

        return result


class DomainNameObject:
    """
    Implements domain object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/DomainNameObj/DomainNameObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        domains = []
        dtype = props.get("type", "FQDN")
        if dtype != "FQDN":
            return []

        if domain_value := props.find("Value"):
            domain_list = domain_value.string.split("##comma##")
            for domain in domain_list:
                domains.append({"indicator": domain, "type": "Domain"})

        return domains


class FileObject:
    """
    Implements file object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/FileObj/FileObjectType/
    """

    @staticmethod
    def _decode_basic_props(props):
        result = {}

        name = next((c for c in props if c.name == "File_Name"), None)
        if name is not None:
            result["stix_file_name"] = name.text

        size = next((c for c in props if c.name == "File_Size"), None)
        if size is not None:
            result["stix_file_size"] = size.text

        file_format = next((c for c in props if c.name == "File_Format"), None)
        if file_format is not None:
            result["stix_file_format"] = file_format.text

        return result

    @staticmethod
    def decode(props, **kwargs):
        result = []

        bprops = FileObject._decode_basic_props(props)

        hashes = props.find_all("Hash")
        for h in hashes:
            value = h.find("Simple_Hash_Value")
            if value is None:
                continue
            value = value.string.lower()
            value_list = value.split("##comma##")
            for v in value_list:
                v = v.strip()
                if type := detect_file_indicator_type(v):
                    result.append({"indicator": v, "htype": type, "type": "File"})

        for r in result:
            for r2 in result:
                if r["htype"] == r2["htype"]:
                    continue

                r[f"stix_file_{r2['htype']}"] = r2["indicator"]

            r.update(bprops)

        return result


class URIObject:
    """
    Implements URI object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/URIObj/URIObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        urls = []
        utype = props.get("type", "URL")
        if utype == "URL":
            type_ = "URL"
        elif utype == "Domain Name":
            type_ = "Domain"
        else:
            return []

        if url_value := props.find("Value"):
            url_list = url_value.string.split("##comma##")
            for url in url_list:
                urls.append({"indicator": url, "type": type_})

        return urls


class SocketAddressObject:
    """
    Implements socket address object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/SocketAddressObj/SocketAddressObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        ip = props.get("ip_address", None)
        if ip:
            return AddressObject.decode(ip)
        return []


class LinkObject:
    """
    Implements link object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/LinkObj/LinkObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        ltype = props.get("type", "URL")
        if ltype != "URL":
            LOG(f"Unhandled LinkObjectType type: {ltype}")
            return []
        value = props.get("value", None)
        if value is None:
            LOG("no value in observable LinkObject")
            return []
        if not isinstance(value, string_types):
            value = value.get("value", None)
            if value is None:
                LOG("no value in observable LinkObject")
                return []
        return [{"indicator": value, "type": ltype}]


class HTTPSessionObject:
    """
    Implements http session object indicator decoding
    based on: https://stixproject.github.io/data-model/1.2/HTTPSessionObj/HTTPSessionObjectType/
    """

    @staticmethod
    def decode(props, **kwargs):
        if props.get("http_request_response"):
            tmp = props.get("http_request_response")

            if len(tmp) == 1:
                item = tmp[0]
                http_client_request = item.get("http_client_request", None)
                if http_client_request is not None:
                    http_request_header = http_client_request.get("http_request_header", None)
                    if http_request_header is not None:
                        raw_header = http_request_header.get("raw_header", None)
                        if raw_header is not None:
                            return [
                                {
                                    "indicator": raw_header.split("\n")[0],
                                    "type": "http-session",  # we don't support this type natively in demisto
                                    "header": raw_header,
                                }
                            ]
            else:
                LOG("multiple HTTPSessionObjectTypes not supported")
        return []


class StixDecode:
    """
    Decode STIX strings formatted as xml, and extract indicators from them
    """

    DECODERS = {
        "DomainNameObjectType": DomainNameObject.decode,
        "FileObjectType": FileObject.decode,
        "WindowsFileObjectType": FileObject.decode,
        "URIObjectType": URIObject.decode,
        "AddressObjectType": AddressObject.decode,
        "SocketAddressObjectType": SocketAddressObject.decode,
        "LinkObjectType": LinkObject.decode,
        "HTTPSessionObjectType": HTTPSessionObject.decode,
    }

    @staticmethod
    def object_extract_properties(props, kwargs):
        type_ = props.get("xsi:type").rsplit(":")[-1]

        if type_ not in StixDecode.DECODERS:
            LOG(f"Unhandled cybox Object type: {type_!r} - {props!r}")
            return []

        return StixDecode.DECODERS[type_](props, **kwargs)

    @staticmethod
    def _parse_stix_timestamp(stix_timestamp):
        dt = dateutil.parser.parse(stix_timestamp)

        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=pytz.UTC)
        delta = dt - EPOCH
        return int(delta.total_seconds() * 1000)

    @staticmethod
    def _deduplicate(indicators):
        result = {}

        for iv in indicators:
            result["{}:{}".format(iv["indicator"], iv["type"])] = iv

        return list(result.values())

    @staticmethod
    def decode(content, **kwargs):
        observable_result = []
        indicator_result: Dict[str, dict] = {}
        ttp_result: Dict[str, dict] = {}

        package = BeautifulSoup(content, "xml")

        timestamp = package.get("timestamp", None)
        if timestamp is not None:
            timestamp = StixDecode._parse_stix_timestamp(timestamp)

        # extract the Observable info
        if observables := package.find_all("Observable"):
            pprops = package_extract_properties(package)

            for o in observables:
                gprops = observable_extract_properties(o)

                obj = next((ob for ob in o if ob.name == "Object"), None)  # type: ignore
                if obj is None:
                    continue

                # main properties
                properties = next((c for c in obj if c.name == "Properties"), None)  # type: ignore
                if properties is not None:
                    for r in StixDecode.object_extract_properties(properties, kwargs):
                        r.update(gprops)
                        r.update(pprops)

                        observable_result.append(r)

                # then related objects
                related = next((c for c in obj if c.name == "Related_Objects"), None)  # type: ignore
                if related is not None:
                    for robj in related:
                        if robj.name != "Related_Object":  # type: ignore
                            continue

                        properties = next((c for c in robj if c.name == "Properties"), None)  # type: ignore
                        if properties is None:
                            continue

                        for r in StixDecode.object_extract_properties(properties, kwargs):
                            r.update(gprops)
                            r.update(pprops)
                            observable_result.append(r)

        # extract the Indicator info
        if (indicators := package.find_all("Indicator")) and observables:
            indicator_ref = observables[0].get("idref")  # type: ignore

            if indicator_ref:
                indicator_info = indicator_extract_properties(indicators[0])
                indicator_result[indicator_ref] = indicator_info  # type: ignore

        # extract the TTP info
        if ttp := package.find_all("TTP"):
            ttp_info: Dict[str, str] = {}

            id_ref = ttp[0].get("id")  # type: ignore

            title = next((c for c in ttp[0] if c.name == "Title"), None)  # type: ignore
            if title is not None:
                title = title.text  # type: ignore
                ttp_info["stix_ttp_title"] = title  # type: ignore

            description = next((c for c in ttp[0] if c.name == "Description"), None)  # type: ignore
            if description is not None:
                description = description.text  # type: ignore
                ttp_info["ttp_description"] = description  # type: ignore

            if behavior := package.find_all("Behavior"):
                if behavior[0].find_all("Malware"):  # type: ignore
                    ttp_info.update(ttp_extract_properties(package.find_all("Malware_Instance")[0], "Malware"))

                elif behavior[0].find_all("Attack_Patterns"):  # type: ignore
                    ttp_info.update(ttp_extract_properties(package.find_all("Attack_Pattern")[0], "Attack Pattern"))

                ttp_result[id_ref] = ttp_info  # type: ignore

        return timestamp, StixDecode._deduplicate(observable_result), indicator_result, ttp_result


def build_observables(file_name):
    tag_stack = collections.deque()  # type: ignore
    observables = []
    indicators = {}
    ttps = {}

    for action, element in etree.iterparse(
        file_name,
        events=("start", "end"),
        recover=True,
        resolve_entities=False,
        load_dtd=False,
        no_network=True,
    ):
        if action == "start":
            tag_stack.append(element.tag)

        else:
            last_tag = tag_stack.pop()
            if last_tag != element.tag:
                raise RuntimeError(f"{SCRIPT_NAME} - error parsing poll response, mismatched tags")

        if action == "end" and element.tag.endswith("STIX_Package"):
            for c in element:
                try:
                    content = etree.tostring(c, encoding="unicode")
                    timestamp, observable, indicator, ttp = StixDecode.decode(content)
                    if observable:
                        observables.extend(observable)
                    if indicator:
                        indicators.update(indicator)
                    if ttp:
                        ttps.update(ttp)
                except Exception as e:
                    demisto.debug(f"Failed parsing response: {e}")

            element.clear()

    for observable in observables:
        if (indicator_ref := observable.get("indicator_ref")) and (indicator_info := indicators.get(indicator_ref)):
            observable.update(indicator_info)

        ttp_ref = observable.get("ttp_ref", [])
        relationships = []

        for reference in ttp_ref:
            if relationship := ttps.get(reference):
                relationships.append(relationship)
        if relationships:
            observable["relationships"] = relationships

    return observables, ttps


def parse_stix(file_name):
    """
    :param file_name: the file with xml indicators
    :return: Parsed indicators in XSOAR format
    """
    indicators = []

    indicator_custom_fields = {
        "title": "stix_title",
        "description": "stix_description",
        "name": "stix_indicator_name",
        "stixdescription": "stix_indicator_description",
        "confidence": "confidence",
    }

    # Create the indicators from the observables
    observables, ttps = build_observables(file_name)
    for item in observables:
        if indicator := item.get("indicator"):
            item["value"] = indicator.strip()
            indicator_obj = {
                "value": indicator.strip(),
                "indicator_type": item.get("type"),
                "customFields": {
                    xsoar_field: item.get(stix_field)
                    for xsoar_field, stix_field in indicator_custom_fields.items()
                    if item.get(stix_field)
                },
            }

            if item.get("relationships"):
                relationships = create_relationships(item)
                indicator_obj["relationships"] = relationships

            indicator_obj["rawJSON"] = item
            indicators.append(indicator_obj)

    # Create the indicators from the ttps
    ttps_custom_fields = {
        "title": "title",
        "description": "description",
        "shortdescription": "short_description",
        "stixdescription": "ttp_description",
        "stixttptitle": "stix_ttp_title",
    }
    for item in ttps.values():
        if indicator := item.get("indicator"):
            item["value"] = indicator.strip()
            indicator_obj = {
                "value": indicator.strip(),
                "indicator_type": item.get("type"),
                "customFields": {
                    xsoar_field: item.get(stix_field)
                    for xsoar_field, stix_field in ttps_custom_fields.items()
                    if item.get(stix_field)
                },
            }

            if item.get("type") == "Malware":
                indicator_obj["score"] = ThreatIntel.ObjectsScore.MALWARE
                indicator_obj["stixmalwaretypes"] = item.get("malware_type", "").lower().replace(" ", "-")
            else:
                indicator_obj["score"] = ThreatIntel.ObjectsScore.ATTACK_PATTERN

            indicator_obj["rawJSON"] = item

            indicators.append(indicator_obj)

    return indicators


def main():  # pragma: no cover
    args = demisto.args()

    indicator_txt = args.get("iocXml")
    entry_id = args.get("entry_id")

    if not indicator_txt and not entry_id:
        raise Exception("You must enter iocXml or entry_id of the Indicator.")
    elif entry_id:
        file_path = demisto.getFilePath(entry_id).get("path")
        with open(file_path) as f:
            indicator_txt = f.read()

    if stix2 := convert_to_json(indicator_txt):
        stix2_parser = STIX2Parser()
        observables = stix2_parser.parse_stix2(stix2)
    else:
        if "file_path" not in locals():
            with tempfile.NamedTemporaryFile() as temp:
                temp.write(str.encode(indicator_txt))
                temp.flush()
                observables = parse_stix(temp.name)
        else:
            observables = parse_stix(file_path)
    json_data = json.dumps(observables)
    return_results(json_data)


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

README

Parse STIX files to Cortex XSOAR indicators by clicking the Upload STIX File button. This script is used for the button only.
This script does not support indicators relationships creation.
In order to create indicators from STIX files using an automation, use CreateIndicatorsFromSTIX. This automation supports indicators relationships creation.

Script Data


Name Description
Script Type python3
Tags stix, ioc
Cortex XSOAR Version 5.0.0

Inputs


Argument Name Description
iocXml IOC XML or JSON in STIX format.
entry_id IOC file entry ID.

Outputs


There are no outputs for this script.