diff --git a/domaintools/api.py b/domaintools/api.py index 51acd5c..8a00f01 100644 --- a/domaintools/api.py +++ b/domaintools/api.py @@ -5,6 +5,7 @@ import re import ssl +import yaml from domaintools.constants import ( Endpoint, @@ -12,6 +13,7 @@ ENDPOINT_TO_SOURCE_MAP, RTTF_PRODUCTS_LIST, RTTF_PRODUCTS_CMD_MAPPING, + SPECS_MAPPING, ) from domaintools._version import current as version from domaintools.results import ( @@ -22,6 +24,7 @@ Results, FeedsResults, ) +from domaintools.decorators import api_endpoint, auto_patch_docstrings from domaintools.filters import ( filter_by_riskscore, filter_by_expire_date, @@ -40,6 +43,7 @@ def delimited(items, character="|"): return character.join(items) if type(items) in (list, tuple, set) else items +@auto_patch_docstrings class API(object): """Enables interacting with the DomainTools API via Python: @@ -94,8 +98,10 @@ def __init__( self.key_sign_hash = key_sign_hash self.default_parameters["app_name"] = app_name self.default_parameters["app_version"] = app_version + self.specs = {} self._build_api_url(api_url, api_port) + self._initialize_specs() if not https: raise Exception( @@ -104,8 +110,25 @@ def __init__( if proxy_url and not isinstance(proxy_url, str): raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'") + def _initialize_specs(self): + for spec_name, file_path in SPECS_MAPPING.items(): + try: + with open(file_path, "r", encoding="utf-8") as f: + spec_content = yaml.safe_load(f) + if not spec_content: + raise ValueError("Spec file is empty or invalid.") + + self.specs[spec_name] = spec_content + + except Exception as e: + print(f"Error loading {file_path}: {e}") + def _get_ssl_default_context(self, verify_ssl: Union[str, bool]): - return ssl.create_default_context(cafile=verify_ssl) if isinstance(verify_ssl, str) else verify_ssl + return ( + ssl.create_default_context(cafile=verify_ssl) + if isinstance(verify_ssl, str) + else verify_ssl + ) def _build_api_url(self, api_url=None, api_port=None): """Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`""" @@ -133,11 +156,18 @@ def _rate_limit(self, product): hours = limit_hours and 3600 / float(limit_hours) minutes = limit_minutes and 60 / float(limit_minutes) - self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)} + self.limits[product["id"]] = { + "interval": timedelta(seconds=minutes or hours or default) + } def _results(self, product, path, cls=Results, **kwargs): """Returns _results for the specified API path with the specified **kwargs parameters""" - if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits: + if ( + product != "account-information" + and self.rate_limit + and not self.limits_set + and not self.limits + ): always_sign_api_key_previous_value = self.always_sign_api_key header_authentication_previous_value = self.header_authentication self._rate_limit(product) @@ -181,7 +211,9 @@ def handle_api_key(self, is_rttf_product, path, parameters): else: raise ValueError( "Invalid value '{0}' for 'key_sign_hash'. " - "Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)) + "Values available are {1}".format( + self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES) + ) ) parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") @@ -193,7 +225,9 @@ def handle_api_key(self, is_rttf_product, path, parameters): def account_information(self, **kwargs): """Provides a snapshot of your accounts current API usage""" - return self._results("account-information", "/v1/account", items_path=("products",), **kwargs) + return self._results( + "account-information", "/v1/account", items_path=("products",), **kwargs + ) def available_api_calls(self): """Provides a list of api calls that you can use based on your account information.""" @@ -396,7 +430,9 @@ def reputation(self, query, include_reasons=False, **kwargs): def reverse_ip(self, domain=None, limit=None, **kwargs): """Pass in a domain name.""" - return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs) + return self._results( + "reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs + ) def host_domains(self, ip=None, limit=None, **kwargs): """Pass in an IP address.""" @@ -570,8 +606,12 @@ def iris_enrich(self, *domains, **kwargs): younger_than_date = kwargs.pop("younger_than_date", {}) or None older_than_date = kwargs.pop("older_than_date", {}) or None updated_after = kwargs.pop("updated_after", {}) or None - include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None - exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None + include_domains_with_missing_field = ( + kwargs.pop("include_domains_with_missing_field", {}) or None + ) + exclude_domains_with_missing_field = ( + kwargs.pop("exclude_domains_with_missing_field", {}) or None + ) filtered_results = DTResultFilter(result_set=results).by( [ @@ -624,6 +664,7 @@ def iris_enrich_cli(self, domains=None, **kwargs): **kwargs, ) + @api_endpoint(spec_name="iris", path="/v1/iris-investigate/", methods="post") def iris_investigate( self, domains=None, @@ -641,29 +682,6 @@ def iris_investigate( **kwargs, ): """Returns back a list of domains based on the provided filters. - The following filters are available beyond what is parameterized as kwargs: - - - ip: Search for domains having this IP. - - email: Search for domains with this email in their data. - - email_domain: Search for domains where the email address uses this domain. - - nameserver_host: Search for domains with this nameserver. - - nameserver_domain: Search for domains with a nameserver that has this domain. - - nameserver_ip: Search for domains with a nameserver on this IP. - - registrar: Search for domains with this registrar. - - registrant: Search for domains with this registrant name. - - registrant_org: Search for domains with this registrant organization. - - mailserver_host: Search for domains with this mailserver. - - mailserver_domain: Search for domains with a mailserver that has this domain. - - mailserver_ip: Search for domains with a mailserver on this IP. - - redirect_domain: Search for domains which redirect to this domain. - - ssl_hash: Search for domains which have an SSL certificate with this hash. - - ssl_subject: Search for domains which have an SSL certificate with this subject string. - - ssl_email: Search for domains which have an SSL certificate with this email in it. - - ssl_org: Search for domains which have an SSL certificate with this organization in it. - - google_analytics: Search for domains which have this Google Analytics code. - - adsense: Search for domains which have this AdSense code. - - tld: Filter by TLD. Must be combined with another parameter. - - search_hash: Use search hash from Iris to bring back domains. You can loop over results of your investigation as if it was a native Python list: diff --git a/domaintools/base_results.py b/domaintools/base_results.py index 35a6479..29c6704 100644 --- a/domaintools/base_results.py +++ b/domaintools/base_results.py @@ -94,8 +94,7 @@ def _get_session_params_and_headers(self): headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT if self.api.header_authentication: - header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key" - headers[header_key_for_api_key] = self.api.key + headers["X-Api-Key"] = self.api.key session_param_and_headers = {"parameters": parameters, "headers": headers} return session_param_and_headers @@ -342,7 +341,9 @@ def html(self): ) def as_list(self): - return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()]) + return "\n".join( + [json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()] + ) def __str__(self): return str( diff --git a/domaintools/constants.py b/domaintools/constants.py index d1dc8b7..b000a26 100644 --- a/domaintools/constants.py +++ b/domaintools/constants.py @@ -56,3 +56,8 @@ class OutputFormat(Enum): "real-time-domain-discovery-feed-(api)": "domaindiscovery", "real-time-domain-discovery-feed-(s3)": "domaindiscovery", } + +SPECS_MAPPING = { + "iris": "domaintools/specs/iris-openapi.yaml", + # "rttf": "domaintools/specs/feeds-openapi.yaml", +} diff --git a/domaintools/decorators.py b/domaintools/decorators.py new file mode 100644 index 0000000..4640a8e --- /dev/null +++ b/domaintools/decorators.py @@ -0,0 +1,57 @@ +import functools + +from typing import List, Union + +from domaintools.docstring_patcher import DocstringPatcher + + +def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]): + """ + Decorator to tag a method as an API endpoint. + + Args: + spec_name: The key for the spec in api_instance.specs + path: The API path (e.g., "/users") + methods: A single method ("get") or list of methods (["get", "post"]) + that this function handles. + """ + + def decorator(func): + func._api_spec_name = spec_name + func._api_path = path + + # Always store the methods as a list + if isinstance(methods, str): + func._api_methods = [methods] + else: + func._api_methods = methods + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + return func(*args, **kwargs) + + # Copy all tags to the wrapper + wrapper._api_spec_name = func._api_spec_name + wrapper._api_path = func._api_path + wrapper._api_methods = func._api_methods + return wrapper + + return decorator + + +def auto_patch_docstrings(cls): + original_init = cls.__init__ + + @functools.wraps(original_init) + def new_init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + try: + # We instantiate our patcher and run it + patcher = DocstringPatcher() + patcher.patch(self) + except Exception as e: + print(f"Auto-patching failed: {e}") + + cls.__init__ = new_init + + return cls diff --git a/domaintools/docstring_patcher.py b/domaintools/docstring_patcher.py new file mode 100644 index 0000000..45f4f54 --- /dev/null +++ b/domaintools/docstring_patcher.py @@ -0,0 +1,334 @@ +import inspect +import functools +import textwrap +import logging + + +class DocstringPatcher: + """ + Patches docstrings for methods decorated with @api_endpoint. + - Uses the 'methods' list provided by the decorator. + - Finds non-standard parameters inside the 'requestBody' object. + - Displays Query Params, Request Body, and Result Body (Responses) + for all operations. + - Unpacks and displays properties of request body schemas. + - Searches components.parameters for request body properties + that match by name. + """ + + def patch(self, api_instance): + method_names = [] + for attr_name in dir(api_instance): + attr = getattr(api_instance, attr_name) + if ( + inspect.ismethod(attr) + and hasattr(attr, "_api_spec_name") + and hasattr(attr, "_api_path") + and hasattr(attr, "_api_methods") + ): + method_names.append(attr_name) + + for attr_name in method_names: + original_method = getattr(api_instance, attr_name) + original_function = original_method.__func__ + + spec_name = getattr(original_function, "_api_spec_name", None) + path = getattr(original_function, "_api_path", None) + http_methods_to_check = getattr(original_function, "_api_methods", []) + + spec_to_use = api_instance.specs.get(spec_name) + original_doc = inspect.getdoc(original_function) or "" + + all_doc_sections = [] + if spec_to_use: + path_item = spec_to_use.get("paths", {}).get(path, {}) + for http_method in http_methods_to_check: + if http_method.lower() in path_item: + api_doc = self._generate_api_doc_string(spec_to_use, path, http_method) + all_doc_sections.append(api_doc) + + if not all_doc_sections: + all_doc_sections.append( + f"\n--- API Details Error ---" + f"\n (Could not find operations {http_methods_to_check} for path '{path}' in spec '{spec_name}')" + ) + + new_doc = textwrap.dedent(original_doc) + "\n\n" + "\n\n".join(all_doc_sections) + + @functools.wraps(original_function) + def method_wrapper(*args, _orig_meth=original_method, **kwargs): + return _orig_meth(*args, **kwargs) + + method_wrapper.__doc__ = new_doc + setattr( + api_instance, + attr_name, + method_wrapper.__get__(api_instance, api_instance.__class__), + ) + + def _generate_api_doc_string(self, spec: dict, path: str, method: str) -> str: + """Creates the formatted API docstring section for ONE operation.""" + + details = self._get_operation_details(spec, path, method) + lines = [f"--- Operation: {method.upper()} {path} ---"] + + lines.append(f"\n Summary: {details.get('summary', 'N/A')}") + lines.append(f" Description: {details.get('description', 'N/A')}") + lines.append(f" External Doc: {details.get('external_doc', 'N/A')}") + + # 1. Always display Query Parameters + lines.append("\n Query Parameters:") + if not details["query_params"]: + lines.append(" (No query parameters)") + else: + for param in details["query_params"]: + lines.append(f"\n **{param['name']}** ({param['type']})") + lines.append(f" Required: {param['required']}") + lines.append(f" Description: {param['description']}") + + # 2. Always display Request Body + lines.append("\n Request Body:") + if not details["request_body"]: + lines.append(" (No request body)") + else: + body = details["request_body"] + lines.append(f"\n **{body['type']}**") + lines.append(f" Required: {body['required']}") + lines.append(f" Description: {body['description']}") + + if body.get("properties"): + lines.append(f" Properties:") + for prop in body["properties"]: + lines.append(f"\n **{prop['name']}** ({prop['type']})") + lines.append(f" Description: {prop['description']}") + + if body.get("parameters"): + lines.append(f" Parameters (associated with this body):") + for param in body["parameters"]: + param_in = param.get("in", "N/A") + lines.append( + f"\n **{param['name']}** ({param['type']}) [in: {param_in}]" + ) + lines.append(f" Required: {param['required']}") + lines.append(f" Description: {param['description']}") + + # 3. Always display Result Body (Responses) + lines.append("\n Result Body (Responses):") + if not details["responses"]: + lines.append(" (No responses defined in spec)") + else: + for resp in details["responses"]: + lines.append(f"\n **{resp['status_code']}**: ({resp['type']})") + lines.append(f" Description: {resp['description']}") + + return "\n".join(lines) + + def _get_operation_details(self, spec: dict, path: str, method: str) -> dict: + """ + Gets all details. Includes: + - Logic to find non-standard 'parameters' in 'requestBody' + - Logic to parse requestBody schema properties + - Logic to parse responses + - **NEW**: Logic to match requestBody properties to components/parameters + """ + details = {"query_params": [], "request_body": None, "responses": []} + if not spec: + return details + + try: + # --- Get component parameters for lookup --- + components = spec.get("components", {}) + all_component_params = components.get("parameters", {}) + + path_item = spec.get("paths", {}).get(path, {}) + operation = path_item.get(method.lower(), {}) + if not operation: + return details + + # --- Parameter Logic --- + path_level_params = path_item.get("parameters", []) + operation_level_params = operation.get("parameters", []) + body_level_params = [] + + body_def = operation.get("requestBody") + resolved_body_def = {} + if body_def: + if "$ref" in body_def: + resolved_body_def = self._resolve_ref(spec, body_def["$ref"]) + else: + resolved_body_def = body_def + body_level_params = resolved_body_def.get("parameters", []) + + all_param_defs = path_level_params + operation_level_params + # --- End Parameter Logic --- + + details["summary"] = operation.get("summary") + details["description"] = operation.get("description") + details["external_doc"] = operation.get("externalDocs", {}).get("url", "N/A") + + # --- Query Param Processing (from path/operation only) --- + resolved_params = [] + for param_def in all_param_defs: + if "$ref" in param_def: + resolved_params.append(self._resolve_ref(spec, param_def["$ref"])) + else: + resolved_params.append(param_def) + + for p in [p for p in resolved_params if p.get("in") == "query"]: + details["query_params"].append( + { + "name": p.get("name"), + "required": p.get("required", False), + "description": p.get("description", "N/A"), + "type": self._get_param_type(spec, p.get("schema")), + } + ) + # --- End Query Param Processing --- + + # --- Request Body Processing --- + if body_def: + content = resolved_body_def.get("content", {}) + media_type = next(iter(content.values()), None) + schema_type = "N/A" + schema = {} + + if media_type and "schema" in media_type: + schema = media_type["schema"] + schema_type = self._get_param_type(spec, schema) + + details["request_body"] = { + "required": resolved_body_def.get("required", False), + "description": resolved_body_def.get("description", "N/A"), + "type": schema_type, + "parameters": [], + "properties": [], + } + + # --- Process schema properties with new lookup logic --- + resolved_schema = {} + if "$ref" in schema: + resolved_schema = self._resolve_ref(spec, schema["$ref"]) + elif schema.get("type") == "object": + resolved_schema = schema + + if resolved_schema.get("type") == "object" and "properties" in resolved_schema: + for prop_name, prop_def in resolved_schema["properties"].items(): + + found_param_match = False + # --- Try to find a match in components/parameters --- + # (Iterate over values, e.g., the LimitParam object) + for component_param_def in all_component_params.values(): + if component_param_def.get("name") == prop_name: + # Found a match! Use its details. + prop_type = self._get_param_type( + spec, component_param_def.get("schema") + ) + prop_desc = component_param_def.get("description", "N/A") + details["request_body"]["properties"].append( + {"name": prop_name, "type": prop_type, "description": prop_desc} + ) + found_param_match = True + break + + if not found_param_match: + # No match, process as a normal schema property + prop_type = self._get_param_type(spec, prop_def) + prop_desc = prop_def.get("description", "N/A") + details["request_body"]["properties"].append( + {"name": prop_name, "type": prop_type, "description": prop_desc} + ) + + # --- Body Parameter Processing (for non-standard spec) --- + resolved_body_params = [] + for param_def in body_level_params: + if "$ref" in param_def: + resolved_body_params.append(self._resolve_ref(spec, param_def["$ref"])) + else: + resolved_body_params.append(param_def) + + for p in resolved_body_params: + details["request_body"]["parameters"].append( + { + "name": p.get("name"), + "in": p.get("in"), + "required": p.get("required", False), + "description": p.get("description", "N/A"), + "type": self._get_param_type(spec, p.get("schema")), + } + ) + # --- End Request Body Processing --- + + # --- Response Processing Logic --- + responses_def = operation.get("responses", {}) + for status_code, resp_def in responses_def.items(): + resolved_resp = {} + if "$ref" in resp_def: + resolved_resp = self._resolve_ref(spec, resp_def["$ref"]) + else: + resolved_resp = resp_def + + description = resolved_resp.get("description", "N/A") + resp_type = "N/A" + content = resolved_resp.get("content", {}) + media_type = next(iter(content.values()), None) + + if media_type and "schema" in media_type: + schema = media_type["schema"] + resp_type = self._get_param_type(spec, schema) + + details["responses"].append( + { + "status_code": status_code, + "description": description, + "type": resp_type, + } + ) + # --- END: Response Processing Logic --- + + return details + except Exception as e: + logging.warning(f"Error parsing spec for {method.upper()} {path}: {e}", exc_info=True) + return details + + def _resolve_ref(self, spec: dict, ref: str): + """Resolves a JSON schema $ref string.""" + if not spec or not ref.startswith("#/"): + return {} + parts = ref.split("/")[1:] + current_obj = spec + for part in parts: + if isinstance(current_obj, list): + try: + current_obj = current_obj[int(part)] + except (IndexError, ValueError): + return {} + elif isinstance(current_obj, dict): + current_obj = current_obj.get(part) + else: + return {} + if current_obj is None: + return {} + return current_obj + + def _get_param_type(self, spec: dict, schema: dict) -> str: + """Gets a display-friendly type name from a schema object.""" + if not schema: + return "N/A" + + # Check for malformed refs (like in your example spec) + schema_ref = schema.get("$ref") + if not schema_ref: + # Handle user's typo: "$ref:" + schema_ref = schema.get("$ref:") + + if schema_ref: + return schema_ref.split("/")[-1] + + schema_type = schema.get("type", "N/A") + + if schema_type == "array": + items_schema = schema.get("items", {}) + items_type = self._get_param_type(spec, items_schema) + return f"array[{items_type}]" + + return schema_type diff --git a/domaintools/specs/iris-openapi.yaml b/domaintools/specs/iris-openapi.yaml new file mode 100644 index 0000000..f2dddb8 --- /dev/null +++ b/domaintools/specs/iris-openapi.yaml @@ -0,0 +1,2741 @@ +openapi: 3.0.3 +info: + title: DomainTools Iris API + version: 1.0.0 + description: | + The OpenAPI spec for DomainTools Iris endpoints. +servers: + - url: https://api.domaintools.com + description: DomainTools APIs +security: + - header_auth: [] + - open_key_auth: [] + - hmac_auth: [] +tags: + - name: Information + description: | + Access the latest information about your account, including service + limits. + - name: Iris Detect + description: | + Iris Detect is an Internet infrastructure detection, monitoring, and enforcement tool. + It rapidly discovers malicious domains that are engaged in brand impersonation, risk-scores them within minutes, and supports your automation of detection, escalation, and enforcement actions. + - name: Iris Enrich + description: | + Designed to support high query volumes with batch processing and fast response times, the Iris Enrich API provides actionable insights-at-scale with enterprise-scale ingestion of DomainTools data + - name: Iris Investigate + description: | + The Iris Investigate API is ideally suited for investigate and orchestrate use cases at human scale. Identify threats, map adversary infrastructure, and streamline investigations. +paths: + /v1/iris-detect/domains/: + patch: + operationId: patchDetectDomains + summary: Add and remove domains from Watchlist or Ignored lists. + tags: + - Iris Detect + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WatchlistState' + responses: + '200': + $ref: '#/components/responses/DetectWatchlistOk' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#add-remove-from-watchlist + /v1/iris-detect/domains/ignored/: + get: + operationId: getDetectIgnored + summary: Provide ignored domains for active monitors in an account. + tags: + - Iris Detect + parameters: + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/changedSince' + - $ref: '#/components/parameters/discoveredBefore' + - $ref: '#/components/parameters/discoveredSince' + - $ref: '#/components/parameters/domainState' + - $ref: '#/components/parameters/escalatedSince' + - $ref: '#/components/parameters/includeDomainData' + - $ref: '#/components/parameters/irisContainsSearch' + - $ref: '#/components/parameters/irisResultsLimit' + - $ref: '#/components/parameters/monitorId' + - $ref: '#/components/parameters/mxExists' + - $ref: '#/components/parameters/resultsOffset' + - $ref: '#/components/parameters/order' + - $ref: '#/components/parameters/preview' + - $ref: '#/components/parameters/riskScoreRanges' + - $ref: '#/components/parameters/sortDetect' + - $ref: '#/components/parameters/tlds' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DetectDomainList' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#domains + /v1/iris-detect/domains/new/: + get: + operationId: getDetectNewDomains + summary: Provide newly discovered domains for active monitors in an account. + tags: + - Iris Detect + parameters: + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/discoveredBefore' + - $ref: '#/components/parameters/discoveredSince' + - $ref: '#/components/parameters/includeDomainData' + - $ref: '#/components/parameters/irisContainsSearch' + - $ref: '#/components/parameters/irisResultsLimit' + - $ref: '#/components/parameters/monitorId' + - $ref: '#/components/parameters/mxExists' + - $ref: '#/components/parameters/resultsOffset' + - $ref: '#/components/parameters/order' + - $ref: '#/components/parameters/preview' + - $ref: '#/components/parameters/riskScoreRanges' + - $ref: '#/components/parameters/sortDetect' + - $ref: '#/components/parameters/tlds' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DetectDomainList' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#domains + /v1/iris-detect/domains/watched/: + get: + operationId: getDomainsWatched + summary: Provide recently changed or escalated domains for active monitors in an account. + tags: + - Iris Detect + parameters: + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/changedSince' + - $ref: '#/components/parameters/discoveredBefore' + - $ref: '#/components/parameters/discoveredSince' + - $ref: '#/components/parameters/escalatedSince' + - $ref: '#/components/parameters/escalationTypes' + - $ref: '#/components/parameters/includeDomainData' + - $ref: '#/components/parameters/irisContainsSearch' + - $ref: '#/components/parameters/irisResultsLimit' + - $ref: '#/components/parameters/monitorId' + - $ref: '#/components/parameters/mxExists' + - $ref: '#/components/parameters/resultsOffset' + - $ref: '#/components/parameters/order' + - $ref: '#/components/parameters/preview' + - $ref: '#/components/parameters/riskScoreRanges' + - $ref: '#/components/parameters/sortDetect' + - $ref: '#/components/parameters/tlds' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DetectDomainList' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#domains + /v1/iris-detect/escalations/: + post: + operationId: postDetectEscalations + summary: Escalate internally and externally. + tags: + - Iris Detect + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WatchlistEscalation' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Escalations' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#escalate + /v1/iris-detect/monitors/: + get: + operationId: getDetectMonitors + summary: Retrieves monitors and monitor IDs for an account. + tags: + - Iris Detect + parameters: + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/datetimeCountsSince' + - $ref: '#/components/parameters/includeCounts' + - $ref: '#/components/parameters/limitMonitors' + - $ref: '#/components/parameters/resultsOffset' + - $ref: '#/components/parameters/order' + - $ref: '#/components/parameters/sortMonitorList' + responses: + '200': + $ref: '#/components/responses/DetectMonitorSuccess' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + externalDocs: + url: https://docs.domaintools.com/api/iris/detect/reference/#monitor-list + /v1/iris-enrich/: + get: + operationId: getIrisEnrich + summary: Returns results from a GET request to Iris Enrich. + tags: + - Iris Enrich + parameters: + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/domainsQueryRequired' + - $ref: '#/components/parameters/parsedDomainRdapFlag' + - $ref: '#/components/parameters/parsedWhoisFlag' + - $ref: '#/components/parameters/responseFormat' + responses: + '200': + $ref: '#/components/responses/EnrichSuccess' + '206': + $ref: '#/components/responses/IrisPartialContent' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + externalDocs: + url: https://docs.domaintools.com/api/iris/enrich/ + post: + operationId: postIrisEnrich + summary: Returns results from a POST request to Iris Enrich. + tags: + - Iris Enrich + requestBody: + $ref: '#/components/requestBodies/Enrich' + responses: + '200': + $ref: '#/components/responses/EnrichSuccess' + '206': + $ref: '#/components/responses/IrisPartialContent' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + externalDocs: + url: https://docs.domaintools.com/api/iris/enrich/ + /v1/iris-investigate/: + get: + operationId: getIrisInvestigate + summary: | + Returns data from a GET request. + Parameters are identified in descriptions as either base or filter. + Search with base parameters and filter down base results with filter parameters. + tags: + - Iris Investigate + description: | + Consider using POST for complex requests. + parameters: + - $ref: '#/components/parameters/adsense' + - $ref: '#/components/parameters/baiduCode' + - $ref: '#/components/parameters/contactName' + - $ref: '#/components/parameters/contactPhone' + - $ref: '#/components/parameters/contactStreet' + - $ref: '#/components/parameters/domainLastIp' + - $ref: '#/components/parameters/domainsQuery' + - $ref: '#/components/parameters/emailAny' + - $ref: '#/components/parameters/emailDnsSoa' + - $ref: '#/components/parameters/emailDomain' + - $ref: '#/components/parameters/emailHistoricalWhois' + - $ref: '#/components/parameters/facebookCode' + - $ref: '#/components/parameters/googleAnalytics4Code' + - $ref: '#/components/parameters/googleAnalyticsCode' + - $ref: '#/components/parameters/googleTagManagerCode' + - $ref: '#/components/parameters/hotJarCode' + - $ref: '#/components/parameters/ianaId' + - $ref: '#/components/parameters/mailserverDomain' + - $ref: '#/components/parameters/mailserverHost' + - $ref: '#/components/parameters/mailserverIp' + - $ref: '#/components/parameters/matomoCode' + - $ref: '#/components/parameters/nameserverDomain' + - $ref: '#/components/parameters/nameserverHost' + - $ref: '#/components/parameters/nameserverIp' + - $ref: '#/components/parameters/redirectDomain' + - $ref: '#/components/parameters/registrant' + - $ref: '#/components/parameters/registrantHistoricalWhois' + - $ref: '#/components/parameters/registrantOrg' + - $ref: '#/components/parameters/registrar' + - $ref: '#/components/parameters/searchHash' + - $ref: '#/components/parameters/serverType' + - $ref: '#/components/parameters/sslAltNames' + - $ref: '#/components/parameters/sslCommonName' + - $ref: '#/components/parameters/sslDuration' + - $ref: '#/components/parameters/sslEmail' + - $ref: '#/components/parameters/sslHash' + - $ref: '#/components/parameters/sslOrg' + - $ref: '#/components/parameters/sslSubject' + - $ref: '#/components/parameters/statCounterProjectCode' + - $ref: '#/components/parameters/statCounterSecurityCode' + - $ref: '#/components/parameters/taggedWithAll' + - $ref: '#/components/parameters/taggedWithAny' + - $ref: '#/components/parameters/websiteTitle' + - $ref: '#/components/parameters/whoisFreeText' + - $ref: '#/components/parameters/whoisHistoricalFreeText' + - $ref: '#/components/parameters/yandexCode' + - $ref: '#/components/parameters/active' + - $ref: '#/components/parameters/createDate' + - $ref: '#/components/parameters/createDateWithin' + - $ref: '#/components/parameters/expirationDate' + - $ref: '#/components/parameters/firstSeenSince' + - $ref: '#/components/parameters/firstSeenWithin' + - $ref: '#/components/parameters/notTaggedWithAll' + - $ref: '#/components/parameters/notTaggedWithAny' + - $ref: '#/components/parameters/topLevelDomain' + - $ref: '#/components/parameters/parsedDomainRdapFlag' + - $ref: '#/components/parameters/parsedWhoisFlag' + - $ref: '#/components/parameters/nextPageUrl' + - $ref: '#/components/parameters/responseFormat' + - $ref: '#/components/parameters/resultsPageSize' + - $ref: '#/components/parameters/resultsPosition' + - $ref: '#/components/parameters/resultsSortBy' + - $ref: '#/components/parameters/resultsSortDirection' + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appVersion' + responses: + '200': + $ref: '#/components/responses/InvestigateSuccess' + '206': + $ref: '#/components/responses/IrisPartialContent' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + externalDocs: + url: https://docs.domaintools.com/api/iris/investigate/ + post: + operationId: postIrisInvestigate + summary: Returns data from a POST request. + tags: + - Iris Investigate + description: The GET method is available for simple queries. + requestBody: + $ref: '#/components/requestBodies/Investigate' + responses: + '200': + $ref: '#/components/responses/InvestigateSuccess' + '206': + $ref: '#/components/responses/IrisPartialContent' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + externalDocs: + url: https://docs.domaintools.com/api/iris/investigate/ + /v1/account/: + get: + operationId: getAccountInfo + summary: Account Information + description: Information of the active API endpoints, rate limits and usage for an account. + tags: + - Information + parameters: + - $ref: '#/components/parameters/appPartner' + - $ref: '#/components/parameters/appName' + - $ref: '#/components/parameters/appVersion' + - $ref: '#/components/parameters/responseFormat' + responses: + '200': + $ref: '#/components/responses/AccountSuccess' + externalDocs: + url: https://docs.domaintools.com/api/general/account_information/ +components: + parameters: + appName: + name: app_name + in: query + description: | + Appliance, module, or playbook, or any combination of these. + schema: + $ref: '#/components/schemas/IdentifierString' + appPartner: + name: app_partner + in: query + description: | + Your product name. + schema: + $ref: '#/components/schemas/IdentifierString' + appVersion: + name: app_version + in: query + description: | + Version of your integration/connector. + schema: + $ref: '#/components/schemas/IdentifierString' + parsedDomainRdapFlag: + name: parsed_domain_rdap + in: query + description: | + Flag. + If set to `true`, includes the full parsed Domain RDAP record in the response. + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + parsedWhoisFlag: + name: parsed_whois + in: query + description: | + Flag. + If set to 'true', includes the full parsed WHOIS record in the response. + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + responseFormat: + name: format + in: query + description: | + Specifies the desired response format. + schema: + $ref: '#/components/schemas/ResponseFormat' + resultsPageSize: + name: page_size + in: query + description: | + Adjusts the number of results returned per page. + The default is 500. Use this parameter to request a smaller page size. + schema: + type: integer + minimum: 1 + maximum: 500 + offset: + $ref: '#/components/parameters/resultsOffset' + active: + in: query + name: active + description: | + Search filter parameter. + Set to `true` to only return domains that have either an entry in the global Domain Name System, OR are listed as registered by the registry. + Set to `false` to only return domains that do not have an entry in the global DNS, AND are not listed as registered by the registry. + schema: + type: boolean + adsense: + in: query + name: adsense + description: | + Base search parameter. Domains with a Google AdSense tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + baiduCode: + in: query + name: baidu_analytics + description: | + Base search parameter. Baidu Analytics code. + schema: + $ref: '#/components/schemas/IdentifierString' + changedSince: + in: query + name: changed_since + description: | + Most relevant for the `/watched` endpoint to control the timeframe for changes to DNS or WHOIS fields for watched domains. + schema: + $ref: '#/components/schemas/TimestampFilter' + contactName: + in: query + name: contact_name + description: | + Base search parameter. + Contact name from domain registration data. + Supports WHOIS/RDAP Fields Search. + schema: + $ref: '#/components/schemas/GenericString' + contactPhone: + in: query + name: contact_phone + description: | + Base search parameter. + Contact phone number from domain registration data. + Supports WHOIS/RDAP Fields Search. + schema: + $ref: '#/components/schemas/GenericString' + contactStreet: + in: query + name: contact_street + description: | + Base search parameter. + Contact street address from domain registration data. + Supports WHOIS/RDAP Fields Search. + schema: + $ref: '#/components/schemas/GenericString' + createDate: + in: query + name: create_date + description: | + Search filter parameter. + Filters domains based on the `create_date` field. + Supports [`>`, `>=`, `<`, `<=`] operators, or no operator for exact matches. + schema: + $ref: '#/components/schemas/DateOperatorsFilter' + createDateWithin: + in: query + name: create_date_within + description: | + Search filter parameter. + Filter domains based on the `create_date` field: the maximum number of days since a domain was first discovered. + schema: + $ref: '#/components/schemas/MaxDaysSince' + datetimeCountsSince: + in: query + name: datetime_counts_since + description: | + Filters counts to include only those generated since the specified ISO-8601 date-time. + This parameter is conditionally required if `include_counts` is set to `true`. + Example: `2022-02-10T00:00:00Z` + schema: + $ref: '#/components/schemas/TimestampFilter' + discoveredBefore: + in: query + name: discovered_before + description: | + Most relevant for the /new endpoint to control the timeframe for when a new domain was discovered. Returns domains discovered before provided date/time. Use with `discovered_since` to return domains discovered in a specific time window. + Example: `2022-02-10T00:00:00Z (ISO-8601)` + schema: + $ref: '#/components/schemas/TimestampFilter' + discoveredSince: + in: query + name: discovered_since + description: | + Most relevant for the /new endpoint to control the timeframe for when a new domain was discovered. + Example: `2022-02-10T00:00:00Z (ISO-8601)` + schema: + $ref: '#/components/schemas/TimestampFilter' + domainsQuery: + in: query + name: domain + description: | + Base search parameter. + One or more domains (comma-separated) to be investigated. + Example: `example.com,domaintools.com`. + schema: + $ref: '#/components/schemas/ApexDomainList' + domainsQueryRequired: + in: query + name: domain + description: | + Required. One or more domains (comma-separated) to be investigated. + Example: `example.com,domaintools.com` + required: true + schema: + $ref: '#/components/schemas/ApexDomainList' + domainState: + in: query + name: domain_state + description: | + Filters domains based on their state. + schema: + type: string + enum: + - active + - inactive + domainLastIp: + in: query + name: ip + description: | + Base search parameter. + IPv4 address the registered domain was last known to point to during an active DNS check. + required: false + schema: + $ref: '#/components/schemas/IPv4Address' + emailAny: + in: query + name: email + description: | + Base search parameter. + Email address from the most recently available WHOIS record, DNS SOA record or SSL certificate. + schema: + $ref: '#/components/schemas/EmailAddress' + emailDnsSoa: + in: query + name: email_dns_soa + description: | + Base search parameter. DNS SOA email. + schema: + $ref: '#/components/schemas/EmailAddress' + emailDomain: + in: query + name: email_domain + description: Only the domain portion of a WHOIS or DNS SOA email address. + schema: + $ref: '#/components/schemas/ApexDomain' + emailHistoricalWhois: + in: query + name: historical_free_text + description: Free text search of a domain's historical WHOIS records. + schema: + $ref: '#/components/schemas/GenericString' + escalatedSince: + in: query + name: escalated_since + description: 'Most relevant for the /watched endpoint to control the timeframe for when a domain was most recently escalated. Example: 2022-02-10T00:00:00Z (ISO-8601)' + schema: + $ref: '#/components/schemas/TimestampFilter' + escalationTypes: + in: query + name: escalation_types + description: Filters domains based on specific escalation types. Multiple types can be provided. + style: form + explode: true + schema: + $ref: '#/components/schemas/EscalationTypeEnum' + expirationDate: + in: query + name: expiration_date + description: | + Search filter parameter. + Only include domains expiring on a specific date. + schema: + $ref: '#/components/schemas/DateFilter' + facebookCode: + in: query + name: facebook + description: | + Facebook/Meta tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + firstSeenSince: + in: query + name: first_seen_since + description: | + Search filter parameter. + Filter domains based on the `first_seen` timestamp. + Returns domains whose `current_lifecycle_first_seen` value is *after* the given datetime. + schema: + $ref: '#/components/schemas/TimestampFilter' + firstSeenWithin: + in: query + name: first_seen_within + description: | + Search filter parameter. + Filter domains based on the `first_seen` field. + Returns only those domains first discovered within the last N seconds. + schema: + $ref: '#/components/schemas/TimeWindowSeconds' + googleAnalyticsCode: + in: query + name: google_analytics + description: | + Base search parameter. + Domains with a Google Analytics tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + googleAnalytics4Code: + in: query + name: google_analytics_4 + description: | + Base search parameter. + Domains with a Google Analytics tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + googleTagManagerCode: + in: query + name: google_tag_manager + description: | + Base search parameter. + Google Tag Manager tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + hotJarCode: + in: query + name: hotjar + description: | + Base search parameter. + Hotjar tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + ianaId: + in: query + name: iana_id + description: | + Base search parameter. + Registrar IANA code from most recent RDAP record for a domain. + schema: + $ref: '#/components/schemas/IdentifierString' + includeCounts: + in: query + name: include_counts + description: | + If set to `true`, the response will include counts for new, watched, changed, and escalated domains for each monitor. + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + includeDomainData: + in: query + name: include_domain_data + description: | + If set to true, includes additional DNS and WHOIS details in the response. + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + irisResultsLimit: + in: query + name: limit + description: | + Specify the maximum number of records to retrieve in an API query. + The maximum value is **100**, but this is reduced to **50** if `include_domain_data=true`. + schema: + type: integer + maximum: 100 + minimum: 1 + limitMonitors: + in: query + name: limit + description: | + Specifies the maximum number of monitors to retrieve. + The maximum value is 100, but this may be further restricted if `include_counts=true`. + schema: + type: integer + minimum: 1 + maximum: 100 + mailserverDomain: + in: query + name: mailserver_domain + description: | + Base search parameter. + Only the registered domain portion of the mail server (e.g., `domaintools.net`). + schema: + $ref: '#/components/schemas/ApexDomain' + mailserverHost: + in: query + name: mailserver_host + description: | + Base search parameter. + Fully-qualified mail server hostname (e.g., mx.domaintools.net). + Performs a Reverse MX lookup to identify domains using this mail server. + schema: + $ref: '#/components/schemas/Fqdn' + mailserverIp: + in: query + name: mailserver_ip + description: | + Base search parameter. + IP address of the mail server. + schema: + $ref: '#/components/schemas/IPv4Address' + matomoCode: + in: query + name: matomo + description: | + Base search parameter. + Matomo tracking code. + schema: + $ref: '#/components/schemas/IdentifierString' + monitorId: + in: query + name: monitor_id + description: Monitor ID from the monitors response - only used when requesting domains for specific monitors. + schema: + type: string + mxExists: + in: query + name: mx_exists + description: Whether domain currently has an MX record in DNS. + schema: + type: boolean + nameserverHost: + in: query + name: nameserver_host + description: | + Base search parameter. + Fully-qualified domain name (FQDN) of the name server. + required: false + schema: + $ref: '#/components/schemas/Hostname' + nameserverDomain: + in: query + name: nameserver_domain + description: | + Base search parameter. + The registered domain name of the nameserver (e.g., `example.com`). + schema: + $ref: '#/components/schemas/Hostname' + nameserverIp: + in: query + name: nameserver_ip + description: | + Base search parameter. + The IPv4 address of the name server. + schema: + $ref: '#/components/schemas/IPv4Address' + nextPageUrl: + in: query + name: next + description: The URL for the next page of search results, emitted as a `next` field in the response, using the `position` marker. + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + notTaggedWithAll: + in: query + name: not_tagged_with_all + description: | + Search filter parameter. + Exclude all domains that are tagged with **all** of the specified tags in the Iris Investigation platform. + This parameter accepts a comma-separated list of tag names. + schema: + $ref: '#/components/schemas/CommaSeparatedTags' + notTaggedWithAny: + in: query + name: not_tagged_with_any + description: | + Search filter parameter. + Exclude all domains that are tagged with **any** of the specified tags in the Iris Investigation platform. + Accepts a comma-separated list of tags. + schema: + $ref: '#/components/schemas/CommaSeparatedTags' + order: + in: query + name: order + description: Specifies the sort order for the results, either ascending or descending. Used in conjunction with the 'sort' parameter. Defaults to `desc`. + schema: + type: string + enum: + - asc + - desc + preview: + in: query + name: preview + description: Use during API implementation and testing. Will limit results to 10 but not be limited by hourly restrictions. + required: false + schema: + $ref: '#/components/schemas/BooleanOptInFlag' + registrar: + in: query + name: registrar + description: | + Base search parameter. + Exact match to the WHOIS registrar field. + schema: + $ref: '#/components/schemas/OrgNameString' + registrant: + in: query + name: registrant + description: | + Base search parameter. + Exact match to the WHOIS registrant field. + schema: + $ref: '#/components/schemas/OrgNameString' + redirectDomain: + in: query + name: redirect_domain + description: | + Base search parameter. + Domains observed to redirect to another domain name. + schema: + $ref: '#/components/schemas/GenericString' + registrantHistoricalWhois: + in: query + name: historical_registrant + description: | + Base search parameter. + Registrant names from historical WHOIS records. + schema: + $ref: '#/components/schemas/OrgNameString' + registrantOrg: + in: query + name: registrant_org + description: | + Base search parameter. + Exact match to the WHOIS registrant organization field + schema: + $ref: '#/components/schemas/OrgNameString' + resultsPosition: + in: query + name: position + description: | + Cursor for paginated results. Use the value returned in a previous response to retrieve the next page. + required: false + schema: + $ref: '#/components/schemas/PositionToken' + dzzzzzzz: + in: query + name: sort_by + description: | + Specifies the field to sort the results by. + schema: + type: string + enum: + - first_seen_since + - create_date + - domain + - risk_score + resultsSortDirection: + in: query + name: sort_direction + description: | + Determines the sort direction, either ascending or descending. + This is used in combination with the 'sort_by' parameter. + schema: + type: string + enum: + - asc + - dsc + riskScoreRanges: + in: query + name: risk_score_ranges + description: | + Filters domains based on their risk score. Multiple ranges can be selected to broaden the filter. Consult [Domain Risk Scores](https://docs.domaintools.com/riskscore/) for help interpreting scores. + schema: + type: array + items: + type: string + enum: + - 0-0 + - 1-39 + - 40-69 + - 70-99 + - 100-100 + style: form + explode: true + irisContainsSearch: + in: query + name: search + description: Performs a `contains` search. + schema: + $ref: '#/components/schemas/BasicString' + searchHash: + in: query + name: search_hash + description: | + Base search parameter. + Token returned by the Iris Investigate UI when exporting a search. + Use this value to import and continue an existing search via the API. + schema: + $ref: '#/components/schemas/SearchHashToken' + serverType: + in: query + name: server_type + description: | + Base search parameter. + Domains hosted on a specific server type (e.g., from the 'Server' HTTP header). Must be an exact match. + schema: + $ref: '#/components/schemas/BasicString' + sortDetect: + in: query + name: sort + description: Sorts the domain list response. Valid fields to sort by are 'discovered_date', 'changed_date', and 'risk_score'. + schema: + type: array + items: + type: string + enum: + - discovered_date + - changed_date + - risk_score + style: form + explode: true + sortMonitorList: + in: query + name: sort[] + description: Provides options for sorting the monitor list. + schema: + type: string + enum: + - term + - created_date + - domain_counts_changed + - domain_counts_discovered + sslAltNames: + in: query + name: ssl_alt_names + description: | + Base search parameter. + Domains with a matching Subject Alternative Name (SAN) in their SSL certificate. + schema: + $ref: '#/components/schemas/GenericString' + sslCommonName: + in: query + name: ssl_common_name + description: | + Base search parameter. + Domains with a matching Common Name (CN) in their SSL certificate's subject. + schema: + $ref: '#/components/schemas/GenericString' + sslDuration: + in: query + name: ssl_duration + description: | + Base search parameter. + Domains with an SSL certificate valid for a specific number of days. + schema: + type: integer + minimum: 1 + sslEmail: + in: query + name: ssl_email + description: | + Base search parameter. + Email address extracted from the SSL certificate associated with a domain. + schema: + $ref: '#/components/schemas/EmailAddress' + sslHash: + in: query + name: ssl_hash + description: | + Base search parameter. + The SHA-1 hash of an SSL certificate, used to filter for domains associated with a specific certificate. + schema: + $ref: '#/components/schemas/Sha1HexString' + sslOrg: + in: query + name: ssl_org + description: | + Base search parameter. + Organization name from the SSL certificate. Must be an exact string match. + schema: + $ref: '#/components/schemas/OrgNameString' + sslSubject: + in: query + name: ssl_subject + description: | + Base search parameter. + Exact match to the Subject distinguished name (DN) string from the SSL certificate. + schema: + $ref: '#/components/schemas/SslSubjectDnString' + statCounterProjectCode: + in: query + name: statcounter_project + description: | + Base search parameter. + Statcounter Project tracker code. + schema: + $ref: '#/components/schemas/IdentifierString' + statCounterSecurityCode: + in: query + name: statcounter_security + description: | + Base search parameter. + Statcounter Security tracker code. + schema: + $ref: '#/components/schemas/IdentifierString' + taggedWithAll: + in: query + name: tagged_with_all + description: | + Search filter parameter. + Comma-separated list of tags. Only returns domains tagged with the full list of tags. + schema: + $ref: '#/components/schemas/GenericString' + taggedWithAny: + in: query + name: tagged_with_any + description: | + Search filter parameter. + Comma-separated list of Iris Investigate tags. Returns domains tagged with any of the tags in a list. + schema: + $ref: '#/components/schemas/GenericString' + tlds: + in: query + name: tlds + description: Filters on specific TLDs + schema: + type: array + items: + type: string + allowReserved: true + style: form + explode: true + topLevelDomain: + in: query + name: tld + description: | + Search filter parameter. + Restrict results to domains under the specified top-level domain (TLD). + schema: + $ref: '#/components/schemas/TopLevelDomain' + websiteTitle: + in: query + name: website_title + description: | + Base search parameter. + The value of the website’s `` HTML tag. Must be an exact match. + schema: + $ref: '#/components/schemas/WebsiteTitleString' + whoisFreeText: + in: query + name: whois + description: | + Base search parameter. + Free text search of a domain's most recent WHOIS record. + schema: + $ref: '#/components/schemas/GenericString' + whoisHistoricalFreeText: + in: query + name: historical_free_text + description: | + Base search parameter. + Free text search of a domain's historical WHOIS records. + schema: + $ref: '#/components/schemas/GenericString' + yandexCode: + in: query + name: yandex_metrica + description: | + Base search parameter. + Yandex Metrica tracker code. + schema: + $ref: '#/components/schemas/IdentifierString' + resultsOffset: + name: offset + in: query + description: Specifies the starting point for the result set, used for paginating when the number of results exceeds the 'limit'. An offset of 0 starts from the first result. + schema: + type: integer + minimum: 0 + requestBodies: + Enrich: + description: A request to the Iris Enrich endpoint. + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/EnrichRequestParameters' + Investigate: + description: A request to the Iris Investigate endpoint. + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/InvestigateRequestParameters' + responses: + DetectMonitorSuccess: + description: OK. A list of monitors was successfully retrieved. + content: + application/json: + schema: + $ref: '#/components/schemas/MonitorList' + DetectWatchlistOk: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Watchlist' + EnrichSuccess: + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: object + properties: + limit_exceeded: + type: boolean + message: + type: string + results_count: + type: integer + has_more_results: + type: boolean + results: + type: array + items: + $ref: '#/components/schemas/EnrichResult' + missing_domains: + type: array + items: + $ref: '#/components/schemas/ApexDomain' + InvestigateSuccess: + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: object + properties: + limit_exceeded: + type: boolean + has_more_results: + type: boolean + message: + type: string + results_count: + type: integer + total_count: + type: integer + results: + type: array + items: + $ref: '#/components/schemas/InvestigateResult' + missing_domains: + type: array + items: + $ref: '#/components/schemas/ApexDomain' + IrisBadRequest: + $ref: '#/components/responses/BadRequest' + IrisForbidden: + $ref: '#/components/responses/Forbidden' + IrisInternalServerError: + $ref: '#/components/responses/InternalServerError' + IrisNotFound: + $ref: '#/components/responses/NotFound' + IrisPartialContent: + description: '206: Partial Content. The response is a subset of the full results. The `has_more_results` field in the response will be `true`.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + IrisServiceUnavailable: + $ref: '#/components/responses/ServiceUnavailable' + IrisUnauthorized: + $ref: '#/components/responses/Unauthorized' + AccountSuccess: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AccountInformation' + BadRequest: + description: '400: Bad Request. The request was invalid. The response will contain details about the error.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Unauthorized: + description: '401: Unauthorized. API credentials are required and were not provided, or the provided credentials are not valid.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Forbidden: + description: '403: Forbidden. The API credentials provided do not have access to the requested resource or endpoint.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + NotFound: + description: '404: Not Found. The requested resource does not exist.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + InternalServerError: + description: '500: Internal Server Error. An unexpected error occurred on the server. If the problem persists, contact DomainTools support.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + ServiceUnavailable: + description: '503: Service Unavailable. The service is temporarily unavailable.' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + schemas: + ApexDomain: + type: string + description: A base (apex) domain like example.com, excluding subdomains. + pattern: ^(?!\-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,63}$ + BasicString: + type: string + minLength: 1 + maxLength: 255 + description: A non-empty string with a standard length limit. + BooleanOptInFlag: + type: boolean + description: | + An optional boolean flag that enables a specific feature or response extension when set to `true`. If `false` or omitted, the feature is ignored. + ErrorResponse: + type: object + properties: + error: + type: object + properties: + code: + type: integer + description: An internal error code. + message: + type: string + description: A human-readable error message. + Hostname: + type: string + format: hostname + description: A valid, fully-qualified domain name (FQDN). + IdentifierString: + type: string + minLength: 1 + maxLength: 128 + description: A short identifier, code, or handle. + IPv4Address: + type: string + format: ipv4 + description: A standard IPv4 address. + ResponseFormat: + type: string + enum: + - html + - json + - xml + description: | + The desired response format. + AccountInformation: + type: object + description: Contains the account details and a list of product subscriptions. + properties: + response: + type: object + properties: + account: + $ref: '#/components/schemas/AccountInfoAccount' + products: + type: array + description: A list of product subscriptions including usage limits and expiration dates. + items: + type: object + description: Details for a single product subscription. Keys include id, rate limits, usage, and expiration_date. + example: + id: domain-profile + per_month_limit: '50000' + per_hour_limit: null + per_minute_limit: '120' + absolute_limit: null + usage: + today: '0' + month: '1' + expiration_date: '2027-12-30' + AccountInfoAccount: + type: object + description: Basic details about the API account. + properties: + api_username: + type: string + description: The username associated with the account. + example: bdfidler + active: + type: boolean + description: Indicates if the account is currently active. + example: true + ApexDomainList: + type: string + pattern: ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(,([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})*$ + description: A comma-separated list of one or more apex domains to investigate. + CommaSeparatedTags: + type: string + description: | + A comma-separated list of tag names. Tags are matched exactly and compared case-insensitively unless otherwise specified. + maxLength: 512 + DateFilter: + type: string + format: date + description: | + A calendar date in the format YYYY-MM-DD, per RFC 3339 (full-date). Does not include a time or timezone. + DateOperatorsFilter: + type: string + description: | + A date in YYYY-MM-DD format, optionally prefixed with an operator (`>`, `>=`, `<`, `<=`) for comparison. + Example: `<=2025-01-01` + pattern: ^(>|>=|<|<=)?\d{4}-\d{2}-\d{2}$ + DetectDomain: + type: object + properties: + state: + type: string + domain: + type: string + status: + type: string + enum: + - active + - inactive + discovered_date: + type: string + format: date-time + changed_date: + type: string + format: date-time + risk_score: + type: integer + risk_score_status: + description: Indicates if the risk score is 'provisional' (initial calculation) or full. + type: string + risk_score_components: + $ref: '#/components/schemas/DetectRiskComponents' + mx_exists: + type: boolean + tld: + type: string + id: + type: string + escalations: + type: array + items: + $ref: '#/components/schemas/DetectEscalation' + monitor_ids: + type: array + items: + type: string + name_server: + type: array + items: + type: object + properties: + host: + type: string + registrant_contact_email: + type: array + items: + type: string + registrar: + type: string + create_date: + type: integer + description: Domain creation date in YYYYMMDD format. + ip: + type: array + items: + type: object + properties: + country_code: + type: string + ip: + type: string + isp: + type: string + mx: + type: array + items: + type: object + properties: + host: + type: string + DetectDomainList: + type: object + properties: + watchlist_domains: + type: array + items: + $ref: '#/components/schemas/DetectDomain' + total_count: + type: integer + count: + type: integer + offset: + type: integer + limit: + type: integer + DetectEscalation: + type: object + properties: + escalation_type: + $ref: '#/components/schemas/EscalationTypeEnum' + id: + type: string + created: + type: string + format: date-time + created_by: + type: string + DetectRiskComponents: + type: object + properties: + proximity: + type: integer + threat_profile: + $ref: '#/components/schemas/DetectThreatProfile' + DetectThreatProfile: + type: object + properties: + phishing: + type: integer + malware: + type: integer + spam: + type: integer + EmailAddress: + type: string + format: email + maxLength: 254 + description: | + A valid email address, as defined by RFC 5322 and commonly used in WHOIS, SSL certificates, RDAP records, or domain registration contacts. + Escalations: + type: object + properties: + watchlist_domains: + type: array + items: + type: object + properties: + state: + type: string + domain: + type: string + discovered_date: + type: string + changed_date: + type: string + id: + type: string + assigned_by: + type: string + assigned_date: + type: string + EscalationTypeEnum: + type: string + description: The type of escalation action. + enum: + - blocked + - google_safe + Fqdn: + type: string + format: hostname + description: | + A Fully Qualified Domain Name (FQDN), including subdomains (e.g., www.example.com). Must be a valid DNS hostname per RFC 1123, excluding trailing dot. + minLength: 1 + maxLength: 253 + pattern: ^(?=.{1,253}$)(?:(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$ + GenericString: + type: string + description: A generic and unconstrained string value. + MaxDaysSince: + type: integer + minimum: 1 + description: A positive integer representing the maximum number of days. + OrgNameString: + type: string + minLength: 1 + maxLength: 256 + pattern: ^[\p{L}\p{N} .,\-&()'/"]+$ + description: | + A human-readable organization name as used in WHOIS or RDAP records, such as a registrar, registrant, or sponsoring organization. Supports Unicode letters and numbers, along with common punctuation. + PositionToken: + type: string + description: | + An opaque string token used for cursor-based pagination. + Returned in the `position` field of a paginated response when `has_more_results` is true, and used as a query parameter to retrieve the next result set. + Example: `2c056abadfb64b67ba18896af2c5b900` + RiskScoreValue: + type: integer + minimum: 0 + maximum: 99 + SearchHashToken: + type: string + description: | + Opaque token representing a saved search from the Iris Investigate UI. + Exported via “Search → Export” in the Iris UI. + Example: `aGVsbG93b3yY2hfaGFzaF9jb2RlJsZF9zZWFcw==` + Sha1HexString: + type: string + description: | + A 40-character hexadecimal SHA-1 hash string, typically used to identify SSL certificates or digital signatures. + pattern: ^[a-fA-F0-9]{40}$ + SslSubjectDnString: + type: string + minLength: 1 + maxLength: 1024 + description: | + A distinguished name (DN) string representing the Subject field of an SSL certificate. + Typically includes components like CN, O, C, ST, and L in a comma-separated format. + Example: `C=US, ST=California, L=San Francisco, O=Example Corp, CN=example.com` + TimestampFilter: + type: string + format: date-time + description: | + An RFC 3339-compliant timestamp (e.g., `2025-01-01T00:00:00Z`) used to filter results by datetime thresholds. + TimeWindowSeconds: + type: integer + format: int64 + minimum: 1 + description: | + A positive integer representing a time window, in seconds. Used to filter results based on how recently an event occurred. + TopLevelDomain: + type: string + minLength: 2 + maxLength: 128 + pattern: ^[a-z]{2,63}(\.[a-z]{2,63})*$ + description: | + A top-level or public suffix domain, such as `com`, `org`, or `co.uk`. Must not include a leading dot. + WebsiteTitleString: + type: string + minLength: 1 + maxLength: 512 + description: | + The exact value of the `<title>` tag from a website's HTML. + DomainProfileTemplate: + type: object + description: A comprehensive template defining the complete structure of a domain profile. Specific data types are defined in inheriting schemas. + properties: + domain: + type: string + whois_url: + type: string + adsense: {} + alexa: + type: integer + popularity_rank: + type: number + active: + type: boolean + google_analytics: {} + ga4: + type: array + items: {} + gtm_codes: + type: array + items: {} + fb_codes: + type: array + items: {} + hotjar_codes: + type: array + items: {} + baidu_codes: + type: array + items: {} + yandex_codes: + type: array + items: {} + matomo_codes: + type: array + items: {} + statcounter_project_codes: + type: array + items: {} + statcounter_security_codes: + type: array + items: {} + admin_contact: + $ref: '#/components/schemas/BaseContact' + billing_contact: + $ref: '#/components/schemas/BaseContact' + registrant_contact: + $ref: '#/components/schemas/BaseContact' + technical_contact: + $ref: '#/components/schemas/BaseContact' + create_date: {} + expiration_date: {} + email_domain: + type: array + items: {} + soa_email: + type: array + items: {} + ssl_email: + type: array + items: {} + additional_whois_email: + type: array + items: {} + ip: + type: array + items: + type: object + properties: + address: {} + asn: + type: array + items: {} + country_code: {} + isp: {} + mx: + type: array + items: + type: object + properties: + host: {} + domain: {} + ip: + type: array + items: {} + priority: + type: number + name_server: + type: array + items: + type: object + properties: + host: {} + domain: {} + ip: + type: array + items: {} + domain_risk: + $ref: '#/components/schemas/DomainRisk' + redirect: {} + redirect_domain: {} + registrant_name: {} + registrant_org: {} + registrar: {} + registrar_status: + type: array + items: + type: string + spf_info: + type: string + ssl_info: + type: array + items: + $ref: '#/components/schemas/BaseSslInfo' + tld: + type: string + website_response: + type: number + data_updated_timestamp: + $ref: '#/components/schemas/TimestampFilter' + website_title: {} + server_type: {} + first_seen: {} + tags: + type: array + items: + type: object + properties: + label: + type: string + scope: + type: string + tagged_at: + type: string + parsed_whois: + $ref: '#/components/schemas/ParsedWhois' + parsed_domain_rdap: + $ref: '#/components/schemas/ParsedDomainRdap' + BaseContact: + type: object + description: A template for contact information blocks. + properties: + name: {} + org: {} + street: {} + city: {} + state: {} + postal: {} + country: {} + phone: {} + fax: {} + email: + type: array + items: {} + BaseSslInfo: + type: object + description: A template for SSL certificate information blocks. + properties: + hash: {} + subject: {} + organization: {} + email: + type: array + items: + type: string + alt_names: + type: array + items: {} + sources: + type: object + properties: + active: + type: integer + passive: + type: integer + common_name: {} + issuer_common_name: {} + not_after: {} + not_before: {} + duration: {} + DomainRisk: + type: object + properties: + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + components: + type: array + description: A list of risk components and their individual scores that contribute to the overall domain risk. + items: + $ref: '#/components/schemas/DomainRiskComponent' + DomainRiskComponent: + description: A polymorphic schema representing one of several types of risk components. + oneOf: + - $ref: '#/components/schemas/Proximity' + - $ref: '#/components/schemas/ThreatProfile' + - $ref: '#/components/schemas/ThreatProfileMalware' + - $ref: '#/components/schemas/ThreatProfilePhishing' + - $ref: '#/components/schemas/ThreatProfileSpam' + - $ref: '#/components/schemas/ZeroListedScore' + discriminator: + propertyName: name + mapping: + proximity: '#/components/schemas/Proximity' + threat_profile: '#/components/schemas/ThreatProfile' + threat_profile_malware: '#/components/schemas/ThreatProfileMalware' + threat_profile_phishing: '#/components/schemas/ThreatProfilePhishing' + threat_profile_spam: '#/components/schemas/ThreatProfileSpam' + zerolist: '#/components/schemas/ZeroListedScore' + EnrichValue: + type: object + properties: + value: + type: string + EnrichTracker: + type: object + description: A tracker identifier. + properties: + value: + $ref: '#/components/schemas/IdentifierString' + EnrichRequestParameters: + type: object + required: + - domain + properties: + domain: + $ref: '#/components/parameters/domainsQueryRequired/schema' + app_partner: + $ref: '#/components/schemas/IdentifierString' + app_name: + $ref: '#/components/schemas/IdentifierString' + app_version: + $ref: '#/components/schemas/IdentifierString' + format: + $ref: '#/components/schemas/ResponseFormat' + parsed_whois: + $ref: '#/components/schemas/BooleanOptInFlag' + parsed_domain_rdap: + $ref: '#/components/schemas/BooleanOptInFlag' + EnrichResult: + allOf: + - $ref: '#/components/schemas/DomainProfileTemplate' + properties: + adsense: + $ref: '#/components/schemas/EnrichTracker' + google_analytics: + $ref: '#/components/schemas/EnrichTracker' + ga4: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + gtm_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + fb_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + hotjar_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + baidu_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + yandex_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + matomo_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + statcounter_project_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + statcounter_security_codes: + type: array + items: + $ref: '#/components/schemas/EnrichTracker' + admin_contact: + $ref: '#/components/schemas/EnrichContact' + billing_contact: + $ref: '#/components/schemas/EnrichContact' + registrant_contact: + $ref: '#/components/schemas/EnrichContact' + technical_contact: + $ref: '#/components/schemas/EnrichContact' + create_date: + $ref: '#/components/schemas/EnrichValue' + expiration_date: + $ref: '#/components/schemas/EnrichValue' + email_domain: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + soa_email: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + ssl_email: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + additional_whois_email: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + ip: + type: array + items: + type: object + properties: + address: + $ref: '#/components/schemas/EnrichValue' + asn: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + country_code: + $ref: '#/components/schemas/EnrichValue' + isp: + $ref: '#/components/schemas/EnrichValue' + mx: + type: array + items: + type: object + properties: + host: + $ref: '#/components/schemas/EnrichValue' + domain: + $ref: '#/components/schemas/EnrichValue' + ip: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + priority: + type: number + name_server: + type: array + items: + type: object + properties: + host: + $ref: '#/components/schemas/EnrichValue' + domain: + $ref: '#/components/schemas/EnrichValue' + ip: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + redirect: + $ref: '#/components/schemas/EnrichValue' + redirect_domain: + $ref: '#/components/schemas/EnrichValue' + registrant_name: + $ref: '#/components/schemas/EnrichValue' + registrant_org: + $ref: '#/components/schemas/EnrichValue' + registrar: + $ref: '#/components/schemas/EnrichValue' + server_type: + $ref: '#/components/schemas/EnrichValue' + first_seen: + $ref: '#/components/schemas/EnrichValue' + website_title: + $ref: '#/components/schemas/EnrichValue' + ssl_info: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseSslInfo' + properties: + hash: + $ref: '#/components/schemas/EnrichValue' + subject: + $ref: '#/components/schemas/EnrichValue' + organization: + $ref: '#/components/schemas/EnrichValue' + alt_names: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + common_name: + $ref: '#/components/schemas/EnrichValue' + issuer_common_name: + $ref: '#/components/schemas/EnrichValue' + not_after: + type: integer + not_before: + type: integer + duration: + type: integer + EnrichContact: + allOf: + - $ref: '#/components/schemas/BaseContact' + properties: + name: + $ref: '#/components/schemas/EnrichValue' + org: + $ref: '#/components/schemas/EnrichValue' + street: + $ref: '#/components/schemas/EnrichValue' + city: + $ref: '#/components/schemas/EnrichValue' + state: + $ref: '#/components/schemas/EnrichValue' + postal: + $ref: '#/components/schemas/EnrichValue' + country: + $ref: '#/components/schemas/EnrichValue' + phone: + $ref: '#/components/schemas/EnrichValue' + fax: + $ref: '#/components/schemas/EnrichValue' + email: + type: array + items: + $ref: '#/components/schemas/EnrichValue' + PivotedValue: + type: object + properties: + value: + type: string + count: + type: integer + format: int64 + description: The number of other domains that share this exact value. + PivotedTracker: + type: object + description: A tracker identifier and its associated pivot count. + properties: + value: + $ref: '#/components/schemas/IdentifierString' + count: + type: integer + format: int64 + description: The number of other domains that share this exact value. + InvestigateResult: + allOf: + - $ref: '#/components/schemas/DomainProfileTemplate' + properties: + adsense: + $ref: '#/components/schemas/PivotedTracker' + google_analytics: + $ref: '#/components/schemas/PivotedTracker' + ga4: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + gtm_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + fb_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + hotjar_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + baidu_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + yandex_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + matomo_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + statcounter_project_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + statcounter_security_codes: + type: array + items: + $ref: '#/components/schemas/PivotedTracker' + admin_contact: + $ref: '#/components/schemas/PivotedContact' + billing_contact: + $ref: '#/components/schemas/PivotedContact' + registrant_contact: + $ref: '#/components/schemas/PivotedContact' + technical_contact: + $ref: '#/components/schemas/PivotedContact' + create_date: + $ref: '#/components/schemas/PivotedValue' + expiration_date: + $ref: '#/components/schemas/PivotedValue' + email_domain: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + soa_email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + ssl_email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + additional_whois_email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + ip: + type: array + items: + type: object + properties: + address: + $ref: '#/components/schemas/PivotedValue' + asn: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + country_code: + $ref: '#/components/schemas/PivotedValue' + isp: + $ref: '#/components/schemas/PivotedValue' + mx: + type: array + items: + type: object + properties: + host: + $ref: '#/components/schemas/PivotedValue' + domain: + $ref: '#/components/schemas/PivotedValue' + ip: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + priority: + type: number + name_server: + type: array + items: + type: object + properties: + host: + $ref: '#/components/schemas/PivotedValue' + domain: + $ref: '#/components/schemas/PivotedValue' + ip: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + redirect: + $ref: '#/components/schemas/PivotedValue' + redirect_domain: + $ref: '#/components/schemas/PivotedValue' + registrant_name: + $ref: '#/components/schemas/PivotedValue' + registrant_org: + $ref: '#/components/schemas/PivotedValue' + registrar: + $ref: '#/components/schemas/PivotedValue' + server_type: + $ref: '#/components/schemas/PivotedValue' + first_seen: + $ref: '#/components/schemas/PivotedValue' + website_title: + $ref: '#/components/schemas/PivotedValue' + ssl_info: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseSslInfo' + properties: + hash: + $ref: '#/components/schemas/PivotedValue' + subject: + $ref: '#/components/schemas/PivotedValue' + organization: + $ref: '#/components/schemas/PivotedValue' + alt_names: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + common_name: + $ref: '#/components/schemas/PivotedValue' + issuer_common_name: + $ref: '#/components/schemas/PivotedValue' + not_after: + $ref: '#/components/schemas/PivotedValue' + not_before: + $ref: '#/components/schemas/PivotedValue' + duration: + $ref: '#/components/schemas/PivotedValue' + Monitor: + type: object + description: Details of a single Iris Detect monitor. + properties: + term: + type: string + match_substring_variations: + type: boolean + nameserver_exclusions: + type: array + items: + type: string + text_exclusions: + type: array + items: + type: string + id: + type: string + created_date: + type: string + format: date-time + updated_date: + type: string + format: date-time + state: + type: string + enum: + - active + - inactive + status: + type: string + domain_counts: + $ref: '#/components/schemas/MonitorDomainCounts' + created_by: + type: string + MonitorDomainCounts: + type: object + description: Counts of domains associated with a monitor in various states. + properties: + new: + type: integer + description: The number of new domains discovered. + watched: + type: integer + description: The number of domains currently on the watchlist. + escalated: + type: integer + description: The number of domains that have been escalated. + changed: + type: integer + description: The number of watched domains that have changed. + MonitorList: + type: object + description: A list of Iris Detect monitors and associated metadata. + properties: + total_count: + type: integer + offset: + type: integer + limit: + type: integer + monitors: + type: array + items: + $ref: '#/components/schemas/Monitor' + PivotedContact: + allOf: + - $ref: '#/components/schemas/BaseContact' + properties: + name: + $ref: '#/components/schemas/PivotedValue' + org: + $ref: '#/components/schemas/PivotedValue' + street: + $ref: '#/components/schemas/PivotedValue' + city: + $ref: '#/components/schemas/PivotedValue' + state: + $ref: '#/components/schemas/PivotedValue' + postal: + $ref: '#/components/schemas/PivotedValue' + country: + $ref: '#/components/schemas/PivotedValue' + phone: + $ref: '#/components/schemas/PivotedValue' + fax: + $ref: '#/components/schemas/PivotedValue' + email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + InvestigateRequestParameters: + type: object + properties: + adsense: + $ref: '#/components/schemas/IdentifierString' + baidu_analytics: + $ref: '#/components/schemas/IdentifierString' + contact_name: + $ref: '#/components/parameters/contactName/schema' + contact_phone: + $ref: '#/components/parameters/contactPhone/schema' + contact_street: + $ref: '#/components/parameters/contactStreet/schema' + ip: + $ref: '#/components/schemas/IPv4Address' + domain: + $ref: '#/components/parameters/domainsQuery/schema' + email: + $ref: '#/components/parameters/emailAny/schema' + email_dns_soa: + $ref: '#/components/parameters/emailDnsSoa/schema' + email_domain: + $ref: '#/components/schemas/ApexDomain' + historical_free_text: + $ref: '#/components/parameters/whoisHistoricalFreeText/schema' + facebook: + $ref: '#/components/schemas/IdentifierString' + google_analytics_4: + $ref: '#/components/schemas/IdentifierString' + google_analytics: + $ref: '#/components/schemas/IdentifierString' + google_tag_manager: + $ref: '#/components/schemas/IdentifierString' + hotjar: + $ref: '#/components/schemas/IdentifierString' + iana_id: + $ref: '#/components/schemas/IdentifierString' + mailserver_domain: + $ref: '#/components/schemas/ApexDomain' + mailserver_host: + $ref: '#/components/parameters/mailserverHost/schema' + mailserver_ip: + $ref: '#/components/schemas/IPv4Address' + matomo: + $ref: '#/components/schemas/IdentifierString' + nameserver_domain: + $ref: '#/components/schemas/Hostname' + nameserver_host: + $ref: '#/components/schemas/Hostname' + nameserver_ip: + $ref: '#/components/schemas/IPv4Address' + redirect_domain: + $ref: '#/components/parameters/redirectDomain/schema' + registrant: + $ref: '#/components/parameters/registrant/schema' + historical_registrant: + $ref: '#/components/parameters/registrantHistoricalWhois/schema' + registrant_org: + $ref: '#/components/parameters/registrantOrg/schema' + registrar: + $ref: '#/components/parameters/registrar/schema' + search_hash: + $ref: '#/components/parameters/searchHash/schema' + server_type: + $ref: '#/components/schemas/BasicString' + ssl_alt_names: + $ref: '#/components/parameters/sslAltNames/schema' + ssl_common_name: + $ref: '#/components/parameters/sslCommonName/schema' + ssl_duration: + $ref: '#/components/parameters/sslDuration/schema' + ssl_email: + $ref: '#/components/parameters/sslEmail/schema' + ssl_hash: + $ref: '#/components/parameters/sslHash/schema' + ssl_org: + $ref: '#/components/parameters/sslOrg/schema' + ssl_subject: + $ref: '#/components/parameters/sslSubject/schema' + statcounter_project: + $ref: '#/components/schemas/IdentifierString' + statcounter_security: + $ref: '#/components/schemas/IdentifierString' + tagged_with_all: + $ref: '#/components/parameters/taggedWithAll/schema' + tagged_with_any: + $ref: '#/components/parameters/taggedWithAny/schema' + website_title: + $ref: '#/components/parameters/websiteTitle/schema' + whois: + $ref: '#/components/parameters/whoisFreeText/schema' + yandex_metrica: + $ref: '#/components/schemas/IdentifierString' + active: + $ref: '#/components/parameters/active/schema' + create_date: + $ref: '#/components/parameters/createDate/schema' + create_date_within: + $ref: '#/components/parameters/createDateWithin/schema' + expiration_date: + $ref: '#/components/parameters/expirationDate/schema' + first_seen_since: + $ref: '#/components/parameters/firstSeenSince/schema' + first_seen_within: + $ref: '#/components/parameters/firstSeenWithin/schema' + not_tagged_with_all: + $ref: '#/components/parameters/notTaggedWithAll/schema' + not_tagged_with_any: + $ref: '#/components/parameters/notTaggedWithAny/schema' + tld: + $ref: '#/components/parameters/topLevelDomain/schema' + parsed_domain_rdap: + $ref: '#/components/schemas/BooleanOptInFlag' + parsed_whois: + $ref: '#/components/schemas/BooleanOptInFlag' + next: + $ref: '#/components/schemas/BooleanOptInFlag' + format: + $ref: '#/components/schemas/ResponseFormat' + page_size: + $ref: '#/components/schemas/schema' + position: + $ref: '#/components/parameters/resultsPosition/schema' + sort_by: + $ref: '#/components/parameters/resultsSortBy/schema' + sort_direction: + $ref: '#/components/parameters/resultsSortDirection/schema' + app_name: + $ref: '#/components/schemas/IdentifierString' + app_partner: + $ref: '#/components/schemas/IdentifierString' + app_version: + $ref: '#/components/schemas/IdentifierString' + ParsedDomainRdap: + type: object + description: Parsed data from the domain's RDAP record. Present only when the request includes `parsed_domain_rdap=true`. + properties: + registrant_contact: + $ref: '#/components/schemas/RdapContact' + admin_contact: + $ref: '#/components/schemas/RdapContact' + technical_contact: + $ref: '#/components/schemas/RdapContact' + registrant_name: + $ref: '#/components/schemas/PivotedValue' + registrar_iana_id: + $ref: '#/components/schemas/PivotedValue' + additional_whois_email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + registrar: + $ref: '#/components/schemas/PivotedValue' + create_date: + $ref: '#/components/schemas/PivotedValue' + expiration_date: + $ref: '#/components/schemas/PivotedValue' + registrar_status: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + email_domain: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + ParsedWhois: + type: object + description: Parsed data from the domain's WHOIS record. Present only when the request includes `parsed_whois=true`. + properties: + technical_contact: + $ref: '#/components/schemas/WhoisContact' + registrant_contact: + $ref: '#/components/schemas/WhoisContact' + admin_contact: + $ref: '#/components/schemas/WhoisContact' + additional_whois_email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + registrar: + $ref: '#/components/schemas/PivotedValue' + create_date: + $ref: '#/components/schemas/PivotedValue' + expiration_date: + $ref: '#/components/schemas/PivotedValue' + registrant_name: + $ref: '#/components/schemas/PivotedValue' + registrant_org: + $ref: '#/components/schemas/PivotedValue' + registrar_status: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + Proximity: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - proximity + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + RdapContact: + type: object + description: Contact details from an RDAP record. + properties: + name: + $ref: '#/components/schemas/PivotedValue' + org: + $ref: '#/components/schemas/PivotedValue' + street: + $ref: '#/components/schemas/PivotedValue' + city: + $ref: '#/components/schemas/PivotedValue' + state: + $ref: '#/components/schemas/PivotedValue' + postal: + $ref: '#/components/schemas/PivotedValue' + country: + $ref: '#/components/schemas/PivotedValue' + phone: + $ref: '#/components/schemas/PivotedValue' + fax: + $ref: '#/components/schemas/PivotedValue' + email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + ThreatProfile: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - threat_profile + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + description: Indicates risk based on machine-learning models. + ThreatProfileMalware: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - threat_profile_malware + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + description: Indicates risk based on a machine-learning model that classifies domains as malware-related. + ThreatProfilePhishing: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - threat_profile_phishing + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + description: Indicates risk based on a machine-learning model that classifies domains as phishing-related. + ThreatProfileSpam: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - threat_profile_spam + risk_score: + $ref: '#/components/schemas/RiskScoreValue' + description: Indicates risk based on a machine-learning model that classifies domains as spam-related. + Watchlist: + type: object + properties: + watchlist_domains: + type: array + items: + type: object + properties: + state: + type: string + enum: + - watched + - ignored + domain: + type: string + discovered_date: + type: string + changed_date: + type: string + id: + type: string + assigned_by: + type: string + assigned_date: + type: string + WatchlistBase: + type: object + properties: + watchlist_domain_ids: + type: array + items: + type: string + app_partner: + $ref: '#/components/schemas/IdentifierString' + app_name: + $ref: '#/components/schemas/IdentifierString' + app_version: + $ref: '#/components/schemas/IdentifierString' + WatchlistState: + allOf: + - $ref: '#/components/schemas/WatchlistBase' + - type: object + properties: + state: + type: string + enum: + - watched + - ignored + WatchlistEscalation: + allOf: + - $ref: '#/components/schemas/WatchlistBase' + - type: object + properties: + escalation_type: + $ref: '#/components/schemas/EscalationTypeEnum' + WhoisContact: + type: object + description: Contact details from a WHOIS record. + properties: + name: + $ref: '#/components/schemas/PivotedValue' + org: + $ref: '#/components/schemas/PivotedValue' + street: + $ref: '#/components/schemas/PivotedValue' + city: + $ref: '#/components/schemas/PivotedValue' + state: + $ref: '#/components/schemas/PivotedValue' + postal: + $ref: '#/components/schemas/PivotedValue' + country: + $ref: '#/components/schemas/PivotedValue' + phone: + $ref: '#/components/schemas/PivotedValue' + fax: + $ref: '#/components/schemas/PivotedValue' + email: + type: array + items: + $ref: '#/components/schemas/PivotedValue' + ZeroListedScore: + type: object + required: + - name + - risk_score + properties: + name: + type: string + description: The type of risk component. + enum: + - zerolist + risk_score: + type: integer + description: The risk score for a zerolisted domain, which is always 0. + enum: + - 0 + description: Indicates the domain is on a known-good 'zero list' and is not considered a threat. + securitySchemes: + header_auth: + type: apiKey + in: header + name: X-Api-Key + description: | + A secure authentication method where the API key is provided in the `X-Api-Key` HTTP request header. + open_key_auth: + type: http + scheme: basic + description: | + A simple authentication scheme using your `api_username` as the username and your `api_key` as the password. We strongly recommend using HMAC or Header authentication instead to avoid exposing your credentials. + hmac_auth: + type: apiKey + in: query + name: signature + description: | + A secure scheme using a Hashed Message Authentication Code (HMAC). + This method requires three query parameters to be sent with the request: + 1. `api_username`: Your API username. + 2. `timestamp`: The current timestamp in ISO 8601 format (e.g., `2020-02-01T22:37:59Z`). + 3. `signature`: The HMAC signature of the request. This is a hash (SHA-256 recommended) of your username, the timestamp, and the request URI, signed with your secret API key. diff --git a/domaintools/utils.py b/domaintools/utils.py index 3cfc75a..9587f85 100644 --- a/domaintools/utils.py +++ b/domaintools/utils.py @@ -1,10 +1,10 @@ +import re + from datetime import datetime from typing import Optional from domaintools.constants import Endpoint, OutputFormat -import re - def get_domain_age(create_date): """ @@ -109,7 +109,9 @@ def prune_data(data_obj): prune_data(item) if not isinstance(item, int) and not item: items_to_prune.append(index) - data_obj[:] = [item for index, item in enumerate(data_obj) if index not in items_to_prune and len(item)] + data_obj[:] = [ + item for index, item in enumerate(data_obj) if index not in items_to_prune and len(item) + ] def find_emails(data_str): diff --git a/tests/test_docstring_patcher.py b/tests/test_docstring_patcher.py new file mode 100644 index 0000000..065bea2 --- /dev/null +++ b/tests/test_docstring_patcher.py @@ -0,0 +1,351 @@ +import logging +import types + +from unittest.mock import Mock + +from domaintools.docstring_patcher import DocstringPatcher + + +class TestDocstringPatcher: + + def _setup_mock_api( + self, + spec_dict: dict, + spec_name: str, + method_name: str, + path: str, + http_methods: list, + docstring: str, + ) -> Mock: + """ + Helper to create a mock API instance with a mock decorated method. + """ + # Create the mock API instance + mock_api = Mock() + mock_api.specs = {spec_name: spec_dict} + + # Create the underlying function that was decorated + def original_func(): + """Original docstring.""" + pass + + original_func.__doc__ = docstring + original_func._api_spec_name = spec_name + original_func._api_path = path + original_func._api_methods = http_methods + + # Create the mock instance method + mock_method = types.MethodType(original_func, mock_api) + setattr(mock_api, method_name, mock_method) + + return mock_api, method_name + + def setup_method(self, method): + """Pytest setup hook, runs before each test.""" + self.patcher = DocstringPatcher() + + # SPEC 1: The very first spec with non-standard 'parameters' in requestBody + self.SPEC_1_NON_STANDARD_PARAMS = { + "openapi": "3.0.0", + "info": {"title": "Spec 1"}, + "components": { + "parameters": { + "LimitParam": { + "name": "limit", + "in": "query", + "description": "Max number of items.", + "schema": {"type": "integer"}, + } + }, + "schemas": {"User": {"type": "object"}}, + "requestBodies": { + "UserBody": { + "description": "User object.", + "required": True, + "content": { + "application/json": {"schema": {"$ref": "#/components/schemas/User"}} + }, + "parameters": [{"$ref": "#/components/parameters/LimitParam"}], + } + }, + }, + "paths": { + "/users": { + "post": { + "summary": "Create user", + "requestBody": {"$ref": "#/components/requestBodies/UserBody"}, + }, + } + }, + } + + # SPEC 2: The spec with UserRequestParameters (name, age) + self.SPEC_2_SCHEMA_PROPS = { + "openapi": "3.0.0", + "info": {"title": "Spec 2"}, + "components": { + "schemas": { + "UserRequestParameters": { + "type": "object", + "properties": { + "name": {"type": "string", "description": "User's name"}, + "age": {"type": "int", "description": "User's age"}, + }, + }, + }, + "requestBodies": { + "UserBody": { + "description": "User object to create.", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserRequestParameters"} + } + }, + } + }, + }, + "paths": { + "/users": { + "post": { + "summary": "Create a new user", + "requestBody": {"$ref": "#/components/requestBodies/UserBody"}, + }, + } + }, + } + + # SPEC 3: The final spec with the "lookup-by-name" logic + self.SPEC_3_LOOKUP_BY_NAME = { + "openapi": "3.0.0", + "info": {"title": "Spec 3"}, + "components": { + "parameters": { + "LimitParam": { + "name": "limit", + "in": "query", + "description": "Max number of items to return.", + "schema": {"type": "integer"}, + } + }, + "schemas": { + "UserRequestParameters": { + "type": "object", + "properties": {"limit": {"$ref:": "#/components/schemas/ApexDomain"}}, + }, + }, + "requestBodies": { + "UserBody": { + "description": "User object to create.", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserRequestParameters"} + } + }, + } + }, + }, + "paths": { + "/users": { + "post": { + "summary": "Create a new user", + "requestBody": {"$ref": "#/components/requestBodies/UserBody"}, + }, + } + }, + } + + # SPEC 4: A full spec for GET, including Responses + self.SPEC_4_WITH_RESPONSE = { + "openapi": "3.0.0", + "info": {"title": "Spec 4"}, + "components": { + "parameters": { + "LimitParam": { + "name": "limit", + "in": "query", + "description": "Max items.", + "schema": {"type": "integer"}, + } + }, + "schemas": {"User": {"type": "object"}}, + }, + "paths": { + "/users": { + "get": { + "summary": "Get all users", + "parameters": [ + { + "name": "status", + "in": "query", + "required": True, + "description": "User status.", + "schema": {"type": "string"}, + }, + {"$ref": "#/components/parameters/LimitParam"}, + ], + "responses": { + "200": { + "description": "A list of users.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + }, + } + }, + } + + def test_spec_1_non_standard_params(self): + """ + Tests the first spec: parameters inside requestBody should + be displayed under 'Request Body'. + """ + mock_api, method_name = self._setup_mock_api( + spec_dict=self.SPEC_1_NON_STANDARD_PARAMS, + spec_name="spec1", + method_name="create_user", + path="/users", + http_methods=["post"], + docstring="This creates a user.", + ) + + self.patcher.patch(mock_api) + + doc = getattr(mock_api, method_name).__doc__ + + assert "This creates a user." in doc + assert "--- Operation: POST /users ---" in doc + assert "Request Body:" in doc + assert "**User**" in doc + assert "Parameters (associated with this body):" in doc + assert "**limit** (integer) [in: query]" in doc + assert "Description: Max number of items." in doc + assert "Query Parameters:" in doc + assert "(No query parameters)" in doc + + def test_spec_2_schema_props(self): + """ + Tests the second spec: requestBody schema properties (name, age) + should be unpacked and displayed. + """ + mock_api, method_name = self._setup_mock_api( + spec_dict=self.SPEC_2_SCHEMA_PROPS, + spec_name="spec2", + method_name="create_user", + path="/users", + http_methods=["post"], + docstring="Creates user.", + ) + + self.patcher.patch(mock_api) + + doc = getattr(mock_api, method_name).__doc__ + + assert "Creates user." in doc + assert "--- Operation: POST /users ---" in doc + assert "Request Body:" in doc + assert "**UserRequestParameters**" in doc + assert "Description: User object to create." in doc + assert "Properties:" in doc + assert "**name** (string)" in doc + assert "Description: User's name" in doc + assert "**age** (int)" in doc + assert "Description: User's age" in doc + assert "Parameters (associated with this body):" not in doc + + def test_spec_3_lookup_by_name(self): + """ + Tests the final spec: requestBody property 'limit' should + be matched with components.parameters.LimitParam by name. + """ + mock_api, method_name = self._setup_mock_api( + spec_dict=self.SPEC_3_LOOKUP_BY_NAME, + spec_name="spec3", + method_name="create_user", + path="/users", + http_methods=["post"], + docstring="Creates user.", + ) + + self.patcher.patch(mock_api) + + doc = getattr(mock_api, method_name).__doc__ + + assert "--- Operation: POST /users ---" in doc + assert "Request Body:" in doc + assert "**UserRequestParameters**" in doc + assert "Properties:" in doc + # This is the key assertion: + assert "**limit** (integer)" in doc + assert "Description: Max number of items to return." in doc + # Ensure it didn't use the $ref: value + assert "ApexDomain" not in doc + + def test_spec_4_get_with_response(self): + """ + Tests a GET operation with query params and a response. + """ + mock_api, method_name = self._setup_mock_api( + spec_dict=self.SPEC_4_WITH_RESPONSE, + spec_name="spec4", + method_name="get_users", + path="/users", + http_methods=["get"], + docstring="Gets users.", + ) + + self.patcher.patch(mock_api) + + doc = getattr(mock_api, method_name).__doc__ + + assert "Gets users." in doc + assert "--- Operation: GET /users ---" in doc + + # Check Query Params + assert "Query Parameters:" in doc + assert "**status** (string)" in doc + assert "Required: True" in doc + assert "Description: User status." in doc + assert "**limit** (integer)" in doc + assert "Description: Max items." in doc + + # Check Request Body + assert "Request Body:" in doc + assert "(No request body)" in doc + + # Check Responses + assert "Result Body (Responses):" in doc + assert "**200**: (array[User])" in doc + assert "Description: A list of users." in doc + + def test_patching_error_path(self, caplog): + """ + Tests that a failure to find the operation generates the + correct error docstring and logs a warning. + """ + mock_api, method_name = self._setup_mock_api( + spec_dict=self.SPEC_1_NON_STANDARD_PARAMS, # Spec doesn't matter + spec_name="spec1", + method_name="get_pets", + path="/pets", # This path doesn't exist in the spec + http_methods=["get"], + docstring="Original doc.", + ) + + with caplog.at_level(logging.WARNING): + self.patcher.patch(mock_api) + + doc = getattr(mock_api, method_name).__doc__ + + assert "Original doc." in doc + assert "--- API Details Error ---" in doc + assert "(Could not find operations ['get'] for path '/pets' in spec 'spec1')" in doc + + # Test that no *parsing* error was logged + assert "Error parsing spec" not in caplog.text