domaintools-api 2.8.0__tar.gz → 2.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {domaintools_api-2.8.0/domaintools_api.egg-info → domaintools_api-2.9.0}/PKG-INFO +1 -1
  2. domaintools_api-2.9.0/VERSION +1 -0
  3. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/_version.py +1 -1
  4. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/api.py +75 -1
  5. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/base_results.py +1 -1
  6. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/feeds.py +167 -7
  7. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/constants.py +2 -0
  8. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/utils.py +3 -1
  9. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/constants.py +8 -0
  10. {domaintools_api-2.8.0 → domaintools_api-2.9.0/domaintools_api.egg-info}/PKG-INFO +1 -1
  11. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_api.egg-info/SOURCES.txt +1 -0
  12. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_api.py +38 -0
  13. domaintools_api-2.9.0/tests/test_iris_enrich_account_info.py +142 -0
  14. domaintools_api-2.8.0/VERSION +0 -1
  15. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/LICENSE +0 -0
  16. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/README.md +0 -0
  17. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/__init__.py +0 -0
  18. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/__init__.py +0 -0
  19. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/api.py +0 -0
  20. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/__init__.py +0 -0
  21. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/accounts.py +0 -0
  22. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/detects.py +0 -0
  23. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/domains.py +0 -0
  24. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/ips.py +0 -0
  25. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/commands/iris.py +0 -0
  26. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/cli/main.py +0 -0
  27. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/decorators.py +0 -0
  28. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/docstring_patcher.py +0 -0
  29. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/exceptions.py +0 -0
  30. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/filters.py +0 -0
  31. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/request_validator.py +0 -0
  32. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/results.py +0 -0
  33. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/specs/iris-openapi.yaml +0 -0
  34. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools/utils.py +0 -0
  35. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  36. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  37. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_api.egg-info/requires.txt +0 -0
  38. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_api.egg-info/top_level.txt +0 -0
  39. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/domaintools_async/__init__.py +0 -0
  40. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/pyproject.toml +0 -0
  41. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/setup.cfg +0 -0
  42. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/setup.py +0 -0
  43. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_async.py +0 -0
  44. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_cli.py +0 -0
  45. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_decorators.py +0 -0
  46. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_docstring_patcher.py +0 -0
  47. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_filters.py +0 -0
  48. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_request_validator.py +0 -0
  49. {domaintools_api-2.8.0 → domaintools_api-2.9.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -0,0 +1 @@
1
+ 2.9.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.8.0"
23
+ current = "2.9.0"
@@ -34,6 +34,7 @@ from domaintools.filters import (
34
34
  filter_by_field,
35
35
  DTResultFilter,
36
36
  )
37
+ from domaintools.exceptions import ServiceUnavailableException, ServiceException
37
38
  from domaintools.utils import validate_feeds_parameters
38
39
 
39
40
 
@@ -163,7 +164,12 @@ class API(object):
163
164
  if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
164
165
  always_sign_api_key_previous_value = self.always_sign_api_key
165
166
  header_authentication_previous_value = self.header_authentication
166
- self._rate_limit(product)
167
+ try:
168
+ self._rate_limit(product)
169
+ except (ServiceUnavailableException, ServiceException):
170
+ # If account_information fails (e.g. rate limited itself),
171
+ # skip client-side rate limiting and proceed with the original call.
172
+ self.limits_set = True
167
173
  # Reset always_sign_api_key and header_authentication to its original
168
174
  # User-set values as these might be affected when self.account_information() was executed
169
175
  self.always_sign_api_key = always_sign_api_key_previous_value
@@ -1406,3 +1412,71 @@ class API(object):
1406
1412
  cls=FeedsResults,
1407
1413
  **kwargs,
1408
1414
  )
1415
+
1416
+ def iphotlist(self, **kwargs) -> FeedsResults:
1417
+ """Returns back list of ip hotlist feed.
1418
+ Captures IP addresses that meet strict criteria for both risk level and recent activity, making it ideal for immediate blocking and threat response.
1419
+
1420
+ before: str: Filter for records before the given time value inclusive or time offset relative to now
1421
+
1422
+ after: str: Filter for records after the given time value inclusive or time offset relative to now
1423
+
1424
+ headers: bool: Use in combination with Accept: text/csv headers to control if headers are sent or not
1425
+
1426
+ sessionID: str: A custom string to distinguish between different sessions
1427
+
1428
+ 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
1429
+
1430
+ top: int: Limits the number of results in the response payload. Primarily intended for testing. When you apply this parameter to risk feeds, results are sorted by all_threats_combined_percent (descending).
1431
+ """
1432
+ validate_feeds_parameters(kwargs)
1433
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1434
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1435
+ if (
1436
+ endpoint == Endpoint.DOWNLOAD.value
1437
+ or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1438
+ ):
1439
+ # headers param is allowed only in Feed API and CSV format
1440
+ kwargs.pop("headers", None)
1441
+
1442
+ return self._results(
1443
+ f"real-time-ip-hotlist-({source})",
1444
+ f"v1/{endpoint}/iphotlist/",
1445
+ response_path=(),
1446
+ cls=FeedsResults,
1447
+ **kwargs,
1448
+ )
1449
+
1450
+ def iprisk(self, **kwargs) -> FeedsResults:
1451
+ """Returns back list of domain hotlist feed.
1452
+ Captures all IP addresses that actively host one or more domains, providing risk assessment and enrichment data for each IP address.
1453
+
1454
+ before: str: Filter for records before the given time value inclusive or time offset relative to now
1455
+
1456
+ after: str: Filter for records after the given time value inclusive or time offset relative to now
1457
+
1458
+ headers: bool: Use in combination with Accept: text/csv headers to control if headers are sent or not
1459
+
1460
+ sessionID: str: A custom string to distinguish between different sessions
1461
+
1462
+ 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
1463
+
1464
+ top: int: Limits the number of results in the response payload. Primarily intended for testing. When you apply this parameter to risk feeds, results are sorted by all_threats_combined_percent (descending).
1465
+ """
1466
+ validate_feeds_parameters(kwargs)
1467
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1468
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1469
+ if (
1470
+ endpoint == Endpoint.DOWNLOAD.value
1471
+ or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
1472
+ ):
1473
+ # headers param is allowed only in Feed API and CSV format
1474
+ kwargs.pop("headers", None)
1475
+
1476
+ return self._results(
1477
+ f"real-time-ip-risk-({source})",
1478
+ f"v1/{endpoint}/iprisk/",
1479
+ response_path=(),
1480
+ cls=FeedsResults,
1481
+ **kwargs,
1482
+ )
@@ -140,7 +140,7 @@ class Results(MutableMapping, MutableSequence):
140
140
  wait_for = self._wait_time()
141
141
  if self.api.rate_limit and (wait_for is None or self.product == "account-information"):
142
142
  data = self._make_request()
143
- if data.status_code == 503: # pragma: no cover
143
+ if data.status_code == 503 and self.product != "account-information": # pragma: no cover
144
144
  sleeptime = 60
145
145
  log.info(
146
146
  "503 encountered for [%s] - sleeping [%s] seconds before retrying request.",
@@ -79,7 +79,7 @@ def feeds_nad(
79
79
  "--headers",
80
80
  help="Adds a header to the first line of response when text/csv is set in header parameters",
81
81
  ),
82
- top: str = typer.Option(
82
+ top: int = typer.Option(
83
83
  None,
84
84
  "--top",
85
85
  help="Number of results to return in the response payload. This is ignored in download endpoint",
@@ -159,7 +159,7 @@ def feeds_nod(
159
159
  "--headers",
160
160
  help="Adds a header to the first line of response when text/csv is set in header parameters",
161
161
  ),
162
- top: str = typer.Option(
162
+ top: int = typer.Option(
163
163
  None,
164
164
  "--top",
165
165
  help="Number of results to return in the response payload. This is ignored in download endpoint",
@@ -227,7 +227,7 @@ def feeds_domainrdap(
227
227
  "--domain",
228
228
  help="A string value used to filter feed results",
229
229
  ),
230
- top: str = typer.Option(
230
+ top: int = typer.Option(
231
231
  None,
232
232
  "--top",
233
233
  help="Number of results to return in the response payload",
@@ -307,7 +307,7 @@ def feeds_domaindiscovery(
307
307
  "--headers",
308
308
  help="Adds a header to the first line of response when text/csv is set in header parameters",
309
309
  ),
310
- top: str = typer.Option(
310
+ top: int = typer.Option(
311
311
  None,
312
312
  "--top",
313
313
  help="Number of results to return in the response payload. This is ignored in download endpoint",
@@ -387,7 +387,7 @@ def feeds_noh(
387
387
  "--headers",
388
388
  help="Adds a header to the first line of response when text/csv is set in header parameters",
389
389
  ),
390
- top: str = typer.Option(
390
+ top: int = typer.Option(
391
391
  None,
392
392
  "--top",
393
393
  help="Number of results to return in the response payload. This is ignored in download endpoint",
@@ -467,7 +467,7 @@ def feeds_domainhotlist(
467
467
  "--headers",
468
468
  help="Adds a header to the first line of response when text/csv is set in header parameters",
469
469
  ),
470
- top: str = typer.Option(
470
+ top: int = typer.Option(
471
471
  None,
472
472
  "--top",
473
473
  help="Number of results to return in the response payload. This is ignored in download endpoint",
@@ -547,10 +547,170 @@ def feeds_realtime_domain_risk(
547
547
  "--headers",
548
548
  help="Adds a header to the first line of response when text/csv is set in header parameters",
549
549
  ),
550
- top: str = typer.Option(
550
+ top: int = typer.Option(
551
551
  None,
552
552
  "--top",
553
553
  help="Number of results to return in the response payload. This is ignored in download endpoint",
554
554
  ),
555
555
  ):
556
556
  DTCLICommand.run(name=c.FEEDS_REALTIME_DOMAIN_RISK, params=ctx.params)
557
+
558
+
559
+ @dt_cli.command(
560
+ name=c.FEEDS_IPHOTLIST,
561
+ help=get_cli_helptext_by_name(command_name=c.FEEDS_IPHOTLIST),
562
+ )
563
+ def feeds_iphotlist(
564
+ ctx: typer.Context,
565
+ user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
566
+ key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
567
+ creds_file: str = typer.Option(
568
+ "~/.dtapi",
569
+ "-c",
570
+ "--credfile",
571
+ help="Optional file with API username and API key, one per line.",
572
+ ),
573
+ no_verify_ssl: bool = typer.Option(
574
+ False,
575
+ "--no-verify-ssl",
576
+ help="Skip verification of SSL certificate when making HTTPs API calls",
577
+ ),
578
+ no_sign_api_key: bool = typer.Option(
579
+ False,
580
+ "--no-sign-api-key",
581
+ help="Skip signing of api key",
582
+ ),
583
+ no_header_authentication: bool = typer.Option(
584
+ False,
585
+ "--no-header-auth",
586
+ help="Don't use header authentication",
587
+ ),
588
+ output_format: str = typer.Option(
589
+ "jsonl",
590
+ "-f",
591
+ "--format",
592
+ help=f"Output format in [{OutputFormat.JSONL.value}, {OutputFormat.CSV.value}]",
593
+ callback=DTCLICommand.validate_feeds_format_input,
594
+ ),
595
+ endpoint: str = typer.Option(
596
+ Endpoint.FEED.value,
597
+ "-e",
598
+ "--endpoint",
599
+ help=f"Valid endpoints: [{Endpoint.FEED.value}, {Endpoint.DOWNLOAD.value}]",
600
+ callback=DTCLICommand.validate_endpoint_input,
601
+ ),
602
+ sessionID: str = typer.Option(
603
+ None,
604
+ "--session-id",
605
+ help="Unique identifier for the session",
606
+ ),
607
+ after: str = typer.Option(
608
+ None,
609
+ "--after",
610
+ help="Start of the time window, relative to the current time in seconds, for which data will be provided",
611
+ callback=DTCLICommand.validate_after_or_before_input,
612
+ ),
613
+ before: str = typer.Option(
614
+ None,
615
+ "--before",
616
+ help="The end of the query window in seconds, relative to the current time, inclusive",
617
+ callback=DTCLICommand.validate_after_or_before_input,
618
+ ),
619
+ fromBeginning: bool = typer.Option(
620
+ None,
621
+ "-fb",
622
+ "--frombeginning",
623
+ 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",
624
+ ),
625
+ top: int = typer.Option(
626
+ None,
627
+ "--top",
628
+ help="Number of results to return in the response payload. This is ignored in download endpoint",
629
+ ),
630
+ headers: bool = typer.Option(
631
+ False,
632
+ "--headers",
633
+ help="Adds a header to the first line of response when text/csv is set in header parameters",
634
+ ),
635
+ ):
636
+ DTCLICommand.run(name=c.FEEDS_IPHOTLIST, params=ctx.params)
637
+
638
+
639
+ @dt_cli.command(
640
+ name=c.FEEDS_IPRISK,
641
+ help=get_cli_helptext_by_name(command_name=c.FEEDS_IPRISK),
642
+ )
643
+ def feeds_iprisk(
644
+ ctx: typer.Context,
645
+ user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
646
+ key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
647
+ creds_file: str = typer.Option(
648
+ "~/.dtapi",
649
+ "-c",
650
+ "--credfile",
651
+ help="Optional file with API username and API key, one per line.",
652
+ ),
653
+ no_verify_ssl: bool = typer.Option(
654
+ False,
655
+ "--no-verify-ssl",
656
+ help="Skip verification of SSL certificate when making HTTPs API calls",
657
+ ),
658
+ no_sign_api_key: bool = typer.Option(
659
+ False,
660
+ "--no-sign-api-key",
661
+ help="Skip signing of api key",
662
+ ),
663
+ no_header_authentication: bool = typer.Option(
664
+ False,
665
+ "--no-header-auth",
666
+ help="Don't use header authentication",
667
+ ),
668
+ output_format: str = typer.Option(
669
+ "jsonl",
670
+ "-f",
671
+ "--format",
672
+ help=f"Output format in [{OutputFormat.JSONL.value}, {OutputFormat.CSV.value}]",
673
+ callback=DTCLICommand.validate_feeds_format_input,
674
+ ),
675
+ endpoint: str = typer.Option(
676
+ Endpoint.FEED.value,
677
+ "-e",
678
+ "--endpoint",
679
+ help=f"Valid endpoints: [{Endpoint.FEED.value}, {Endpoint.DOWNLOAD.value}]",
680
+ callback=DTCLICommand.validate_endpoint_input,
681
+ ),
682
+ sessionID: str = typer.Option(
683
+ None,
684
+ "--session-id",
685
+ help="Unique identifier for the session",
686
+ ),
687
+ after: str = typer.Option(
688
+ None,
689
+ "--after",
690
+ help="Start of the time window, relative to the current time in seconds, for which data will be provided",
691
+ callback=DTCLICommand.validate_after_or_before_input,
692
+ ),
693
+ before: str = typer.Option(
694
+ None,
695
+ "--before",
696
+ help="The end of the query window in seconds, relative to the current time, inclusive",
697
+ callback=DTCLICommand.validate_after_or_before_input,
698
+ ),
699
+ fromBeginning: bool = typer.Option(
700
+ None,
701
+ "-fb",
702
+ "--frombeginning",
703
+ 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",
704
+ ),
705
+ top: int = typer.Option(
706
+ None,
707
+ "--top",
708
+ help="Number of results to return in the response payload. This is ignored in download endpoint",
709
+ ),
710
+ headers: bool = typer.Option(
711
+ False,
712
+ "--headers",
713
+ help="Adds a header to the first line of response when text/csv is set in header parameters",
714
+ ),
715
+ ):
716
+ DTCLICommand.run(name=c.FEEDS_IPRISK, params=ctx.params)
@@ -48,3 +48,5 @@ FEEDS_DOMAINHOTLIST = "domainhotlist"
48
48
  FEEDS_DOMAINRDAP = "domainrdap"
49
49
  FEEDS_DOMAINDISCOVERY = "domaindiscovery"
50
50
  FEEDS_REALTIME_DOMAIN_RISK = "realtime_domain_risk"
51
+ FEEDS_IPHOTLIST = "iphotlist"
52
+ FEEDS_IPRISK = "iprisk"
@@ -65,10 +65,12 @@ _command_help_mapper = {
65
65
  c.FEEDS_NAD: "Returns back newly active domains feed.",
66
66
  c.FEEDS_NOD: "Returns back newly observed domains feed.",
67
67
  c.FEEDS_NOH: "Returns back newly observed hosts feed.",
68
- c.FEEDS_DOMAINHOTLIST: "Returns domaint hotlist feed.",
68
+ c.FEEDS_DOMAINHOTLIST: "Returns domain hotlist feed.",
69
69
  c.FEEDS_DOMAINRDAP: "Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP).",
70
70
  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.",
71
71
  c.FEEDS_REALTIME_DOMAIN_RISK: "Returns realtime domain risk information for apex-level domains, regardless of observed traffic.",
72
+ c.FEEDS_IPHOTLIST: "Returns ip hotlist feed.",
73
+ c.FEEDS_IPRISK: "Returns ip risk feed.",
72
74
  }
73
75
 
74
76
 
@@ -38,6 +38,10 @@ RTTF_PRODUCTS_LIST = [
38
38
  "real-time-domain-risk-(s3)",
39
39
  "real-time-domain-discovery-feed-(api)",
40
40
  "real-time-domain-discovery-feed-(s3)",
41
+ "real-time-ip-hotlist-(api)",
42
+ "real-time-ip-hotlist-(s3)",
43
+ "real-time-ip-risk-(api)",
44
+ "real-time-ip-risk-(s3)",
41
45
  ]
42
46
 
43
47
  RTTF_PRODUCTS_CMD_MAPPING = {
@@ -55,6 +59,10 @@ RTTF_PRODUCTS_CMD_MAPPING = {
55
59
  "real-time-domain-risk-(s3)": "realtime_domain_risk",
56
60
  "real-time-domain-discovery-feed-(api)": "domaindiscovery",
57
61
  "real-time-domain-discovery-feed-(s3)": "domaindiscovery",
62
+ "real-time-ip-hotlist-(api)": "iphotlist",
63
+ "real-time-ip-hotlist-(s3)": "iphotlist",
64
+ "real-time-ip-risk-(api)": "iprisk",
65
+ "real-time-ip-risk-(s3)": "iprisk",
58
66
  }
59
67
 
60
68
  SPECS_MAPPING = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: domaintools_api
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -42,5 +42,6 @@ tests/test_cli.py
42
42
  tests/test_decorators.py
43
43
  tests/test_docstring_patcher.py
44
44
  tests/test_filters.py
45
+ tests/test_iris_enrich_account_info.py
45
46
  tests/test_request_validator.py
46
47
  tests/test_utils.py
@@ -827,6 +827,43 @@ def test_domain_hotlist():
827
827
  assert "proximity_risk" in feed_result.keys()
828
828
  assert "overall_risk" in feed_result.keys()
829
829
 
830
+ @vcr.use_cassette
831
+ def test_ip_hotlist():
832
+ results = feeds_api.iphotlist(after="-60", top=5)
833
+ for response in results.response():
834
+ assert results.status == 200
835
+
836
+ response = response.strip()
837
+ assert response is not None
838
+
839
+ feed_result = json.loads(response)
840
+ assert "timestamp" in feed_result.keys()
841
+ assert "ip" in feed_result.keys()
842
+ assert "asn" in feed_result.keys()
843
+ assert "organization" in feed_result.keys()
844
+ assert "total_domains" in feed_result.keys()
845
+ assert "all_threats_combined_count" in feed_result.keys()
846
+ assert "all_threats_combined_percent" in feed_result.keys()
847
+
848
+
849
+ @vcr.use_cassette
850
+ def test_ip_risk():
851
+ results = feeds_api.iprisk(after="-60", top=5)
852
+ for response in results.response():
853
+ assert results.status == 200
854
+
855
+ response = response.strip()
856
+ assert response is not None
857
+
858
+ feed_result = json.loads(response)
859
+ assert "timestamp" in feed_result.keys()
860
+ assert "ip" in feed_result.keys()
861
+ assert "asn" in feed_result.keys()
862
+ assert "organization" in feed_result.keys()
863
+ assert "total_domains" in feed_result.keys()
864
+ assert "all_threats_combined_count" in feed_result.keys()
865
+ assert "all_threats_combined_percent" in feed_result.keys()
866
+
830
867
 
831
868
  @vcr.use_cassette
832
869
  def test_feeds_endpoint_should_raise_error_if_signed_api_key_is_used():
@@ -835,3 +872,4 @@ def test_feeds_endpoint_should_raise_error_if_signed_api_key_is_used():
835
872
  feeds_api.domaindiscovery(after="-60")
836
873
 
837
874
  assert str(excinfo.value) == "Real Time Threat Feeds do not support signed API keys."
875
+
@@ -0,0 +1,142 @@
1
+ """Test that iris_enrich only triggers account_information once per API instance."""
2
+
3
+ from unittest.mock import patch, MagicMock
4
+
5
+ from domaintools import API
6
+ from domaintools.exceptions import ServiceUnavailableException
7
+
8
+
9
+ def test_iris_enrich_does_not_repeat_account_info_call():
10
+ """Confirm account_information is fetched only once for multiple iris_enrich calls on same API instance."""
11
+ # Reset class-level state that may have been set by previous tests
12
+ API.limits = {}
13
+ API.limits_set = False
14
+
15
+ api = API("test_user", "test_key", rate_limit=True)
16
+
17
+ # Fake account info response
18
+ fake_account_data = {
19
+ "response": {
20
+ "products": [
21
+ {"id": "iris-enrich", "per_minute_limit": 10, "per_hour_limit": 100}
22
+ ]
23
+ }
24
+ }
25
+
26
+ # Fake iris-enrich response
27
+ fake_enrich_data = {
28
+ "response": {
29
+ "results": [{"domain": "google.com"}],
30
+ "results_count": 1,
31
+ "missing_domains": [],
32
+ "limit_exceeded": False,
33
+ }
34
+ }
35
+
36
+ call_log = []
37
+
38
+ with patch("domaintools.base_results.Client") as mock_client:
39
+ mock_session = MagicMock()
40
+ mock_client.return_value.__enter__ = MagicMock(return_value=mock_session)
41
+ mock_client.return_value.__exit__ = MagicMock(return_value=False)
42
+
43
+ def fake_get(url, **kwargs):
44
+ call_log.append(("GET", url))
45
+ resp = MagicMock()
46
+ resp.status_code = 200
47
+ if "/v1/account" in url:
48
+ resp.json.return_value = fake_account_data
49
+ return resp
50
+
51
+ def fake_post(url, **kwargs):
52
+ call_log.append(("POST", url))
53
+ resp = MagicMock()
54
+ resp.status_code = 200
55
+ resp.json.return_value = fake_enrich_data
56
+ return resp
57
+
58
+ mock_session.get.side_effect = fake_get
59
+ mock_session.post.side_effect = fake_post
60
+
61
+ # First call - should trigger account_information (GET /v1/account)
62
+ api.iris_enrich("google.com")
63
+ # Second call - should NOT trigger account_information again
64
+ api.iris_enrich("amazon.com")
65
+
66
+ # Count how many times /v1/account was called
67
+ account_calls = [c for c in call_log if "/v1/account" in c[1]]
68
+ enrich_calls = [c for c in call_log if "/v1/iris-enrich" in c[1]]
69
+
70
+ assert len(account_calls) == 1, (
71
+ f"Expected exactly 1 account_information call, got {len(account_calls)}. "
72
+ f"Full call log: {call_log}"
73
+ )
74
+ assert len(enrich_calls) == 2, (
75
+ f"Expected exactly 2 iris_enrich calls, got {len(enrich_calls)}. "
76
+ f"Full call log: {call_log}"
77
+ )
78
+
79
+
80
+ def test_iris_enrich_succeeds_when_account_info_is_rate_limited():
81
+ """Verify that iris_enrich proceeds normally even if account_information returns a 503."""
82
+ # Reset class-level state that may have been set by previous tests
83
+ API.limits = {}
84
+ API.limits_set = False
85
+
86
+ api = API("test_user", "test_key", rate_limit=True)
87
+
88
+ # Fake iris-enrich response
89
+ fake_enrich_data = {
90
+ "response": {
91
+ "results": [{"domain": "google.com"}],
92
+ "results_count": 1,
93
+ "missing_domains": [],
94
+ "limit_exceeded": False,
95
+ }
96
+ }
97
+
98
+ call_log = []
99
+
100
+ with patch("domaintools.base_results.Client") as mock_client, \
101
+ patch("domaintools.base_results.time.sleep"):
102
+ mock_session = MagicMock()
103
+ mock_client.return_value.__enter__ = MagicMock(return_value=mock_session)
104
+ mock_client.return_value.__exit__ = MagicMock(return_value=False)
105
+
106
+ def fake_get(url, **kwargs):
107
+ call_log.append(("GET", url))
108
+ resp = MagicMock()
109
+ if "/v1/account" in url:
110
+ # Simulate a 503 rate limit on account_information
111
+ resp.status_code = 503
112
+ resp.json.return_value = {"error": {"message": "Rate limit exceeded"}}
113
+ else:
114
+ resp.status_code = 200
115
+ resp.json.return_value = {}
116
+ return resp
117
+
118
+ def fake_post(url, **kwargs):
119
+ call_log.append(("POST", url))
120
+ resp = MagicMock()
121
+ resp.status_code = 200
122
+ resp.json.return_value = fake_enrich_data
123
+ return resp
124
+
125
+ mock_session.get.side_effect = fake_get
126
+ mock_session.post.side_effect = fake_post
127
+
128
+ # This should NOT raise an exception despite account_info returning 503
129
+ result = api.iris_enrich("google.com")
130
+
131
+ # Verify the iris_enrich call succeeded
132
+ enrich_calls = [c for c in call_log if "/v1/iris-enrich" in c[1]]
133
+ assert len(enrich_calls) == 1, (
134
+ f"Expected 1 iris_enrich call, got {len(enrich_calls)}. Full call log: {call_log}"
135
+ )
136
+
137
+ # Verify limits_set was set to True so it won't retry account_info
138
+ assert api.limits_set is True
139
+
140
+ # Verify the result contains expected data
141
+ assert result["results_count"] == 1
142
+ assert result["results"][0]["domain"] == "google.com"
@@ -1 +0,0 @@
1
- 2.8.0
File without changes