From 1889fec47cc08e1c2791ff7ab08382c24e18676d Mon Sep 17 00:00:00 2001 From: JD Babac Date: Tue, 4 Aug 2026 01:14:08 +0800 Subject: [PATCH 1/4] IDEV-2524: Exted FeedDomainTools pack to support IP Hotlist and IP Risk commands --- .../FeedDomainTools/FeedDomainTools.py | 33 +++- .../FeedDomainTools/FeedDomainTools.yml | 6 +- .../FeedDomainTools/FeedDomainTools_test.py | 52 +++++- .../Integrations/FeedDomainTools/README.md | 2 +- .../FeedDomainTools/command_examples.txt | 4 +- .../test_data/feed_mock_response.py | 157 ++++++++++++++++++ 6 files changed, 248 insertions(+), 6 deletions(-) diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py index 08d8c616f839..d199b2ba228f 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py @@ -28,6 +28,8 @@ class DomainToolsClient: DOMAINDISCOVERY = "domaindiscovery" DOMAINRISK = "domainrisk" DOMAINHOTLIST = "domainhotlist" + IPHOTLIST = "iphotlist" + IPRISK = "iprisk" FEED_METHOD_MAP = { "nod": "nod", @@ -169,8 +171,13 @@ def build_iterator(self, feed_type: str = "nod", dt_feed_kwargs: dict = {}) -> I json_feed = json.loads(feed) timestamp = json_feed.get("timestamp", "") - indicator = json_feed.get("domain") - indicator_type = FeedIndicatorType.Domain + + if self.feed_type in (self.IPHOTLIST, self.IPRISK): + indicator = json_feed.get("ip") + indicator_type = FeedIndicatorType.IP + else: + indicator = json_feed.get("domain") + indicator_type = FeedIndicatorType.Domain # for `domainrdap` feed, we have more data to display including the parsed data. parsed_record = json_feed.get("parsed_record", {}) @@ -203,6 +210,22 @@ def build_iterator(self, feed_type: str = "nod", dt_feed_kwargs: dict = {}) -> I # update the parsed dt feed data dt_feed_data["risk_score_details"] = risk_score_details + if self.feed_type in (self.IPHOTLIST, self.IPRISK): + ip_threat_data = { + "asn": json_feed.get("asn"), + "organization": json_feed.get("organization"), + "city": json_feed.get("city"), + "country": json_feed.get("country"), + "latitude": json_feed.get("latitude"), + "longitude": json_feed.get("longitude"), + "pdns_resolutions": json_feed.get("pdns_resolutions"), + "bad_pdns_resolutions": json_feed.get("bad_pdns_resolutions"), + "total_domains": json_feed.get("total_domains"), + "all_threats_combined_count": json_feed.get("all_threats_combined_count"), + "third_party_threats": json_feed.get("third_party_threats"), + } + dt_feed_data["ip_threat_data"] = ip_threat_data + if indicator and indicator_type: yield dt_feed_data @@ -278,6 +301,7 @@ def fetch_indicators(client: DomainToolsClient, feed_type: str = "nod", dt_feed_ parsed_record_ = item.get("parsed_record") overall_risk_score_ = item.get("overall_risk_score") risk_score_details_ = item.get("risk_score_details") + ip_threat_data_ = item.get("ip_threat_data") indicator_tags = ",".join(tags_).rstrip(",") @@ -293,6 +317,9 @@ def fetch_indicators(client: DomainToolsClient, feed_type: str = "nod", dt_feed_ if risk_score_details_: raw_data["risk_score_details"] = risk_score_details_ + if ip_threat_data_: + raw_data["ip_threat_data"] = ip_threat_data_ + # Create indicator object for each value. indicator_obj = { "value": value_, @@ -384,6 +411,8 @@ def fetch_indicators_command(client: DomainToolsClient, params: dict[str, Any] = client.DOMAINDISCOVERY, client.DOMAINRISK, client.DOMAINHOTLIST, + client.IPHOTLIST, + client.IPRISK, ] dt_feed_kwargs = {"top": top, "after": after, "session_id": session_id} diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml index 039a76d4f185..80d2a9618631 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml @@ -49,6 +49,8 @@ configuration: - domaindiscovery - domainrisk - domainhotlist + - iphotlist + - iprisk additionalinfo: The DomainTools feed type fo fetch. Defaults to 'ALL'. section: Collect - display: Fetch indicators @@ -160,6 +162,8 @@ script: - "domaindiscovery" - "domainrisk" - "domainhotlist" + - "iphotlist" + - "iprisk" defaultValue: "nod" description: The DomainTools integration feed type to fetch. isArray: false @@ -199,7 +203,7 @@ script: default: false required: false secret: false - dockerimage: demisto/vendors-sdk:1.0.0.10470199 + dockerimage: demisto/vendors-sdk:1.0.0.11252302 feed: true isfetch: false longRunning: false diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools_test.py b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools_test.py index 2cc57c78a1db..1253be643cc5 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools_test.py +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools_test.py @@ -133,9 +133,55 @@ def test_get_dbot_score(overall_riskscore, expected_dbot_score): assert actual_dbot_score == expected_dbot_score +class TestIPFeedsBuildIterator: + def test_iphotlist_build_iterator(self, mocker, dt_feeds_client): + """ + Given: + - Output of the IP hotlist feed API + When: + - When calling fetch_indicators or get_indicators + Then: + - Returns an iterator of IP indicators parsed from the API's response + """ + mocker.patch.object( + dt_feeds_client, + "_get_dt_feeds", + return_value=feed_mock_response.IPHOTLIST_RESPONSE, + ) + indicators = list(dt_feeds_client.build_iterator(feed_type="iphotlist")) + ips = [indicator.get("value") for indicator in indicators] + + assert "203.0.113.5" in ips + assert len(indicators) == 3 + assert indicators[0].get("type") == FeedIndicatorType.IP + assert indicators[0].get("ip_threat_data", {}).get("asn") == 12345 + + def test_iprisk_build_iterator(self, mocker, dt_feeds_client): + """ + Given: + - Output of the IP risk feed API + When: + - When calling fetch_indicators or get_indicators + Then: + - Returns an iterator of IP indicators parsed from the API's response + """ + mocker.patch.object( + dt_feeds_client, + "_get_dt_feeds", + return_value=feed_mock_response.IPRISK_RESPONSE, + ) + indicators = list(dt_feeds_client.build_iterator(feed_type="iprisk")) + ips = [indicator.get("value") for indicator in indicators] + + assert "203.0.113.10" in ips + assert len(indicators) == 2 + assert indicators[0].get("type") == FeedIndicatorType.IP + assert indicators[0].get("ip_threat_data", {}).get("asn") == 22222 + + @pytest.mark.parametrize( "feed_type", - ["nod", "nad", "noh", "domaindiscovery", "domainrdap", "domainrisk", "domainhotlist"], + ["nod", "nad", "noh", "domaindiscovery", "domainrdap", "domainrisk", "domainhotlist", "iphotlist", "iprisk"], ) def test_get_indicators_command(mocker, dt_feeds_client, feed_type): """ @@ -156,6 +202,8 @@ def test_get_indicators_command(mocker, dt_feeds_client, feed_type): "domainrdap": feed_mock_response.DOMAINRDAP_RESPONSE, "domainrisk": feed_mock_response.DOMAINRISK_RESPONSE, "domainhotlist": feed_mock_response.DOMAINHOTLIST_RESPONSE, + "iphotlist": feed_mock_response.IPHOTLIST_RESPONSE, + "iprisk": feed_mock_response.IPRISK_RESPONSE, } mocker.patch.object( @@ -173,6 +221,8 @@ def test_get_indicators_command(mocker, dt_feeds_client, feed_type): "domainrdap": feed_mock_response.DOMAINRDAP_PARSED_INDICATOR_RESPONSE, "domainrisk": feed_mock_response.DOMAINRISK_PARSED_INDICATOR_RESPONSE, "domainhotlist": feed_mock_response.DOMAINHOTLIST_PARSED_INDICATOR_RESPONSE, + "iphotlist": feed_mock_response.IPHOTLIST_PARSED_INDICATOR_RESPONSE, + "iprisk": feed_mock_response.IPRISK_PARSED_INDICATOR_RESPONSE, } human_readable = tableToMarkdown( diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/README.md b/Packs/FeedDomainTools/Integrations/FeedDomainTools/README.md index 40f1230ae2ed..719c45a2101e 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/README.md +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/README.md @@ -40,7 +40,7 @@ Gets indicators from the feed. | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| feed_type | The DomainTools integration feed type to fetch. Possible values are: nod, nad, noh, domainrdap, domaindiscovery, domainrisk, domainhotlist. Default is nod. | Optional | +| feed_type | The DomainTools integration feed type to fetch. Possible values are: nod, nad, noh, domainrdap, domaindiscovery, domainrisk, domainhotlist, iphotlist, iprisk. Default is nod. | Optional | | session_id | The session id to serve as unique indentifier. On it's initial use, it will retrieve data from the past 5 days. Default is dt-cortex-feeds. | Optional | | domain | The top level domain to query (e.g. `*.com`). | Optional | | after | The start of the query window in seconds, relative to the current time, inclusive. Defaults to 3600 seconds (1h). Default is -3600. | Optional | diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/command_examples.txt b/Packs/FeedDomainTools/Integrations/FeedDomainTools/command_examples.txt index 533ce057c4fa..6589c0df51df 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/command_examples.txt +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/command_examples.txt @@ -1,2 +1,4 @@ !dtfeeds-get-indicators limit="10" feed_type="nod" session_id="dt-nod-cortex-integrations" -!dtfeeds-get-indicators limit="10" feed_type="nad" session_id="dt-nad-cortex-integrations" \ No newline at end of file +!dtfeeds-get-indicators limit="10" feed_type="nad" session_id="dt-nad-cortex-integrations" +!dtfeeds-get-indicators limit="10" feed_type="iphotlist" session_id="dt-iphotlist-cortex-integrations" +!dtfeeds-get-indicators limit="10" feed_type="iprisk" session_id="dt-iprisk-cortex-integrations" \ No newline at end of file diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/test_data/feed_mock_response.py b/Packs/FeedDomainTools/Integrations/FeedDomainTools/test_data/feed_mock_response.py index b0f585230100..762d3f367293 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/test_data/feed_mock_response.py +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/test_data/feed_mock_response.py @@ -849,6 +849,163 @@ } ] +IPHOTLIST_RESPONSE = [ + '{"timestamp":"2025-09-06T22:00:00Z","ip":"203.0.113.5","asn":12345,"organization":"Evil Corp","city":"Moscow","country":"RU","latitude":55.7558,"longitude":37.6173,"pdns_resolutions":150,"bad_pdns_resolutions":120,"total_domains":80,"all_threats_combined_count":95,"third_party_threats":10}', + '{"timestamp":"2025-09-06T22:01:00Z","ip":"198.51.100.22","asn":67890,"organization":"BadNet","city":"Beijing","country":"CN","latitude":39.9042,"longitude":116.4074,"pdns_resolutions":200,"bad_pdns_resolutions":180,"total_domains":60,"all_threats_combined_count":75,"third_party_threats":5}', + '{"timestamp":"2025-09-06T22:02:00Z","ip":"192.0.2.100","asn":11111,"organization":"SpamCo","city":"Lagos","country":"NG","latitude":6.5244,"longitude":3.3792,"pdns_resolutions":50,"bad_pdns_resolutions":45,"total_domains":30,"all_threats_combined_count":40,"third_party_threats":2}', +] + +IPHOTLIST_PARSED_INDICATOR_RESPONSE = [ + { + "value": "203.0.113.5", + "type": "IP", + "fields": { + "tags": "DomainToolsFeeds,iphotlist", + "service": "DomainTools Feeds", + "firstseenbysource": "2025-09-06T22:00:00Z", + "sourcebrands": "FeedDomainTools", + }, + "rawJSON": { + "value": "203.0.113.5", + "type": "IP", + "timestamp": "2025-09-06T22:00:00Z", + "ip_threat_data": { + "asn": 12345, + "organization": "Evil Corp", + "city": "Moscow", + "country": "RU", + "latitude": 55.7558, + "longitude": 37.6173, + "pdns_resolutions": 150, + "bad_pdns_resolutions": 120, + "total_domains": 80, + "all_threats_combined_count": 95, + "third_party_threats": 10, + }, + }, + }, + { + "value": "198.51.100.22", + "type": "IP", + "fields": { + "tags": "DomainToolsFeeds,iphotlist", + "service": "DomainTools Feeds", + "firstseenbysource": "2025-09-06T22:01:00Z", + "sourcebrands": "FeedDomainTools", + }, + "rawJSON": { + "value": "198.51.100.22", + "type": "IP", + "timestamp": "2025-09-06T22:01:00Z", + "ip_threat_data": { + "asn": 67890, + "organization": "BadNet", + "city": "Beijing", + "country": "CN", + "latitude": 39.9042, + "longitude": 116.4074, + "pdns_resolutions": 200, + "bad_pdns_resolutions": 180, + "total_domains": 60, + "all_threats_combined_count": 75, + "third_party_threats": 5, + }, + }, + }, + { + "value": "192.0.2.100", + "type": "IP", + "fields": { + "tags": "DomainToolsFeeds,iphotlist", + "service": "DomainTools Feeds", + "firstseenbysource": "2025-09-06T22:02:00Z", + "sourcebrands": "FeedDomainTools", + }, + "rawJSON": { + "value": "192.0.2.100", + "type": "IP", + "timestamp": "2025-09-06T22:02:00Z", + "ip_threat_data": { + "asn": 11111, + "organization": "SpamCo", + "city": "Lagos", + "country": "NG", + "latitude": 6.5244, + "longitude": 3.3792, + "pdns_resolutions": 50, + "bad_pdns_resolutions": 45, + "total_domains": 30, + "all_threats_combined_count": 40, + "third_party_threats": 2, + }, + }, + }, +] + +IPRISK_RESPONSE = [ + '{"timestamp":"2025-09-06T22:10:00Z","ip":"203.0.113.10","asn":22222,"organization":"RiskNet","city":"Tehran","country":"IR","latitude":35.6892,"longitude":51.3890,"pdns_resolutions":300,"bad_pdns_resolutions":250,"total_domains":90,"all_threats_combined_count":110,"third_party_threats":15}', + '{"timestamp":"2025-09-06T22:11:00Z","ip":"198.51.100.50","asn":33333,"organization":"MalHost","city":"Pyongyang","country":"KP","latitude":39.0194,"longitude":125.7381,"pdns_resolutions":180,"bad_pdns_resolutions":160,"total_domains":55,"all_threats_combined_count":85,"third_party_threats":8}', +] + +IPRISK_PARSED_INDICATOR_RESPONSE = [ + { + "value": "203.0.113.10", + "type": "IP", + "fields": { + "tags": "DomainToolsFeeds,iprisk", + "service": "DomainTools Feeds", + "firstseenbysource": "2025-09-06T22:10:00Z", + "sourcebrands": "FeedDomainTools", + }, + "rawJSON": { + "value": "203.0.113.10", + "type": "IP", + "timestamp": "2025-09-06T22:10:00Z", + "ip_threat_data": { + "asn": 22222, + "organization": "RiskNet", + "city": "Tehran", + "country": "IR", + "latitude": 35.6892, + "longitude": 51.3890, + "pdns_resolutions": 300, + "bad_pdns_resolutions": 250, + "total_domains": 90, + "all_threats_combined_count": 110, + "third_party_threats": 15, + }, + }, + }, + { + "value": "198.51.100.50", + "type": "IP", + "fields": { + "tags": "DomainToolsFeeds,iprisk", + "service": "DomainTools Feeds", + "firstseenbysource": "2025-09-06T22:11:00Z", + "sourcebrands": "FeedDomainTools", + }, + "rawJSON": { + "value": "198.51.100.50", + "type": "IP", + "timestamp": "2025-09-06T22:11:00Z", + "ip_threat_data": { + "asn": 33333, + "organization": "MalHost", + "city": "Pyongyang", + "country": "KP", + "latitude": 39.0194, + "longitude": 125.7381, + "pdns_resolutions": 180, + "bad_pdns_resolutions": 160, + "total_domains": 55, + "all_threats_combined_count": 85, + "third_party_threats": 8, + }, + }, + }, +] + DOMAINHOTLIST_PARSED_INDICATOR_RESPONSE = [ { "value": "scmipgf.icu", From 7e5f386df42fe0d702436359289e010b00edf12f Mon Sep 17 00:00:00 2001 From: JD Babac Date: Tue, 4 Aug 2026 22:43:53 +0800 Subject: [PATCH 2/4] IDEV-2524: Fix test failures. --- .../Integrations/FeedDomainTools/FeedDomainTools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py index d199b2ba228f..a60c7c35ebe0 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py @@ -172,7 +172,7 @@ def build_iterator(self, feed_type: str = "nod", dt_feed_kwargs: dict = {}) -> I timestamp = json_feed.get("timestamp", "") - if self.feed_type in (self.IPHOTLIST, self.IPRISK): + if feed_type in (self.IPHOTLIST, self.IPRISK): indicator = json_feed.get("ip") indicator_type = FeedIndicatorType.IP else: @@ -210,7 +210,7 @@ def build_iterator(self, feed_type: str = "nod", dt_feed_kwargs: dict = {}) -> I # update the parsed dt feed data dt_feed_data["risk_score_details"] = risk_score_details - if self.feed_type in (self.IPHOTLIST, self.IPRISK): + if feed_type in (self.IPHOTLIST, self.IPRISK): ip_threat_data = { "asn": json_feed.get("asn"), "organization": json_feed.get("organization"), From 75616d945ed3e733199487c3b66e145c0684558a Mon Sep 17 00:00:00 2001 From: JD Babac Date: Wed, 5 Aug 2026 01:11:56 +0800 Subject: [PATCH 3/4] IDEV-2524: Add iphotlist and iprisk to FEED_METHOD_MAP. --- .../Integrations/FeedDomainTools/FeedDomainTools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py index a60c7c35ebe0..6097c8f54562 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.py @@ -39,6 +39,8 @@ class DomainToolsClient: "domaindiscovery": "domaindiscovery", "domainrisk": "realtime_domain_risk", "domainhotlist": "domainhotlist", + "iphotlist": "iphotlist", + "iprisk": "iprisk", } def __init__( From ee18ce01aef11088e98b7186965594474b2b60ff Mon Sep 17 00:00:00 2001 From: JD Babac Date: Mon, 10 Aug 2026 19:45:43 +0800 Subject: [PATCH 4/4] IDEV-2524: Bump demisto/vendors-sdk image. --- .../Integrations/FeedDomainTools/FeedDomainTools.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml index 80d2a9618631..b8bdaaa104b3 100644 --- a/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml +++ b/Packs/FeedDomainTools/Integrations/FeedDomainTools/FeedDomainTools.yml @@ -203,7 +203,7 @@ script: default: false required: false secret: false - dockerimage: demisto/vendors-sdk:1.0.0.11252302 + dockerimage: demisto/vendors-sdk:1.0.0.11778894 feed: true isfetch: false longRunning: false