BechtleDarkWebScan
Notifies you if login credentials to your company are being sold on the dark web.
Identity and Access Management · Bechtle Dark Web Scan
Details
| ID | BechtleDarkWebScan |
|---|---|
| Provider | Bechtle |
| Category | Identity and Access Management |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.12042988 |
README
The dark web scan integration notifies you if login credentials to your company are being sold on the dark web.
Configure BechtleDarkWebScan in Cortex
| Parameter | Description | Required |
|---|---|---|
| API Key | Your darkwebscan.app API Key | True |
| Additional request headers | Additional request headers | False |
| Fetch incidents | Whether to fetch incidents | False |
| Incidents Fetch Interval | The interval incidents should be fetched | False |
| First fetch time | The first fetch time | False |
| Incident type | The incident type | False |
| Maximum incidents per fetch | Maxmimum amount of incidents per fetch | False |
| Trust any certificate (not secure) | Whether to trust any certificate | False |
| Use system proxy settings | Whether to use system proxy settings | False |
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.
darkwebscan-getcompanies
Returns a list of all companies associated with your API key.
Base Command
darkwebscan-getcompanies
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.Companies | String | All companies that you have access to. |
darkwebscan-getleaks
Returns leaked credentials for a given company_id.
Base Command
darkwebscan-getleaks
Input
| Argument Name | Description | Required |
|---|---|---|
| company_id | ID of the company to retrieve leaked credentials for. | Required |
| only_new | Only return leaks not previously seen by this integration instance. Possible values are: true, false. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.LeakedCredentials | String | Leaked credentials for a given company_id. |
darkwebscan-getosint
Returns OSINT information about the associated domain of a given company_id.
Base Command
darkwebscan-getosint
Input
| Argument Name | Description | Required |
|---|---|---|
| company_id | ID of the company to retrieve OSINT information for. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.OSINT | String | OSINT information (email addresses, subdomains, …) for the associated domain of a given company_id. |
darkwebscan-getemailsecurity
Returns information about the email security for the associated domain of a given company_id.
Base Command
darkwebscan-getemailsecurity
Input
| Argument Name | Description | Required |
|---|---|---|
| company_id | ID of the company to retrieve email security information for. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.EmailSecurity | String | Email security (SPF, DMARC, DANE, …) for the associated domain of a given company_id. |
darkwebscan-getwaf
Returns Web Application Firewall Status information about the associated domain of a given company_id.
Base Command
darkwebscan-getwaf
Input
| Argument Name | Description | Required |
|---|---|---|
| company_id | ID of the company to retrieve Web Application Firewall Status information for. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.WAF | String | Web Application Firewall status and product name (if applicable) |
darkwebscan-resetcontext
Resets integration context for the DarkWebScan. May result in many alerts reappearing.
Base Command
darkwebscan-resetcontext
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| BechtleDarkWebScan.ResetContext | String | Returns text whether the reset was successful or not. |
Configuration parameters
apikey— API Key (required)addreqheaders— Additional request headersisFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch Intervalfirst_fetch— First fetch timeincidentType— Incident typemax_fetch— Maximum incidents per fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (6)
-
darkwebscan-getcompaniesReturns a list of all companies associated with your API key.
-
darkwebscan-getemailsecurityReturns information about the email security for the associated domain of a given company_id.
-
darkwebscan-getleaksReturns leaked credentials for a given company_id.
-
darkwebscan-getosintReturns OSINT information about the associated domain of a given company_id.
-
darkwebscan-getwafReturns Web Application Firewall Status information about the associated domain of a given company_id.
-
darkwebscan-resetcontextResets integration context for the DarkWebScan. May result in many alerts reappearing.
import json from typing import Any import demistomock as demisto import urllib3 from datetime import datetime, UTC from CommonServerPython import * from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) """ CONSTANTS """ DEFAULT_BASE_URL = "https://api.darkwebscan.app/v1" DEFAULT_PAGE_SIZE = 50 INTEGRATION_NAME = "BechtleDarkWebScan" INTEGRATION_VERSION = "1.0.0" VENDOR = "Bechtle" SENSITIVE_FIELDS_FOR_REDACTION = {"password", "password_hash", "cvv", "card_number"} CONTEXT_SEEN_LEAK_IDS_KEY = "seen_leak_ids" CONTEXT_LAST_FETCH_KEY = "last_leak_fetch" MAX_SEEN_IDS_TO_RETAIN = 5000 MAX_SEEN_IDS_PER_COMPANY = 1000 """ CLIENT CLASS """ class Client(BaseClient): """ Client class to interact with the DarkWebScan API. All API-specific logic (auth, pagination, request shaping) lives here. Command functions should call this client rather than using requests directly. """ def __init__(self, base_url: str, api_key: str, additional_request_headers: str, verify: bool, proxy: bool): headers = { "apiKey": f"{api_key}", "Content-Type": "application/json", "User-Agent": f"{INTEGRATION_NAME}-PANW-XSOAR/{INTEGRATION_VERSION}", } if additional_request_headers: for line in additional_request_headers.strip().splitlines(): if ":" in line: key, val = line.split(":", 1) headers[key.strip()] = val.strip() super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers) def get_companies(self, page: int = 1, page_size: int = DEFAULT_PAGE_SIZE) -> dict: """ Retrieve the list of companies/assets currently being monitored. """ params = { "page": page, "page_size": page_size, } response = self._http_request(method="GET", url_suffix="/user/companies", params=params, resp_type="response") if not response.text or response.status_code == 204: return {} try: return response.json() except ValueError: raise DemistoException( f"API endpoint returned invalid, non-JSON text data (Status {response.status_code}): {response.text}" ) def get_leaks( self, company_id: int = 0, since: Optional[str] = None, page: int = 1, page_size: int = DEFAULT_PAGE_SIZE ) -> dict: """ Retrieve leaked credentials for a given company_id. """ try: params = { "companyId": company_id, "page": page, "amount": page_size, } return self._http_request( method="GET", url_suffix="/user/lookout/my-leaked-data", params=params, ) except Exception as e: demisto.error(f"get_leaks: {e}") return {} def get_email_security( self, company_id: int = 0, ) -> dict: """ Retrieve information about the email security of the domain from a given company_id. """ params = {"companyId": company_id} return self._http_request( method="GET", url_suffix="/scan/email-security", params=params, ) def get_osint( self, company_id: int = 0, ) -> dict: """ Retrieve OSINT information about the associated domain of a given company_id. """ params = {"companyId": company_id} return self._http_request( method="GET", url_suffix="/scan/osint", params=params, ) def get_waf( self, company_id: int = 0, ) -> dict: """ Retrieve Web Application Firewall Status information about the associated domain of a given company_id. """ params = {"companyId": company_id} return self._http_request( method="GET", url_suffix="/scan/waf", params=params, ) """ HELPER FUNCTIONS """ def _redact_rows(rows: list[dict[str, Any]], redact: bool) -> list[dict[str, Any]]: """ Mask sensitive fields (e.g. plaintext passwords) before they hit the War Room / context unless the integration is explicitly configured not to. """ if not redact: return rows return [ {k: ("***" if k in SENSITIVE_FIELDS_FOR_REDACTION and v not in (None, "", []) else v) for k, v in row.items()} for row in rows ] def _should_redact_secrets(params: dict) -> bool: raw = 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"} """ COMMAND FUNCTIONS """ def test_module(client: Client) -> str: """ Called when clicking the 'Test' button in the integration configuration. Validates the API key / connectivity by making a lightweight call. """ client.get_companies(page=1, page_size=1) return "ok" def _color_tag(text: str, color: str) -> str: """ Wrap text in XSOAR-flavored markdown color syntax. """ if not color: return text return f"{{{{color:{color}}}}}({text})" def _get_all_company_ids(client: Client) -> list[int]: """ Fetch all monitored companies in a single call. """ response = client.get_companies(page=1, page_size=DEFAULT_PAGE_SIZE) if isinstance(response, list): companies = response else: companies = response.get("companies", response.get("results", [])) return [c["company_id"] for c in companies if c.get("company_id") is not None] def _to_iso8601(date_str: Optional[str]) -> str: """ Normalize a date/datetime string to full ISO 8601 (YYYY-MM-DDTHH:MM:SSZ), as required by the 'occurred' field in incident dicts. """ if not date_str: return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ") dt = arg_to_datetime(date_str) if dt is None: return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") return dt.strftime("%Y-%m-%dT%H:%M:%SZ") def get_company_id_arg(args: dict) -> int: """ Extracts, validates, and explicitly casts 'company_id' from command arguments. Raises a ValueError if missing or invalid. """ company_id_raw = args.get("company_id") if company_id_raw is None: raise ValueError("Missing required argument: 'company_id'") company_id = arg_to_number(company_id_raw) if company_id is None: raise ValueError(f"Invalid integer value provided for 'company_id': {company_id_raw}") return int(company_id) def darkwebscan_getcompanies_command(client: Client, args: dict) -> CommandResults: """ darkwebscan-getcompanies Lists the companies/assets currently monitored by BechtleDarkWebScan. """ page = min(50000, max(0, arg_to_number(args.get("page")) or 0)) page_size = min(50000, max(0, arg_to_number(args.get("page_size")) or DEFAULT_PAGE_SIZE)) companies = client.get_companies(page=page, page_size=page_size) if not isinstance(companies, list): companies_list = [companies] if companies else [] else: companies_list = companies # Map the exact keys returned by the API to the markdown table headers readable_output = tableToMarkdown( name="DarkWebScan Companies", t=companies_list, headers=["company_id", "printableName", "fqdn", "scanCreditsAmount"], removeNull=True, ) return CommandResults( outputs_prefix="BechtleDarkWebScan.Companies", outputs_key_field="id", outputs=companies_list, readable_output=readable_output, raw_response=companies_list, ) def darkwebscan_getleaks_command(client: Client, args: dict) -> CommandResults: """ darkwebscan-getleaks Retrieves leaked-credential records, optionally filtered by company and/or a start date, with optional redaction of sensitive fields. """ only_new = argToBoolean(args.get("only_new", "false")) company_id = get_company_id_arg(args) since = args.get("since") page = arg_to_number(args.get("page")) or 1 page_size = arg_to_number(args.get("page_size")) or DEFAULT_PAGE_SIZE redact = argToBoolean(args.get("redact_secrets", "true")) if args.get("company_id") is not None and company_id is None: raise ValueError(f"Invalid 'company_id' value: {args.get('company_id')}. Must be an integer.") integration_context = get_integration_context() seen_ids = set(integration_context.get(CONTEXT_SEEN_LEAK_IDS_KEY, [])) last_fetch = integration_context.get(CONTEXT_LAST_FETCH_KEY) response = client.get_leaks(company_id=company_id, since=since, page=page, page_size=page_size) leaks = response.get("searchResults", None) if only_new: new_leaks = [leak for leak in leaks if leak.get("id") not in seen_ids] else: new_leaks = leaks # Update context: remember these IDs if new_leaks: newest_timestamp = max( (leak.get("date") for leak in new_leaks if leak.get("date")), default=last_fetch, ) seen_ids.update(leak.get("id") for leak in new_leaks if leak.get("id")) if len(seen_ids) > MAX_SEEN_IDS_TO_RETAIN: seen_ids = set(list(seen_ids)[-MAX_SEEN_IDS_TO_RETAIN:]) set_integration_context({CONTEXT_SEEN_LEAK_IDS_KEY: list(seen_ids), CONTEXT_LAST_FETCH_KEY: newest_timestamp}) new_leaks = _redact_rows(new_leaks, redact) readable_output = tableToMarkdown( name="DarkWebScan Leaked Credentials (only new)" if only_new else "DarkWebScan Leaked Credentials", t=new_leaks, headers=["id", "domain", "links", "stealer", "source", "username", "price", "size", "date"], removeNull=True, ) return CommandResults( outputs_prefix="BechtleDarkWebScan.LeakedCredentials", outputs_key_field="id", outputs=new_leaks, readable_output=readable_output, raw_response=response, ) def format_subdomains_and_emails(api_response: dict) -> str: """ Transforms the API JSON response into two human-readable Markdown tables. """ markdown_outputs = [] subdomains_list = api_response.get("subdomains", []) if subdomains_list: subdomains_table = tableToMarkdown( name="Monitored Subdomains", t=subdomains_list, headers=["subdomain", "description"], headerTransform=pascalToSpace ) markdown_outputs.append(subdomains_table) emails_list = api_response.get("emailAddresses", []) if emails_list: emails_table = tableToMarkdown( name="Monitored Email Addresses", t=emails_list, headers=["email"], headerTransform=string_to_table_header ) markdown_outputs.append(emails_table) if markdown_outputs: return "\n\n".join(markdown_outputs) return "No subdomains or email addresses found." def darkwebscan_getosint_command(client: Client, args: dict) -> CommandResults: """ darkwebscan-getosint Returns OSINT information about the associated domain of a given company_id. """ company_id = get_company_id_arg(args) response = client.get_osint(company_id=company_id) return CommandResults( outputs_prefix="BechtleDarkWebScan.OSINT", outputs_key_field="domain", outputs=response, readable_output=format_subdomains_and_emails(response), raw_response=response, ) def darkwebscan_getemailsecurity_command(client: Client, args: dict) -> CommandResults: """ darkwebscan-getemailsecurity Returns information about the email security for the associated domain of a given company_id. """ company_id = get_company_id_arg(args) response = client.get_email_security(company_id=company_id) spf = response.get("spf", {}) dmarc = response.get("dmarc", {}) dane = response.get("dane", {}) output = { "SPF": {"Record": spf.get("spfRecord"), "Info": spf.get("warning"), "Summary": spf.get("summary")}, "DMARC": {"Record": dmarc.get("dmarcRecord"), "Info": dmarc.get("warning"), "Summary": dmarc.get("summary")}, "DANE": {"EmailHosts": dane.get("emailHosts"), "Info": dane.get("warning"), "Summary": dane.get("summary")}, } summary_rows = [ {"Check": "SPF", "Status": _color_tag(spf.get("summary"), spf.get("warningColor"))}, {"Check": "DMARC", "Status": _color_tag(dmarc.get("summary"), dmarc.get("warningColor"))}, {"Check": "DANE", "Status": _color_tag(dane.get("summary"), dane.get("warningColor"))}, ] # Map the exact keys returned by the API to the markdown table headers readable_output = tableToMarkdown( name="DarkWebScan Email Security", t=summary_rows, headers=["Check", "Status"], removeNull=True, ) return CommandResults( outputs_prefix="BechtleDarkWebScan.EmailSecurity", outputs_key_field="SPF", outputs=output, readable_output=readable_output, raw_response=response, ) def darkwebscan_getwaf_command(client: Client, args: dict) -> CommandResults: """ darkwebscan-getosint Returns OSINT information about the associated domain of a given company_id. """ company_id = get_company_id_arg(args) response = client.get_waf(company_id=company_id) summary = { "product": response.get("product"), "summary": response.get("summary"), "warning": _color_tag(response.get("warning") or "", response.get("warningColor") or ""), } # Map the exact keys returned by the API to the markdown table headers readable_output = tableToMarkdown( name="DarkWebScan Web Application Firewall Status", t=summary, headers=["product", "summary", "warning"], removeNull=True, ) return CommandResults( outputs_prefix="BechtleDarkWebScan.WAF", outputs_key_field="summary", outputs=response, readable_output=readable_output, raw_response=response, ) def fetch_incidents(client: Client, last_run: dict, first_fetch: str, max_fetch: int) -> tuple[dict, list]: parsed_dt = arg_to_datetime(first_fetch) default_since = parsed_dt.strftime("%Y-%m-%dT%H:%M:%SZ") if parsed_dt else "" companies_state = last_run.get("companies", {}) company_ids = _get_all_company_ids(client) incidents: list[dict] = [] next_companies_state = {} for cid in company_ids: demisto.debug(f"cid: {cid}") cid_key = str(cid) state = companies_state.get(cid_key, {}) since = state.get("last_fetch", default_since) seen_ids = set(state.get("seen_ids", [])) demisto.debug(f"Getting leaks for company ID {cid}") response = client.get_leaks(company_id=cid, since=since, page=1, page_size=max_fetch) leaks = response.get("searchResults", []) newest_timestamp = since # demisto.debug("Iterating leaks...") for leak in leaks: leak_id = leak.get("id") if leak_id in seen_ids: demisto.debug("Already seen a leak with this ID. Ignoring...") continue if len(incidents) >= max_fetch: demisto.debug("Reached max_fetch. Skipping...") break alert_json = { "id": leak.get("id", None), "date": leak.get("date", "Unknown"), "domain": leak.get("domain", "Unknown"), "links": leak.get("links", "Unknown"), "username": leak.get("username", "Username not included with initial offer"), "price": leak.get("price", ""), "size": leak.get("size", None), "source": leak.get("source", None), "stealer": leak.get("stealer", None), "dataSource": "BechtleDarkWebScan", } occurred = _to_iso8601(leak.get("date") or None) demisto.debug("Preparing leak info...") incidents.append( { "name": ( f"DarkWebScan Leak: {leak.get('links', 'unknown')} " f"({leak.get('source', leak.get('stealer', 'unknown source'))})" ), "type": "BechtleDarkWebScan Incident", "details": ( f"New Dark Web Leak: {leak.get('links', 'unknown URL')}, " f"Username: {leak.get('username', 'Username not included with initial offer')}, " f"Price: {leak.get('price', 'Unknown')}, " f"Source: {leak.get('source', leak.get('stealer', 'unknown') + ' Stealer')}" ), "occurred": occurred, "severity": 2, "rawJSON": json.dumps(alert_json), } ) demisto.debug("Adding this leak's id to the known leak ids...") seen_ids.add(leak_id) if occurred > newest_timestamp: newest_timestamp = occurred if len(seen_ids) > MAX_SEEN_IDS_PER_COMPANY: seen_ids = set(list(seen_ids)[-MAX_SEEN_IDS_PER_COMPANY:]) demisto.debug("Setting next_companies_state...") next_companies_state[cid_key] = {"last_fetch": newest_timestamp, "seen_ids": list(seen_ids)} return {"companies": next_companies_state}, incidents def darkwebscan_resetcontext_command() -> CommandResults: set_integration_context({}) return CommandResults(readable_output="Integration context cleared.") """ MAIN FUNCTION """ def main() -> None: params = demisto.params() args = demisto.args() command = demisto.command() api_key = params.get("apikey", {}).get("password") if isinstance(params.get("apikey"), dict) else params.get("apikey") additional_request_headers = params.get("addreqheaders") base_url = params.get("url", DEFAULT_BASE_URL).rstrip("/") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) demisto.debug(f"Command being called is {command}") try: client = Client( base_url=base_url, api_key=api_key, additional_request_headers=additional_request_headers, verify=verify_certificate, proxy=proxy, ) if command in ("test-module", ""): return_results(test_module(client)) elif command == "fetch-incidents": first_fetch = params.get("first_fetch", "6 months") max_fetch = arg_to_number(params.get("max_fetch")) or 50 next_run, incidents = fetch_incidents(client, demisto.getLastRun(), first_fetch, max_fetch) demisto.setLastRun(next_run) demisto.incidents(incidents) elif command == "darkwebscan-getcompanies": return_results(darkwebscan_getcompanies_command(client, args)) elif command == "darkwebscan-getleaks": return_results(darkwebscan_getleaks_command(client, args)) elif command == "darkwebscan-getemailsecurity": return_results(darkwebscan_getemailsecurity_command(client, args)) elif command == "darkwebscan-getosint": return_results(darkwebscan_getosint_command(client, args)) elif command == "darkwebscan-getwaf": return_results(darkwebscan_getwaf_command(client, args)) elif command == "darkwebscan-resetcontext": return_results(darkwebscan_resetcontext_command()) else: raise NotImplementedError(f"Command '{command}' is not implemented.") except Exception as e: demisto.error(traceback.format_exc()) return_error(f"Failed to execute {command} command.\nError:\n{str(e)}", error=e) """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()