Anomali ThreatStream Feed

Use the Anomali ThreatStream Feed Integration to fetch indicators from the Anomali ThreatStream.

Data Enrichment & Threat Intelligence · Anomali ThreatStream Feed · Feed

Details

IDAnomali ThreatStream Feed
ProviderAnomali
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/py3-tools:1.0.0.10120494

README

Use the Anomali ThreatStream Feed Integration to fetch indicators from the Anomali ThreatStream.

Configure Anomali ThreatStream Feed in Cortex

Parameter Description Required
Fetch indicators Enable this checkbox to automatically pull indicators from the Anomali ThreatStream at regular intervals.  
Fetch by Fetch by the modification or creation time of the indicators. True
Server URL (e.g., https://www.test.com) Confirm that the pre-filled URL matches the correct API endpoint for your Anomali ThreatStream instance. True
Username   True
API Key   True
Feed Fetch Interval   False
Confidence Threshold Will only return indicators above the confidence threshold. False
Source Reliability Reliability of the source providing the intelligence data. True
Traffic Light Protocol Color The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. Indicator’s TLP will override the default value. False
Indicator Reputation Indicators from this integration instance will be marked with this reputation. If not selected, Indicators’ verdicts are determined by their Dbot score from the API. The default is Unknown. False
Indicator Expiration Method The method by which to expire indicators from this feed for this integration instance. False
Create relationships   False
Trust any certificate (not secure)   False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

threatstream-feed-get-indicators


Gets indicators from the feed. This command is mainly used for testing and debugging purposes.

Base Command

threatstream-feed-get-indicators

Input

Argument Name Description Required
indicator_type The indicator type to analyze. If not selected, indicators from all types are retrieved. Possible values are: domain, ip, md5, url, email. Optional
limit Maximum number of objects to return. Default is 10. Optional
sort_by Sort the records in descending order according to the Created or Modified Time. Possible values are: Created Time, Modified Time. Default is Modified Time. Optional

Context Output

There is no context output for this command.

Command example

!threatstream-feed-get-indicators indicator_type="domain" limit="5" sort_by="Created Time"

Human Readable Output

Indicators from Anomali ThreatStream Feed

Source ThreatStreamID Domain Modified Confidence Creation Tags TrafficLightProtocol
Demisto 440576095 my.domainnn_test.com 2023-12-24T00:00:05.890Z 50 2023-06-20T08:07:33.841Z values: tag3452, tag23452  
Demisto 440126275 my.domain_987.com 2023-12-24T00:00:05.877Z 50 2023-06-19T12:14:52.216Z values: tag3452, tag23452  
Demisto 439658732 my.domain1357.com 2023-09-16T10:10:05.788Z 50 2023-06-18T10:02:07.876Z    
dummydomain.com 284008208 test_domain_121.com 2025-04-05T01:48:33.997Z 0 2021-11-16T09:40:10.407Z values: tag4567 amber
Analyst 231953546 abc_test_domain1.com 2023-07-17T09:55:54.228Z 60 2021-04-06T09:36:09.122Z values: tag1356  

Configuration parameters

  • feed — Fetch indicators
  • fetchBy — Fetch by (required)
  • url — Server URL (e.g., https://www.test.com) (required)
  • credentials — Username (required)
  • feedFetchInterval — Feed Fetch Interval
  • confidenceThreshold — Confidence Threshold
  • feedReliability — Source Reliability (required)
  • tlp_color — Traffic Light Protocol Color
  • feedReputation — Indicator Reputation
  • feedExpirationPolicy
  • createRelationships — Create relationships
  • insecure — Trust any certificate (not secure)

Commands (1)

  • threatstream-feed-get-indicators

    Gets indicators from the feed. This command is mainly used for testing and debugging purposes.

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401
from typing import Any

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """
DEFAULT_MALICIOUS_THRESHOLD = 65
DEFAULT_SUSPICIOUS_THRESHOLD = 25
DEFAULT_BENIGN_THRESHOLD = 0
THREAT_STREAM = "Anomali ThreatStream Feed"
RETRY_COUNT = 2
LIMIT_RES_FROM_API = 1000
INDICATOR_STATUS = "active"
URL_SUFFIX = "v2/intelligence"
DEFAULT_CONFIDENCE_THRESHOLD = 65
DEFAULT_FEED_FETCH_INTERVAL = 240
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

INDICATOR_TYPE = {
    "domain": FeedIndicatorType.Domain,
    "ip": FeedIndicatorType.IP,
    "md5": FeedIndicatorType.File,
    "url": FeedIndicatorType.URL,
    "email": FeedIndicatorType.Email,
}

INDICATOR_TYPE_UPPER = {
    "domain": "Domain",
    "ip": "IP",
    "md5": "MD5",
    "url": "URL",
    "email": "Email",
}

RELATIONSHIPS_MAPPING = {
    "ip": [
        {"name": EntityRelationship.Relationships.RESOLVES_TO, "raw_field": "rdns", "entity_b_type": FeedIndicatorType.Domain},
        {"name": EntityRelationship.Relationships.INDICATOR_OF, "raw_field": "meta.maltype", "entity_b_type": "Malware"},
    ],
    "domain": [
        {"name": EntityRelationship.Relationships.RESOLVED_FROM, "raw_field": "ip", "entity_b_type": FeedIndicatorType.IP},
        {"name": EntityRelationship.Relationships.INDICATOR_OF, "raw_field": "meta.maltype", "entity_b_type": "Malware"},
    ],
    "url": [
        {"name": EntityRelationship.Relationships.RESOLVED_FROM, "raw_field": "ip", "entity_b_type": FeedIndicatorType.IP},
        {"name": EntityRelationship.Relationships.INDICATOR_OF, "raw_field": "meta.maltype", "entity_b_type": "Malware"},
    ],
    "md5": [{"name": EntityRelationship.Relationships.INDICATOR_OF, "raw_field": "meta.maltype", "entity_b_type": "Malware"}],
    "email": [{"name": EntityRelationship.Relationships.INDICATOR_OF, "raw_field": "meta.maltype", "entity_b_type": "Malware"}],
}

""" CLIENT CLASS """


class Client(BaseClient):
    """
    Client to use in the Anomali ThreatStream Feed integration. Overrides BaseClient
    """

    def __init__(self, base_url, user_name, api_key, verify):
        super().__init__(base_url=base_url, verify=verify, ok_codes=(200, 201, 202))
        self.credentials = {
            "Authorization": f"apikey {user_name}:{api_key}",
        }

    def http_request(
        self,
        method,
        url_suffix,
        params=None,
        data=None,
        headers=None,
        files=None,
        json=None,
        without_credentials=False,
        resp_type="json",
    ):
        """
        A wrapper for requests lib to send our requests and handle requests and responses better.
        """
        headers = headers or {}
        if not without_credentials:
            headers.update(self.credentials)
        res = super()._http_request(
            method=method,
            url_suffix=url_suffix,
            headers=headers,
            params=params,
            data=data,
            json_data=json,
            files=files,
            resp_type=resp_type,
            error_handler=self.error_handler,
            retries=RETRY_COUNT,
        )
        return res

    def error_handler(self, res: requests.Response):  # pragma: no cover
        """
        Error handler to call by super()._http_request in case an error was occurred.
        Handles specific HTTP status codes and raises a DemistoException.

        Args:
            res (requests.Response): The HTTP response object.
        """
        # Handle error responses gracefully
        if res.status_code == 401:
            raise DemistoException(f"{THREAT_STREAM} - Got unauthorized from the server. Check the credentials. {res.text}")
        elif res.status_code == 204:
            return
        elif res.status_code == 404:
            raise DemistoException(f"{THREAT_STREAM} - The resource was not found. {res.text}")
        raise DemistoException(f"{THREAT_STREAM} - Error in API call {res.status_code} - {res.text}")


class DBotScoreCalculator:
    """
    Class for DBot score calculation based on thresholds and confidence.
    It supports instance-defined thresholds per indicator type or default thresholds.
    """

    def calculate_score(self, indicator):
        """
        Calculates the DBot score according to the indicator's confidence.

        Args:
            indicator (dict[str, Any]): The raw indicator dictionary from the API response.

        Returns:
            int: The calculated DBot score (Common.DBotScore.NONE, GOOD, SUSPICIOUS, BAD).
        """
        confidence = arg_to_number(indicator.get("confidence", None))
        if not confidence:
            demisto.debug(f"{THREAT_STREAM} - Confidence not found for indicator. Assigning default score.")
            return Common.DBotScore.NONE

        else:
            if confidence > DEFAULT_MALICIOUS_THRESHOLD:
                return Common.DBotScore.BAD
            if confidence > DEFAULT_SUSPICIOUS_THRESHOLD:
                return Common.DBotScore.SUSPICIOUS
            if confidence > DEFAULT_BENIGN_THRESHOLD:
                return Common.DBotScore.GOOD
            else:
                return Common.DBotScore.NONE


def test_module(client: Client) -> str:
    """
    Tests API connectivity and authentication.

    Args:
        client (Client): The client object to use for API requests.

    Returns:
        str: 'ok' if the test passed, otherwise raises an exception.
    """

    client.http_request("GET", f"{URL_SUFFIX}/", params={"limit": 1})
    return "ok"


def handle_get_pagination(client: Client, initial_response: dict[str, Any], initial_limit: int) -> list[dict[str, Any]]:
    """
    Handles pagination for retrieving indicators from the feed.

    Args:
        client (Client): The client object to use for API requests.
        initial_response (dict[str, Any]): The initial API response containing indicators and pagination info.
        initial_limit (int): The initial limit set for the API request.

    Returns:
        list[dict[str, Any]]: A list of all fetched indicators, including those from subsequent pages.
    """
    indicators_raw: list[dict[str, Any]] = initial_response.get("objects", [])
    if not indicators_raw:
        return []

    remaining_limit = initial_limit - LIMIT_RES_FROM_API
    next_page = initial_response.get("meta", {}).get("next")

    while next_page and remaining_limit > 0:  # Loop continues as long as there's a next page link and the limit hasn't reached
        try:
            if next_page.startswith("/api/"):
                # /api/ is removed here as it appears in the base_url of the client.
                url_suffix_for_next_page = next_page[len("/api/") :]
            else:
                url_suffix_for_next_page = next_page  # Use as is if it's already relative

            url_suffix_for_next_page = url_suffix_for_next_page.replace("limit=1000", f"limit={remaining_limit}")

            demisto.debug(f"{THREAT_STREAM} - Fetching next page: {url_suffix_for_next_page}")
            res = client.http_request(method="GET", url_suffix=url_suffix_for_next_page)
            current_page_indicators = res.get("objects", [])

            if current_page_indicators:
                indicators_raw.extend(current_page_indicators)
            else:
                demisto.debug(f"{THREAT_STREAM} - No more indicators found on current page during pagination, breaking.")
                break

            remaining_limit = remaining_limit - LIMIT_RES_FROM_API
            next_page = res.get("meta", {}).get("next")

        except Exception as e:
            demisto.error(f"{THREAT_STREAM} - Error during pagination: {e}. Continuing with fetched indicators.")
            break  # Break pagination on error but process what's already fetched

    return indicators_raw


def get_indicators_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Wrapper for retrieving indicators from the feed to the war-room.
    This command is mainly used for testing and debugging purposes.

    Args:
        client (Client): The client object to use for API requests.
        args (dict[str, Any]): Arguments passed to the command (e.g., indicator_type, limit).

    Returns:
        CommandResults: An object containing the indicators' data.
    """

    indicator_type = args.get("indicator_type", "")
    limit = arg_to_number(args.get("limit")) if args.get("limit") else 10

    params: dict[str, Any] = {"limit": limit}
    if indicator_type:
        if indicator_type not in ["domain", "email", "ip", "md5", "url"]:
            demisto.error(f"{THREAT_STREAM} - Invalid indicator type.")
            return CommandResults(
                readable_output="""### Invalid indicator type. Select one of the following types: domain, email, ip, md5, url."""
            )
        else:
            params["type"] = indicator_type

    demisto.debug(f"{THREAT_STREAM} - Calling API to get indicators with params: {params}")
    res = client.http_request(method="GET", url_suffix=URL_SUFFIX, params=params)

    indicators_raw = handle_get_pagination(client, res, limit)  # type: ignore

    if not indicators_raw:
        demisto.debug(f"""{THREAT_STREAM} - No indicators found for the given criteria in the
                     'threatstream-feed-get-indicators' command.""")
        return CommandResults(readable_output="### No indicators were found.")

    parsed_indicators = parse_indicators_for_get_command(indicators_raw, sort_by=args.get("sort_by", "Modified Time"))
    demisto.debug(f"{THREAT_STREAM}-get-indicators command got {len(parsed_indicators)} indicators.")

    # Define headers dynamically based on whether an indicator type was specified
    headers = [
        "TargetIndustries",
        "Source",
        "ThreatStreamID",
        "Country Code",
        "Description",
        "Modified",
        "Organization",
        "Confidence",
        "Creation",
        "Expiration",
        "Tags",
        "TrafficLightProtocol",
        "Location",
        "ASN",
    ]

    if indicator_type:
        # Insert the dynamic indicator type header (e.g., "IP", "Domain") right after "Country Code"
        indicator_type = INDICATOR_TYPE_UPPER.get(indicator_type)
        headers.insert(headers.index("Country Code") + 1, indicator_type)

    human_readable = tableToMarkdown(
        name=f"Indicators from {THREAT_STREAM}:",
        t=parsed_indicators,
        headers=headers,
        removeNull=True,
        is_auto_json_transform=True,
    )

    return CommandResults(readable_output=human_readable, raw_response=indicators_raw)


def extract_tag_names(indicator: dict[str, Any]) -> list[str]:
    """
    Extracts the 'name' values from the 'tags' list within an indicator dictionary.

    This function safely handles cases where the 'tags' key might be missing or its
    value might be None, returning an empty list in such scenarios.

    Args:
        indicator (Dict[str, Any]): A dictionary representing an indicator, which may
                                     contain a 'tags' key with a list of tag dictionaries.

    Returns:
        List[str]: A list of tag names (strings) found in the 'tags' list.
                   Returns an empty list if 'tags' is not present, is None, or
                   contains no dictionaries with a 'name' key.
    """
    tags = indicator.get("tags")
    if tags is None:
        return []

    # Ensure tags is a list before attempting to iterate
    if not isinstance(tags, list):
        # This case handles if 'tags' exists but is not a list (e.g., a string, int, etc.)
        return []

    names = []
    for tag in tags:
        if isinstance(tag, dict) and "name" in tag:
            names.append(tag["name"])
    return names


def parse_indicators_for_get_command(indicators, sort_by: str = "Modified Time") -> list[dict[str, Any]]:
    """
    Parses a list of raw indicators from the API response into a format suitable for the War Room.
    The returned list is ordered in descending order by the specified timestamp.

    Args:
        indicators (list[dict[str, Any]]): List of raw indicator dictionaries from API response.
        sort_by (str): The field to sort the indicators by. Can be "Created" or "Modified". Defaults to "Modified".

    Returns:
        list[dict[str, Any]]: List of indicators formatted for War Room display,
    """
    res: list[dict[str, Any]] = []
    for indicator in indicators:
        indicator_type = INDICATOR_TYPE_UPPER.get(indicator.get("type"))  # e.g., "IP", "Domain", "Email"
        indicator_value = indicator.get("value")  # The actual value of the indicator

        dynamic_field: dict[str, Any] = {}
        if indicator_type and indicator_value:
            dynamic_field[indicator_type] = indicator_value  # Map "ip": "1.1.1.1" or "domain": "example.com"

        res.append(
            assign_params(
                TargetIndustries=indicator.get("target_industry"),
                Source=indicator.get("source"),
                ThreatStreamID=str(indicator.get("id")),
                CountryCode=indicator.get("country"),
                **dynamic_field,  # Unpack the dynamic field into the params
                Description=indicator.get("description"),
                Modified=indicator.get("modified_ts"),
                Organization=indicator.get("org"),
                Confidence=str(indicator.get("confidence")),
                Creation=indicator.get("created_ts"),
                Expiration=indicator.get("expires_ts"),
                Tags=extract_tag_names(indicator),
                TrafficLightProtocol=indicator.get("tlp"),
                Location=indicator.get("locations"),
                ASN=indicator.get("asn"),
            )
        )

    if sort_by == "Created Time":
        res.sort(key=lambda x: x.get("Creation") or "", reverse=True)
    else:  # sort_by == "Modified Time":
        res.sort(key=lambda x: x.get("Modified") or "", reverse=True)
    return res


def get_current_utc_time():
    """
    Returns the current UTC time as an aware datetime object.

    Returns:
        datetime: The current time in UTC with timezone information.
    """
    return datetime.now(timezone.utc)


def get_past_time(minutes_interval):
    """
    Calculates the time that is now minus the given time interval in minutes,
    and returns it in ISO 8601 format with milliseconds.

    Args:
        minutes_interval (int): The time interval in minutes to go back.

    Returns:
        str: The calculated past time in 'YYYY-MM-DDTHH:MM:SS.sss' format. i.e., 2023-08-01T11:57:00.080
    """
    now = get_current_utc_time()
    past_time = now - timedelta(minutes=minutes_interval)
    return past_time.replace(tzinfo=None).isoformat(timespec="milliseconds")


def handle_fetch_pagination(client: Client, initial_response: dict[str, Any]) -> list[dict[str, Any]]:
    """
    Handles fetching all indicators from an API feed that supports pagination.

    This function takes the initial API response and iteratively fetches subsequent pages
    until no more 'next' pages are indicated or an error occurs during pagination.

    Args:
        client (Client): The client object configured for API requests.
        initial_response (dict[str, Any]): The response from the initial API call.
                                            Expected to contain 'objects' (list of indicators)
                                            and 'meta' (with a 'next' key for pagination).

    Returns:
        list[dict[str, Any]]: A consolidated list of all raw indicators fetched from
                              the initial response and all subsequent paginated responses.
    """
    all_raw_indicators: list[dict[str, Any]] = initial_response.get("objects", [])
    next_page = initial_response.get("meta", {}).get("next")

    while next_page:  # Loop continues as long as there's a next page link
        try:
            # Determine the URL suffix for the next page
            if next_page.startswith("/api/"):
                # /api/ is removed here as it appears in the base_url of the client.
                url_suffix_for_next_page = next_page[len("/api/") :]
            else:
                url_suffix_for_next_page = next_page  # Use as is if it's already relative

            demisto.debug(f"{THREAT_STREAM} - Fetching next page: {url_suffix_for_next_page}")
            response = client.http_request(method="GET", url_suffix=url_suffix_for_next_page)
            current_page_indicators = response.get("objects", [])

            if current_page_indicators:
                all_raw_indicators.extend(current_page_indicators)
            else:
                demisto.debug(f"{THREAT_STREAM} - No more indicators found on current page during pagination, breaking.")
                break

            next_page = response.get("meta", {}).get("next")  # Get the 'next' link for the subsequent page
        except Exception as e:
            # Log the error and break the pagination, but keep already fetched indicators
            demisto.error(f"{THREAT_STREAM} - Error during pagination: {e}. Continuing with fetched indicators.")
            break  # Break pagination on error but process what's already fetched

    return all_raw_indicators


def fetch_indicators_command(
    client: Client, params: dict[str, Any], last_run: dict[str, Any]
) -> tuple[str, list[dict[str, Any]]]:
    """
    Wrapper for fetching indicators from the feed to the Threat Intel Management (TIM) in Cortex XSOAR.
    This function handles pagination and updates the last run time.

    Args:
        client (Client): The client object to use for API requests.
        params (dict[str, Any]): Integration parameters.
        last_run (dict[str, Any]): The last run object from Demisto, used for incremental fetching.

    Returns:
        Tuple[str, list[dict[str, Any]]]: A tuple containing:
            - str: The timestamp for the next successful run.
            - list[dict[str, Any]]: A list of parsed indicators ready for Cortex XSOAR.
    """
    create_relationship = argToBoolean(params.get("createRelationships", True))
    tlp_color = params.get("tlp_color", "WHITE")
    reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(params.get("feedReliability", DBotScoreReliability.C))
    now = get_current_utc_time()
    order_by = params.get("fetchBy", "Modified Time")
    if order_by == "Created Time":
        order_by = "created_ts"
    else:  # order_by == "Modified Time"
        order_by = "modified_ts"
    confidence_threshold = arg_to_number(params.get("confidenceThreshold", DEFAULT_CONFIDENCE_THRESHOLD))

    # Initialize last_fetch_time based on the last_run object.
    # If it's the first run, or last_run is empty, fetch from a default interval.
    last_fetch_time = last_run.get("last_successful_run")
    feed_fetch_interval = arg_to_number(params.get("feedFetchInterval", DEFAULT_FEED_FETCH_INTERVAL))

    if not last_fetch_time:
        demisto.info(
            f"{THREAT_STREAM} - First fetch detected. Retrieving indicators from the last {feed_fetch_interval} minutes."
        )
        # For the very first run, fetch from the past interval.
        last_fetch_time = get_past_time(feed_fetch_interval)
    else:
        demisto.info(f"{THREAT_STREAM} - Fetching indicators {order_by.replace('_ts', '')} since {last_fetch_time}.")

    query: dict[str, Any] = assign_params(
        limit=LIMIT_RES_FROM_API, status=INDICATOR_STATUS, order_by=order_by, confidence__gt=confidence_threshold
    )

    if order_by == "modified_ts":
        query["modified_ts__gte"] = last_fetch_time
    else:  # order_by == "created_ts"
        query["created_ts__gte"] = last_fetch_time

    demisto.debug(f"{THREAT_STREAM} - Initial API call for fetch-indicators with params: {query}")
    response = client.http_request(method="GET", url_suffix=URL_SUFFIX, params=query)
    all_raw_indicators: list[dict[str, Any]] = handle_fetch_pagination(client, response)

    if not all_raw_indicators:
        demisto.info(f"{THREAT_STREAM} - No new indicators found since last run or no indicators matching criteria.")
        # Update last_run even if no indicators are found, to avoid refetching the same period.
        return now.strftime(DATE_FORMAT), []

    demisto.debug(f"{THREAT_STREAM} - Total raw indicators fetched: {len(all_raw_indicators)}")
    parsed_indicators_list: list[dict[str, Any]] = []

    for indicator_raw in all_raw_indicators:
        try:
            # Parse each raw indicator and calculate its DBot score
            parsed_indicator = parse_indicator_for_fetch(indicator_raw, tlp_color, create_relationship, reliability)
            parsed_indicators_list.append(parsed_indicator)
        except Exception as e:
            demisto.error(f"{THREAT_STREAM} - Error parsing indicator ID {indicator_raw.get('id')}:{e}. Skipping this indicator.")
            continue  # Continue to the next indicator even if one fails

    demisto.debug(f"{THREAT_STREAM} - Successfully parsed {len(parsed_indicators_list)} indicators for fetch.")
    # Return the current UTC timestamp for the next successful run and the list of parsed indicators
    return now.strftime(DATE_FORMAT), parsed_indicators_list


def create_relationships(reliability: str, indicator: dict[str, Any]) -> list[dict[str, Any]]:
    """
    Generates a list of relationship objects for a given indicator.
    Handles single and multiple related entities.

    Args:
        reliability (str): The reliability score for the relationship source.
        indicator (dict[str, Any]): The raw indicator dictionary.

    Returns:
        list[dict[str, Any]]: A list of relationship dictionaries formatted for Cortex XSOAR.
    """
    relationships: list[dict[str, Any]] = []
    indicator_type = indicator.get("type")
    indicator_value = indicator.get("value")

    if not indicator_type or not indicator_value:
        demisto.debug(f"{THREAT_STREAM} - Skipping relationship creation for indicator with missing type or value: {indicator}")
        return relationships

    # Get the relationship specifications for the current indicator type
    relations_specs = RELATIONSHIPS_MAPPING.get(indicator_type)
    if relations_specs:
        for relation_spec in relations_specs:
            raw_field_name = relation_spec["raw_field"]
            entity_b_type = relation_spec["entity_b_type"]
            relationship_name = relation_spec["name"]
            # Retrieve the value(s) for the related entity from the raw indicator
            entity_b_values = demisto.get(indicator, raw_field_name)
            if entity_b_values:
                relationships.append(
                    EntityRelationship(
                        entity_a=indicator_value,
                        entity_a_type=INDICATOR_TYPE.get(indicator_type, "Unknown"),
                        name=relationship_name,
                        entity_b=entity_b_values,  # entity_b_item,
                        entity_b_type=entity_b_type,
                        source_reliability=reliability,
                        brand=THREAT_STREAM,
                    ).to_indicator()  # Convert the EntityRelationship object to a dictionary
                )
        demisto.debug(f"{THREAT_STREAM} - Relationship successfully created for indicator {indicator}")
    return relationships


def parse_indicator_for_fetch(
    indicator: dict[str, Any], tlp_color: str, create_relationship_param: bool, reliability: str
) -> dict[str, Any]:
    """
    Parses a single raw indicator from the API response into a format suitable for Cortex XSOAR's Indicators tab.
    This includes calculating DBot score and creating relationships.

    Args:
        indicator (dict[str, Any]): The raw data of the indicator from the API.
        tlp_color (str): The TLP color to assign to the indicator.
        create_relationship_param (bool): Flag to determine if relationships should be created.
        reliability (str): The reliability score for the indicator source.

    Returns:
        dict[str, Any]: An indicator dictionary formatted for Cortex XSOAR's `createIndicators` API.
    """
    # Calculate relationships first, as they depend on the raw indicator data
    if not create_relationship_param:
        demisto.debug(f"{THREAT_STREAM} - Skipping relationship creation for indicator {indicator}")
        relationships = []
    else:
        relationships = create_relationships(reliability, indicator)

    # indicator_type = indicator.get("type")  # e.g., "ip", "domain", "email", "md5", "url"
    indicator_type = INDICATOR_TYPE.get(str(indicator.get("type", "Unknown")))
    indicator_value = indicator.get("value")  # The actual value of the indicator (e.g., "1.1.1.1", "example.com")

    if not indicator_type or not indicator_value:
        raise ValueError(f"Indicator missing 'type' or 'value': {indicator}")

    dbot_score = DBotScoreCalculator().calculate_score(indicator)

    # Prepare dynamic field for the indicator value (e.g., {"ip": "1.1.1.1"})
    dynamic_field: dict[str, Any] = {}
    dynamic_field[indicator_type] = indicator_value

    # Assign common fields and dynamic fields
    fields = assign_params(
        TargetIndustries=indicator.get("target_industry"),
        Source=indicator.get("source"),
        ThreatStreamID=str(indicator.get("id")),
        CountryCode=indicator.get("country"),
        **dynamic_field,  # Unpack the dynamic field into the params for Cortex XSOAR
        Description=indicator.get("description"),
        Modified=indicator.get("modified_ts"),
        Organization=indicator.get("org"),
        Confidence=str(indicator.get("confidence")),
        Creation=indicator.get("created_ts"),
        Expiration=indicator.get("expires_ts"),
        Tags=extract_tag_names(indicator),
        TrafficLightProtocol=tlp_color,  # Use the configured TLP color
        Location=indicator.get("locations"),
        ASN=indicator.get("asn"),
    )

    return assign_params(
        value=indicator_value,
        type=indicator_type,
        fields=fields,
        relationships=relationships,
        rawJSON=indicator,
        score=dbot_score,
    )


def main():
    """
    Initiate integration command
    """
    command = demisto.command()
    demisto.debug(f"Command being called is {command}")

    params = demisto.params()

    # init credentials
    user_name = params.get("credentials", {}).get("identifier")
    api_key = params.get("credentials", {}).get("password")
    server_url = params.get("url", "").strip("/")

    try:
        client = Client(
            base_url=f"{server_url}/api/",
            user_name=user_name,
            api_key=api_key,
            verify=not params.get("insecure", False),
        )

        if command == "test-module":
            # This call is made when clicking the integration 'Test' button.
            return_results(test_module(client))

        elif command == "threatstream-feed-get-indicators":
            return_results(get_indicators_command(client, demisto.args()))

        elif command == "fetch-indicators":
            next_run, res = fetch_indicators_command(client, params, demisto.getLastRun())
            for b in batch(res, batch_size=2000):
                demisto.debug(f"{THREAT_STREAM} {b=}")
                demisto.createIndicators(b)
            demisto.setLastRun({"last_successful_run": next_run})
            demisto.info(f"{THREAT_STREAM} - Fetch-indicators completed. Next run will fetch from: {next_run}")

        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    except Exception as e:
        demisto.error(traceback.format_exc())  # Print the traceback stack
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()