FeedUstaThreatStream

Fetches indicators from the USTAv4 Threat Stream feed. The indicators can be of type malicious URLs or malware hashes.

Data Enrichment & Threat Intelligence · USTAv4 Cyber Threat Intelligence Platform · Feed

Details

IDFeedUstaThreatStream
ProviderAnomali
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

This integration fetches indicators from the USTAv4 Threat Stream feed. The indicators can be of type malicious URLs or malware hashes.
This integration was integrated and tested with version 4.1.0 of FeedUstaThreatStream.

Configure USTAv4 Threat Stream IOC Feed in Cortex

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for USTAv4 Threat Stream IOC Feed.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Fetch indicators   False
    Server’s URL   True
    API Key The API Key to use for connection True
    IOC Feed Type   True
    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
    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
    Trust any certificate (not secure)   False
    Use system proxy settings   False
        False
        False
    Tags Supports CSV values. False
  4. Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands from the Cortex XSOAR 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.

usta-tsa-search-malware-hash


Search malware hash indicators from the feed.

Base Command

usta-tsa-search-malware-hash

Input

Argument Name Description Required
limit The maximum number of results to return. Default is 10. Optional
hash The hash to search for. It can be a SHA-1, SHA-256, or MD5 hash. Required

Context Output

Path Type Description
USTA.ThreatStreamMalwareHashes.id String The ID of the alert
USTA.ThreatStreamMalwareHashes.hashes[0].sha256 String The SHA-256 hash of the malware
USTA.ThreatStreamMalwareHashes.hashes[0].md5 String The MD5 hash of the malware
USTA.ThreatStreamMalwareHashes.hashes[0].sha1 String The SHA-1 hash of the malware
USTA.ThreatStreamMalwareHashes.tags Array The tags of the malware
USTA.ThreatStreamMalwareHashes.created Date The creation date of the malware
USTA.ThreatStreamMalwareHashes.valid_from Date The valid from date of the malware
USTA.ThreatStreamMalwareHashes.valid_until Date The valid until date of the malware

Command Example

!usta-tsa-search-malware-hash hash=d5d8c33957e90d1caca4b5207d8da5ab1bc4caa9f702abc0ec006d0518ea9aec

Context Example

{
    "USTA" :{
        "ThreatStreamMalwareHashes":[
             {
                "id": "bf89614f-0ec8-4a88-a4e7-085b113a871b",
                "hashes": {
                    "sha256": "d5d8c33957e90d1caca4b5207d8da5ab1bc4caa9f702abc0ec006d0518ea9aec",
                    "sha1": "659661291eb5fd6452d6cabdc24cd9fbc1fb17f7",
                    "md5": "4a15ed0feb9e90b56e82c2e45a3b3f5e"
                },
                "tags": [
                    "SnakeKeylogger"
                ],
                "valid_from": "2024-11-22T07:30:07.000Z",
                "valid_until": "2025-11-22T07:30:07.000Z",
                "created": "2024-11-22T07:37:40.729Z"
            }
        ]
    }
}

usta-tsa-search-malicious-url


Search malicious URL indicators from the feed.

Base Command

usta-tsa-search-malicious-url

Input

Argument Name Description Required
limit The maximum number of results to return. Default is 10. Optional
url The URL to search for. Required

Context Output

Path Type Description
USTA.ThreatStreamMaliciousUrls.id String The ID of the alert
USTA.ThreatStreamMaliciousUrls.url String The URL of the malicious site
USTA.ThreatStreamMaliciousUrls.is_domain Boolean Whether the malicious site is a domain
USTA.ThreatStreamMaliciousUrls.ip_addresses Array The IP addresses of the malicious site
USTA.ThreatStreamMaliciousUrls.tags Array The tags of the malicious site
USTA.ThreatStreamMaliciousUrls.created Date The creation date of the malicious site
USTA.ThreatStreamMaliciousUrls.valid_from Date The valid from date of the malicious site
USTA.ThreatStreamMaliciousUrls.valid_until Date The valid until date of the malicious site

Command Example

!usta-tsa-search-malicious-url url=http://192.168.100.1:38082/i

Context Example


{
    "USTA" :{
        "ThreatStreamMaliciousUrls":[
             {
                "id": "28cffb9a-add5-480c-8968-539863695770",
                "url": "http://192.168.100.1:38082/i",
                "host": "192.168.100.1",
                "is_domain": false,
                "ip_addresses": [
                    "192.168.100.1"
                ],
                "tags": [
                    "elf.mozi"
                ],
                "valid_from": "2024-11-22T07:24:06.000Z",
                "valid_until": "2025-11-22T07:24:06.000Z",
                "created": "2024-11-22T08:30:03.055Z"
            }
        ]
    }
}

usta-tsa-search-phishing-site


Search malicious URL indicators from the feed.

Base Command

usta-tsa-search-phishing-site

Input

Argument Name Description Required
limit The maximum number of results to return. Default is 10. Optional
url The URL to search for. Required

Context Output

Path Type Description
USTA.ThreatStreamPhishingSites.id String The ID of the alert
USTA.ThreatStreamPhishingSites.url String The URL of the phishing site
USTA.ThreatStreamPhishingSites.is_domain Boolean Whether the phishing site is a domain
USTA.ThreatStreamPhishingSites.ip_addresses Array The IP addresses of the phishing site
USTA.ThreatStreamPhishingSites.created Date The creation date of the phishing site

Command Example

!usta-tsa-search-phishing-site url=example.com

Context Example

{
    "USTA" :{
        "ThreatStreamMaliciousUrls":[
            {
                "id": 219286,
                "url": "https://example.com",
                "host": "example.com",
                "is_domain": true,
                "ip_addresses": [
                    "192.168.100.1"
                ],
                "country": null,
                "created": "2024-02-05T15:23:11.646011Z"
            }
        ]
    }
}

Configuration parameters

  • feed — Fetch indicators
  • url — Server's URL (required)
  • api_key — API Key (required)
  • ioc_feed_type — IOC Feed Type (required)
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability (required)
  • tlp_color — Traffic Light Protocol Color
  • feedFetchInterval — Feed Fetch Interval
  • feedBypassExclusionList — Bypass exclusion list
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • feedExpirationPolicy
  • feedExpirationInterval
  • feedTags — Tags

Commands (3)

  • usta-tsa-search-malicious-url

    Search malicious URL indicators from the feed.

  • usta-tsa-search-malware-hash

    Search malware hash indicators from the feed.

  • usta-tsa-search-phishing-site

    Search malicious URL indicators from the feed.

from datetime import datetime, timedelta
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()

USTA_API_PREFIX = "api/threat-stream/v4/"

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

USTA_IOC_FEED_TYPES = ["malicious-urls", "malware-hashes", "phishing-sites"]

SERVICE_NAME = "FeedUstaThreatStream"

MAX_HISTORICAL_DAYS = 7


class Client(BaseClient):
    def __init__(self, base_url, verify, proxy, headers):
        super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers)

    def check_auth(self):
        self._http_request("GET", "company/me", error_handler=self._http_error_handler)

    def build_iterator(self, ioc_feed_type: str, start_time: str, limit: int = 0) -> list:
        params = assign_params(start=start_time, size=limit)
        res = self._http_request("GET", f"security-intelligence/ioc/{ioc_feed_type}", params=params, headers=self._headers)
        next_url = res.get("next", None)
        results = res.get("results", [])

        # Make sure limit is not exceeded on returned results
        if len(results) > limit:
            demisto.debug(f"Limit of {limit} exceeded. Truncating results.")
            return results[:limit]

        while next_url:
            res = self._http_request("GET", full_url=next_url, headers=self._headers)
            results += res.get("results", [])
            next_url = res.get("next", None)

        return results

    def search_iterator_without_pagination(self, ioc_feed_type: str, **kwargs) -> dict:
        params = assign_params(**kwargs)
        return self._http_request("GET", f"security-intelligence/ioc/{ioc_feed_type}", params=params, headers=self._headers)

    @staticmethod
    def _http_error_handler(response):
        # Handle error responses here to proper error messages to the user
        if response.status_code == 401:
            raise DemistoException("Authorization Error: make sure API Key is correctly set")
        if response.status_code == 429:
            raise DemistoException("Rate limit exceeded. Please try again later..!")


def check_module(client: Client):
    try:
        client.check_auth()
    except DemistoException as e:
        if "Connection Timeout Error" in str(e):
            return ValueError("Unable to connect to the USTA API! Make sure that your IP is whitelisted in the USTA.")
        raise e
    return "ok"


def parse_malware_hashes(indicator: dict) -> dict:
    _value = indicator.get("hashes", {}).get("sha256")
    _type = FeedIndicatorType.File

    parsed_indicator = {
        "value": _value,
        "type": _type,
        "rawJSON": indicator,
        "fields": {
            "md5": indicator.get("hashes", "{}").get("md5"),
            "sha1": indicator.get("hashes", {}).get("sha1"),
            "sha256": indicator.get("hashes", {}).get("sha256"),
            "tags": indicator.get("tags", []),
        },
    }
    parsed_indicator["fields"]["tags"].append("usta-malware-hashes")

    return parsed_indicator


def parse_malicious_urls(indicator: dict):
    _value = indicator.get("url")
    _type = FeedIndicatorType.URL

    # _value may contain only domain. Thus following function may return different indicator type.
    if new_type := auto_detect_indicator_type(_value):
        _type = new_type

    parsed_indicator = {
        "value": _value,
        "type": _type,
        "rawJSON": indicator,
        "fields": {
            "ip_addresses": indicator.get("ip_addresses", []),
            "host": indicator.get("host", ""),
            "tags": indicator.get("tags", []),
        },
    }
    parsed_indicator["fields"]["tags"].append("usta-malicious-urls")  # type: ignore
    return parsed_indicator


def parse_phishing_sites(indicator: dict):
    _value = indicator.get("url")
    _type = FeedIndicatorType.URL

    # _value may contain only domain. Thus following function may return different indicator type.
    if new_type := auto_detect_indicator_type(_value):
        _type = new_type

    parsed_indicator = {
        "value": _value,
        "type": _type,
        "rawJSON": indicator,
        "fields": {
            "country": indicator.get("country", ""),
            "ip_addresses": indicator.get("ip_addresses", []),
            "host": indicator.get("host", ""),
            "tags": indicator.get("tags", []),
        },
    }
    parsed_indicator["fields"]["tags"].append("usta-phishing-sites")  # type: ignore
    return parsed_indicator


def search_command(client: Client, args: dict, ioc_feed_type: str) -> CommandResults:
    limit = int(args.get("limit", 10))
    search_value = args.get("hash") if ioc_feed_type == "malware-hashes" else args.get("url")
    if results := client.search_iterator_without_pagination(ioc_feed_type=ioc_feed_type, search=search_value, size=limit):
        indicators = results.get("results", [])
        human_readable = tableToMarkdown(f"Indicators from USTA Feed ({ioc_feed_type}):", indicators)
        return CommandResults(
            readable_output=human_readable,
            outputs_prefix="",
            outputs_key_field="",
            raw_response=indicators,
            outputs={},
        )
    return CommandResults(readable_output="No results found.")


def fetch_indicators_command(client: Client, last_run: dict, params: Dict[str, Any]):  # -> tuple[dict, list[dict]]:
    """Fetches indicators from the chosen feed. The indicators are fetched based on the last fetch time we received
    from the last fetch. The indicators are then iterated over, and for each indicator, we create a dictionary with
    the indicator's value, type, and raw data. We then append this dictionary to a list of indicators.

    Args:
        client (Client): The HTTP client object.
        last_run (dict): A dictionary containing the last_fetch key with the last fetch time.
        params (Dict[str, Any]): The integration parameters.
    """
    feed_tags = argToList(params.get("feedTags", ""))
    tlp_color = params.get("tlp_color")
    ioc_feed_type = argToList(params.get("ioc_feed_type"))
    if "ALL" in ioc_feed_type:
        ioc_feed_type = USTA_IOC_FEED_TYPES

    all_indicators = {}

    parsed_indicators = []

    # Fetch indicators for each feed type
    for ioc_type in ioc_feed_type:
        start_time = (datetime.now() - timedelta(days=MAX_HISTORICAL_DAYS)).strftime("%Y-%m-%dT00:00:00")
        if last_fetch := last_run.get(ioc_type):
            start_time = last_fetch.get("created")

        indicators = client.build_iterator(ioc_feed_type=ioc_type, start_time=start_time, limit=100)

        if indicators:
            all_indicators[ioc_type] = indicators

        demisto.debug(f"Found {len(indicators)} indicators for feed type {ioc_type}")

    # process indicators and skip if indicator id in last_run
    for feed in all_indicators:
        indicators = all_indicators[feed]
        last_run_for_feed = last_run.get(feed, {})
        for indicator in indicators:
            # Skip if indicator is already saved ! deduplication
            if indicator.get("created") == last_run_for_feed.get("created"):
                demisto.debug(f"Skipping indicator {indicator.get('id')} as it was already fetched.")
                indicators.remove(indicator)
                continue
            # Initialize indicator_obj to avoid using it before assignment
            indicator_obj = {}

            # If type is malware-hashes, then we need to convert the hashes to hash type indicator of Cortex XSOAR
            if feed == "malware-hashes":
                indicator_obj = parse_malware_hashes(indicator)

            elif feed == "malicious-urls":
                indicator_obj = parse_malicious_urls(indicator)

            elif feed == "phishing-sites":
                indicator_obj = parse_phishing_sites(indicator)

            if feed_tags:
                indicator_obj["fields"]["tags"].extend(feed_tags)

            if tlp_color:
                indicator_obj["fields"]["trafficlightprotocol"] = tlp_color

            # Adding name of the service supplying this feed.
            indicator_obj["service"] = SERVICE_NAME

            # make sure tags are unique
            indicator_obj["fields"]["tags"] = list(set(indicator_obj["fields"]["tags"]))

            parsed_indicators.append(indicator_obj)

    # Update last_run with the latest indicator for each feed type
    for feed in all_indicators:
        if not all_indicators[feed]:
            continue

        latest_item = all_indicators[feed][0]
        last_run[feed] = {"created": latest_item.get("created"), "id": latest_item.get("id")}
    return last_run, parsed_indicators


def search_malware_hashes_command(client: Client, args: dict) -> CommandResults:
    return search_command(client, args, "malware-hashes")


def search_malicious_urls_command(client: Client, args: dict) -> CommandResults:
    return search_command(client, args, "malicious-urls")


def search_phishing_site_command(client: Client, args: dict) -> CommandResults:
    return search_command(client, args, "phishing-sites")


def main():
    # demisto params and args
    params: dict[str, Any] = demisto.params()
    args: dict[str, Any] = demisto.args()

    # Instance parameters
    verify_certificate: bool = not params.get("insecure", False)
    base_url = urljoin(params["url"], USTA_API_PREFIX)
    proxy = params.get("proxy", False)
    api_key = params.get("api_key")

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

    try:
        headers: dict = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

        client = Client(base_url=base_url, verify=verify_certificate, headers=headers, proxy=proxy)

        commands = {
            "usta-tsa-search-malware-hash": search_malware_hashes_command,
            "usta-tsa-search-malicious-url": search_malicious_urls_command,
            "usta-tsa-search-phishing-site": search_phishing_site_command,
        }

        if cmd == "test-module":
            return_results(check_module(client))

        elif cmd == "fetch-indicators":
            next_run, indicators = fetch_indicators_command(client=client, last_run=demisto.getLastRun(), params=params)
            demisto.debug(f"All fetching is done. Total found {len(indicators)} indicators.")
            for iter_ in batch(indicators, batch_size=2000):
                demisto.createIndicators(iter_)
            demisto.setLastRun(next_run)
        elif cmd in commands:
            return_results(commands[cmd](client, args))
        else:
            raise NotImplementedError(f"Command {cmd} is not implemented.")

    # Log exceptions and return errors
    except Exception as e:
        demisto.error(traceback.format_exc())  # Print the traceback
        return_error(f"Failed to execute {cmd} command.\nError:\n{e!s}")


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