QintelQWatch

Qintel's QWatch system contains credentials obtained from dump sites, hacker collaboratives, and command and control infrastructures of eCrime- and APT-related malware. With this integration, users can fetch exposure alerts as incidents and discover exposed credentials associated with their organization.

Data Enrichment & Threat Intelligence · Qintel

Details

IDQintelQWatch
ProviderQintel
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Qintel’s QWatch system contains credentials obtained from dump sites, hacker collaboratives, and command and control infrastructures of eCrime- and APT-related malware. With this integration, users can fetch exposure alerts as incidents and discover exposed credentials associated with their organization.
This integration was integrated and tested with version 1.1.6 of QWatch

Configure QintelQWatch in Cortex

Parameter Required
QWatch API URL (optional) False
Qintel Credentials True
Password True
Trust any certificate (not secure) False
Use system proxy settings False
Fetch incidents False
Fetch plaintext passwords False
Limit number of records per fetch False
First fetch time False
Incidents Fetch Interval False
Default Incident Severity True

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.

qintel-qwatch-exposures


Search QWatch for exposed credentials

Base Command

qintel-qwatch-exposures

Input

Argument Name Description Required
email Email to search. Optional
domain Domain to search. Optional

Context Output

Path Type Description
Qintel.QWatch.Exposures String QWatch Exposure Records

Command Example

!qintel-qwatch-exposures email=test@example.local

Context Example

{
    "Qintel": {
        "QWatch": {
            "Exposures": [
                {
                    "email": "test@example.local",
                    "firstseen": "2020-03-25 09:38:40",
                    "lastseen": "2021-02-05 04:35:33",
                    "loaded": "2021-02-05 04:35:33",
                    "password": "SuperSecretPassword",
                    "source": "combo-BigComboList"
                },
                {
                    "email": "test@example.local",
                    "firstseen": "2020-03-25 09:38:40",
                    "lastseen": "2021-02-05 04:35:33",
                    "loaded": "2020-08-10 02:10:11",
                    "password": "SuperSecretPassword",
                    "source": "dump-example.local"
                },
                {
                    "email": "test@example.local",
                    "firstseen": "2020-03-25 09:38:40",
                    "lastseen": "2021-02-05 04:35:33",
                    "loaded": "2020-03-25 09:38:40",
                    "password": "SuperSecretPassword",
                    "source": "malware-evilbot_March_22_2020"
                }
            ]
        }
    }
}

Human Readable Output

Qintel QWatch exposures for: test@example.local

Email Password Source Loaded First Seen Last Seen
test@example.local SuperSecretPassword combo-BigComboList 2021-02-05 04:35:33 2020-03-25 09:38:40 2021-02-05 04:35:33
test@example.local SuperSecretPassword dump-example.local 2020-08-10 02:10:11 2020-03-25 09:38:40 2021-02-05 04:35:33
test@example.local SuperSecretPassword malware-evilbot_March_22_2020 2020-03-25 09:38:40 2020-03-25 09:38:40 2021-02-05 04:35:33

Configuration parameters

  • remote — QWatch API URL (optional)
  • credentials — Qintel Credentials (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • fetch_passwords — Fetch plaintext passwords
  • max_fetch — Limit number of records per fetch
  • first_fetch — First fetch time
  • incidentFetchInterval — Incidents Fetch Interval
  • fetch_severity — Default Incident Severity (required)
  • incidentType — Incident type

Commands (1)

  • qintel-qwatch-exposures

    Search QWatch for exposed credentials.

import demistomock as demisto
from CommonServerPython import *  # noqa # pylint: disable=unused-wildcard-import

""" IMPORTS """

import traceback
from datetime import datetime
from typing import Any

import urllib3
from dateparser import parse as parse_date
from dateutil.parser import parse as parse_dt
from pytz import utc as pyutc

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """
DATE_FORMAT = "%Y-%m-%d %I:%M:%S"
ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
VERSION = "v1.0.0"
USER_AGENT = f"Qintel-CortexXSOAR/{VERSION}"
VENDOR = "Qintel"

QWATCH_REMOTE = "https://api.qwatch.qintel.com"

MAX_START_TIME = "90 days"
INCIDENT_NAME = "Qintel QWatch Alert"
INCIDENT_TYPE = "Qintel - QWatch Alert"

EXPOSURE_FIELDS = {
    "email": "attributes.login_name",
    "password": "attributes.password",
    "source": "attributes.source_name",
}

EXPOSURE_HR_MAP = {
    "Email": "email",
    "Password": "password",
    "Source": "source",
    "Loaded": "loaded",
    "First Seen": "firstseen",
    "Last Seen": "lastseen",
}


class Client(BaseClient):
    """Client class to interact with Qintel APIs"""

    def __init__(self, base_url, verify=True, proxy=False, **kwargs):
        super().__init__(base_url, verify=verify, proxy=proxy)

        self._headers = {
            "User-Agent": USER_AGENT,
            "Cf-Access-Client-Id": kwargs.get("client_id"),
            "Cf-Access-Client-Secret": kwargs.get("client_secret"),
        }

    def search(self, endpoint: str, params: dict) -> dict[str, Any]:
        return self._http_request(method="GET", url_suffix=endpoint, params=params, backoff_factor=0.5, retries=5)

    def ping(self) -> dict[str, Any]:
        return self._http_request(
            method="GET",
            url_suffix="/users/me",
        )


def test_module(client) -> str:
    try:
        client.ping()
    except Exception as e:
        return f"Test failed: {e}"

    return "ok"


def _make_timestamp(ts):
    if not ts:
        return None

    if isinstance(ts, int):
        return datetime.utcfromtimestamp(ts)

    if isinstance(ts, str):  # noqa: RET503
        return parse_dt(ts).replace(tzinfo=pyutc)


def _set_fetch_params(params, now):
    fetch_params: dict[str, Any] = {"meta[total]": True, "stats": True}

    limit = arg_to_number(arg=demisto.params().get("max_fetch"), arg_name="max_fetch", required=False)

    if limit is not None:
        if limit > 10000:
            demisto.info("Adjusting artifact limit to maximum of 10000")
            limit = 10000

        fetch_params["limit"] = limit

    last_run = demisto.getLastRun()

    max_start_time_date = parse_date(MAX_START_TIME)
    assert max_start_time_date is not None
    max_time = max_start_time_date.timestamp()
    first_fetch_date = parse_date(params.get("first_fetch"))
    assert first_fetch_date is not None, f"could not parse {params.get('first_fetch')}"
    start_time = first_fetch_date.timestamp()
    if last_run and "start_time" in last_run:
        start_time = last_run.get("start_time")

    if max_time > start_time:
        raise DemistoException("First fetch time can be a maximum of 90 days")

    fetch_params["date[start]"] = int(start_time)
    fetch_params["date[end]"] = int(now.timestamp())
    return fetch_params


def _process_exposure_data(data, fetch_passwords):
    return_data = []

    for r in data:
        entry: dict = {}
        for k, v in EXPOSURE_FIELDS.items():
            if k == "password" and not fetch_passwords:
                entry[k] = None
                continue

            entry[k] = demisto.get(r, v)

        timestamps = demisto.get(r, "attributes.timestamps") or []
        for ts in timestamps:
            if ts["context"] == "loaded":
                entry["loaded"] = _make_timestamp(ts["value"]).strftime(DATE_FORMAT)

        timestamps = demisto.get(r, "meta.stats.credential.timestamps") or []
        for ts in timestamps:
            if ts["context"] == "first_seen":
                entry["firstseen"] = _make_timestamp(ts["value"]).strftime(DATE_FORMAT)

            if ts["context"] == "last_seen":
                entry["lastseen"] = _make_timestamp(ts["value"]).strftime(DATE_FORMAT)

        return_data.append(entry)

    return {"QWatch": {"Exposures": return_data}}


def fetch_incidents(client, params):
    now = datetime.utcnow()
    incidents = []

    fetch_params = _set_fetch_params(params, now)

    fetch_passwords = params.get("fetch_passwords")
    severity = getattr(IncidentSeverity, params["fetch_severity"].upper())
    incident_type = params.get("incidentType")
    if not incident_type or incident_type == "":
        incident_type = INCIDENT_TYPE

    response = client.search("exposures", fetch_params)

    if response and response.get("data"):
        data = _process_exposure_data(response["data"], fetch_passwords)

        i = {
            "name": INCIDENT_NAME,
            "occurred": now.strftime(ISO8601_FORMAT),
            "rawJSON": json.dumps(data),
            "type": incident_type,
            "severity": severity,
        }
        incidents.append(i)

    demisto.setLastRun({"start_time": now.timestamp()})
    return incidents


def search_exposures(client, args, params):
    searches = []
    command_results = []
    fetch_passwords = params.get("fetch_passwords")

    email = args.get("email")
    if email:
        searches.append({"search_term": email, "search_type": "email"})

    domain = args.get("domain")
    if domain:
        searches.append({"search_term": domain, "search_type": "domain"})

    for search in searches:
        search.update({"meta[total]": True, "stats": True})

        response = client.search("exposures", search)

        if response and response.get("data"):
            data = _process_exposure_data(response["data"], fetch_passwords)

            hr_data = []
            for r in data["QWatch"]["Exposures"]:
                entry = {}
                for k, v in EXPOSURE_HR_MAP.items():
                    entry[k] = r.get(v)
                hr_data.append(entry)
                demisto.debug(entry)

            header = f"Qintel QWatch exposures for: {search['search_term']}\n"
            hr = tableToMarkdown(header, hr_data, headers=list(EXPOSURE_HR_MAP.keys()))

            command_results.append(
                CommandResults(outputs_prefix="Qintel", outputs=data, outputs_key_field="", readable_output=hr)
            )

        else:
            header = f"Qintel QWatch exposures for: {search['search_term']}\n"
            hr = tableToMarkdown(header, {})

            command_results.append(CommandResults(readable_output=hr))

    return command_results


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

    client_args = {
        "client_id": params.get("credentials").get("identifier"),
        "client_secret": params.get("credentials").get("password"),
    }

    remote = params.get("remote", QWATCH_REMOTE)

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

    command = demisto.command()

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

    try:
        client = Client(remote, verify_ssl, proxy, **client_args)

        args = demisto.args()

        if command == "test-module":
            demisto.results(test_module(client))

        elif command == "fetch-incidents":
            demisto.incidents(fetch_incidents(client, params))

        elif command == "qintel-qwatch-exposures":
            return_results(search_exposures(client, args, params))

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


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