ThreatFox Feed

ThreatFox is a free platform from abuse.ch with the goal of sharing indicators of compromise (IOCs) associated with malware. Use the ThreatFox Feed integration to fetch indicators from the feed.

Data Enrichment & Threat Intelligence · ThreatFox Feed · Feed

Details

IDThreatFox Feed
ProviderAbuseCh
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10399583
Supported ModulesAgentix XSIAM

README

ThreatFox is a platform from abuse.ch and Spamhaus dedicated to sharing indicators of compromise (IOCs) associated with malware, with the infosec community, AV vendors and cyber threat intelligence providers.
For more information visit: https://threatfox.abuse.ch/

Create an Auth Key for abuse.ch

Note: If you already have a profile, you can skip steps 1 and 2.

  1. Sign up for an abuse.ch account. You can do this easily by using an existing account that you may already have on X, LinkedIn, Google or Github. Just log in with the authentication provider of your choice here: https://auth.abuse.ch/

  2. Once you are authenticated on abuse.ch, ensure that you connect at least one additional authentication provider. This will ensure that you have access to abuse.ch platforms, even if one of the authentication providers you use shuts down (yes, it happened with Twitter!)

  3. Ensure that you hit the “Save profile” button. In the “Optional” section, you can now generate an “Auth-Key”. This is your personal Auth-Key that you can now use in the integration.

Configure ThreatFox Feed in Cortex

Parameter Description Required
Server URL   True
Auth Key Auth Key for authentication with abuse.ch True
Use system proxy settings   False
Trust any certificate (not secure)   False
Fetch indicators   False
Source Reliability Reliability of the source providing the intelligence data. True
Indicator Reputation Indicators from this integration instance will be marked with this reputation. False
Traffic Light Protocol Color The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. False
Indicator Expiration Method The method to be used to expire indicators from this feed. Default: indicatorType False
Bypass exclusion list When selected, the exclusion list is ignored for indicators from this feed. This means that if an indicator from this feed is on the exclusion list, the indicator might still be added to the system. False
Feed Fetch Interval (in days)   False
Return IOCs with Ports If selected, IP indicators will include a tag with the port value False
Confidence Threshold   False
Create relationship If selected, indicators will be created with relationships 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.

threatfox-get-indicators


Retrieves indicators from the ThreatFox API.

Base Command

threatfox-get-indicators

Input

Argument Name Description Required
search_term Indicator value to search for Optional
id Indicator ID to search for. Optional
hash Hash to search for. Optional
tag Tag to search by. For available tag options, please refer to the API documentation- https://threatfox.abuse.ch/api/. Optional
malware Malware to search by. For available malware options, please refer to the API documentation- https://threatfox.abuse.ch/api/. Optional
limit Maximum indicators to search for. Available only when searching by ‘malware’ or ‘tag’. Default is 50. Max is 1000. Optional

Context Output

There is no context output for this command.

Create an API Key

  1. Sign up for an abuse.ch account by using an existing account that you may already have on X, LinkedIn, Google or Github. Just log in with the authentication provider of your choice here: https://auth.abuse.ch/
  2. Once you’ve logged in to abuse.ch, add at least one more way to log in. This helps ensure you can always access abuse.ch platforms, even if one of your login methods stops working.
  3. Click the Save profile button. In the Optional section, you can now create an Auth-Key. This is your personal authentication key that you can use to query any abuse.ch APIs.

If you already have a profile, you only need to follow step 3. There’s nothing further to do for your authentication set up.

Configuration parameters

  • feed — Fetch indicators
  • url — Server URL (required)
  • credentials
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability (required)
  • confidence_threshold — Confidence Threshold
  • tlp_color — Traffic Light Protocol Color
  • feedExpirationPolicy
  • feedExpirationInterval
  • feedFetchInterval — Feed Fetch Interval
  • with_ports — Return IOCs with Ports
  • create_relationship — Create relationship
  • feedBypassExclusionList — Bypass exclusion list
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)
  • feedTags — Tags

Commands (1)

  • threatfox-get-indicators

    Retrieves indicators from the ThreatFox API. Choose one field to search by.

from typing import Any

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

from CommonServerUserPython import *  # noqa

# Disable insecure warnings
urllib3.disable_warnings()


""" CONSTANTS """

LOG_LINE = "THREAT FOX -"
LIMIT_SUPPORTED_QUERIES = ["tag", "malware"]


class Client(BaseClient):
    def get_indicators_request(self, query: dict) -> dict:
        url_suffix = "/api/v1"
        body = query
        return self._http_request("POST", url_suffix=url_suffix, json_data=body)

    def test_module(self) -> str:
        """Tests API connectivity and authentication'

        Returning 'ok' indicates that the integration works like it is supposed to.
        Connection to the service is successful.
        Raises exceptions if something goes wrong.

        :type client: ``Client``
        :param Client: client to use

        :return: 'ok' if test passed, anything else will fail the test.
        :rtype: ``str``
        """
        self.get_indicators_request({"days": 1, "limit": 5})
        return "ok"


def check_args_for_query(args: dict) -> tuple[bool, str | None]:
    """Validates that exactly one of these fields is provided:
        'search_term', 'id', 'hash', 'tag', or 'malware'.
    Args:
        args: dict
    Returns:
        Boolean: True if params are valid and False otherwise.
        Str: The query type (one of these: 'search_term', 'id', 'hash', 'tag', 'malware').
            If args are not valid than it will be None.
    """
    args_lst = list(args.keys())
    if "limit" in args_lst:
        args_lst.remove("limit")
    if len(args_lst) != 1:
        raise DemistoException("Arguments given are invalid. Please specify exactly one argument to search by.")
    else:
        return args_lst[0]


def create_query(
    query_arg,
    id: str | None = None,
    search_term: str | None = None,
    hash: str | None = None,
    tag: str | None = None,
    malware: str | None = None,
    days: str | None = None,
    limit: str | None = None,
) -> dict:
    """Creates a valid query to send to the API.

    Args:
        query_arg (str): the query type (should be one of those:
        'search_term', 'id', 'hash', 'tag', 'malware', 'days').

    Returns:
        Str: The query to send to the API.
    """

    query_dict = {"search_term": "search_ioc", "id": "ioc", "hash": "search_hash", "tag": "taginfo", "malware": "malwareinfo"}

    q_id = arg_to_number(id)

    query = assign_params(
        query=query_dict[query_arg],
        id=q_id,
        search_term=search_term,
        hash=hash,
        tag=tag,
        malware=malware,
    )

    # Only queries searching by tag or malware can specify a limit.
    if query_arg in LIMIT_SUPPORTED_QUERIES:
        q_limit = arg_to_number(limit) or 50
        if q_limit > 1000:
            demisto.debug("Limit higher than 1000. Getting first 1000 indicators.")
            q_limit = 1000
        query["limit"] = q_limit

    return query


def parse_indicators_for_get_command(indicators) -> List[dict[str, Any]]:
    """Parses the list of indicators returned from the api to indicators that can be returned to the war room.

    Args:
        indicators (list): list of indicators from api raw response.

    Returns:
        List[dict[str, Any]]: List of indicators that can be returned to the war room.
    """
    res = []
    indicators = [indicators] if type(indicators) is not list else indicators
    for indicator in indicators:
        res.append(
            assign_params(
                ID=indicator.get("id"),
                Value=get_value(indicator),
                Description=indicator.get("threat_type_desc"),
                MalwareFamilyTags=indicator.get("malware_printable")
                if indicator.get("malware_printable") != "Unknown malware"
                else None,
                AliasesTags=indicator.get("malware_alias"),
                FirstSeenBySource=indicator.get("first_seen"),
                LastSeenBySource=indicator.get("last_seen"),
                ReportedBy=indicator.get("reporter"),
                Tags=tags(indicator, with_ports=True),
                Confidence=str(indicator.get("confidence_level")),
                Publications=publications(indicator),
            )
        )
    return res


def indicator_type(indicator: dict) -> str:
    """Returns the Demisto ioc type according to 'ioc_type' field in the indicator"""
    type = indicator.get("ioc_type")
    if type == "domain":
        return FeedIndicatorType.FQDN
    elif type == "url":
        return FeedIndicatorType.URL
    elif type == "ip:port":
        return FeedIndicatorType.IP
    elif type == "envelope_from" or type == "body_from":
        return FeedIndicatorType.Email
    else:  # 'sha1_hash' 'sha256_hash' 'md5_hash'
        return FeedIndicatorType.File


def parse_indicator_for_fetch(
    indicator: dict, with_ports: bool, create_relationship: bool, tlp_color: str, feed_tags: list = []
) -> dict[str, Any]:
    """Parses the indicator given from the api to an indicator that can be sent to Threat Intel min XSOAR.

    Args:
        indicator (dict): The raw data of the indicator.
        with_ports (bool): whether to return the indicator with a tag representing it's port (relevant for ip indicators only).
        create_relationship (bool): Whether to create the indicator with relationships.
        tlp_color (str): The tlp color of the indicator

    Returns:
        dict[str, Any]: An indicator that can be sent to Threat Intel in XSOAR.
    """

    demisto_ioc_type = indicator_type(indicator)
    ioc_value = get_value(indicator)
    relationships = (
        create_relationships(ioc_value, indicator["ioc_type"], indicator.get("malware_printable"), demisto_ioc_type)
        if create_relationship
        else None
    )

    fields = assign_params(
        indicatoridentification=indicator.get("id"),
        description=indicator.get("threat_type_desc"),
        malwarefamily=indicator.get("malware_printable") if indicator.get("malware_printable") != "Unknown malware" else None,
        aliases=indicator.get("malware_alias"),
        firstseenbysource=to_date(indicator.get("first_seen")),
        lastseenbysource=to_date(indicator.get("last_seen")),
        reportedby=indicator.get("reporter"),
        Tags=tags(indicator, with_ports, feed_tags),
        publications=publications(indicator),
        confidence=indicator.get("confidence_level"),
        trafficlightprotocol=tlp_color,
    )

    return assign_params(value=ioc_value, type=demisto_ioc_type, fields=fields, relationships=relationships, rawJSON=indicator)


def publications(indicator: dict) -> Optional[List[dict[str, Any]]]:
    """creates the publications field for the indicator.

    Args:
        indicator (dict): The indicator.

    Returns:
        Optional[List[dict[str, Any]]]: The list of relevant publications for this indicator
        or None if the indicator has no publications.
    """
    if not indicator.get("reference"):
        return None
    malware_printable = indicator.get("malware_printable")

    return [
        {
            "link": indicator.get("reference"),
            "title": malware_printable if malware_printable and malware_printable != "Unknown malware" else "Malware",
            "source": "ThreatFox",
        }
    ]


def to_date(date) -> Optional[str]:
    """parses the date returned from raw response to a date in the right format for indicator fields in XSOAR."""
    if date:
        parsed_date = arg_to_datetime(date, required=False)
        if parsed_date:
            return parsed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
    return None


def tags(indicator: dict, with_ports: bool, feed_tags: list = []) -> List[str]:
    """Returns a list of tags to add to the indicator given

    Args:
        indicator (dict): The raw response of the indicator.
        with_ports (bool): whether to return the indicator with a tag representing it's port (relevant for ip indicators only).

    Returns:
        List[str]: List of tags to add to the indicator.
    """
    res = [
        indicator.get("malware_printable") if indicator.get("malware_printable") != "Unknown malware" else None,
        indicator.get("threat_type"),
    ]
    if indicator.get("tags"):
        res.extend(indicator["tags"])
    if indicator.get("malware_alias"):
        res.extend(indicator["malware_alias"].split(","))
    if with_ports and indicator.get("ioc_type") == "ip:port":
        res.append("port: " + indicator["ioc"].split(":")[1])
    if feed_tags:
        for feed_tag in feed_tags:
            res.append(feed_tag)

    res = [tag.lower() for tag in res if tag]

    # remove duplicate tags
    res = list(set(res))

    return res


def get_value(indicator) -> str:
    """Returns the value of the indicator.
    For example, when indicator is: {'id': '123', 'ioc': '1.1.1.1:80', 'ioc_type': 'ip:port'}
    then returned value will be '1.1.1.1'.
    When indicator is: {'id': '456', 'ioc': 'habbkj', 'ioc_type': 'sha1_hash'}
    then returned value will be 'habbkj'.
    """
    if indicator.get("ioc_type") == "ip:port":
        return indicator.get("ioc").split(":")[0]
    return indicator.get("ioc")


def create_relationships(value: str, type: str, related_malware: Optional[str], demisto_ioc_type: str) -> list:
    """Returns a list of relationships of the indicator.

    Args:
        value (str): The indicator value.
        type (str): The indicator type as given in the raw response.
        related_malware (Optional[str]): The malware related to the indicator to create a relationship to.
        demisto_ioc_type (str): The indicator type as a Demisto type.

    Returns:
        list: List of relationships.
    """

    if related_malware and related_malware != "Unknown malware":
        name = (
            EntityRelationship.Relationships.COMMUNICATED_BY
            if type == "domain" or type == "ip:port" or type == "url"
            else EntityRelationship.Relationships.RELATED_TO
        )
        reverse_name = (
            EntityRelationship.Relationships.COMMUNICATED_WITH
            if type == "domain" or type == "ip:port" or type == "url"
            else EntityRelationship.Relationships.RELATED_TO
        )
        return [
            EntityRelationship(
                entity_a=value,
                entity_a_type=demisto_ioc_type,
                name=name,
                entity_b=related_malware,
                entity_b_type=FeedIndicatorType.Malware,
                brand="ThreatFox Feed",
                reverse_name=reverse_name,
            ).to_indicator()
        ]
    return []


def validate_interval(interval: int) -> int:
    """Validates that the given interval is in days between 1 to 7,
    due to using the standard interval input type which supports minutes as well.

    Raises:
        DemistoException: If the interval is invalid.

    Returns:
        int: The interval, if it is valid.
    """
    if interval % 1440 != 0:  # 1440 is the number of minutes in a day
        raise DemistoException("The fetch interval must be in whole days, between 1-7.")
    elif interval > 10080:
        raise DemistoException("The fetch interval must not be more than 7 days.")
    return interval


def threatfox_get_indicators_command(client: Client, args: dict[str, Any]) -> CommandResults:
    search_term = args.get("search_term")
    id = args.get("id")
    hash = args.get("hash")
    tag = args.get("tag")
    malware = args.get("malware")
    limit = args.get("limit")

    query_type = check_args_for_query(args)

    query = create_query(query_type, id, search_term, hash, tag, malware, limit=limit)

    demisto.debug(f"{LOG_LINE} calling api with {query=}")
    try:
        result = client.get_indicators_request(query)
    except DemistoException as e:
        if "malware" in query and "502" in str(e):  # if illegal malware is provided an 502 error response returns
            raise DemistoException("Error in API call [502] - Bad Gateway. Make sure the malware you entered in valid")

    query_status = result.get("query_status")
    query_data = result.get("data")

    if query_status != "ok" and query_status:
        raise DemistoException(f"failed to run command, {query_status=}, {query_data=}")

    parsed_indicators = parse_indicators_for_get_command(result.get("data") or result)

    demisto.debug(f"{LOG_LINE} got {len(parsed_indicators)} indicators")

    human_readable = tableToMarkdown(
        name="Indicators",
        t=parsed_indicators,
        headers=[
            "ID",
            "Value",
            "Description",
            "MalwareFamilyTags",
            "AliasesTags",
            "FirstSeenBySource",
            "LastSeenBySource",
            "ReportedBy",
            "Tags",
            "Confidence",
            "Publications",
        ],
        removeNull=True,
        is_auto_json_transform=True,
    )

    return CommandResults(readable_output=human_readable)


def fetch_indicators_command(
    client: Client,
    with_ports: bool,
    confidence_threshold: int,
    create_relationship: bool,
    interval: int,
    tlp_color: str,
    last_run: dict,
    feed_tags: list = [],
):
    now = datetime.now(timezone.utc)
    days_for_query = int(interval / 1440)  # The interval is validated already in the main

    if last_run:
        last_successful_run = dateparser.parse(
            last_run["last_successful_run"], settings={"TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": True}
        )
        if last_successful_run:
            time_delta = now - last_successful_run
            days_for_query = time_delta.days + 1
        else:
            raise DemistoException("failed to fetch indicators")  # not supposed to happen

    # handling case of more than 7 days history, as the API fail longer-fetching queries.
    if days_for_query > 7:  # api can get up to 7 days
        days_for_query = 7

    response = client.get_indicators_request({"query": "get_iocs", "days": days_for_query})

    if response.get("query_status") != "ok":
        raise DemistoException(f"couldn't fetch, {response.get('query_status')}")

    indicators = response["data"]
    demisto.debug(f"{LOG_LINE} got {len(indicators)}")

    results = []

    for indicator in indicators:
        if indicator.get("ioc_type") == "sha3_384_hash":
            demisto.debug(f'{LOG_LINE} got indicator of indicator type "sha3" skipping it')
            continue
        if (arg_to_number(indicator.get("confidence_level")) or 75) < confidence_threshold:
            demisto.debug(f"{LOG_LINE} got indicator with low confidence level, skipping it")
            continue

        results.append(parse_indicator_for_fetch(indicator, with_ports, create_relationship, tlp_color, feed_tags))

    return now.strftime("%Y-%m-%dT%H:%M:%SZ"), results


""" MAIN FUNCTION """


def main() -> None:
    command = demisto.command()

    params = demisto.params()
    base_url = urljoin(params["url"], "/api/v1")
    with_ports = argToBoolean(params.get("with_ports") or False)
    confidence_threshold = arg_to_number(params.get("confidence_threshold")) or 75
    create_relationship = argToBoolean(params.get("create_relationship") or False)
    tlp_color = params.get("tlp_color") or "CLEAR"
    interval = validate_interval(arg_to_number(params.get("feedFetchInterval")) or 1440)
    feed_tags = argToList(params.get("feedTags"))

    auth_key = demisto.params().get("credentials", {}).get("password")

    demisto.debug(f"Command being called is {demisto.command()}")
    try:
        if not auth_key:
            raise ValueError("Missing required parameter Auth Key. Please set this parameter in the instance configuration.")

        client = Client(base_url=base_url, headers={"Auth-Key": auth_key})

        if command == "test-module":
            result = client.test_module()
            return_results(result)

        elif command == "threatfox-get-indicators":
            return_results(threatfox_get_indicators_command(client, demisto.args()))

        elif command == "fetch-indicators":
            next_run, res = fetch_indicators_command(
                client=client,
                with_ports=with_ports,
                confidence_threshold=confidence_threshold,
                create_relationship=create_relationship,
                interval=interval,
                tlp_color=tlp_color,
                last_run=demisto.getLastRun(),
                feed_tags=feed_tags,
            )
            for iter_ in batch(res, batch_size=2000):
                demisto.debug(f"{LOG_LINE} {iter_=}")
                demisto.createIndicators(iter_)
            demisto.setLastRun({"last_successful_run": next_run})

    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{e!s}")


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