Darkmon
Stay ahead of cyber threats with Darkmon TIP - real-time threat intelligence from the Clear, Deep, and Dark Web tailored to your assets. Pack also helps with integration with Cortex XSOAR and provides pre-made playbooks/templates to ease integration use.
Data Enrichment & Threat Intelligence · Darkmon
Details
| ID | Darkmon |
|---|---|
| Provider | Darkmon |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.8.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Cloud Posture Security Cortex Cloud Cloud Runtime Security EDR Attack Surface Management Threat Intelligence Management Application Security XSIAM Exposure Management Agentix Email Security |
README
Stay ahead of cyber threats with Darkmon TIP - real-time threat intelligence from the Clear, Deep, and Dark Web tailored to your assets.
Pack also helps with integration with Cortex XSOAR and provides pre-made playbooks/templates to ease integration use.
Configure Darkmon in Cortex
| Parameter | Description | Required |
|---|---|---|
| API Base URL | Override the Darkmon TIP API base URL only if your tenant points at a non-default endpoint. The default value already targets the production Darkmon TIP service (https://api.darkmon.com/tip/2025.1). Leave blank to use the default. | False |
| API key | True | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Redact secrets in War Room output | When enabled, replaces password/card-number values in markdown table output with ‘***’. Raw values remain in rawJSON for playbook automation. Disable only in non-production debugging contexts. | False |
| Employee compromise disable mode | Controls how the Compromised Employee Auto-Disable playbook reacts when a new compromised employee account is observed. notify-only (default - safe): creates an incident, no AD action. approval-required: creates an incident with a manual approval task; on approve, runs the disable. auto-disable: disables the account immediately. Accounts in the “Darkmon - Auto-Disable Allowlist” list are NEVER auto-disabled regardless of mode. | False |
| First fetch time | First fetch query time range when starting from a clean state. Accepts ISO timestamps or relative durations (e.g. “3 days”, “12 hours”). | False |
| Maximum number of incidents per fetch | Caps the number of Darkmon records ingested as incidents per fetch cycle to protect the war room from sudden backlogs. | False |
| Darkmon incident types to fetch | Which Darkmon record kinds the integration ingests as XSOAR incidents. Defaults to the high-signal trio. Lower-signal kinds (e.g. Ransomware Mention) are typically better handled via the monitoring playbooks rather than native fetch. | False |
| Incident type | ||
| Fetch incidents |
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.
dmontip-global-search
The dmontip-global-search command performs a comprehensive search across the Darkmon Threat Intelligence Platform. This command allows users to search for indicators, threat actors, malware, and other intelligence data using keywords or specific search terms. It queries multiple data sources simultaneously and returns consolidated results, helping analysts quickly find relevant intelligence across the platform.
Base Command
dmontip-global-search
Input
| Argument Name | Description | Required |
|---|---|---|
| type | Type of the value. Possible values are: Domain, IP, URL, Hash, CVE, Email, Username, Malware, Source, Keyword, Card, CardNumber, CardHolder. | Required |
| query | A specific value. | Required |
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results matching the query, with type-specific fields. |
| Darkmon.Pagination.number | Number | Current page number (zero-indexed at the API). |
| Darkmon.Pagination.totalPages | Number | Total number of pages available. |
| Darkmon.Pagination.totalElements | Number | Total number of items across all pages. |
ip
Searches the Darkmon platform for intelligence related to a specific IP address. A focused interface for threat intelligence lookup of IP indicators.
Base Command
ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | One or more IP addresses to enrich (comma-separated). | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results for the IP indicator. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Source reliability per the Admiralty code. |
| IP.Address | String | The IP address. |
| IP.Malicious.Vendor | String | The vendor that flagged this IP as malicious. |
| IP.Malicious.Description | String | Reason this IP was flagged as malicious. |
url
Searches for URL-specific threat intelligence across the Darkmon platform. Quickly identifies malicious or suspicious URLs and associated threat data.
Base Command
url
Input
| Argument Name | Description | Required |
|---|---|---|
| url | One or more URLs to enrich (comma-separated). | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results for the URL indicator. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Source reliability per the Admiralty code. |
| URL.Data | String | The URL. |
| URL.Malicious.Vendor | String | The vendor that flagged this URL as malicious. |
| URL.Malicious.Description | String | Reason this URL was flagged as malicious. |
domain
Performs domain-focused threat intelligence searches in the Darkmon platform. Returns comprehensive information about potentially malicious domains.
Base Command
domain
Input
| Argument Name | Description | Required |
|---|---|---|
| domain | One or more domains to enrich (comma-separated). | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results for the domain indicator. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Source reliability per the Admiralty code. |
| Domain.Name | String | The domain name. |
| Domain.Malicious.Vendor | String | The vendor that flagged this domain as malicious. |
| Domain.Malicious.Description | String | Reason this domain was flagged as malicious. |
Searches for threat intelligence related to specific email addresses. Identifies compromised accounts or emails associated with malicious activities.
Base Command
email
Input
| Argument Name | Description | Required |
|---|---|---|
| One or more email addresses to enrich (comma-separated). | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results for the email indicator. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Source reliability per the Admiralty code. |
| Account.Email.Address | String | The email address. |
| Account.Email.Malicious.Vendor | String | The vendor that flagged this email as malicious. |
| Account.Email.Malicious.Description | String | Reason this email was flagged as malicious. |
file
Searches the Darkmon platform using file hash values (MD5, SHA-1, SHA-256). Identifies malware and provides associated threat intelligence data.
Base Command
file
Input
| Argument Name | Description | Required |
|---|---|---|
| file | One or more file hashes (MD5, SHA-1, SHA-256) to enrich (comma-separated). | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.SearchResult | Unknown | Search results for the file-hash indicator. |
| DBotScore.Indicator | String | The indicator that was tested. |
| DBotScore.Type | String | The indicator type. |
| DBotScore.Vendor | String | The vendor used to calculate the score. |
| DBotScore.Score | Number | The actual score. |
| DBotScore.Reliability | String | Source reliability per the Admiralty code. |
| File.MD5 | String | MD5 of the file (when the input was an MD5 hash). |
| File.SHA1 | String | SHA-1 of the file (when the input was a SHA-1 hash). |
| File.SHA256 | String | SHA-256 of the file (when the input was a SHA-256 hash). |
| File.Malicious.Vendor | String | The vendor that flagged this file as malicious. |
| File.Malicious.Description | String | Reason this file was flagged as malicious. |
dmontip-get-compromised
Retrieve compromised data of a given type from Darkmon - leaked accounts, leaked bank cards, combo lists, public breaches, or compromised employee accounts. Use the ‘type’ argument to choose the data set.
Base Command
dmontip-get-compromised
Input
| Argument Name | Description | Required |
|---|---|---|
| type | Which compromised data set to retrieve. Possible values are: accounts, bank-cards, combo-lists, public-breaches, employees. | Required |
| size | Page size (1-500). Default is 20. | Optional |
| page | 1-indexed page number. Default is 1. | Optional |
| sort | Sort field and direction in Spring Pageable format, e.g. ‘firstSeen,desc’ or ‘lastCompromiseDate,asc’. Leave blank to use the default: combo-lists defaults to firstSeen,desc; other types use the backend default order. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.Compromised.Account | Unknown | Leaked account records (when type=accounts). |
| Darkmon.Compromised.BankCard | Unknown | Leaked bank card records (when type=bank-cards). |
| Darkmon.Compromised.ComboList | Unknown | Combo list records (when type=combo-lists). |
| Darkmon.Compromised.PublicBreach | Unknown | Public breach records (when type=public-breaches). |
| Darkmon.Compromised.Employee | Unknown | Compromised employee account records (when type=employees). |
| Darkmon.Compromised.Page | Unknown | Pagination metadata (number, totalPages, totalElements). |
dmontip-get-vpn
Retrieve known VPN exit-node IOCs with pagination, sorted newest first by firstSeen unless overridden.
Base Command
dmontip-get-vpn
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| sort | Sort field and direction in Spring Pageable format. Default sorts newest first by firstSeen. Default is firstSeen,desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.VPN | Unknown | Known VPN exit-node records. |
| Darkmon.VPN.Page | Unknown | Pagination metadata. |
dmontip-get-proxy
Retrieve known open-proxy IOCs with pagination, sorted newest first by firstSeen unless overridden.
Base Command
dmontip-get-proxy
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| sort | Sort field and direction in Spring Pageable format. Default sorts newest first by firstSeen. Default is firstSeen,desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.Proxy | Unknown | Known open-proxy records. |
| Darkmon.Proxy.Page | Unknown | Pagination metadata. |
dmontip-get-cve
Retrieve security vulnerabilities (CVEs) with severity, CVSS score, published/lastModified timestamps, source identifier, and tags.
Base Command
dmontip-get-cve
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.CVE | Unknown | CVE records. |
| Darkmon.CVE.Page | Unknown | Pagination metadata. |
dmontip-get-nrd
Retrieve newly registered domains (NRD) recently observed by Darkmon, sorted newest first by timestamp unless overridden. Filters the IOC feed by classification NEWLY_REGISTERED_DOMAIN.
Base Command
dmontip-get-nrd
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| sort | Sort field and direction in Spring Pageable format. Default sorts newest first by timestamp. Default is timestamp,desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.NRD | Unknown | Newly registered domain records. |
| Darkmon.NRD.Page | Unknown | Pagination metadata. |
dmontip-get-tbf
Retrieve telnet brute-force IOCs - sources observed attempting telnet brute-force attacks, sorted newest first by timestamp unless overridden. Filters the IOC feed by classification TELNET_BRUTE_FORCE.
Base Command
dmontip-get-tbf
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| sort | Sort field and direction in Spring Pageable format. Default sorts newest first by timestamp. Default is timestamp,desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.TBF | Unknown | Telnet brute-force IOC records. |
| Darkmon.TBF.Page | Unknown | Pagination metadata. |
dmontip-get-ransomware
Retrieve ransomware articles or company-specific ransomware mentions with details such as victim name, threat actor, published date, and matched keywords. Sorted newest first by publishedAt unless overridden.
Base Command
dmontip-get-ransomware
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 10. | Optional |
| type | Use ‘mentions’ to retrieve company-specific ransomware mentions, or ‘all-topics’ to retrieve all ransomware articles. Possible values are: mentions, all-topics. Default is mentions. | Required |
| sort | Sort field and direction in Spring Pageable format. Default sorts newest first by publishedAt. Default is publishedAt,desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.Ransomware | Unknown | Ransomware article or mention records. |
| Darkmon.Ransomware.Page | Unknown | Pagination metadata. |
dmontip-get-landscape
Retrieve cybersecurity landscape news articles or company-specific landscape mentions with title, link, source, author, and matched keywords.
Base Command
dmontip-get-landscape
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 10. | Optional |
| type | Use ‘mentions’ to retrieve company-specific landscape news mentions, or ‘all-topics’ to retrieve all landscape news articles. Possible values are: mentions, all-topics. Default is mentions. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.Landscape | Unknown | Landscape article or mention records. |
| Darkmon.Landscape.Page | Unknown | Pagination metadata. |
dmontip-get-boardprotection
Lists the emails currently under board-leak protection (monitored) including request state, owner name, and tokens. Backed by the board-leak/request endpoint.
Base Command
dmontip-get-boardprotection
Input
| Argument Name | Description | Required |
|---|---|---|
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| term | Optional search term filtering across all available attributes. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.BoardProtection | Unknown | Board protection request records (monitored emails with state and owner details). |
| Darkmon.BoardProtection.Page | Unknown | Pagination metadata. |
dmontip-get-boardemails
Retrieves leaked accounts, combo lists, or public breaches associated with a board-protected email. Use dmontip-get-boardprotection first to list monitored emails.
Base Command
dmontip-get-boardemails
Input
| Argument Name | Description | Required |
|---|---|---|
| type | Which board-leak data set to retrieve for the given email. Possible values are: accounts, combo-lists, public-breaches. | Required |
| The protected email to query (must be an email already under board protection). | Required | |
| page | 1-indexed page number. Default is 1. | Optional |
| size | Page size (1-100). Default is 20. | Optional |
| term | Optional search term filtering inside the chosen data set. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Darkmon.BoardLeak.Account | Unknown | Leaked account records for the protected email (when type=accounts). |
| Darkmon.BoardLeak.ComboList | Unknown | Combo list records for the protected email (when type=combo-lists). |
| Darkmon.BoardLeak.PublicBreach | Unknown | Public breach records for the protected email (when type=public-breaches). |
| Darkmon.BoardLeak.Page | Unknown | Pagination metadata. |
Configuration parameters
base_url— API Base URLX-API-KEY— (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsintegrationReliability— Source Reliabilityredact_secrets— Redact secrets in War Room outputfirst_fetch— First fetch timemax_fetch— Maximum number of incidents per fetchincident_types_to_fetch— Darkmon incident types to fetchincidentType— Incident typeincidentFetchInterval— Incidents Fetch IntervalisFetch— Fetch incidents
Commands (16)
-
dmontip-get-boardemailsRetrieves leaked accounts, combo lists, or public breaches associated with a board-protected email. Use dmontip-get-boardprotection first to list monitored emails.
-
dmontip-get-boardprotectionLists the emails currently under board-leak protection (monitored) including request state, owner name, and tokens. Backed by the board-leak/request endpoint.
-
dmontip-get-compromisedRetrieve compromised data of a given type from Darkmon - leaked accounts, leaked bank cards, combo lists, public breaches, or compromised employee accounts. Use the 'type' argument to choose the data set.
-
dmontip-get-cveRetrieve security vulnerabilities (CVEs) with severity, CVSS score, published/lastModified timestamps, source identifier, and tags.
-
dmontip-get-landscapeRetrieve cybersecurity landscape news articles or company-specific landscape mentions with title, link, source, author, and matched keywords.
-
dmontip-get-nrdRetrieve newly registered domains (NRD) recently observed by Darkmon, sorted newest first by timestamp unless overridden. Filters the IOC feed by classification NEWLY_REGISTERED_DOMAIN.
-
dmontip-get-proxyRetrieve known open-proxy IOCs with pagination, sorted newest first by firstSeen unless overridden.
-
dmontip-get-ransomwareRetrieve ransomware articles or company-specific ransomware mentions with details such as victim name, threat actor, published date, and matched keywords. Sorted newest first by publishedAt unless overridden.
-
dmontip-get-tbfRetrieve telnet brute-force IOCs - sources observed attempting telnet brute-force attacks, sorted newest first by timestamp unless overridden. Filters the IOC feed by classification TELNET_BRUTE_FORCE.
-
dmontip-get-vpnRetrieve known VPN exit-node IOCs with pagination, sorted newest first by firstSeen unless overridden.
-
dmontip-global-searchThe dmontip-global-search command performs a comprehensive search across the Darkmon Threat Intelligence Platform. This command allows users to search for indicators, threat actors, malware, and other intelligence data using keywords or specific search terms. It queries multiple data sources simultaneously and returns consolidated results, helping analysts quickly find relevant intelligence across the platform.
-
domainPerforms domain-focused threat intelligence searches in the Darkmon platform. Returns comprehensive information about potentially malicious domains.
-
emailSearches for threat intelligence related to specific email addresses. Identifies compromised accounts or emails associated with malicious activities.
-
fileSearches the Darkmon platform using file hash values (MD5, SHA-1, SHA-256). Identifies malware and provides associated threat intelligence data.
-
ipSearches the Darkmon platform for intelligence related to a specific IP address. A focused interface for threat intelligence lookup of IP indicators.
-
urlSearches for URL-specific threat intelligence across the Darkmon platform. Quickly identifies malicious or suspicious URLs and associated threat data.
import json from typing import Any import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) DEFAULT_SIZE = 20 DEFAULT_BASE_URL = "https://api.darkmon.com/tip/2025.1" VENDOR = "Darkmon" DBOT_SCORE_NONE = 0 DBOT_SCORE_GOOD = 1 DBOT_SCORE_SUSPICIOUS = 2 DBOT_SCORE_BAD = 3 DBOT_SCORE_BY_CLASSIFICATION: dict[str, int] = { "malicious": DBOT_SCORE_BAD, "phishing": DBOT_SCORE_BAD, "malware": DBOT_SCORE_BAD, "ransomware": DBOT_SCORE_BAD, "c2": DBOT_SCORE_BAD, "botnet": DBOT_SCORE_BAD, "exploit": DBOT_SCORE_BAD, "suspicious": DBOT_SCORE_SUSPICIOUS, "clean": DBOT_SCORE_GOOD, "benign": DBOT_SCORE_GOOD, "safe": DBOT_SCORE_GOOD, "whitelisted": DBOT_SCORE_GOOD, } SENSITIVE_FIELDS_FOR_REDACTION = {"password", "cardNumber", "cvv", "cvvs"} def classification_to_dbot_score(classification: str | None) -> int: if not classification: return DBOT_SCORE_NONE return DBOT_SCORE_BY_CLASSIFICATION.get(classification.lower().strip(), DBOT_SCORE_NONE) def _resolve_reliability() -> str: return demisto.params().get("integrationReliability") or "F - Reliability cannot be judged" def _should_redact_secrets() -> bool: raw = demisto.params().get("redact_secrets") if raw is None: return True if isinstance(raw, bool): return raw return str(raw).strip().lower() in {"true", "1", "yes", "on"} def _redact_rows( rows: list[dict[str, Any]], redact: bool, sensitive: set = SENSITIVE_FIELDS_FOR_REDACTION, ) -> list[dict[str, Any]]: if not redact: return rows return [{k: ("***" if (k in sensitive and v not in (None, "", [])) else v) for k, v in row.items()} for row in rows] def _detect_file_hash_field(value: str) -> str: v = (value or "").strip().lower() if len(v) == 32 and all(c in "0123456789abcdef" for c in v): return "MD5" if len(v) == 40 and all(c in "0123456789abcdef" for c in v): return "SHA1" if len(v) == 64 and all(c in "0123456789abcdef" for c in v): return "SHA256" return "MD5" def build_dbot_outputs(value: str, indicator_type: str, search_results: list[dict[str, Any]]) -> dict[str, Any]: """Build DBotScore + Common.<Type> outputs for a reputation enrichment. `indicator_type` is the lowercase XSOAR DBotScore type: 'ip', 'url', 'domain', 'file', 'email'. `search_results` is the list of cell-flattened dicts produced by extract_search_result(); the first cell with a 'classification' key drives the score. """ classification = next( (item.get("classification") for item in (search_results or []) if item.get("classification")), None, ) score = classification_to_dbot_score(classification) reliability = _resolve_reliability() dbot = { "Indicator": value, "Type": indicator_type, "Vendor": VENDOR, "Score": score, "Reliability": reliability, } common_specs: dict[str, tuple] = { "ip": ("Common.IP(val.Address && val.Address == obj.Address)", "Address"), "domain": ("Common.Domain(val.Name && val.Name == obj.Name)", "Name"), "url": ("Common.URL(val.Data && val.Data == obj.Data)", "Data"), "email": ("Common.EMAIL(val.Address && val.Address == obj.Address)", "Address"), } if indicator_type == "file": common_key = ( "Common.File(val.MD5 && val.MD5 == obj.MD5 || " "val.SHA1 && val.SHA1 == obj.SHA1 || " "val.SHA256 && val.SHA256 == obj.SHA256)" ) common_obj: dict[str, Any] = {_detect_file_hash_field(value): value} else: common_key, value_field = common_specs[indicator_type] common_obj = {value_field: value} if score == DBOT_SCORE_BAD: common_obj["Malicious"] = { "Vendor": VENDOR, "Description": f"Darkmon classified as {classification}", } return {"DBotScore": dbot, common_key: common_obj} def extract_feature_value(item: dict, key: str) -> Any: if not item.get("feature"): return None for feature in item.get("feature", []): if feature.get("accessorKey") == key: return feature.get("value") return None def extract_search_result(item: dict) -> dict: """Flatten a /search response item into a context-friendly dict. The /search endpoint returns SearchFeatureDTO objects of the shape: {"type": "<TipFeature>", "feature": [{"accessorKey", "displayName", "type", "value"}, ...]} This function is fully dynamic - whatever cells the backend sends become keys in the returned dict, so new TipFeature types and new columns work without code changes. The 'type' field (the TipFeature enum value, e.g. "Domains") is preserved at the top level. """ out: dict[str, Any] = {} feature_type = item.get("type") if feature_type is not None: out["type"] = feature_type cells = item.get("feature") or [] if not isinstance(cells, list): return out for cell in cells: if not isinstance(cell, dict): continue key = cell.get("accessorKey") if not key: continue value = cell.get("value") if value is None: continue out[key] = value return out def extract_features_to_dict(item: dict) -> dict: result = { "id": item.get("id"), "type": item.get("type"), "value": item.get("value"), "eventId": item.get("eventId"), "eventInfo": item.get("eventInfo"), "timestamp": item.get("timestamp"), "expired": item.get("expired"), } indicator_type = item.get("type", "") if indicator_type == "domain": result.update( { "name": item.get("name"), "classification": item.get("classification"), "ips": item.get("ips", []), } ) elif indicator_type == "url": result.update({"url": item.get("url"), "ips": item.get("ips", [])}) elif indicator_type == "file": result.update( { "name": item.get("name"), "md5": item.get("md5"), "sha1": item.get("sha1"), "sha256": item.get("sha256"), "sha3_384": item.get("sha3_384"), "tlsh": item.get("tlsh"), "ssdeep": item.get("ssdeep"), "size": item.get("size"), "mimeType": item.get("mimeType"), } ) elif indicator_type == "ip": result.update({"ip": item.get("ip")}) elif indicator_type == "email": result.update({"src": item.get("src")}) elif indicator_type == "tlsssl": result.update({"jarm_fingerprint": item.get("jarm_fingerprint")}) elif indicator_type == "vulnerabilityioc": result.update( { "vulnerabilityId": item.get("vulnerabilityId"), "name": item.get("name"), "description": item.get("description"), "severity": item.get("severity"), "exploitation": item.get("exploitation"), "cvssScore": item.get("cvssScore"), "sourceIdentifier": item.get("sourceIdentifier"), "published": item.get("published"), "lastModified": item.get("lastModified"), "vectorString": item.get("vectorString"), "tags": item.get("tags", []), "exploitabilityScore": item.get("exploitabilityScore"), } ) return {k: v for k, v in result.items() if v is not None} class Client(BaseClient): def __init__(self, base_url, headers, verify=True, proxy=False): super().__init__(base_url=base_url, headers=headers, verify=verify, proxy=proxy) def validate_api_key(self) -> bool: try: response = self._http_request(method="GET", url_suffix="/test-api-key", resp_type="text") return response == "The API key is valid!" except Exception: return False def global_search(self, query: str, indicator_type: str, page: int = 0, size: int = DEFAULT_SIZE) -> dict: allowed_types = [ "Domain", "IP", "URL", "Hash", "CVE", "Email", "Username", "Malware", "Source", "Keyword", "Card", "CardNumber", "CardHolder", ] if indicator_type not in allowed_types: raise ValueError(f"Invalid indicator type: {indicator_type}. Allowed types: {', '.join(allowed_types)}") formatted_query = f'{indicator_type}: "{query}"' params = {"page": page, "size": size, "query": formatted_query} return self._http_request(method="GET", url_suffix="search", params=params, resp_type="json") def get_compromised_data( self, data_type: str, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None, ) -> dict: endpoint_map = { "accounts": "leaks/accounts", "bank-cards": "leaks/bank-cards", "combo-lists": "leaks/combo-lists", "public-breaches": "leaks/public-breaches", "employees": "leaks/accounts/employees", } if data_type not in endpoint_map: raise ValueError(f"Unsupported compromised data type: {data_type}. Supported types: {', '.join(endpoint_map.keys())}") params: dict[str, Any] = {"page": page, "size": size} if sort: params["sort"] = sort return self._http_request(method="GET", url_suffix=endpoint_map[data_type], params=params) def get_indicators(self, size: int = DEFAULT_SIZE) -> dict: params = {"size": size} return self._http_request(method="GET", url_suffix="ioc-feed", params=params) def get_vpn(self, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None) -> dict: params: dict[str, Any] = {"page": page, "size": size} if sort: params["sort"] = sort return self._http_request( method="GET", url_suffix="vpn", params=params, ) def get_proxy(self, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None) -> dict: params: dict[str, Any] = {"page": page, "size": size} if sort: params["sort"] = sort return self._http_request( method="GET", url_suffix="proxy", params=params, ) def get_cve(self, page: int = 0, size: int = DEFAULT_SIZE) -> dict: params = {"page": page, "size": size} return self._http_request(method="GET", url_suffix="vulnerabilities", params=params) def get_nrd(self, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None) -> dict: params: dict[str, Any] = { "page": page, "size": size, "filter": json.dumps({"iocClassifications": ["NEWLY_REGISTERED_DOMAIN"]}), } if sort: params["sort"] = sort return self._http_request( method="GET", url_suffix="ioc", params=params, ) def get_tbf(self, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None) -> dict: params: dict[str, Any] = { "page": page, "size": size, "filter": json.dumps({"iocClassifications": ["TELNET_BRUTE_FORCE"]}), } if sort: params["sort"] = sort return self._http_request( method="GET", url_suffix="ioc", params=params, ) def get_ransomware( self, mentions: bool = False, page: int = 0, size: int = DEFAULT_SIZE, sort: str | None = None, ) -> dict: url_suffix = "/mentions/ransomware" if mentions else "/articles/ransomware" params: dict[str, Any] = {"page": page, "size": size} if sort: params["sort"] = sort return self._http_request( method="GET", url_suffix=url_suffix, params=params, ) def get_landscape(self, mentions: bool = False, page: int = 0, size: int = DEFAULT_SIZE) -> dict: url_suffix = "/mentions/landscape-news" if mentions else "/articles/landscape-news" return self._http_request(method="GET", url_suffix=url_suffix, params={"page": page, "size": size}) def get_board_protection_requests(self, page: int = 0, size: int = DEFAULT_SIZE, term: str | None = None) -> dict: params: dict[str, Any] = {"page": page, "size": size} if term: params["term"] = term return self._http_request( method="GET", url_suffix="board-leak/request", params=params, ) def get_board_leaks( self, leak_type: str, email: str, page: int = 0, size: int = DEFAULT_SIZE, term: str | None = None, ) -> dict: endpoint_map = { "accounts": "board-leak/leaks/accounts", "combo-lists": "board-leak/leaks/comboLists", "public-breaches": "board-leak/leaks/publicBreaches", } if leak_type not in endpoint_map: raise ValueError(f"Unsupported board leak type: {leak_type}. Supported types: {', '.join(endpoint_map.keys())}") params: dict[str, Any] = {"page": page, "size": size, "email": email} if term: params["term"] = term return self._http_request( method="GET", url_suffix=endpoint_map[leak_type], params=params, ) def test_module(client: Client) -> str: if client.validate_api_key(): return "ok" raise DemistoException("Failed to validate API key. Please check your credentials.") def generate_feature_tables(content: list) -> str: """Render /search results as one markdown table per TipFeature type. Fully dynamic: any TipFeature type and any cell list works automatically. Column order is taken from the first seen item of each type (matching the backend's deliberate ColumnDto order), with any new columns from later items appended in first-seen order. """ if not content: return "No data found" type_groups: dict[str, list[dict[str, Any]]] = {} type_header_order: dict[str, list[str]] = {} for item in content: feature_type = item.get("type", "Unknown") type_groups.setdefault(feature_type, []) type_header_order.setdefault(feature_type, []) seen = type_header_order[feature_type] cells = item.get("feature") or [] if not isinstance(cells, list) or not cells: continue row: dict[str, Any] = {} for cell in cells: if not isinstance(cell, dict): continue accessor_key = cell.get("accessorKey") or "" display_name = cell.get("displayName") or pascalToSpace(accessor_key) if not display_name: continue value = cell.get("value") if isinstance(value, list): value = ", ".join(str(v) for v in value) if value else "" elif isinstance(value, dict): value = json.dumps(value, default=str) elif value is None: value = "" row[display_name] = value if display_name not in seen: seen.append(display_name) if row: type_groups[feature_type].append(row) tables_md = [] for feature_type, rows in type_groups.items(): if not rows: continue headers = type_header_order[feature_type] table_md = tableToMarkdown( f"{feature_type} Information", rows, headers=headers, removeNull=False, ) tables_md.append(table_md) return "\n\n".join(tables_md) if tables_md else "No data found" def generate_ioc_tables(ioc_objects: list) -> str: if not ioc_objects: return "No indicators found" type_groups: dict[str, list[dict[str, Any]]] = {} for item in ioc_objects: ioc_type = item.get("type", "Unknown") if ioc_type not in type_groups: type_groups[ioc_type] = [] row = {} row["ID"] = item.get("id") row["Value"] = item.get("value") row["Event Info"] = item.get("eventInfo") row["Timestamp"] = item.get("timestamp") row["Expired"] = item.get("expired") if ioc_type == "domain": row["Name"] = item.get("name") row["Classification"] = item.get("classification") if item.get("ips"): row["IPs"] = ", ".join(item.get("ips", [])) elif ioc_type == "url": row["URL"] = item.get("url") if item.get("ips"): row["IPs"] = ", ".join(item.get("ips", [])) elif ioc_type == "file": row["Name"] = item.get("name") row["MD5"] = item.get("md5") row["SHA1"] = item.get("sha1") row["SHA256"] = item.get("sha256") row["Size"] = item.get("size") row["MIME Type"] = item.get("mimeType") elif ioc_type == "ip": row["IP"] = item.get("ip") elif ioc_type == "email": row["Source"] = item.get("src") elif ioc_type == "tlsssl": row["JARM Fingerprint"] = item.get("jarm_fingerprint") elif ioc_type == "vulnerabilityioc": row["Vulnerability ID"] = item.get("vulnerabilityId") row["Name"] = item.get("name") row["Description"] = item.get("description") row["Severity"] = item.get("severity") row["CVSS Score"] = item.get("cvssScore") row["Published"] = item.get("published") row["Last Modified"] = item.get("lastModified") row = {k: v for k, v in row.items() if v is not None} if row: type_groups[ioc_type].append(row) tables_md = [] for ioc_type, rows in type_groups.items(): if not rows: continue all_headers: set[str] = set() for row in rows: all_headers.update(row.keys()) headers = sorted(all_headers) table_md = tableToMarkdown(f"{ioc_type.upper()} Indicators", rows, headers=headers, removeNull=True) tables_md.append(table_md) return "\n\n".join(tables_md) if tables_md else "No indicators found" def dmontip_global_search_command(client: Client, args: dict) -> CommandResults: query = args.get("query") if not query: raise ValueError("Query parameter is required") indicator_type = args.get("type") or "" user_page = arg_to_number(args.get("page", 1)) or 1 page = max(0, user_page - 1) size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE result = client.global_search(query=query, indicator_type=indicator_type, page=page, size=size) search_results = result.get("content", []) pagination = result.get("page", {}) transformed_results = [extract_search_result(item) for item in search_results] outputs = { "Darkmon.SearchResult": transformed_results, "Darkmon.Pagination": pagination, } readable_output = generate_feature_tables(search_results) if pagination: current_page = pagination.get("number", 0) + 1 total_pages = pagination.get("totalPages", 1) total_elements = pagination.get("totalElements", 0) pagination_info = f"\n\n**Pagination**: Page {current_page} of {total_pages} ({total_elements} total items)" if total_pages > 1: pagination_info += "\n\nTo navigate pages, use the 'page' argument:" if current_page > 1: pagination_info += ( f'\n- Previous page: `!dmontip-global-search query="{query}" type="{indicator_type}" page={page}`' ) if current_page < total_pages: pagination_info += ( f'\n- Next page: `!dmontip-global-search query="{query}" type="{indicator_type}" page={page + 2}`' ) pagination_info += ( f"\n\nTo change page size: " f'`!dmontip-global-search query="{query}" type="{indicator_type}" page={page} size=<number>`' ) readable_output += pagination_info return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_compromised_command(client: Client, args: dict) -> CommandResults: data_type = (args.get("type") or "").strip() if not data_type: raise ValueError("type argument is required (accounts, bank-cards, combo-lists, public-breaches, employees)") size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 500: raise ValueError("size must be between 1 and 500") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 default_sort_by_type = {"combo-lists": "firstSeen,desc"} sort = (args.get("sort") or "").strip() or default_sort_by_type.get(data_type) result = client.get_compromised_data(data_type=data_type, page=api_page, size=size, sort=sort) content_items = result.get("content", []) or [] page_obj = result.get("page") or {} singular_map = { "accounts": "Account", "bank-cards": "BankCard", "combo-lists": "ComboList", "public-breaches": "PublicBreach", "employees": "Employee", } singular = singular_map.get(data_type, data_type.capitalize()) outputs: dict[str, Any] = {f"Darkmon.Compromised.{singular}": content_items} if page_obj: outputs["Darkmon.Compromised.Page"] = page_obj priority_columns: dict[str, list[str]] = { "accounts": [ "id", "username", "password", "url", "firstSeen", "firstSeenDate", "firstCompromiseDate", "lastCompromiseDate", "state", "valid", "compromiseSourcesCount", "countries", "sources", "stealers", ], "employees": [ "id", "username", "password", "url", "firstSeen", "firstSeenDate", "firstCompromiseDate", "lastCompromiseDate", "state", "valid", "compromiseSourcesCount", "countries", "sources", "stealers", ], "combo-lists": [ "id", "username", "password", "source", "firstSeen", "messageTime", "firstSeenDate", "state", "valid", ], "bank-cards": [ "id", "cardNumber", "cardHolders", "cvvs", "expiry", "firstSeen", "firstSeenDate", "state", "valid", "compromiseSourcesCount", "countries", "sources", "stealers", "chatIds", "chatNames", "chatUsernames", ], "public-breaches": [ "id", "source", "breachTime", "name", "username", "password", "emails", "phoneNumbers", "address", "country", "gender", "birthDate", "facebookUsername", "githubUsername", "linkedinUsername", "twitterUsername", "firstSeen", "firstSeenDate", "state", "valid", "photoUrl", ], } if content_items: headers_set: set[str] = set() simplified_rows: list[dict[str, Any]] = [] for item in content_items: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, dict): continue if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): display_val = ", ".join(str(x) for x in v) else: continue else: display_val = v row[k] = display_val if row: headers_set.update(row.keys()) simplified_rows.append(row) priority = priority_columns.get(data_type, []) ordered_headers: list[str] = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = tableToMarkdown( f"Compromised {singular} Data (type={data_type}, page={user_page}, size={size})", _redact_rows(simplified_rows, _should_redact_secrets()), headers=ordered_headers, removeNull=True, ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" else: readable_output = f"No compromised data found for type {data_type}" return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_vpn_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 sort = (args.get("sort") or "").strip() or "firstSeen,desc" result = client.get_vpn(page=api_page, size=size, sort=sort) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = ["ip", "port", "name", "firstSeen", "lastUpdated", "id"] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = ( tableToMarkdown( f"VPN Exit Nodes (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else "No VPN data found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.VPN": content} if page_obj: outputs["Darkmon.VPN.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_proxy_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 sort = (args.get("sort") or "").strip() or "firstSeen,desc" result = client.get_proxy(page=api_page, size=size, sort=sort) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = ["ip", "port", "type", "firstSeen", "lastUpdated", "id"] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = ( tableToMarkdown( f"Open Proxies (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else "No proxy data found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.Proxy": content} if page_obj: outputs["Darkmon.Proxy.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_cve_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 result = client.get_cve(page=api_page, size=size) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = [ "name", "cvssScore", "description", "published", "lastModified", "sourceIdentifier", "tags", ] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = ( tableToMarkdown( f"Vulnerabilities (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else "No vulnerability data found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.CVE": content} if page_obj: outputs["Darkmon.CVE.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_nrd_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 sort = (args.get("sort") or "").strip() or "timestamp,desc" result = client.get_nrd(page=api_page, size=size, sort=sort) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = ["value", "timestamp"] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = ( tableToMarkdown( f"Newly Registered Domains (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else "No newly-registered domains found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.NRD": content} if page_obj: outputs["Darkmon.NRD.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_tbf_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 sort = (args.get("sort") or "").strip() or "timestamp,desc" result = client.get_tbf(page=api_page, size=size, sort=sort) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = ["value", "timestamp"] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) readable_output = ( tableToMarkdown( f"Telnet Brute Force IOCs (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else "No Telnet Brute Force IOCs found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.TBF": content} if page_obj: outputs["Darkmon.TBF.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_ransomware_command(client: Client, args: dict) -> CommandResults: mentions = (args.get("type") or "").strip().lower() == "mentions" size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 sort = (args.get("sort") or "").strip() or "publishedAt,desc" result = client.get_ransomware(mentions=mentions, page=api_page, size=size, sort=sort) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = [ "id", "victimName", "victimDomain", "threatActor", "description", "publishedAt", "updatedAt", "firstSeen", "state", "valid", "matchedKeywordsLength", "matchedKeywords", "screenShotUrl", "articleSourceId", ] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) title_type = "Mentions" if mentions else "Articles" readable_output = ( tableToMarkdown( f"Ransomware {title_type} (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else f"No ransomware {title_type.lower()} found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.Ransomware": content} if page_obj: outputs["Darkmon.Ransomware.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_landscape_command(client: Client, args: dict) -> CommandResults: mentions = (args.get("type") or "").strip().lower() == "mentions" size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 result = client.get_landscape(mentions=mentions, page=api_page, size=size) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = [ "id", "title", "link", "publicationDate", "source", "author", "firstSeen", "firstSeenDate", "state", "valid", "categories", "description", "matchedKeywords", "matchedKeywordsLength", ] rows: list[dict[str, Any]] = [] headers_set: set[str] = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if k == "content": continue if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): v = ", ".join(str(x) for x in v) else: continue elif isinstance(v, dict): continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted([h for h in headers_set if h not in priority])) title_type = "Mentions" if mentions else "Articles" readable_output = ( tableToMarkdown( f"Landscape {title_type} (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) if rows else f"No landscape {title_type.lower()} found" ) if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = {"Darkmon.Landscape": content} if page_obj: outputs["Darkmon.Landscape.Page"] = page_obj return CommandResults(readable_output=readable_output, outputs=outputs, raw_response=result) def dmontip_get_boardprotection_command(client: Client, args: dict) -> CommandResults: size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 term = (args.get("term") or "").strip() or None result = client.get_board_protection_requests(page=api_page, size=size, term=term) content = result.get("content", []) or [] page_obj = result.get("page") or {} priority = [ "value", "type", "state", "firstName", "middleName", "lastName", "reason", "createdBy", "createdAt", "updatedAt", "tokens", "id", ] rows: list[dict[str, Any]] = [] headers_set: set = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, dict): continue if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): row[k] = ", ".join(str(x) for x in v) continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted(h for h in headers_set if h not in priority)) if rows: readable_output = tableToMarkdown( f"Board Protection Requests (page={user_page}, size={size})", rows, headers=ordered_headers, removeNull=True, ) else: readable_output = "No board protection requests found" if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = { "Darkmon.BoardProtection": content, } if page_obj: outputs["Darkmon.BoardProtection.Page"] = page_obj return CommandResults( readable_output=readable_output, outputs=outputs, raw_response=result, ) def dmontip_get_boardemails_command(client: Client, args: dict) -> CommandResults: leak_type = (args.get("type") or "").strip() if not leak_type: raise ValueError("type argument is required (accounts, combo-lists, public-breaches)") email = (args.get("email") or "").strip() if not email: raise ValueError("email argument is required") size = arg_to_number(args.get("size", DEFAULT_SIZE)) or DEFAULT_SIZE if size < 1 or size > 100: raise ValueError("size must be between 1 and 100") user_page = arg_to_number(args.get("page", 1)) or 1 if user_page < 1: raise ValueError("page must be >= 1") api_page = user_page - 1 term = (args.get("term") or "").strip() or None result = client.get_board_leaks(leak_type=leak_type, email=email, page=api_page, size=size, term=term) content = result.get("content", []) or [] page_obj = result.get("page") or {} singular_map = { "accounts": "Account", "combo-lists": "ComboList", "public-breaches": "PublicBreach", } singular = singular_map.get(leak_type, leak_type.capitalize()) priority_columns: dict[str, list[str]] = { "accounts": [ "email", "id", "compromiseDate", "username", "password", "url", "machineUsername", "ip", "country", "stealer", "source", ], "combo-lists": [ "email", "id", "messageTime", "username", "password", "source", ], "public-breaches": [ "email", "id", "breachTime", "source", "name", "username", "password", "address", "country", "birthDate", "gender", "firstSeen", "firstSeenDate", "facebookUsername", "githubUsername", "linkedinUsername", "twitterUsername", "photoUrl", ], } rows: list[dict[str, Any]] = [] headers_set: set = set() for item in content: row: dict[str, Any] = {} for k, v in item.items(): if isinstance(v, dict): if k == "ip" and "address" in v: row["ip"] = v.get("address") continue if isinstance(v, list): if all(not isinstance(x, dict | list) for x in v): row[k] = ", ".join(str(x) for x in v) continue row[k] = v if row: headers_set.update(row.keys()) rows.append(row) priority = priority_columns.get(leak_type, []) ordered_headers = [h for h in priority if h in headers_set] ordered_headers.extend(sorted(h for h in headers_set if h not in priority)) if rows: readable_output = tableToMarkdown( f"Board Leak {singular} (email={email}, page={user_page}, size={size})", _redact_rows(rows, _should_redact_secrets()), headers=ordered_headers, removeNull=True, ) else: readable_output = f"No board leak {leak_type} found for email {email}" if page_obj: current_page = (page_obj.get("number", api_page)) + 1 total_pages = page_obj.get("totalPages") total_elements = page_obj.get("totalElements") pagination_line = f"Page {current_page}" if total_pages is not None: pagination_line += f" / {total_pages}" if total_elements is not None: pagination_line += f" | Total Items: {total_elements}" readable_output += f"\n\n{pagination_line}" outputs: dict[str, Any] = { f"Darkmon.BoardLeak.{singular}": content, } if page_obj: outputs["Darkmon.BoardLeak.Page"] = page_obj return CommandResults( readable_output=readable_output, outputs=outputs, raw_response=result, ) _DBOT_TYPE_BY_LABEL = { "IP": "ip", "URL": "url", "Domain": "domain", "Email": "email", "Hash": "file", } def _search_each(client: Client, args: dict, arg_key: str, type_label: str, missing_msg: str) -> list[CommandResults]: values = argToList(args.get(arg_key)) if not values: raise ValueError(missing_msg) indicator_type = _DBOT_TYPE_BY_LABEL[type_label] results: list[CommandResults] = [] for v in values: cr = dmontip_global_search_command( client, { "query": v, "type": type_label, "page": args.get("page", 1), "size": args.get("size", DEFAULT_SIZE), }, ) outputs_dict: dict[str, Any] = cr.outputs if isinstance(cr.outputs, dict) else {} search_items = outputs_dict.get("Darkmon.SearchResult", []) cr.outputs = { **outputs_dict, **build_dbot_outputs(v, indicator_type, search_items), } results.append(cr) return results def dmontip_search_ip_command(client: Client, args: dict) -> list[CommandResults]: return _search_each(client, args, "ip", "IP", "IP parameter is required") def dmontip_search_url_command(client: Client, args: dict) -> list[CommandResults]: return _search_each(client, args, "url", "URL", "URL parameter is required") def dmontip_search_domain_command(client: Client, args: dict) -> list[CommandResults]: return _search_each(client, args, "domain", "Domain", "Domain parameter is required") def dmontip_search_email_command(client: Client, args: dict) -> list[CommandResults]: return _search_each(client, args, "email", "Email", "Email parameter is required") def dmontip_search_file_command(client: Client, args: dict) -> list[CommandResults]: return _search_each(client, args, "file", "Hash", "File hash parameter is required") INCIDENT_TYPE_MAP: dict[str, str] = { "Compromised Credential": "Darkmon Compromised Credential", "Compromised Employee": "Darkmon Compromised Employee", "Critical CVE": "Darkmon Critical CVE", "Ransomware Mention": "Darkmon Ransomware Mention", "Typosquatting Threat": "Darkmon Typosquatting Threat", "VIP Email Leak": "Darkmon VIP Email Leak", } SEVERITY_MAP: dict[str, int] = { "critical": 4, "high": 3, "medium": 2, "low": 1, "informational": 0, "4": 4, "3": 3, "2": 2, "1": 1, "0": 0, } def _severity_from_record(record: dict) -> int: raw = record.get("severity") or record.get("cve_severity") or "medium" return SEVERITY_MAP.get(str(raw).lower(), 2) def _occurred_from_record(record: dict) -> str: return ( record.get("occurred") or record.get("timestamp") or record.get("first_compromise") or record.get("published_at") or record.get("created_at") or "" ) def _fetch_kind(client: Client, kind: str, since: str, size: int) -> list[dict]: """Pull a single Darkmon record kind for fetch_incidents. Returns each record annotated with `darkmon_kind` so the classifier can route it to the right IncidentType. """ raw: dict = {} if kind == "Compromised Credential": raw = client.get_compromised_data("compromised-accounts", size=size, sort="-timestamp") elif kind == "Compromised Employee": raw = client.get_compromised_data("compromised-employees", size=size, sort="-timestamp") elif kind == "Critical CVE": raw = client.get_cve(size=size) elif kind == "Ransomware Mention": raw = client.get_ransomware(mentions=True, size=size, sort="-timestamp") elif kind == "Typosquatting Threat": raw = client.get_nrd(size=size, sort="-timestamp") else: # VIP Email Leak is not fetched directly. The Darkmon API requires a # per-email query (leak_type + email), which the Darkmon - VIP Email # Monitor playbook handles by iterating over board-protected emails. return [] records: list[dict] = [] payload = raw.get("content") or raw.get("items") or raw.get("results") or raw.get("data") or [] items_iter = payload if isinstance(payload, list) else [] for item in items_iter: item = dict(item) item["darkmon_kind"] = kind occurred = _occurred_from_record(item) if since and occurred and occurred <= since: continue records.append(item) return records def fetch_incidents_command(client: Client, params: dict) -> tuple[list[dict], dict]: """Native incident fetch. Pulls each enabled Darkmon record kind newer than the last high-water-mark, annotates each with `darkmon_kind` so the classifier routes it, and emits an incident dict with `name`, `type`, `occurred`, `severity`, and `rawJSON`. The mapper turns rawJSON into XSOAR IncidentField values. """ enabled = argToList(params.get("incident_types_to_fetch") or "") if not enabled: enabled = ["Compromised Credential", "Critical CVE", "VIP Email Leak"] max_fetch = arg_to_number(params.get("max_fetch", 50)) or 50 first_fetch = params.get("first_fetch", "3 days") last_run = demisto.getLastRun() or {} since_per_kind: dict[str, str] = last_run.get("since_per_kind") or {} bootstrap: str = "" if not since_per_kind: dt = dateparser.parse(first_fetch, settings={"TIMEZONE": "UTC"}) bootstrap = dt.strftime("%Y-%m-%dT%H:%M:%SZ") if dt else "" per_kind = max(1, max_fetch // max(1, len(enabled))) incidents: list[dict] = [] new_since_per_kind: dict[str, str] = dict(since_per_kind) for kind in enabled: since = since_per_kind.get(kind) or bootstrap records = _fetch_kind(client, kind, since, per_kind) highest = since for record in records: occurred = _occurred_from_record(record) or "" if occurred > highest: highest = occurred incidents.append( { "name": f"Darkmon {kind}: {record.get('value') or record.get('id') or record.get('cve_id') or 'record'}", "type": INCIDENT_TYPE_MAP.get(kind, "Darkmon Compromised Credential"), "occurred": occurred, "severity": _severity_from_record(record), "rawJSON": json.dumps(record), } ) if highest: new_since_per_kind[kind] = highest next_last_run = {"since_per_kind": new_since_per_kind} return incidents[:max_fetch], next_last_run def main() -> None: params = demisto.params() args = demisto.args() command = demisto.command() api_key = params.get("X-API-KEY", {}).get("password") if not api_key: raise DemistoException("API key is required") base_url = (params.get("base_url") or DEFAULT_BASE_URL).rstrip("/") headers = { "X-API-KEY": api_key, "Accept": "application/json", } client = Client( base_url=base_url, headers=headers, verify=not params.get("insecure", False), proxy=params.get("proxy", False), ) try: if command == "test-module": return_results(test_module(client)) elif command == "fetch-incidents": incidents, next_last_run = fetch_incidents_command(client, params) demisto.incidents(incidents) demisto.setLastRun(next_last_run) elif command == "dmontip-global-search": return_results(dmontip_global_search_command(client, args)) elif command == "dmontip-get-compromised": return_results(dmontip_get_compromised_command(client, args)) elif command == "dmontip-get-vpn": return_results(dmontip_get_vpn_command(client, args)) elif command == "dmontip-get-proxy": return_results(dmontip_get_proxy_command(client, args)) elif command == "dmontip-get-cve": return_results(dmontip_get_cve_command(client, args)) elif command == "dmontip-get-nrd": return_results(dmontip_get_nrd_command(client, args)) elif command == "dmontip-get-tbf": return_results(dmontip_get_tbf_command(client, args)) elif command == "dmontip-get-ransomware": return_results(dmontip_get_ransomware_command(client, args)) elif command == "dmontip-get-landscape": return_results(dmontip_get_landscape_command(client, args)) elif command == "dmontip-get-boardprotection": return_results(dmontip_get_boardprotection_command(client, args)) elif command == "dmontip-get-boardemails": return_results(dmontip_get_boardemails_command(client, args)) elif command == "ip": return_results(dmontip_search_ip_command(client, args)) elif command == "url": return_results(dmontip_search_url_command(client, args)) elif command == "domain": return_results(dmontip_search_domain_command(client, args)) elif command == "email": return_results(dmontip_search_email_command(client, args)) elif command == "file": return_results(dmontip_search_file_command(client, args)) else: raise NotImplementedError(f"Command {command} is not implemented") except Exception as e: return_error(f"Failed to execute {command} command. Error: {str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): main()