ThreatMon Threat Feed

Fetches Indicators of Compromise (IOCs) from the ThreatMon IOC platform and ingests them into Cortex as indicators.

Data Enrichment & Threat Intelligence · ThreatMon Threat Feed · Feed

Details

IDThreatMon Threat Feed
ProviderThreatMon
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM

README

Fetches Indicators of Compromise (IOCs) from the ThreatMon IOC platform and ingests them into Cortex as indicators.
This integration was integrated and tested with the ThreatMon IOC API.

Configure ThreatMon Threat Feed in Cortex

Parameter Description Required
Server URL The ThreatMon IOC API base URL. True
API Token The ThreatMon API token used to authenticate against the IOC API. True
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch indicators   False
Data Type to Fetch The type of IOC data to fetch from ThreatMon. False
Maximum number of indicators per fetch The maximum number of indicators to fetch in a single run. False
Collection IDs A comma-separated list of ThreatMon collection IDs to filter the fetch by. Leave empty to fetch from all collections. False
Indicator Reputation Indicators from this integration instance will be marked with this reputation. 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. False
Feed Fetch Interval   False
Tags Supports CSV values. 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

Fetched Indicators

The feed maps the ThreatMon ioc_type field to Cortex indicator types as follows.

ThreatMon ioc_type Cortex indicator type
Contains ip IP
Contains domain Domain
Contains url URL
Contains file or hash File

For IP indicators, the extracted_ip field is preferred over ioc_value, and any port suffix is stripped, so that 1.2.3.4:8080 is ingested as 1.2.3.4.

The following indicator fields are populated from the ThreatMon response.

Indicator field ThreatMon field
description A summary built from source, confidence level, severity, status, ISP, resolved IPs, categories, tags, timestamp and score
tags tags, categories and source, merged with the ThreatMon tag
modified updated_at, falling back to timestamp and then created_at
confidence confidence_level
threatseverity severity
status status
isp isp
resolvedips resolved_ips
geolocation geo_location
trafficlightprotocol The Traffic Light Protocol Color parameter

The feed is incremental. On every run it stores the newest indicator timestamp it has seen, and on subsequent runs it skips indicators that are not newer than that timestamp.

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.

threatmon-get-indicators


Gets a sample of indicators from the ThreatMon feed. This command is used mainly for testing and debugging, and does not create indicators in the system.

Base Command

threatmon-get-indicators

Input

Argument Name Description Required
limit The maximum number of indicators to return. Default is 10. Optional

Context Output

There is no context output for this command.

Indicators are also ingested automatically according to the Feed Fetch Interval parameter.

Troubleshooting

  • Test failed - Verify the API token and that the Server URL is reachable from the Cortex engine.
  • No indicators fetched - Verify that the configured Collection IDs contain data, and that Data Type to Fetch is not filtering out the indicators you expect. Because the feed is incremental, a run returns nothing when the API has not published indicators newer than the previous run.

<~PLATFORM>

License Requirements

The following configuration parameters require one of these licenses: Cortex XSIAM or Agentix:

  • Fetch indicators
  • Incremental Feed
  • Bypass exclusion list

</~PLATFORM>

Configuration parameters

  • url — Server URL (required)
  • credentials — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • feed — Fetch indicators
  • data_type — Data Type to Fetch
  • limit — Maximum number of indicators per fetch
  • collection_ids — Collection IDs
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability (required)
  • tlp_color — Traffic Light Protocol Color
  • feedIncremental — Incremental Feed
  • feedExpirationPolicy
  • feedExpirationInterval
  • feedFetchInterval — Feed Fetch Interval
  • feedBypassExclusionList — Bypass exclusion list
  • feedTags — Tags

Commands (1)

  • threatmon-get-indicators

    Gets a sample of indicators from the ThreatMon feed. This command is used mainly for testing and debugging, and does not create indicators in the system.

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

DEFAULT_BASE_URL = "https://ioc.threatmonit.io"
DEFAULT_LIMIT = 500
CREATE_INDICATORS_BATCH_SIZE = 2000
FEED_SOURCE_TAG = "ThreatMon"


class Client(BaseClient):
    """
    Client class to interact with the ThreatMon IOC API.
    """

    def __init__(self, base_url: str, api_token: str, verify: bool, proxy: bool):
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.api_token = api_token

    def get_daily_iocs(self, data_type: str = "all", size: int = DEFAULT_LIMIT, collection_ids: str = None) -> dict:
        """
        Fetches daily IOC details from the API.
        """
        params: dict[str, Any] = {"api_token": self.api_token, "data_type": data_type, "size": size}
        if collection_ids:
            params["collection_id"] = collection_ids

        return self._http_request(
            method="GET", url_suffix="/api/daily-ioc-details/", params=params, headers={"Accept": "application/json"}
        )


def strip_port(value: str) -> str:
    """
    Strips a trailing port from an indicator value while keeping IPv6 addresses intact.

    Handles the following forms:
    - ``[2001:db8::1]:443`` -> ``2001:db8::1``
    - ``1.2.3.4:8080``      -> ``1.2.3.4``
    - ``2001:db8::1``       -> ``2001:db8::1`` (unbracketed IPv6, left untouched)
    """
    if value.startswith("["):
        return value[1:].split("]", 1)[0]
    # Only strip when there is a single colon, i.e. an IPv4:port pair. Unbracketed
    # IPv6 addresses contain multiple colons and must not be split.
    if value.count(":") == 1:
        return value.split(":", 1)[0]
    return value


def map_indicator_type(ioc_type: str) -> str | None:
    """
    Maps a ThreatMon ioc_type to an XSOAR FeedIndicatorType.

    Returns ``None`` when the ioc_type is missing or not recognized, so the caller can
    skip the indicator instead of ingesting it with an invalid type.
    """
    if ioc_type:
        ioc_type = ioc_type.lower()
        if "ip" in ioc_type:
            return FeedIndicatorType.IP
        elif "domain" in ioc_type:
            return FeedIndicatorType.Domain
        elif "url" in ioc_type:
            return FeedIndicatorType.URL
        elif "file" in ioc_type or "hash" in ioc_type:
            return FeedIndicatorType.File
    return None


def parse_indicator(ioc: dict, feed_tags: list, tlp_color: str | None) -> dict | None:
    """
    Parses a ThreatMon IOC object into an XSOAR indicator.

    Returns ``None`` when the indicator has no value or its type cannot be determined,
    so the caller can skip it instead of failing the whole fetch.
    """
    value = ioc.get("ioc_value")
    ioc_type = ioc.get("ioc_type", "")

    mapped_type = map_indicator_type(ioc_type)
    if not value or not mapped_type:
        return None

    # Prefer the extracted IP for IP indicators, to avoid a port number in the value.
    extracted_ip = ioc.get("extracted_ip")

    if mapped_type == FeedIndicatorType.IP:
        value = extracted_ip or value
        if value:
            value = strip_port(value)

    source = ioc.get("source") or []
    tags = ioc.get("tags") or []
    categories = ioc.get("categories") or []

    # Combine tags from the API and from the feed settings, removing duplicates.
    combined_tags = list(dict.fromkeys([t for t in (tags + categories + source + feed_tags) if t]))

    # Construct the description showing all metadata.
    description_parts = []
    if source:
        description_parts.append(f"Source: {', '.join(source) if isinstance(source, list) else source}")
    if ioc.get("confidence_level"):
        description_parts.append(f"Confidence Level: {ioc.get('confidence_level')}")
    if ioc.get("severity"):
        description_parts.append(f"Severity: {ioc.get('severity')}")
    if ioc.get("status"):
        description_parts.append(f"Status: {ioc.get('status')}")
    if ioc.get("isp"):
        description_parts.append(f"ISP: {ioc.get('isp')}")

    resolved_ips = ioc.get("resolved_ips")
    if resolved_ips:
        description_parts.append(f"Resolved IPs: {', '.join(resolved_ips)}")
    if categories:
        description_parts.append(f"Categories: {', '.join(categories)}")
    if tags:
        description_parts.append(f"Tags: {', '.join(tags)}")
    if ioc.get("timestamp"):
        description_parts.append(f"Timestamp: {ioc.get('timestamp')}")
    if ioc.get("score") is not None:
        description_parts.append(f"API Score: {ioc.get('score')}")

    description = "\n".join(description_parts)

    fields = {
        "tags": combined_tags,
        "description": description,
        "modified": ioc.get("updated_at") or ioc.get("timestamp") or ioc.get("created_at"),
        "confidence": ioc.get("confidence_level"),
        "threatseverity": ioc.get("severity"),
        "status": ioc.get("status"),
        "isp": ioc.get("isp"),
        "resolvedips": resolved_ips,
        "geolocation": ioc.get("geo_location"),
        "threat_score": ioc.get("score"),
        "threatscore": ioc.get("score"),
        "extractedip": extracted_ip,
        "extracted_ip": extracted_ip,
    }

    if tlp_color:
        fields["trafficlightprotocol"] = tlp_color

    return {
        "value": value,
        "type": mapped_type,
        "service": FEED_SOURCE_TAG,
        "rawJSON": ioc,
        "fields": fields,
    }


def calculate_verdict(feed_reputation: str) -> int:
    """
    Calculates the XSOAR DBot score based on the feedReputation setting.
    """
    mapping = {"Unknown": 0, "None": 0, "Benign": 1, "Good": 1, "Suspicious": 2, "Malicious": 3, "Bad": 3}
    return mapping.get(feed_reputation, 3)


def build_indicators(client: Any, params: dict, limit: int, last_timestamp: str | None = None) -> tuple[list, str | None]:
    """
    Fetches IOCs from the API and builds XSOAR indicators.

    Returns the list of indicators and the newest timestamp seen, so both the fetch and
    the get-indicators commands can share the same building logic.
    """
    data_type = params.get("data_type") or "all"
    tlp_color = params.get("tlp_color")
    feed_reputation = params.get("feedReputation") or "Bad"
    collection_ids = params.get("collection_ids")
    feed_tags = [FEED_SOURCE_TAG] + argToList(params.get("feedTags"))

    response = client.get_daily_iocs(data_type=data_type, size=limit, collection_ids=collection_ids)
    iocs = response.get("iocs", []) or []

    indicators = []
    latest_timestamp = last_timestamp

    for ioc in iocs:
        # Track the newest timestamp to skip previously fetched IOCs.
        ioc_timestamp = ioc.get("created_at") or ioc.get("timestamp") or ioc.get("updated_at")

        if last_timestamp and ioc_timestamp and ioc_timestamp <= last_timestamp:
            continue

        parsed = parse_indicator(ioc, feed_tags=feed_tags, tlp_color=tlp_color)
        if parsed:
            parsed["score"] = calculate_verdict(feed_reputation)
            indicators.append(parsed)

        if ioc_timestamp and (not latest_timestamp or ioc_timestamp > latest_timestamp):
            latest_timestamp = ioc_timestamp

    return indicators, latest_timestamp


def test_module(client: Any, params: dict) -> str:
    """
    Tests API connectivity and that a returned IOC can be parsed into an indicator.
    """
    try:
        data_type = params.get("data_type") or "all"
        response = client.get_daily_iocs(data_type=data_type, size=1)
        iocs = response.get("iocs", []) or []
        if iocs:
            # Exercise the indicator builder to make sure the response shape is understood.
            parse_indicator(iocs[0], feed_tags=[FEED_SOURCE_TAG], tlp_color=params.get("tlp_color"))
        return "ok"
    except Exception as e:
        return f"Test failed: {str(e)}"


def fetch_indicators_command(
    client: Any, params: dict, limit: int, last_run: dict
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    """
    Fetches indicators from the ThreatMon API for the feed fetch interval.
    """
    new_last_run = last_run.copy() if last_run else {}
    last_timestamp = new_last_run.get("last_timestamp")

    try:
        indicators, latest_timestamp = build_indicators(client, params, limit, last_timestamp)
        if latest_timestamp:
            new_last_run["last_timestamp"] = latest_timestamp
    except Exception as e:
        demisto.error(f"Error fetching indicators from ThreatMon: {str(e)}\n{traceback.format_exc()}")
        raise e

    return indicators, new_last_run


def get_indicators_command(client: Any, params: dict, args: dict) -> CommandResults:
    """
    Retrieves a sample of indicators from the feed to the war room, for testing and debugging.
    Does not create indicators in the system.
    """
    limit = arg_to_number(args.get("limit")) or 10
    indicators, _ = build_indicators(client, params, limit)
    indicators = indicators[:limit]

    if not indicators:
        return CommandResults(readable_output="### No indicators were found.")

    table = [
        {
            "Value": indicator.get("value"),
            "Type": indicator.get("type"),
            "Score": indicator.get("score"),
            "Tags": ", ".join(indicator.get("fields", {}).get("tags", [])),
        }
        for indicator in indicators
    ]
    human_readable = tableToMarkdown(f"Indicators from {FEED_SOURCE_TAG}:", table, removeNull=True)

    return CommandResults(
        readable_output=human_readable,
        raw_response=[indicator.get("rawJSON") for indicator in indicators],
    )


def main():
    params = demisto.params()
    base_url = (params.get("url") or "").strip() or DEFAULT_BASE_URL

    api_token = params.get("api_token") or params.get("credentials", {}).get("password")
    if not api_token:
        # Fallback to the credentials identifier if the token was placed there.
        api_token = params.get("credentials", {}).get("identifier")

    if not api_token:
        return_error("Missing API Token (api_token).")

    verify_cert = not argToBoolean(params.get("insecure", False))
    proxy = argToBoolean(params.get("proxy", False))

    client = Client(base_url=base_url, api_token=api_token, verify=verify_cert, proxy=proxy)

    command = demisto.command()
    demisto.debug(f"Command being called is {command}")

    try:
        if command == "test-module":
            return_results(test_module(client, params))

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

        elif command == "fetch-indicators":
            last_run = demisto.getLastRun() or {}
            limit = arg_to_number(params.get("limit")) or arg_to_number(params.get("max_fetch")) or DEFAULT_LIMIT

            indicators, new_last_run = fetch_indicators_command(client, params, limit, last_run)

            # Send the indicators to XSOAR in batches to avoid overloading the server.
            for indicators_batch in batch(indicators, batch_size=CREATE_INDICATORS_BATCH_SIZE):
                demisto.createIndicators(indicators_batch)

            demisto.setLastRun(new_last_run)

        else:
            raise DemistoException(f"Unsupported command: {command}")

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


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