From 7e1fdc9c41feb52403751020052f2553a554000a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Clark=20Pa=C3=B1ares?= Date: Sat, 18 Jul 2026 03:14:04 +0800 Subject: [PATCH 1/3] Add IP Feeds to DT Python Wrapper --- CHANGELOG.md | 66 ++++++++++++- VERSION | 2 +- domaintools/_version.py | 2 +- domaintools/api.py | 64 +++++++++++++ domaintools/cli/commands/feeds.py | 150 ++++++++++++++++++++++++++++++ domaintools/cli/constants.py | 2 + domaintools/cli/utils.py | 4 +- domaintools/constants.py | 8 ++ 8 files changed, 293 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 859cfde..5bad7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +### 2.9.0 +- [NEW] Add support for querying real-time IP feeds (`iphotlist` and `iprisk`). + +### 2.8.1 +- [FIX] Update python wrapper with a patch that allows the wrapper to proceed with Iris Enrich call even if `account_information` returns a 503 error only (rate limit error). + +### 2.8.0 +- [NEW] Add IrisQL Support in `iris_investigate` function. +- [NEW] Add `domain_history` command/function in the wrapper. + +### 2.7.4 +- [FIX] Handle validation error for none value arguments. + +### 2.7.3 +- [FIX] Issue on missing `risk_score` when passing it on `iris_investigate` function. + +### 2.7.2 +- [FIX] Issue on importing OpenAPI spec. + +### 2.7.0 +- [FIX] `iris_investigate` API parity updates (update help documentation) +- [FIX] `iris_investigate` `active` should raise an error as it requires a boolean not a string + +### 2.6.0 +- [NEW] Implement streaming request for RTTF endpoints to handle the feeds properly and yield the feed line by line and not storing all of it in memory. +- [UPDATE] Test cases. +- [FIX] `available_api_calls` function for RTTF products + +### 2.5.3 +- [UPDATE] Add IrisQL Support in `iris_investigate` function. +- [UPDATE] Change default Feeds authentication behavior + +### 2.5.2 +- [FIX] Total count bug for Iris Investigate and Iris Enrich results. + +### 2.5.1 +- [FIX] Fix for bug found in `realtime_domain_risk` endpoint. Changed product name from `domain-risk-` to `domain-risk-feed-` + +### 2.5.0 +- [NEW] Add support for Real Time Domain Risk Feed +- [NEW] Add support for Domain Hotlist Feed +- [NEW] Add e2e tests for proxy and ssl +- [UPDATE] Integrate e2e tests in CI pipeline. +- [FIX] Bugs found in `/domainrisk` and `/domainhotlist` endpoints + +### 2.4.1 +- [UPDATE] Remove support for MD5 based signing. + +### 2.4.0 +- [NEW] Integrate the NOH Feed to be supportable with the Python Wrapper. +- [NEW] Improve worfklow to automate publishing the package to PyPI. +- [UPDATE] Remove PhishEye. +- [FIX] Improvements on help texts. +- [FIX] Pegged httpxdependency to v.0.28.1 to prevent proxy key error. + +### 2.3.0 +- [NEW] Integrate the Domain RDAP Feed to be supportable with the Python Wrapper. +- [NEW] Integrate the Domain Discovery Feed to be supportable with the Python Wrapper. +- [UPDATE] Enhancements to RTUF endpoints to support the following: download API, header authentication, csv format. +- [UPDATE] Processing of Iterative Response (HTTP 206) from RTUF endpoints. +- [UPDATE] Help Text and Information Using New Documentation. +- [FIX] Simplification of using the new `proxy` param of httpx.Client. Before we’re using proxy mounts equivalent which we used `proxies` but this was deprecated on httpx v.0.28.x and onward causing errors in the python_wrapper + ### 2.0.0 - [NEW] Modernize package - migrate package settings to pyproject.toml - [NEW] Migrate CLI wrapper to use `typer` library. (CLI comes now with new interface.) @@ -11,12 +74,11 @@ - Filtering of results based on `updated_after` field. - Filtering of results based on a missing field. (include_domains_with_missing_field` or `exclude_domains_with_missing_field`). - [NEW] Add support on removing/stripping colon in when passing a value in `--ssl_hash` in `iris_investigate` cli command. -- [UPDATE] replace use of upcoming deprecated `datetime.uctnow()` to `timezone.utc` +- [UPDATE] replace use of upcoming deprecated `datetime.utcnow()` to `timezone.utc` - [UPDATE] Improve help text in CLI commands. - [UPDATE] Remove `dateparser` dependency and use native python `datetime` library. - [FIX] Fix error in `-o` or `--out-file` parameter. - ### 1.0.1 - Adds support for the hourly query limit on the Account API endpoint diff --git a/VERSION b/VERSION index dbe5900..c8e38b6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.8.1 +2.9.0 diff --git a/domaintools/_version.py b/domaintools/_version.py index 569a3cd..ffde9f3 100644 --- a/domaintools/_version.py +++ b/domaintools/_version.py @@ -20,4 +20,4 @@ """ -current = "2.8.1" +current = "2.9.0" diff --git a/domaintools/api.py b/domaintools/api.py index 933436a..2ccf0c9 100644 --- a/domaintools/api.py +++ b/domaintools/api.py @@ -1412,3 +1412,67 @@ def domainhotlist(self, **kwargs) -> FeedsResults: cls=FeedsResults, **kwargs, ) + + def iphotlist(self, **kwargs) -> FeedsResults: + """Returns back list of ip hotlist feed. + Captures IP addresses that meet strict criteria for both risk level and recent activity, making it ideal for immediate blocking and threat response. + + before: str: Filter for records before the given time value inclusive or time offset relative to now + + after: str: Filter for records after the given time value inclusive or time offset relative to now + + headers: bool: Use in combination with Accept: text/csv headers to control if headers are sent or not + + sessionID: str: A custom string to distinguish between different sessions + + fromBeginning: bool: Requires a sessionID. When used with a new session ID, returns the first hour of data in the time window (rather than the last). Returns an error if the session ID already exists + """ + validate_feeds_parameters(kwargs) + endpoint = kwargs.pop("endpoint", Endpoint.FEED.value) + source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value + if ( + endpoint == Endpoint.DOWNLOAD.value + or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value + ): + # headers param is allowed only in Feed API and CSV format + kwargs.pop("headers", None) + + return self._results( + f"real-time-ip-hotlist-({source})", + f"v1/{endpoint}/iphotlist/", + response_path=(), + cls=FeedsResults, + **kwargs, + ) + + def iprisk(self, **kwargs) -> FeedsResults: + """Returns back list of domain hotlist feed. + Captures all IP addresses that actively host one or more domains, providing risk assessment and enrichment data for each IP address. + + before: str: Filter for records before the given time value inclusive or time offset relative to now + + after: str: Filter for records after the given time value inclusive or time offset relative to now + + headers: bool: Use in combination with Accept: text/csv headers to control if headers are sent or not + + sessionID: str: A custom string to distinguish between different sessions + + fromBeginning: bool: Requires a sessionID. When used with a new session ID, returns the first hour of data in the time window (rather than the last). Returns an error if the session ID already exists + """ + validate_feeds_parameters(kwargs) + endpoint = kwargs.pop("endpoint", Endpoint.FEED.value) + source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value + if ( + endpoint == Endpoint.DOWNLOAD.value + or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value + ): + # headers param is allowed only in Feed API and CSV format + kwargs.pop("headers", None) + + return self._results( + f"real-time-ip-risk-({source})", + f"v1/{endpoint}/iprisk/", + response_path=(), + cls=FeedsResults, + **kwargs, + ) diff --git a/domaintools/cli/commands/feeds.py b/domaintools/cli/commands/feeds.py index accb23b..c4c062f 100644 --- a/domaintools/cli/commands/feeds.py +++ b/domaintools/cli/commands/feeds.py @@ -554,3 +554,153 @@ def feeds_realtime_domain_risk( ), ): DTCLICommand.run(name=c.FEEDS_REALTIME_DOMAIN_RISK, params=ctx.params) + + +@dt_cli.command( + name=c.FEEDS_IPHOTLIST, + help=get_cli_helptext_by_name(command_name=c.FEEDS_IPHOTLIST), +) +def feeds_iphotlist( + ctx: typer.Context, + user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."), + key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"), + creds_file: str = typer.Option( + "~/.dtapi", + "-c", + "--credfile", + help="Optional file with API username and API key, one per line.", + ), + no_verify_ssl: bool = typer.Option( + False, + "--no-verify-ssl", + help="Skip verification of SSL certificate when making HTTPs API calls", + ), + no_sign_api_key: bool = typer.Option( + False, + "--no-sign-api-key", + help="Skip signing of api key", + ), + no_header_authentication: bool = typer.Option( + False, + "--no-header-auth", + help="Don't use header authentication", + ), + output_format: str = typer.Option( + "jsonl", + "-f", + "--format", + help=f"Output format in [{OutputFormat.JSONL.value}, {OutputFormat.CSV.value}]", + callback=DTCLICommand.validate_feeds_format_input, + ), + endpoint: str = typer.Option( + Endpoint.FEED.value, + "-e", + "--endpoint", + help=f"Valid endpoints: [{Endpoint.FEED.value}, {Endpoint.DOWNLOAD.value}]", + callback=DTCLICommand.validate_endpoint_input, + ), + sessionID: str = typer.Option( + None, + "--session-id", + help="Unique identifier for the session", + ), + after: str = typer.Option( + None, + "--after", + help="Start of the time window, relative to the current time in seconds, for which data will be provided", + callback=DTCLICommand.validate_after_or_before_input, + ), + before: str = typer.Option( + None, + "--before", + help="The end of the query window in seconds, relative to the current time, inclusive", + callback=DTCLICommand.validate_after_or_before_input, + ), + fromBeginning: bool = typer.Option( + None, + "-fb", + "--frombeginning", + help="Requires a sessionID. When used with a new session ID, returns the first hour of data in the time window (rather than the last). Returns an error if the session ID already exists", + ), + headers: bool = typer.Option( + False, + "--headers", + help="Adds a header to the first line of response when text/csv is set in header parameters", + ), +): + DTCLICommand.run(name=c.FEEDS_IPHOTLIST, params=ctx.params) + + +@dt_cli.command( + name=c.FEEDS_IPRISK, + help=get_cli_helptext_by_name(command_name=c.FEEDS_IPRISK), +) +def feeds_iprisk( + ctx: typer.Context, + user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."), + key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"), + creds_file: str = typer.Option( + "~/.dtapi", + "-c", + "--credfile", + help="Optional file with API username and API key, one per line.", + ), + no_verify_ssl: bool = typer.Option( + False, + "--no-verify-ssl", + help="Skip verification of SSL certificate when making HTTPs API calls", + ), + no_sign_api_key: bool = typer.Option( + False, + "--no-sign-api-key", + help="Skip signing of api key", + ), + no_header_authentication: bool = typer.Option( + False, + "--no-header-auth", + help="Don't use header authentication", + ), + output_format: str = typer.Option( + "jsonl", + "-f", + "--format", + help=f"Output format in [{OutputFormat.JSONL.value}, {OutputFormat.CSV.value}]", + callback=DTCLICommand.validate_feeds_format_input, + ), + endpoint: str = typer.Option( + Endpoint.FEED.value, + "-e", + "--endpoint", + help=f"Valid endpoints: [{Endpoint.FEED.value}, {Endpoint.DOWNLOAD.value}]", + callback=DTCLICommand.validate_endpoint_input, + ), + sessionID: str = typer.Option( + None, + "--session-id", + help="Unique identifier for the session", + ), + after: str = typer.Option( + None, + "--after", + help="Start of the time window, relative to the current time in seconds, for which data will be provided", + callback=DTCLICommand.validate_after_or_before_input, + ), + before: str = typer.Option( + None, + "--before", + help="The end of the query window in seconds, relative to the current time, inclusive", + callback=DTCLICommand.validate_after_or_before_input, + ), + fromBeginning: bool = typer.Option( + None, + "-fb", + "--frombeginning", + help="Requires a sessionID. When used with a new session ID, returns the first hour of data in the time window (rather than the last). Returns an error if the session ID already exists", + ), + headers: bool = typer.Option( + False, + "--headers", + help="Adds a header to the first line of response when text/csv is set in header parameters", + ), +): + DTCLICommand.run(name=c.FEEDS_IPRISK, params=ctx.params) \ No newline at end of file diff --git a/domaintools/cli/constants.py b/domaintools/cli/constants.py index 190a6af..6255731 100644 --- a/domaintools/cli/constants.py +++ b/domaintools/cli/constants.py @@ -48,3 +48,5 @@ FEEDS_DOMAINRDAP = "domainrdap" FEEDS_DOMAINDISCOVERY = "domaindiscovery" FEEDS_REALTIME_DOMAIN_RISK = "realtime_domain_risk" +FEEDS_IPHOTLIST = "iphotlist" +FEEDS_IPRISK = "iprisk" diff --git a/domaintools/cli/utils.py b/domaintools/cli/utils.py index 81b3dcb..c65cb23 100644 --- a/domaintools/cli/utils.py +++ b/domaintools/cli/utils.py @@ -65,10 +65,12 @@ def _iris_investigate_helptext(): c.FEEDS_NAD: "Returns back newly active domains feed.", c.FEEDS_NOD: "Returns back newly observed domains feed.", c.FEEDS_NOH: "Returns back newly observed hosts feed.", - c.FEEDS_DOMAINHOTLIST: "Returns domaint hotlist feed.", + c.FEEDS_DOMAINHOTLIST: "Returns domain hotlist feed.", c.FEEDS_DOMAINRDAP: "Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP).", c.FEEDS_DOMAINDISCOVERY: "Returns new domains as they are either discovered in domain registration information, observed by our global sensor network, or reported by trusted third parties.", c.FEEDS_REALTIME_DOMAIN_RISK: "Returns realtime domain risk information for apex-level domains, regardless of observed traffic.", + c.FEEDS_IPHOTLIST: "Returns ip hotlist feed.", + c.FEEDS_IPRISK: "Returns ip risk feed.", } diff --git a/domaintools/constants.py b/domaintools/constants.py index 4acee15..e5e7126 100644 --- a/domaintools/constants.py +++ b/domaintools/constants.py @@ -38,6 +38,10 @@ class OutputFormat(Enum): "real-time-domain-risk-(s3)", "real-time-domain-discovery-feed-(api)", "real-time-domain-discovery-feed-(s3)", + "real-time-ip-hotlist-(api)", + "real-time-ip-hotlist-(s3)", + "real-time-ip-risk-(api)", + "real-time-ip-risk-(s3)", ] RTTF_PRODUCTS_CMD_MAPPING = { @@ -55,6 +59,10 @@ class OutputFormat(Enum): "real-time-domain-risk-(s3)": "realtime_domain_risk", "real-time-domain-discovery-feed-(api)": "domaindiscovery", "real-time-domain-discovery-feed-(s3)": "domaindiscovery", + "real-time-ip-hotlist-(api)": "iphotlist", + "real-time-ip-hotlist-(s3)": "iphotlist", + "real-time-ip-risk-(api)": "iprisk", + "real-time-ip-risk-(s3)": "iprisk", } SPECS_MAPPING = { From c4b85b25fdbbcc7bcf66372e65274771be7dd5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Clark=20Pa=C3=B1ares?= Date: Tue, 21 Jul 2026 01:53:28 +0800 Subject: [PATCH 2/3] add test cases for ip feeds --- VERSION | 2 +- domaintools/_version.py | 2 +- tests/fixtures/vcr/test_ip_hotlist.yaml | 115 ++++++++++++++++++++++++ tests/fixtures/vcr/test_ip_risk.yaml | 115 ++++++++++++++++++++++++ tests/test_api.py | 38 ++++++++ 5 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/vcr/test_ip_hotlist.yaml create mode 100644 tests/fixtures/vcr/test_ip_risk.yaml diff --git a/VERSION b/VERSION index c8e38b6..dbe5900 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.9.0 +2.8.1 diff --git a/domaintools/_version.py b/domaintools/_version.py index ffde9f3..569a3cd 100644 --- a/domaintools/_version.py +++ b/domaintools/_version.py @@ -20,4 +20,4 @@ """ -current = "2.9.0" +current = "2.8.1" diff --git a/tests/fixtures/vcr/test_ip_hotlist.yaml b/tests/fixtures/vcr/test_ip_hotlist.yaml new file mode 100644 index 0000000..5ca3b71 --- /dev/null +++ b/tests/fixtures/vcr/test_ip_hotlist.yaml @@ -0,0 +1,115 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.domaintools.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.domaintools.com/v1/account?app_name=python_wrapper&app_version=2.8.1 + response: + body: + string: '{"timestamp":"2025-01-06T15:30:42Z","ip":"192.0.2.1","asn":12345,"organization":"Example + Hosting LLC","city":"Amsterdam","country":"NL","latitude":52.3676,"longitude":4.9041,"pdns_resolutions":47,"bad_pdns_resolutions":42,"total_domains":183,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":38,"all_threats_combined_count":45,"all_threats_combined_percent":91.2,"all_threats_percent":91.2,"combined_phishing_percent":72.1,"combined_malware_percent":85.3,"combined_spam_percent":61.4,"malicious_phishing":28,"malicious_malware":33,"malicious_spam":22,"percent_phishing":57.4,"percent_malware":67.8,"percent_spam":45.2,"compromised_phishing":6,"compromised_malware":8,"compromised_spam":4,"predicted_phishing":7,"predicted_malware":9,"predicted_spam":5} + + {"timestamp":"2025-01-06T15:30:45Z","ip":"192.0.2.2","asn":67890,"organization":"Bulletproof + Networks Inc","city":"Moscow","country":"RU","latitude":55.7558,"longitude":37.6173,"pdns_resolutions":31,"bad_pdns_resolutions":29,"total_domains":94,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":25,"all_threats_combined_count":30,"all_threats_combined_percent":88.7,"all_threats_percent":88.7,"combined_phishing_percent":65.9,"combined_malware_percent":79.4,"combined_spam_percent":55.3,"malicious_phishing":19,"malicious_malware":24,"malicious_spam":15,"percent_phishing":48.9,"percent_malware":61.7,"percent_spam":38.3,"compromised_phishing":4,"compromised_malware":5,"compromised_spam":3,"predicted_phishing":6,"predicted_malware":7,"predicted_spam":4} + + ' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline''' + Content-Type: + - application/json;charset=utf-8 + Date: + - Mon, 28 Apr 2025 18:00:47 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Set-Cookie: + - dtsession=fsli3ccledtncc8l0agq08ceif; expires=Wed, 28-May-2025 18:00:47 GMT; + Max-Age=2592000; path=/; domain=.domaintools.test; secure; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-API-USERNAME: + - jbabac + X-GROUP-ID: + - '1117' + X-Powered-By: + - PHP/7.1.33 + X-TIME: + - '49818' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + host: + - api.domaintools.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.domaintools.com/v1/feed/iphotlist/?after=-60&app_name=python_wrapper&app_version=2.8.1&top=5 + response: + body: + string: '{"timestamp":"2025-01-06T15:30:42Z","ip":"192.0.2.1","asn":12345,"organization":"Example + Hosting LLC","city":"Amsterdam","country":"NL","latitude":52.3676,"longitude":4.9041,"pdns_resolutions":47,"bad_pdns_resolutions":42,"total_domains":183,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":38,"all_threats_combined_count":45,"all_threats_combined_percent":91.2,"all_threats_percent":91.2,"combined_phishing_percent":72.1,"combined_malware_percent":85.3,"combined_spam_percent":61.4,"malicious_phishing":28,"malicious_malware":33,"malicious_spam":22,"percent_phishing":57.4,"percent_malware":67.8,"percent_spam":45.2,"compromised_phishing":6,"compromised_malware":8,"compromised_spam":4,"predicted_phishing":7,"predicted_malware":9,"predicted_spam":5} + + {"timestamp":"2025-01-06T15:30:45Z","ip":"192.0.2.2","asn":67890,"organization":"Bulletproof + Networks Inc","city":"Moscow","country":"RU","latitude":55.7558,"longitude":37.6173,"pdns_resolutions":31,"bad_pdns_resolutions":29,"total_domains":94,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":25,"all_threats_combined_count":30,"all_threats_combined_percent":88.7,"all_threats_percent":88.7,"combined_phishing_percent":65.9,"combined_malware_percent":79.4,"combined_spam_percent":55.3,"malicious_phishing":19,"malicious_malware":24,"malicious_spam":15,"percent_phishing":48.9,"percent_malware":61.7,"percent_spam":38.3,"compromised_phishing":4,"compromised_malware":5,"compromised_spam":3,"predicted_phishing":6,"predicted_malware":7,"predicted_spam":4} + + ' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Length: + - '932' + Content-Security-Policy: + - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline''' + Content-Type: + - application/x-ndjson + Date: + - Thu, 23 Oct 2025 18:33:09 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Set-Cookie: + - dtsession=41acrdm0r445tr0kqd8rsdu121jfg5tv05s572e3qtcn424ii0lpg6cm7gn5lliuk3fgk7dpflgr6potnis2mefki53adjcits91k1g; + expires=Sat, 22-Nov-2025 18:33:08 GMT; Max-Age=2592000; path=/; domain=.domaintools.com; + secure; HttpOnly + - 0566fae076d9b6615cd8f47a9e9500f2=4556b46b52fb2ae62de3b26ddc27dfa0; path=/; + HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Request-Id: + - 68211925-a5bb-4487-90d0-9ed3887f3728 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/fixtures/vcr/test_ip_risk.yaml b/tests/fixtures/vcr/test_ip_risk.yaml new file mode 100644 index 0000000..175839e --- /dev/null +++ b/tests/fixtures/vcr/test_ip_risk.yaml @@ -0,0 +1,115 @@ +interactions: +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate + connection: + - keep-alive + host: + - api.domaintools.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.domaintools.com/v1/account?app_name=python_wrapper&app_version=2.8.1 + response: + body: + string: '{"timestamp":"2025-01-06T15:30:42Z","ip":"192.0.2.10","asn":12345,"organization":"Example + Hosting LLC","city":"Amsterdam","country":"NL","latitude":52.3676,"longitude":4.9041,"pdns_resolutions":12,"bad_pdns_resolutions":3,"total_domains":47,"zerolist_domains":2,"zerolist_ip":false,"third_party_threats":2,"all_threats_combined_count":5,"all_threats_combined_percent":10.6,"all_threats_percent":10.6,"combined_phishing_percent":6.4,"combined_malware_percent":8.5,"combined_spam_percent":4.3,"malicious_phishing":1,"malicious_malware":2,"malicious_spam":1,"percent_phishing":2.1,"percent_malware":4.3,"percent_spam":2.1,"compromised_phishing":0,"compromised_malware":1,"compromised_spam":0,"predicted_phishing":1,"predicted_malware":2,"predicted_spam":1} + + {"timestamp":"2025-01-06T15:30:45Z","ip":"192.0.2.11","asn":67890,"organization":"Cloud + Provider Corp","city":"Frankfurt","country":"DE","latitude":50.1109,"longitude":8.6821,"pdns_resolutions":5,"bad_pdns_resolutions":0,"total_domains":18,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":0,"all_threats_combined_count":1,"all_threats_combined_percent":5.6,"all_threats_percent":5.6,"combined_phishing_percent":0.0,"combined_malware_percent":5.6,"combined_spam_percent":0.0,"malicious_phishing":0,"malicious_malware":0,"malicious_spam":0,"percent_phishing":0.0,"percent_malware":0.0,"percent_spam":0.0,"compromised_phishing":0,"compromised_malware":0,"compromised_spam":0,"predicted_phishing":0,"predicted_malware":1,"predicted_spam":0} + + ' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Security-Policy: + - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline''' + Content-Type: + - application/json;charset=utf-8 + Date: + - Mon, 28 Apr 2025 18:00:47 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Set-Cookie: + - dtsession=fsli3ccledtncc8l0agq08ceif; expires=Wed, 28-May-2025 18:00:47 GMT; + Max-Age=2592000; path=/; domain=.domaintools.test; secure; HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + X-API-USERNAME: + - jbabac + X-GROUP-ID: + - '1117' + X-Powered-By: + - PHP/7.1.33 + X-TIME: + - '49818' + status: + code: 200 + message: OK +- request: + body: '' + headers: + accept: + - '*/*' + accept-encoding: + - identity + connection: + - keep-alive + host: + - api.domaintools.com + user-agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.domaintools.com/v1/feed/iprisk/?after=-60&app_name=python_wrapper&app_version=2.8.1&top=5 + response: + body: + string: '{"timestamp":"2025-01-06T15:30:42Z","ip":"192.0.2.10","asn":12345,"organization":"Example + Hosting LLC","city":"Amsterdam","country":"NL","latitude":52.3676,"longitude":4.9041,"pdns_resolutions":12,"bad_pdns_resolutions":3,"total_domains":47,"zerolist_domains":2,"zerolist_ip":false,"third_party_threats":2,"all_threats_combined_count":5,"all_threats_combined_percent":10.6,"all_threats_percent":10.6,"combined_phishing_percent":6.4,"combined_malware_percent":8.5,"combined_spam_percent":4.3,"malicious_phishing":1,"malicious_malware":2,"malicious_spam":1,"percent_phishing":2.1,"percent_malware":4.3,"percent_spam":2.1,"compromised_phishing":0,"compromised_malware":1,"compromised_spam":0,"predicted_phishing":1,"predicted_malware":2,"predicted_spam":1} + + {"timestamp":"2025-01-06T15:30:45Z","ip":"192.0.2.11","asn":67890,"organization":"Cloud + Provider Corp","city":"Frankfurt","country":"DE","latitude":50.1109,"longitude":8.6821,"pdns_resolutions":5,"bad_pdns_resolutions":0,"total_domains":18,"zerolist_domains":0,"zerolist_ip":false,"third_party_threats":0,"all_threats_combined_count":1,"all_threats_combined_percent":5.6,"all_threats_percent":5.6,"combined_phishing_percent":0.0,"combined_malware_percent":5.6,"combined_spam_percent":0.0,"malicious_phishing":0,"malicious_malware":0,"malicious_spam":0,"percent_phishing":0.0,"percent_malware":0.0,"percent_spam":0.0,"compromised_phishing":0,"compromised_malware":0,"compromised_spam":0,"predicted_phishing":0,"predicted_malware":1,"predicted_spam":0} + + ' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Content-Length: + - '932' + Content-Security-Policy: + - 'default-src * data: blob: ''unsafe-eval'' ''unsafe-inline''' + Content-Type: + - application/x-ndjson + Date: + - Thu, 23 Oct 2025 18:33:09 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Pragma: + - no-cache + Set-Cookie: + - dtsession=41acrdm0r445tr0kqd8rsdu121jfg5tv05s572e3qtcn424ii0lpg6cm7gn5lliuk3fgk7dpflgr6potnis2mefki53adjcits91k1g; + expires=Sat, 22-Nov-2025 18:33:08 GMT; Max-Age=2592000; path=/; domain=.domaintools.com; + secure; HttpOnly + - 0566fae076d9b6615cd8f47a9e9500f2=4556b46b52fb2ae62de3b26ddc27dfa0; path=/; + HttpOnly + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Request-Id: + - 68211925-a5bb-4487-90d0-9ed3887f3728 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_api.py b/tests/test_api.py index 8a05307..8948afb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -835,3 +835,41 @@ def test_feeds_endpoint_should_raise_error_if_signed_api_key_is_used(): feeds_api.domaindiscovery(after="-60") assert str(excinfo.value) == "Real Time Threat Feeds do not support signed API keys." + + +@vcr.use_cassette +def test_ip_hotlist(): + results = feeds_api.iphotlist(after="-60", top=5) + for response in results.response(): + assert results.status == 200 + + response = response.strip() + assert response is not None + + feed_result = json.loads(response) + assert "timestamp" in feed_result.keys() + assert "ip" in feed_result.keys() + assert "asn" in feed_result.keys() + assert "organization" in feed_result.keys() + assert "total_domains" in feed_result.keys() + assert "all_threats_combined_count" in feed_result.keys() + assert "all_threats_combined_percent" in feed_result.keys() + + +@vcr.use_cassette +def test_ip_risk(): + results = feeds_api.iprisk(after="-60", top=5) + for response in results.response(): + assert results.status == 200 + + response = response.strip() + assert response is not None + + feed_result = json.loads(response) + assert "timestamp" in feed_result.keys() + assert "ip" in feed_result.keys() + assert "asn" in feed_result.keys() + assert "organization" in feed_result.keys() + assert "total_domains" in feed_result.keys() + assert "all_threats_combined_count" in feed_result.keys() + assert "all_threats_combined_percent" in feed_result.keys() From 1e743f1b0d020fdac251a9509db3792b91feafb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Clark=20Pa=C3=B1ares?= Date: Tue, 21 Jul 2026 01:57:37 +0800 Subject: [PATCH 3/3] adjust test_api order --- tests/test_api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 8948afb..1715166 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -827,16 +827,6 @@ def test_domain_hotlist(): assert "proximity_risk" in feed_result.keys() assert "overall_risk" in feed_result.keys() - -@vcr.use_cassette -def test_feeds_endpoint_should_raise_error_if_signed_api_key_is_used(): - feeds_api.always_sign_api_key = True - with pytest.raises(ValueError) as excinfo: - feeds_api.domaindiscovery(after="-60") - - assert str(excinfo.value) == "Real Time Threat Feeds do not support signed API keys." - - @vcr.use_cassette def test_ip_hotlist(): results = feeds_api.iphotlist(after="-60", top=5) @@ -873,3 +863,13 @@ def test_ip_risk(): assert "total_domains" in feed_result.keys() assert "all_threats_combined_count" in feed_result.keys() assert "all_threats_combined_percent" in feed_result.keys() + + +@vcr.use_cassette +def test_feeds_endpoint_should_raise_error_if_signed_api_key_is_used(): + feeds_api.always_sign_api_key = True + with pytest.raises(ValueError) as excinfo: + feeds_api.domaindiscovery(after="-60") + + assert str(excinfo.value) == "Real Time Threat Feeds do not support signed API keys." +