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
| ID | QintelQWatch |
|---|---|
| Provider | Qintel |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix 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 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
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 settingsisFetch— Fetch incidentsfetch_passwords— Fetch plaintext passwordsmax_fetch— Limit number of records per fetchfirst_fetch— First fetch timeincidentFetchInterval— Incidents Fetch Intervalfetch_severity— Default Incident Severity (required)incidentType— Incident type
Commands (1)
-
qintel-qwatch-exposuresSearch 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()