USTA Account Takeover Prevention

Collects compromised credentials sourced from stealer malware attacks, helping organizations identify potential account takeovers and enhance their security posture. Provided by PRODAFT.

Data Enrichment & Threat Intelligence · USTAv4 Cyber Threat Intelligence Platform

Details

IDUSTA Account Takeover Prevention
ProviderPRODAFT
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

USTAv4 Account Takeover Prevention is designed to collect compromised credentials sourced from stealer malware attacks, helping organizations identify potential account takeovers and enhance their security posture. Provided by PRODAFT.
This integration was integrated and tested with version 4.1.0 of USTAv4 Account Takeover Prevention.

Configure USTAv4 Account Takeover Prevention in Cortex

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for USTAv4 Account Takeover Prevention.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Your server URL   True
    API Key The API Key to use for connection True
    Fetch incidents by status   False
    Trust any certificate (not secure)   False
    Use system proxy settings   False
    Fetch incidents   False
    First Fetch Time The time range to consider for the initial data fetch. Warning: Fetching a large time range may cause performance issues! True
  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-atp-search-username


Search for compromised credentials by username

Base Command

usta-atp-search-username

Input

Argument Name Description Required
username Username to search. Required
page_size Number of result that should appear on each page. Optional
page 1-indexed page number to get a particular page of results. Optional

Context Output

Path Type Description
USTA.AccountTakeoverPrevention.id Number The ID of the alert
USTA.AccountTakeoverPrevention.username String The username of the compromised credential
USTA.AccountTakeoverPrevention.password String The password of the compromised credential
USTA.AccountTakeoverPrevention.url String The URL of the compromised credential
USTA.AccountTakeoverPrevention.is_corporate Boolean Whether the compromised credential is corporate
USTA.AccountTakeoverPrevention.created String The creation date of the compromised credential
USTA.AccountTakeoverPrevention.victim_detail.ip String The IP address of the victim
USTA.AccountTakeoverPrevention.victim_detail.country String The country of the victim
USTA.AccountTakeoverPrevention.victim_detail.phone_number String The phone number of the victim
USTA.AccountTakeoverPrevention.victim_detail.computer_name String The computer name of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.victim_os String The OS of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.language String The language of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.memory String The memory of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.cpu String The CPU of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.gpu String The GPU of the victim computer
USTA.AccountTakeoverPrevention.victim_detail.malware String The family of the malware that infected the victim computer
USTA.AccountTakeoverPrevention.victim_detail.infection_date String The infection date of the victim computer

Command Example

!usta-atp-search-username username=user123456 page_size=1 page=1

Context Example

{
    "USTA" : {
        "AccountTakeoverPrevention": [
            {
                "id": 1234567,
                "status": "open",
                "username": "user123456",
                "password": "******",
                
                "url": "https://example.com/login",
                "is_corporate": "False",                
                "created": "2024-11-18T00:00:00.000000Z",
                "victim_detail": {
                    "username": "anonymous",
                    "ip": "0.0.0.0",
                    "country": "Unknown",
                    "phone_number": "N/A",
                    "computer_name": "DESKTOP-XXXXX",
                    "victim_os": "OS x64",
                    "language": "N/A",
                    "memory": "XXXX MB",
                    "cpu": "Generic CPU",
                    "gpu": "Generic GPU",
                    "malware": "Unknown",
                    "infection_date": "N/A",
                    "created": "2024-11-18T00:00:00.000000Z"
                }
            }
        ]
    }
}

Configuration parameters

  • url — Your server URL (required)
  • api_key — API Key (required)
  • status — Fetch incidents by status
  • max_fetch — Maximum number of alerts per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type
  • first_fetch — First Fetch Time (required)

Commands (1)

  • usta-atp-search-username

    Search for compromised credentials by username.

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_TICKET_STATUSES = {
    "all": None,
    "in_progress": "in_progress",
    "open": "open",
    "closed": "closed",
    "out of scope": "out_of_scope",
    "passive": "passive",
}

MAX_ALERTS_TO_FETCH = 100


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 compromised_credentials_api_request(self, **kwargs) -> list:
        params = assign_params(**kwargs)
        headers = self._headers

        demisto.debug(f"compromised_credentials_api_request: {params}")

        response = self._http_request(
            "GET",
            "security-intelligence/account-takeover-prevention/compromised-credentials-tickets",
            params=params,
            headers=headers,
        )
        count = response.get("count", 0)
        next_url = response.get("next", None)
        results = response.get("results", [])

        demisto.debug(f"compromised_credentials_api_request: Fetched {count} results")

        while next_url:
            demisto.debug(f"compromised_credentials_api_request: Fetching next page: {next_url}")
            response_next = self._http_request("GET", full_url=next_url, headers=headers)
            results += response_next.get("results", [])
            next_url = response_next.get("next", None)
        return results

    def compromised_credentials_search_api_request(self, **kwargs) -> dict:
        params = assign_params(**kwargs)
        headers = self._headers
        demisto.debug(f"compromised_credentials_search_api_request: {params}")
        return self._http_request(
            "GET",
            "security-intelligence/account-takeover-prevention/compromised-credentials-tickets",
            params=params,
            headers=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 convert_to_demisto_severity(severity: str) -> int:
    return {
        "low": IncidentSeverity.LOW,
        "medium": IncidentSeverity.MEDIUM,
        "high": IncidentSeverity.HIGH,
        "critical": IncidentSeverity.CRITICAL,
        "unknown": IncidentSeverity.UNKNOWN,
    }[severity]


def create_paging_header(results_num: int, page: int, size: int) -> str:
    header = f"Showing {results_num} results"
    if size is not None:
        header += f", Size={size}"
    if page is not None:
        header += f", from Page {page}"
    return header + "\n"


def fetch_incidents(
    client: Client, max_results: int, last_run: dict, first_fetch_time: str, status: Union[str, None] = None
) -> tuple[dict, list[dict]]:
    """Fetches the account takeover prevention module incidents using the USTA API. If the last_run is empty, fetch incidents
    from the given first_fetch_time. Otherwise, fetch incidents from the last run.

    Args:
        client (Client): USTA Account Takeover Prevention HTTP client.
        status (Union[int, None]): The status of the ticket to fetch. If None, fetch all tickets.
        first_fetch_time (str): The first fetch time to fetch incidents from.
    """
    if last_fetch := last_run.get("last_fetch", None):
        first_fetch_time = last_fetch

    assert first_fetch_time

    last_ids: list[int] = last_run.get("last_ids", []) or []

    incidents: list[dict[str, Any]] = []

    alerts = client.compromised_credentials_api_request(status=status, start=first_fetch_time, size=max_results)
    demisto.debug(f"Received {len(alerts)} alerts from server.")

    # API returns the newest alerts first so instead of -1 we need to get the first alert's created time
    last_fetched_time = alerts[0]["created"] if alerts else last_fetch

    new_last_ids: list[int] = []

    for alert in alerts:
        # skip the alerts which are already fetched and it is always sorted by created field.
        if alert["created"] == last_fetched_time:
            new_last_ids.append(alert["id"])

        if alert["id"] in last_ids:
            demisto.debug(f"Skipping already fetched alert: {alert['id']}")
            continue

        # if "is_corporate" is True, then the alert is Compromised Employee Credentials
        # if "is_corporate" is False, then the alert is Compromised End-User Credentials

        alert_corporate_type = "Employee" if alert.get("is_corporate") else "End-User"
        severity = "critical" if alert_corporate_type == "Employee" else "medium"
        ticket_id = alert.get("id")

        incident = {
            "name": f"[{alert_corporate_type}] Compromised Credentials: USTA Ticket ID : {ticket_id}",
            "occurred": alert.get("created"),
            "severity": convert_to_demisto_severity(severity),
            "rawJSON": json.dumps(alert),
        }
        incidents.append(incident)

    demisto.debug(f"setting next run- {last_fetched_time=}")
    next_run = {"last_fetch": last_fetched_time, "last_ids": new_last_ids}

    return next_run, incidents


def compromised_credentials_search_command(client: Client, args: dict) -> CommandResults:
    username = args.get("username", None)
    size = args.get("page_size", None)
    page = args.get("page", None)

    if not username:
        raise ValueError("Please provide a username to search for.")

    if results := client.compromised_credentials_search_api_request(username=username, page=page, size=size):
        readable_output = create_paging_header(
            results_num=results.get("count", 0),
            page=page,
            size=size,
        ) + tableToMarkdown("Account Takeover Prevention", results.get("results", []))
        return CommandResults(
            outputs_prefix="USTA.AccountTakeoverPrevention",
            outputs_key_field="id",
            outputs=results,
            readable_output=readable_output,
        )
    return CommandResults(
        readable_output="No results found.",
        outputs={},
    )


def main() -> None:
    # 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()

    # How much time before the first fetch to retrieve alerts
    first_fetch_time = arg_to_datetime(arg=params.get("first_fetch", "3 days"), arg_name="First fetch time", required=True)
    assert first_fetch_time

    demisto.debug(f"Command being called is {demisto.command()}")
    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-atp-search-username": compromised_credentials_search_command,
        }

        if cmd == "test-module":
            return_results(check_module(client))
        elif cmd == "fetch-incidents":
            status = USTA_TICKET_STATUSES.get(params.get("status", "Open").lower())
            max_results = arg_to_number(arg=params.get("max_fetch"), arg_name="max_fetch", required=False)
            if not max_results or max_results > MAX_ALERTS_TO_FETCH:
                max_results = MAX_ALERTS_TO_FETCH

            next_run, incidents = fetch_incidents(
                client=client,
                max_results=max_results,
                last_run=demisto.getLastRun(),
                first_fetch_time=datetime.strftime(first_fetch_time, DATE_FORMAT),
                status=status,
            )
            demisto.incidents(incidents)
            demisto.setLastRun(next_run)
        elif cmd in commands:
            return_results(commands[cmd](client, args))

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


""" ENTRY POINT """


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