domaintools-api-test-upload 2.2.1__tar.gz → 2.4.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 (42) hide show
  1. {domaintools_api_test_upload-2.2.1/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.4.0}/PKG-INFO +42 -6
  2. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/README.md +39 -4
  3. domaintools_api_test_upload-2.4.0/VERSION +1 -0
  4. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/_version.py +1 -1
  5. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/api.py +29 -41
  6. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/base_results.py +35 -37
  7. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/api.py +7 -2
  8. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/__init__.py +0 -1
  9. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/feeds.py +80 -0
  10. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/constants.py +1 -4
  11. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/utils.py +2 -21
  12. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/constants.py +2 -0
  13. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/results.py +27 -0
  14. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/utils.py +7 -2
  15. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0/domaintools_api_test_upload.egg-info}/PKG-INFO +42 -6
  16. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -1
  17. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_async/__init__.py +4 -18
  18. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/setup.py +2 -1
  19. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/tests/test_api.py +139 -89
  20. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/tests/test_utils.py +54 -52
  21. domaintools_api_test_upload-2.2.1/VERSION +0 -1
  22. domaintools_api_test_upload-2.2.1/domaintools/cli/commands/phisheye.py +0 -100
  23. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/LICENSE +0 -0
  24. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/__init__.py +0 -0
  25. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/__init__.py +0 -0
  26. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/accounts.py +0 -0
  27. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/detects.py +0 -0
  28. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/domains.py +0 -0
  29. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/ips.py +0 -0
  30. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/commands/iris.py +0 -0
  31. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/cli/main.py +0 -0
  32. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/exceptions.py +0 -0
  33. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools/filters.py +0 -0
  34. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
  35. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
  36. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
  37. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
  38. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/pyproject.toml +0 -0
  39. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/setup.cfg +0 -0
  40. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/tests/test_async.py +0 -0
  41. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/tests/test_cli.py +0 -0
  42. {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.4.0}/tests/test_filters.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: domaintools_api_test_upload
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -46,6 +46,7 @@ Requires-Dist: typer
46
46
  Provides-Extra: test
47
47
  Requires-Dist: pytest; extra == "test"
48
48
  Requires-Dist: mock; extra == "test"
49
+ Dynamic: license-file
49
50
 
50
51
  ![domaintools](https://github.com/DomainTools/python_api/raw/main/artwork/logo.png)
51
52
  ===================
@@ -272,16 +273,51 @@ Real-Time Threat Intelligence Feeds provide data on the different stages of the
272
273
  Custom parameters aside from the common `GET` Request parameters:
273
274
  - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
274
275
  ```python
275
- api = API(USERNAME, KEY)
276
+ api = API(USERNAME, KEY, always_sign_api_key=False)
276
277
  api.nod(endpoint="feed", **kwargs)
277
278
  ```
278
- - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so you need to set this to `False` when calling `download` API endpoints.
279
+ - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
279
280
  ```python
280
- api = API(USERNAME, KEY)
281
+ api = API(USERNAME, KEY, always_sign_api_key=False)
281
282
  api.nod(header_authentication=False, **kwargs)
282
283
  ```
283
284
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
284
285
  ```python
285
- api = API(USERNAME, KEY)
286
+ api = API(USERNAME, KEY, always_sign_api_key=False)
286
287
  api.nod(output_format="csv", **kwargs)
287
288
  ```
289
+
290
+ The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
291
+
292
+ 1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
293
+ - Each subsequent call to the API using your `sessionID` will return all data since the last.
294
+ - Any single request returns a maximum of 10M results. Requests that exceed 10M results will return a HTTP 206 response code; repeat the same request (with the same `sessionID`) to receive the next tranche of data until receiving a HTTP 200 response code.
295
+ 2. Or, specify the time range in one of two ways:
296
+ - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
297
+ - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
298
+
299
+ ## Handling iterative response from RTUF endpoints:
300
+
301
+ Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
302
+
303
+ ### Single request because the requested data is within the maximum result:
304
+ ```python
305
+ from domaintools import API
306
+
307
+ api = API(USERNAME, KEY, always_sign_api_key=False)
308
+ results = api.nod(sessionID="my-session-id", after=-60)
309
+
310
+ for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
311
+ # do things to result
312
+ ```
313
+
314
+ ## Multiple requests because the requested data is more than the maximum result per request:
315
+ ```python
316
+ from domaintools import API
317
+
318
+ api = API(USERNAME, KEY, always_sign_api_key=False)
319
+ results = api.nod(sessionID="my-session-id", after=-7200)
320
+
321
+ for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
322
+ # do things to partial_result
323
+ ```
@@ -223,16 +223,51 @@ Real-Time Threat Intelligence Feeds provide data on the different stages of the
223
223
  Custom parameters aside from the common `GET` Request parameters:
224
224
  - `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
225
225
  ```python
226
- api = API(USERNAME, KEY)
226
+ api = API(USERNAME, KEY, always_sign_api_key=False)
227
227
  api.nod(endpoint="feed", **kwargs)
228
228
  ```
229
- - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so you need to set this to `False` when calling `download` API endpoints.
229
+ - `header_authentication`: by default, we're using API Header Authentication. Set this False if you want to use API Key and Secret Authentication. Apparently, you can't use API Header Authentication for `download` endpoints so this will be defaulted to `False` even without explicitly setting it.
230
230
  ```python
231
- api = API(USERNAME, KEY)
231
+ api = API(USERNAME, KEY, always_sign_api_key=False)
232
232
  api.nod(header_authentication=False, **kwargs)
233
233
  ```
234
234
  - `output_format`: (choose either `csv` or `jsonl` - default is `jsonl`). Cannot be used in `domainrdap` feeds. Additionally, `csv` is not available for `download` endpoints.
235
235
  ```python
236
- api = API(USERNAME, KEY)
236
+ api = API(USERNAME, KEY, always_sign_api_key=False)
237
237
  api.nod(output_format="csv", **kwargs)
238
238
  ```
239
+
240
+ The Feed API standard access pattern is to periodically request the most recent feed data, as often as every 60 seconds. Specify the range of data you receive in one of two ways:
241
+
242
+ 1. With `sessionID`: Make a call and provide a new `sessionID` parameter of your choosing. The API will return the last hour of data by default.
243
+ - Each subsequent call to the API using your `sessionID` will return all data since the last.
244
+ - Any single request returns a maximum of 10M results. Requests that exceed 10M results will return a HTTP 206 response code; repeat the same request (with the same `sessionID`) to receive the next tranche of data until receiving a HTTP 200 response code.
245
+ 2. Or, specify the time range in one of two ways:
246
+ - Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
247
+ - Or `after` and `before` query parameters for a time range, with each parameter accepting an ISO-8601 UTC formatted timestamp (a UTC date and time of the format YYYY-MM-DDThh:mm:ssZ)
248
+
249
+ ## Handling iterative response from RTUF endpoints:
250
+
251
+ Since we may dealing with large feeds datasets, the python wrapper uses `generator` for efficient memory handling. Therefore, we need to iterate through the `generator` if we're accessing the partial results of the feeds data.
252
+
253
+ ### Single request because the requested data is within the maximum result:
254
+ ```python
255
+ from domaintools import API
256
+
257
+ api = API(USERNAME, KEY, always_sign_api_key=False)
258
+ results = api.nod(sessionID="my-session-id", after=-60)
259
+
260
+ for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
261
+ # do things to result
262
+ ```
263
+
264
+ ## Multiple requests because the requested data is more than the maximum result per request:
265
+ ```python
266
+ from domaintools import API
267
+
268
+ api = API(USERNAME, KEY, always_sign_api_key=False)
269
+ results = api.nod(sessionID="my-session-id", after=-7200)
270
+
271
+ for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
272
+ # do things to partial_result
273
+ ```
@@ -0,0 +1 @@
1
+ 2.4.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.2.1"
23
+ current = "2.4.0"
@@ -1,6 +1,7 @@
1
1
  from datetime import datetime, timedelta, timezone
2
2
  from hashlib import sha1, sha256, md5
3
3
  from hmac import new as hmac
4
+
4
5
  import re
5
6
 
6
7
  from domaintools.constants import Endpoint, ENDPOINT_TO_SOURCE_MAP, FEEDS_PRODUCTS_LIST, OutputFormat
@@ -11,6 +12,7 @@ from domaintools.results import (
11
12
  ParsedDomainRdap,
12
13
  Reputation,
13
14
  Results,
15
+ FeedsResults,
14
16
  )
15
17
  from domaintools.filters import (
16
18
  filter_by_riskscore,
@@ -87,11 +89,8 @@ class API(object):
87
89
 
88
90
  if not https:
89
91
  raise Exception("The DomainTools API endpoints no longer support http traffic. Please make sure https=True.")
90
- if proxy_url:
91
- if isinstance(proxy_url, str):
92
- self.proxy_url = {"http://": proxy_url, "https://": proxy_url}
93
- else:
94
- raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
92
+ if proxy_url and not isinstance(proxy_url, str):
93
+ raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
95
94
 
96
95
  def _build_api_url(self, api_url=None, api_port=None):
97
96
  """Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
@@ -412,38 +411,6 @@ class API(object):
412
411
  **kwargs,
413
412
  )
414
413
 
415
- def phisheye(self, query, days_back=None, **kwargs):
416
- """Returns domain results for the specified term for today or the specified number of days_back.
417
- Terms must be setup for monitoring via the web interface: https://research.domaintools.com/phisheye.
418
-
419
- NOTE: Properties of a domain are only provided if we have been able to obtain them.
420
- Many domains will have incomplete data because that information isn't available in their Whois records,
421
- or they don't have DNS results for a name server or IP address.
422
- """
423
- return self._results(
424
- "phisheye",
425
- "/v1/phisheye",
426
- query=query,
427
- days_back=days_back,
428
- items_path=("domains",),
429
- **kwargs,
430
- )
431
-
432
- def phisheye_term_list(self, include_inactive=False, **kwargs):
433
- """Provides a list of terms that are set up for this account.
434
- This call is not charged against your API usage limit.
435
-
436
- NOTE: The terms must be configured in the PhishEye web interface: https://research.domaintools.com/phisheye.
437
- There is no API call to set up the terms.
438
- """
439
- return self._results(
440
- "phisheye_term_list",
441
- "/v1/phisheye/term-list",
442
- include_inactive=include_inactive,
443
- items_path=("terms",),
444
- **kwargs,
445
- )
446
-
447
414
  def iris(
448
415
  self,
449
416
  domain=None,
@@ -1065,7 +1032,7 @@ class API(object):
1065
1032
  **kwargs,
1066
1033
  )
1067
1034
 
1068
- def nod(self, **kwargs):
1035
+ def nod(self, **kwargs) -> FeedsResults:
1069
1036
  """Returns back list of the newly observed domains feed"""
1070
1037
  validate_feeds_parameters(kwargs)
1071
1038
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
@@ -1078,10 +1045,11 @@ class API(object):
1078
1045
  f"newly-observed-domains-feed-({source.value})",
1079
1046
  f"v1/{endpoint}/nod/",
1080
1047
  response_path=(),
1048
+ cls=FeedsResults,
1081
1049
  **kwargs,
1082
1050
  )
1083
1051
 
1084
- def nad(self, **kwargs):
1052
+ def nad(self, **kwargs) -> FeedsResults:
1085
1053
  """Returns back list of the newly active domains feed"""
1086
1054
  validate_feeds_parameters(kwargs)
1087
1055
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
@@ -1094,10 +1062,11 @@ class API(object):
1094
1062
  f"newly-active-domains-feed-({source})",
1095
1063
  f"v1/{endpoint}/nad/",
1096
1064
  response_path=(),
1065
+ cls=FeedsResults,
1097
1066
  **kwargs,
1098
1067
  )
1099
1068
 
1100
- def domainrdap(self, **kwargs):
1069
+ def domainrdap(self, **kwargs) -> FeedsResults:
1101
1070
  """Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP)"""
1102
1071
  validate_feeds_parameters(kwargs)
1103
1072
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
@@ -1107,10 +1076,11 @@ class API(object):
1107
1076
  f"domain-registration-data-access-protocol-feed-({source})",
1108
1077
  f"v1/{endpoint}/domainrdap/",
1109
1078
  response_path=(),
1079
+ cls=FeedsResults,
1110
1080
  **kwargs,
1111
1081
  )
1112
1082
 
1113
- def domaindiscovery(self, **kwargs):
1083
+ def domaindiscovery(self, **kwargs) -> FeedsResults:
1114
1084
  """Returns new domains as they are either discovered in domain registration information, observed by our global sensor network, or reported by trusted third parties"""
1115
1085
  validate_feeds_parameters(kwargs)
1116
1086
  endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
@@ -1123,5 +1093,23 @@ class API(object):
1123
1093
  f"real-time-domain-discovery-feed-({source})",
1124
1094
  f"v1/{endpoint}/domaindiscovery/",
1125
1095
  response_path=(),
1096
+ cls=FeedsResults,
1097
+ **kwargs,
1098
+ )
1099
+
1100
+ def noh(self, **kwargs) -> FeedsResults:
1101
+ """Returns back list of the newly observed hostnames feed"""
1102
+ validate_feeds_parameters(kwargs)
1103
+ endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
1104
+ source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
1105
+ if endpoint == Endpoint.DOWNLOAD.value or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value:
1106
+ # headers param is allowed only in Feed API and CSV format
1107
+ kwargs.pop("headers", None)
1108
+
1109
+ return self._results(
1110
+ f"newly-observed-hosts-feed-({source})",
1111
+ f"v1/{endpoint}/noh/",
1112
+ response_path=(),
1113
+ cls=FeedsResults,
1126
1114
  **kwargs,
1127
1115
  )
@@ -53,8 +53,6 @@ class Results(MutableMapping, MutableSequence):
53
53
  self._response = None
54
54
  self._items_list = None
55
55
  self._data = None
56
- self._limit_exceeded = None
57
- self._limit_exceeded_message = None
58
56
 
59
57
  def _wait_time(self):
60
58
  if not self.api.rate_limit or not self.product in self.api.limits:
@@ -77,6 +75,23 @@ class Results(MutableMapping, MutableSequence):
77
75
 
78
76
  return wait_for
79
77
 
78
+ def _get_session_params(self):
79
+ parameters = deepcopy(self.kwargs)
80
+ parameters.pop("output_format", None)
81
+ parameters.pop(
82
+ "format", None
83
+ ) # For some unknownn reasons, even if "format" is not included in the cli params for feeds endpoint, it is being populated thus we need to remove it. Happens only if using CLI.
84
+ headers = {}
85
+ if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
86
+ parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
87
+ headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
88
+
89
+ header_api_key = parameters.pop("X-Api-Key", None)
90
+ if header_api_key:
91
+ headers["X-Api-Key"] = header_api_key
92
+
93
+ return {"parameters": parameters, "headers": headers}
94
+
80
95
  def _make_request(self):
81
96
 
82
97
  with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
@@ -93,20 +108,9 @@ class Results(MutableMapping, MutableSequence):
93
108
  patch_data.update(self.api.extra_request_params)
94
109
  return session.patch(url=self.url, json=patch_data)
95
110
  elif self.product in FEEDS_PRODUCTS_LIST:
96
- parameters = deepcopy(self.kwargs)
97
- parameters.pop("output_format", None)
98
- parameters.pop(
99
- "format", None
100
- ) # For some unknownn reasons, even if "format" is not included in the cli params for feeds endpoint, it is being populated thus we need to remove it. Happens only if using CLI.
101
- headers = {}
102
- if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
103
- parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
104
- headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
105
-
106
- header_api_key = parameters.pop("X-Api-Key", None)
107
- if header_api_key:
108
- headers["X-Api-Key"] = header_api_key
109
-
111
+ session_params = self._get_session_params()
112
+ parameters = session_params.get("parameters")
113
+ headers = session_params.get("headers")
110
114
  return session.get(url=self.url, params=parameters, headers=headers, **self.api.extra_request_params)
111
115
  else:
112
116
  return session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
@@ -136,32 +140,26 @@ class Results(MutableMapping, MutableSequence):
136
140
  if self._data is None:
137
141
  results = self._get_results()
138
142
  self.setStatus(results.status_code, results)
139
- if (
140
- self.kwargs.get("format", "json") == "json"
141
- and self.product not in FEEDS_PRODUCTS_LIST # Special handling of feeds products' data to preserve the result in jsonline format
142
- ):
143
+ if self.kwargs.get("format", "json") == "json":
143
144
  self._data = results.json()
144
145
  else:
145
146
  self._data = results.text
146
- limit_exceeded, message = self.check_limit_exceeded()
147
147
 
148
- if limit_exceeded:
149
- self._limit_exceeded = True
150
- self._limit_exceeded_message = message
148
+ self.check_limit_exceeded()
151
149
 
152
- if self._limit_exceeded is True:
153
- raise ServiceException(503, "Limit Exceeded{}".format(self._limit_exceeded_message))
154
- else:
155
- return self._data
150
+ return self._data
156
151
 
157
152
  def check_limit_exceeded(self):
158
- if self.kwargs.get("format", "json") == "json" and self.product not in FEEDS_PRODUCTS_LIST:
159
- if "response" in self._data and "limit_exceeded" in self._data["response"] and self._data["response"]["limit_exceeded"] is True:
160
- return True, self._data["response"]["message"]
161
- # TODO: handle html, xml response errors better.
153
+ limit_exceeded, reason = False, ""
154
+ if isinstance(self._data, dict) and (
155
+ "response" in self._data and "limit_exceeded" in self._data["response"] and self._data["response"]["limit_exceeded"] is True
156
+ ):
157
+ limit_exceeded, reason = True, self._data["response"]["message"]
162
158
  elif "response" in self._data and "limit_exceeded" in self._data:
163
- return True, "limit exceeded"
164
- return False, ""
159
+ limit_exceeded = True
160
+
161
+ if limit_exceeded:
162
+ raise ServiceException(503, f"Limit Exceeded {reason}")
165
163
 
166
164
  @property
167
165
  def status(self):
@@ -172,7 +170,7 @@ class Results(MutableMapping, MutableSequence):
172
170
 
173
171
  def setStatus(self, code, response=None):
174
172
  self._status = code
175
- if code == 200:
173
+ if code == 200 or (self.product in FEEDS_PRODUCTS_LIST and code == 206):
176
174
  return
177
175
 
178
176
  reason = None
@@ -184,9 +182,9 @@ class Results(MutableMapping, MutableSequence):
184
182
  if callable(reason):
185
183
  reason = reason()
186
184
 
187
- if code == 400:
185
+ if code in (400, 422):
188
186
  raise BadRequestException(code, reason)
189
- elif code == 403:
187
+ elif code in (401, 403):
190
188
  raise NotAuthorizedException(code, reason)
191
189
  elif code == 404:
192
190
  raise NotFoundException(code, reason)
@@ -9,7 +9,7 @@ from typing import Optional, Dict, Tuple
9
9
  from rich.progress import Progress, SpinnerColumn, TextColumn
10
10
 
11
11
  from domaintools.api import API
12
- from domaintools.constants import Endpoint, OutputFormat
12
+ from domaintools.constants import Endpoint, FEEDS_PRODUCTS_LIST, OutputFormat
13
13
  from domaintools.cli.utils import get_file_extension
14
14
  from domaintools.exceptions import ServiceException
15
15
  from domaintools._version import current as version
@@ -110,6 +110,8 @@ class DTCLICommand:
110
110
  def _get_formatted_output(cls, cmd_name: str, response, out_format: str = "json"):
111
111
  if cmd_name in ("available_api_calls",):
112
112
  return "\n".join(response)
113
+ if response.product in FEEDS_PRODUCTS_LIST:
114
+ return "\n".join([data for data in response.response()])
113
115
  return str(getattr(response, out_format) if out_format != "list" else response.as_list())
114
116
 
115
117
  @classmethod
@@ -227,7 +229,10 @@ class DTCLICommand:
227
229
 
228
230
  if isinstance(out_file, _io.TextIOWrapper):
229
231
  # use rich `print` command to prettify the ouput in sys.stdout
230
- print(response)
232
+ if response.product in FEEDS_PRODUCTS_LIST:
233
+ print(output)
234
+ else:
235
+ print(response)
231
236
  else:
232
237
  # if it's a file then write
233
238
  out_file.write(output if output.endswith("\n") else output + "\n")
@@ -2,6 +2,5 @@ from .accounts import *
2
2
  from .iris import *
3
3
  from .domains import *
4
4
  from .ips import *
5
- from .phisheye import *
6
5
  from .detects import *
7
6
  from .feeds import *
@@ -314,3 +314,83 @@ def feeds_domaindiscovery(
314
314
  ),
315
315
  ):
316
316
  DTCLICommand.run(name=c.FEEDS_DOMAINDISCOVERY, params=ctx.params)
317
+
318
+
319
+ @dt_cli.command(
320
+ name=c.FEEDS_NOH,
321
+ help=get_cli_helptext_by_name(command_name=c.FEEDS_NOH),
322
+ )
323
+ def feeds_noh(
324
+ ctx: typer.Context,
325
+ user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
326
+ key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
327
+ creds_file: str = typer.Option(
328
+ "~/.dtapi",
329
+ "-c",
330
+ "--credfile",
331
+ help="Optional file with API username and API key, one per line.",
332
+ ),
333
+ no_verify_ssl: bool = typer.Option(
334
+ False,
335
+ "--no-verify-ssl",
336
+ help="Skip verification of SSL certificate when making HTTPs API calls",
337
+ ),
338
+ no_sign_api_key: bool = typer.Option(
339
+ False,
340
+ "--no-sign-api-key",
341
+ help="Skip signing of api key",
342
+ ),
343
+ header_authentication: bool = typer.Option(
344
+ True,
345
+ "--no-header-auth",
346
+ help="Don't use header authentication",
347
+ ),
348
+ output_format: str = typer.Option(
349
+ "jsonl",
350
+ "-f",
351
+ "--format",
352
+ help=f"Output format in [{OutputFormat.JSONL.value}, {OutputFormat.CSV.value}]",
353
+ callback=DTCLICommand.validate_feeds_format_input,
354
+ ),
355
+ endpoint: str = typer.Option(
356
+ Endpoint.FEED.value,
357
+ "-e",
358
+ "--endpoint",
359
+ help=f"Valid endpoints: [{Endpoint.FEED.value}, {Endpoint.DOWNLOAD.value}]",
360
+ callback=DTCLICommand.validate_endpoint_input,
361
+ ),
362
+ sessionID: str = typer.Option(
363
+ None,
364
+ "--session-id",
365
+ help="Unique identifier for the session",
366
+ ),
367
+ after: str = typer.Option(
368
+ None,
369
+ "--after",
370
+ help="Start of the time window, relative to the current time in seconds, for which data will be provided",
371
+ callback=DTCLICommand.validate_after_or_before_input,
372
+ ),
373
+ before: str = typer.Option(
374
+ None,
375
+ "--before",
376
+ help="The end of the query window in seconds, relative to the current time, inclusive",
377
+ callback=DTCLICommand.validate_after_or_before_input,
378
+ ),
379
+ domain: str = typer.Option(
380
+ None,
381
+ "-d",
382
+ "--domain",
383
+ help="A string value used to filter feed results",
384
+ ),
385
+ headers: bool = typer.Option(
386
+ False,
387
+ "--headers",
388
+ help="Adds a header to the first line of response when text/csv is set in header parameters",
389
+ ),
390
+ top: str = typer.Option(
391
+ None,
392
+ "--top",
393
+ help="Number of results to return in the response payload. This is ignored in download endpoint",
394
+ ),
395
+ ):
396
+ DTCLICommand.run(name=c.FEEDS_NOH, params=ctx.params)
@@ -39,12 +39,9 @@ IRIS_DETECT_MANAGE_WATCHLIST_DOMAINS = "iris_detect_manage_watchlist_domains"
39
39
  IRIS_DETECT_ESCALATE_DOMAINS = "iris_detect_escalate_domains"
40
40
  IRIS_DETECT_IGNORED_DOMAINS = "iris_detect_ignored_domains"
41
41
 
42
- # phisheye
43
- PHISHEYE = "phisheye"
44
- PHISHEYE_TERM_LIST = "phisheye_term_list"
45
-
46
42
  # feeds
47
43
  FEEDS_NAD = "nad"
48
44
  FEEDS_NOD = "nod"
45
+ FEEDS_NOH = "noh"
49
46
  FEEDS_DOMAINRDAP = "domainrdap"
50
47
  FEEDS_DOMAINDISCOVERY = "domaindiscovery"
@@ -29,25 +29,6 @@ def _iris_investigate_helptext():
29
29
  """
30
30
 
31
31
 
32
- def _phisheye_helptext():
33
- return """
34
- Returns domain results for the specified term for today or the specified number of days_back. Terms must be setup for monitoring via the web interface: https://research.domaintools.com/phisheye. \n
35
-
36
- NOTE: Properties of a domain are only provided if we have been able to obtain them. \n
37
- Many domains will have incomplete data because that information isn't available in their Whois records,
38
- or they don't have DNS results for a name server or IP address.
39
- """
40
-
41
-
42
- def _phisheye_termlist():
43
- return """
44
- Provides a list of terms that are set up for this account. This call is not charged against your API usage limit.
45
-
46
- NOTE: The terms must be configured in the PhishEye web interface: https://research.domaintools.com/phisheye. \n
47
- There is no API call to set up the terms.
48
- """
49
-
50
-
51
32
  _command_help_mapper = {
52
33
  c.ACCOUNT_INFORMATION: "Provides a snapshot of your accounts current API usage.",
53
34
  c.AVAILABLE_API_CALLS: "Provides a list of api calls that you can use based on your account information.",
@@ -71,8 +52,6 @@ _command_help_mapper = {
71
52
  c.REVERSE_WHOIS: "Provides list of one or more terms to search for in the Whois record, as a Python list or separated with the pipe character ( | ).",
72
53
  c.WHOIS: "Pass in a domain name or an IP address to perform a whois lookup.",
73
54
  c.WHOIS_HISTORY: "Retrieve historical Whois records of a given domain name.",
74
- c.PHISHEYE: _phisheye_helptext(),
75
- c.PHISHEYE_TERM_LIST: _phisheye_termlist(),
76
55
  c.IRIS: "Performs a search for the provided search terms ANDed together, returning the pivot engine row data for the resulting domains.",
77
56
  c.RISK: "Returns back the risk score for a given domain.",
78
57
  c.RISK_EVIDENCE: "Returns back the detailed risk evidence associated with a given domain.",
@@ -84,7 +63,9 @@ _command_help_mapper = {
84
63
  c.IRIS_DETECT_IGNORED_DOMAINS: "Returns back a list of ignored domains in Iris Detect based on the provided filters.",
85
64
  c.FEEDS_NAD: "Returns back newly active domains feed.",
86
65
  c.FEEDS_NOD: "Returns back newly observed domains feed.",
66
+ c.FEEDS_NOH: "Returns back newly observed hosts feed.",
87
67
  c.FEEDS_DOMAINRDAP: "Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP).",
68
+ 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.",
88
69
  }
89
70
 
90
71
 
@@ -28,6 +28,8 @@ FEEDS_PRODUCTS_LIST = [
28
28
  "newly-active-domains-feed-(s3)",
29
29
  "newly-observed-domains-feed-(api)",
30
30
  "newly-observed-domains-feed-(s3)",
31
+ "newly-observed-hosts-feed-(api)",
32
+ "newly-observed-hosts-feed-(s3)",
31
33
  "domain-registration-data-access-protocol-feed-(api)",
32
34
  "domain-registration-data-access-protocol-feed-(s3)",
33
35
  "real-time-domain-discovery-feed-(api)",
@@ -11,6 +11,8 @@ except ImportError: # pragma: no cover
11
11
  from ordereddict import OrderedDict
12
12
 
13
13
  from itertools import zip_longest
14
+ from typing import Generator
15
+
14
16
  from domaintools_async import AsyncResults as Results
15
17
 
16
18
 
@@ -141,3 +143,28 @@ class ParsedDomainRdap(Results):
141
143
  flat[f"contact_{contact_key}_{i}"] = " | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
142
144
 
143
145
  return flat
146
+
147
+
148
+ class FeedsResults(Results):
149
+ """Returns the generator for feeds results"""
150
+
151
+ def response(self) -> Generator:
152
+ status_code = None
153
+ while status_code != 200:
154
+ resp_data = self.data()
155
+ status_code = self.status
156
+ yield resp_data
157
+
158
+ self._data = None # clear the data here
159
+ if not self.kwargs.get("sessionID"):
160
+ # we'll only do iterative request for queries that has sessionID.
161
+ # Otherwise, we will have an infinite request if sessionID was not provided but the required data asked is more than the maximum (1 hour of data)
162
+ break
163
+
164
+ def data(self):
165
+ results = self._get_results()
166
+ self.setStatus(results.status_code, results)
167
+ self._data = results.text
168
+ self.check_limit_exceeded()
169
+
170
+ return self._data
@@ -177,8 +177,13 @@ def validate_feeds_parameters(params):
177
177
  after = params.get("after")
178
178
  before = params.get("before")
179
179
  if not (sessionID or after or before):
180
- raise ValueError("sessionID or after or before must be defined")
180
+ raise ValueError("sessionID or after or before must be provided")
181
181
 
182
182
  format = params.get("output_format")
183
- if params.get("endpoint") == Endpoint.DOWNLOAD.value and format == OutputFormat.CSV.value:
183
+ endpoint = params.get("endpoint")
184
+ if endpoint == Endpoint.DOWNLOAD.value and format == OutputFormat.CSV.value:
184
185
  raise ValueError(f"{format} format is not available in {Endpoint.DOWNLOAD.value} API.")
186
+
187
+ if endpoint == Endpoint.DOWNLOAD.value and params.get("header_authentication", True):
188
+ # For download endpoint, header_authentication will be False by default
189
+ params["header_authentication"] = False