domaintools-api 2.5.2__tar.gz → 2.6.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.
- {domaintools_api-2.5.2/domaintools_api.egg-info → domaintools_api-2.6.0}/PKG-INFO +13 -9
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/README.md +12 -8
- domaintools_api-2.6.0/VERSION +1 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/_version.py +1 -1
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/api.py +107 -24
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/base_results.py +49 -30
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/api.py +28 -13
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/feeds.py +14 -14
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/iris.py +24 -27
- domaintools_api-2.6.0/domaintools/constants.py +58 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/results.py +121 -30
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/utils.py +0 -4
- {domaintools_api-2.5.2 → domaintools_api-2.6.0/domaintools_api.egg-info}/PKG-INFO +13 -9
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_async/__init__.py +7 -9
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/pyproject.toml +3 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_api.py +94 -82
- domaintools_api-2.6.0/tests/test_async.py +50 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_utils.py +0 -11
- domaintools_api-2.5.2/VERSION +0 -1
- domaintools_api-2.5.2/domaintools/constants.py +0 -41
- domaintools_api-2.5.2/tests/test_async.py +0 -64
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/LICENSE +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/__init__.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/constants.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/main.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/cli/utils.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/exceptions.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools/filters.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/entry_points.txt +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/requires.txt +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/domaintools_api.egg-info/top_level.txt +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/setup.cfg +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/setup.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_cli.py +0 -0
- {domaintools_api-2.5.2 → domaintools_api-2.6.0}/tests/test_filters.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -265,25 +265,25 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
|
|
|
265
265
|
for the DomainTools Python support policy.
|
|
266
266
|
|
|
267
267
|
|
|
268
|
-
Real-Time Threat
|
|
268
|
+
Real-Time Threat Feeds
|
|
269
269
|
===================
|
|
270
270
|
|
|
271
|
-
Real-Time Threat
|
|
271
|
+
Real-Time Threat Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
|
|
272
272
|
|
|
273
273
|
Custom parameters aside from the common `GET` Request parameters:
|
|
274
274
|
- `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
|
|
275
275
|
```python
|
|
276
|
-
api = API(USERNAME, KEY
|
|
276
|
+
api = API(USERNAME, KEY)
|
|
277
277
|
api.nod(endpoint="feed", **kwargs)
|
|
278
278
|
```
|
|
279
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.
|
|
280
280
|
```python
|
|
281
|
-
api = API(USERNAME, KEY,
|
|
282
|
-
api.nod(
|
|
281
|
+
api = API(USERNAME, KEY, header_authentication=False)
|
|
282
|
+
api.nod(**kwargs)
|
|
283
283
|
```
|
|
284
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.
|
|
285
285
|
```python
|
|
286
|
-
api = API(USERNAME, KEY
|
|
286
|
+
api = API(USERNAME, KEY)
|
|
287
287
|
api.nod(output_format="csv", **kwargs)
|
|
288
288
|
```
|
|
289
289
|
|
|
@@ -304,7 +304,7 @@ Since we may dealing with large feeds datasets, the python wrapper uses `generat
|
|
|
304
304
|
```python
|
|
305
305
|
from domaintools import API
|
|
306
306
|
|
|
307
|
-
api = API(USERNAME, KEY
|
|
307
|
+
api = API(USERNAME, KEY)
|
|
308
308
|
results = api.nod(sessionID="my-session-id", after=-60)
|
|
309
309
|
|
|
310
310
|
for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
|
|
@@ -315,7 +315,7 @@ for result in results.response() # generator that holds NOD feeds data for the p
|
|
|
315
315
|
```python
|
|
316
316
|
from domaintools import API
|
|
317
317
|
|
|
318
|
-
api = API(USERNAME, KEY
|
|
318
|
+
api = API(USERNAME, KEY)
|
|
319
319
|
results = api.nod(sessionID="my-session-id", after=-7200)
|
|
320
320
|
|
|
321
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
|
|
@@ -354,6 +354,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
354
354
|
export TEST_USER=<user-key>
|
|
355
355
|
export TEST_KEY=<api-key>
|
|
356
356
|
```
|
|
357
|
+
- Run unit tests.
|
|
358
|
+
```bash
|
|
359
|
+
tox -e
|
|
360
|
+
```
|
|
357
361
|
|
|
358
362
|
## Run the end-to-end test script
|
|
359
363
|
- Before running the test, be sure that docker is running.
|
|
@@ -215,25 +215,25 @@ Please see the [supported versions](https://github.com/DomainTools/python_api/ra
|
|
|
215
215
|
for the DomainTools Python support policy.
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
Real-Time Threat
|
|
218
|
+
Real-Time Threat Feeds
|
|
219
219
|
===================
|
|
220
220
|
|
|
221
|
-
Real-Time Threat
|
|
221
|
+
Real-Time Threat Feeds provide data on the different stages of the domain lifecycle: from first-observed in the wild, to newly re-activated after a period of quiet. Access current feed data in real-time or retrieve historical feed data through separate APIs.
|
|
222
222
|
|
|
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)
|
|
227
227
|
api.nod(endpoint="feed", **kwargs)
|
|
228
228
|
```
|
|
229
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,
|
|
232
|
-
api.nod(
|
|
231
|
+
api = API(USERNAME, KEY, header_authentication=False)
|
|
232
|
+
api.nod(**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)
|
|
237
237
|
api.nod(output_format="csv", **kwargs)
|
|
238
238
|
```
|
|
239
239
|
|
|
@@ -254,7 +254,7 @@ Since we may dealing with large feeds datasets, the python wrapper uses `generat
|
|
|
254
254
|
```python
|
|
255
255
|
from domaintools import API
|
|
256
256
|
|
|
257
|
-
api = API(USERNAME, KEY
|
|
257
|
+
api = API(USERNAME, KEY)
|
|
258
258
|
results = api.nod(sessionID="my-session-id", after=-60)
|
|
259
259
|
|
|
260
260
|
for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
|
|
@@ -265,7 +265,7 @@ for result in results.response() # generator that holds NOD feeds data for the p
|
|
|
265
265
|
```python
|
|
266
266
|
from domaintools import API
|
|
267
267
|
|
|
268
|
-
api = API(USERNAME, KEY
|
|
268
|
+
api = API(USERNAME, KEY)
|
|
269
269
|
results = api.nod(sessionID="my-session-id", after=-7200)
|
|
270
270
|
|
|
271
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
|
|
@@ -304,6 +304,10 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
304
304
|
export TEST_USER=<user-key>
|
|
305
305
|
export TEST_KEY=<api-key>
|
|
306
306
|
```
|
|
307
|
+
- Run unit tests.
|
|
308
|
+
```bash
|
|
309
|
+
tox -e
|
|
310
|
+
```
|
|
307
311
|
|
|
308
312
|
## Run the end-to-end test script
|
|
309
313
|
- Before running the test, be sure that docker is running.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.6.0
|
|
@@ -6,7 +6,13 @@ from typing import Union
|
|
|
6
6
|
import re
|
|
7
7
|
import ssl
|
|
8
8
|
|
|
9
|
-
from domaintools.constants import
|
|
9
|
+
from domaintools.constants import (
|
|
10
|
+
Endpoint,
|
|
11
|
+
OutputFormat,
|
|
12
|
+
ENDPOINT_TO_SOURCE_MAP,
|
|
13
|
+
RTTF_PRODUCTS_LIST,
|
|
14
|
+
RTTF_PRODUCTS_CMD_MAPPING,
|
|
15
|
+
)
|
|
10
16
|
from domaintools._version import current as version
|
|
11
17
|
from domaintools.results import (
|
|
12
18
|
GroupedIterable,
|
|
@@ -63,7 +69,8 @@ class API(object):
|
|
|
63
69
|
verify_ssl=True,
|
|
64
70
|
rate_limit=True,
|
|
65
71
|
proxy_url=None,
|
|
66
|
-
always_sign_api_key=
|
|
72
|
+
always_sign_api_key=None,
|
|
73
|
+
header_authentication=None,
|
|
67
74
|
key_sign_hash="sha256",
|
|
68
75
|
app_name="python_wrapper",
|
|
69
76
|
app_version=version,
|
|
@@ -83,6 +90,7 @@ class API(object):
|
|
|
83
90
|
self.proxy_url = proxy_url
|
|
84
91
|
self.extra_request_params = {}
|
|
85
92
|
self.always_sign_api_key = always_sign_api_key
|
|
93
|
+
self.header_authentication = header_authentication
|
|
86
94
|
self.key_sign_hash = key_sign_hash
|
|
87
95
|
self.default_parameters["app_name"] = app_name
|
|
88
96
|
self.default_parameters["app_version"] = app_version
|
|
@@ -90,7 +98,9 @@ class API(object):
|
|
|
90
98
|
self._build_api_url(api_url, api_port)
|
|
91
99
|
|
|
92
100
|
if not https:
|
|
93
|
-
raise Exception(
|
|
101
|
+
raise Exception(
|
|
102
|
+
"The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
|
|
103
|
+
)
|
|
94
104
|
if proxy_url and not isinstance(proxy_url, str):
|
|
95
105
|
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
|
|
96
106
|
|
|
@@ -108,8 +118,12 @@ class API(object):
|
|
|
108
118
|
|
|
109
119
|
self._rest_api_url = rest_api_url
|
|
110
120
|
|
|
111
|
-
def _rate_limit(self):
|
|
121
|
+
def _rate_limit(self, product):
|
|
112
122
|
"""Pulls in and enforces the latest rate limits for the specified user"""
|
|
123
|
+
if product in RTTF_PRODUCTS_LIST:
|
|
124
|
+
self.limits_set = False
|
|
125
|
+
return
|
|
126
|
+
|
|
113
127
|
self.limits_set = True
|
|
114
128
|
for product in self.account_information():
|
|
115
129
|
limit_minutes = product["per_minute_limit"] or None
|
|
@@ -124,24 +138,44 @@ class API(object):
|
|
|
124
138
|
def _results(self, product, path, cls=Results, **kwargs):
|
|
125
139
|
"""Returns _results for the specified API path with the specified **kwargs parameters"""
|
|
126
140
|
if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
|
|
127
|
-
self.
|
|
141
|
+
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
142
|
+
header_authentication_previous_value = self.header_authentication
|
|
143
|
+
self._rate_limit(product)
|
|
144
|
+
# Reset always_sign_api_key and header_authentication to its original
|
|
145
|
+
# User-set values as these might be affected when self.account_information() was executed
|
|
146
|
+
self.always_sign_api_key = always_sign_api_key_previous_value
|
|
147
|
+
self.header_authentication = header_authentication_previous_value
|
|
128
148
|
|
|
129
149
|
uri = "/".join((self._rest_api_url, path.lstrip("/")))
|
|
130
150
|
parameters = self.default_parameters.copy()
|
|
131
151
|
parameters["api_username"] = self.username
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
152
|
+
is_rttf_product = product in RTTF_PRODUCTS_LIST
|
|
153
|
+
self._handle_api_key_parameters(is_rttf_product)
|
|
154
|
+
self.handle_api_key(is_rttf_product, path, parameters)
|
|
155
|
+
parameters.update(
|
|
156
|
+
{
|
|
157
|
+
key: str(value).lower() if value in (True, False) else value
|
|
158
|
+
for key, value in kwargs.items()
|
|
159
|
+
if value is not None
|
|
160
|
+
}
|
|
161
|
+
)
|
|
135
162
|
|
|
136
163
|
return cls(self, product, uri, **parameters)
|
|
137
164
|
|
|
138
|
-
def
|
|
165
|
+
def _handle_api_key_parameters(self, is_rttf_product):
|
|
166
|
+
if self.always_sign_api_key is None:
|
|
167
|
+
self.always_sign_api_key = not is_rttf_product
|
|
168
|
+
|
|
169
|
+
if self.header_authentication is None:
|
|
170
|
+
self.header_authentication = is_rttf_product
|
|
171
|
+
|
|
172
|
+
def handle_api_key(self, is_rttf_product, path, parameters):
|
|
139
173
|
if self.https and not self.always_sign_api_key:
|
|
140
|
-
|
|
141
|
-
parameters["X-Api-Key"] = self.key
|
|
142
|
-
else:
|
|
143
|
-
parameters["api_key"] = self.key
|
|
174
|
+
parameters["api_key"] = self.key
|
|
144
175
|
else:
|
|
176
|
+
if is_rttf_product:
|
|
177
|
+
# As per requirement in IDEV-2272, raise this error when the user explicitly sets signing of API key for RTTF endpoints
|
|
178
|
+
raise ValueError("Real Time Threat Feeds do not support signed API keys.")
|
|
145
179
|
if self.key_sign_hash and self.key_sign_hash in AVAILABLE_KEY_SIGN_HASHES:
|
|
146
180
|
signing_hash = eval(self.key_sign_hash)
|
|
147
181
|
else:
|
|
@@ -174,8 +208,30 @@ class API(object):
|
|
|
174
208
|
string[1:],
|
|
175
209
|
)
|
|
176
210
|
|
|
177
|
-
api_calls = tuple(
|
|
178
|
-
|
|
211
|
+
api_calls = tuple(
|
|
212
|
+
(
|
|
213
|
+
api_call
|
|
214
|
+
for api_call in dir(API)
|
|
215
|
+
if not api_call.startswith("_") and callable(getattr(API, api_call, None))
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
account_information = self.account_information()
|
|
220
|
+
|
|
221
|
+
available_calls = set()
|
|
222
|
+
for product in self.account_information():
|
|
223
|
+
product_id = product["id"]
|
|
224
|
+
# for RTUF endpoints as we use different func name in our wrapper
|
|
225
|
+
if product_id in RTTF_PRODUCTS_LIST:
|
|
226
|
+
if rttf_api_command := RTTF_PRODUCTS_CMD_MAPPING.get(product_id):
|
|
227
|
+
available_calls.add(rttf_api_command)
|
|
228
|
+
|
|
229
|
+
# for IRIS endpoints
|
|
230
|
+
snakecase_pid = snakecase(product_id)
|
|
231
|
+
if snakecase_pid in api_calls:
|
|
232
|
+
available_calls.add(snakecase_pid)
|
|
233
|
+
|
|
234
|
+
return sorted(available_calls)
|
|
179
235
|
|
|
180
236
|
def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
|
|
181
237
|
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
|
|
@@ -430,7 +486,16 @@ class API(object):
|
|
|
430
486
|
"""Performs a search for the provided search terms ANDed together,
|
|
431
487
|
returning the pivot engine row data for the resulting domains.
|
|
432
488
|
"""
|
|
433
|
-
if
|
|
489
|
+
if (
|
|
490
|
+
not domain
|
|
491
|
+
and not ip
|
|
492
|
+
and not email
|
|
493
|
+
and not nameserver
|
|
494
|
+
and not registrar
|
|
495
|
+
and not registrant
|
|
496
|
+
and not registrant_org
|
|
497
|
+
and not kwargs
|
|
498
|
+
):
|
|
434
499
|
raise ValueError("At least one search term must be specified")
|
|
435
500
|
|
|
436
501
|
return self._results(
|
|
@@ -1054,7 +1119,10 @@ class API(object):
|
|
|
1054
1119
|
validate_feeds_parameters(kwargs)
|
|
1055
1120
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1056
1121
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
|
|
1057
|
-
if
|
|
1122
|
+
if (
|
|
1123
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1124
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1125
|
+
):
|
|
1058
1126
|
# headers param is allowed only in Feed API and CSV format
|
|
1059
1127
|
kwargs.pop("headers", None)
|
|
1060
1128
|
|
|
@@ -1086,7 +1154,10 @@ class API(object):
|
|
|
1086
1154
|
validate_feeds_parameters(kwargs)
|
|
1087
1155
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1088
1156
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1089
|
-
if
|
|
1157
|
+
if (
|
|
1158
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1159
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1160
|
+
):
|
|
1090
1161
|
# headers param is allowed only in Feed API and CSV format
|
|
1091
1162
|
kwargs.pop("headers", None)
|
|
1092
1163
|
|
|
@@ -1147,7 +1218,10 @@ class API(object):
|
|
|
1147
1218
|
validate_feeds_parameters(kwargs)
|
|
1148
1219
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1149
1220
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1150
|
-
if
|
|
1221
|
+
if (
|
|
1222
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1223
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1224
|
+
):
|
|
1151
1225
|
# headers param is allowed only in Feed API and CSV format
|
|
1152
1226
|
kwargs.pop("headers", None)
|
|
1153
1227
|
|
|
@@ -1179,7 +1253,10 @@ class API(object):
|
|
|
1179
1253
|
validate_feeds_parameters(kwargs)
|
|
1180
1254
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1181
1255
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1182
|
-
if
|
|
1256
|
+
if (
|
|
1257
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1258
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1259
|
+
):
|
|
1183
1260
|
# headers param is allowed only in Feed API and CSV format
|
|
1184
1261
|
kwargs.pop("headers", None)
|
|
1185
1262
|
|
|
@@ -1210,12 +1287,15 @@ class API(object):
|
|
|
1210
1287
|
validate_feeds_parameters(kwargs)
|
|
1211
1288
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1212
1289
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1213
|
-
if
|
|
1290
|
+
if (
|
|
1291
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1292
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1293
|
+
):
|
|
1214
1294
|
# headers param is allowed only in Feed API and CSV format
|
|
1215
1295
|
kwargs.pop("headers", None)
|
|
1216
1296
|
|
|
1217
1297
|
return self._results(
|
|
1218
|
-
f"domain-risk-
|
|
1298
|
+
f"real-time-domain-risk-({source})",
|
|
1219
1299
|
f"v1/{endpoint}/domainrisk/",
|
|
1220
1300
|
response_path=(),
|
|
1221
1301
|
cls=FeedsResults,
|
|
@@ -1241,12 +1321,15 @@ class API(object):
|
|
|
1241
1321
|
validate_feeds_parameters(kwargs)
|
|
1242
1322
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1243
1323
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1244
|
-
if
|
|
1324
|
+
if (
|
|
1325
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1326
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1327
|
+
):
|
|
1245
1328
|
# headers param is allowed only in Feed API and CSV format
|
|
1246
1329
|
kwargs.pop("headers", None)
|
|
1247
1330
|
|
|
1248
1331
|
return self._results(
|
|
1249
|
-
f"domain-hotlist-
|
|
1332
|
+
f"real-time-domain-hotlist-({source})",
|
|
1250
1333
|
f"v1/{endpoint}/domainhotlist/",
|
|
1251
1334
|
response_path=(),
|
|
1252
1335
|
cls=FeedsResults,
|
|
@@ -9,7 +9,11 @@ from copy import deepcopy
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from httpx import Client
|
|
11
11
|
|
|
12
|
-
from domaintools.constants import
|
|
12
|
+
from domaintools.constants import (
|
|
13
|
+
RTTF_PRODUCTS_LIST,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
HEADER_ACCEPT_KEY_CSV_FORMAT,
|
|
16
|
+
)
|
|
13
17
|
from domaintools.exceptions import (
|
|
14
18
|
BadRequestException,
|
|
15
19
|
InternalServerErrorException,
|
|
@@ -53,6 +57,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
53
57
|
self._response = None
|
|
54
58
|
self._items_list = None
|
|
55
59
|
self._data = None
|
|
60
|
+
self._status = None
|
|
56
61
|
|
|
57
62
|
def _wait_time(self):
|
|
58
63
|
if not self.api.rate_limit or not self.product in self.api.limits:
|
|
@@ -75,26 +80,30 @@ class Results(MutableMapping, MutableSequence):
|
|
|
75
80
|
|
|
76
81
|
return wait_for
|
|
77
82
|
|
|
78
|
-
def
|
|
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.
|
|
83
|
+
def _get_session_params_and_headers(self):
|
|
84
84
|
headers = {}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
parameters = deepcopy(self.kwargs)
|
|
86
|
+
is_rttf_product = self.product in RTTF_PRODUCTS_LIST
|
|
87
|
+
if is_rttf_product:
|
|
88
|
+
parameters.pop("output_format", None)
|
|
89
|
+
parameters.pop(
|
|
90
|
+
"format", None
|
|
91
|
+
) # 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.
|
|
92
|
+
if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
|
|
93
|
+
parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
|
|
94
|
+
headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
|
|
95
|
+
|
|
96
|
+
if self.api.header_authentication:
|
|
97
|
+
header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
|
|
98
|
+
headers[header_key_for_api_key] = self.api.key
|
|
99
|
+
|
|
100
|
+
session_param_and_headers = {"parameters": parameters, "headers": headers}
|
|
101
|
+
return session_param_and_headers
|
|
94
102
|
|
|
95
103
|
def _make_request(self):
|
|
96
|
-
|
|
97
104
|
with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
|
|
105
|
+
session_params_and_headers = self._get_session_params_and_headers()
|
|
106
|
+
headers = session_params_and_headers.get("headers")
|
|
98
107
|
if self.product in [
|
|
99
108
|
"iris-investigate",
|
|
100
109
|
"iris-enrich",
|
|
@@ -102,18 +111,19 @@ class Results(MutableMapping, MutableSequence):
|
|
|
102
111
|
]:
|
|
103
112
|
post_data = self.kwargs.copy()
|
|
104
113
|
post_data.update(self.api.extra_request_params)
|
|
105
|
-
return session.post(url=self.url, data=post_data)
|
|
114
|
+
return session.post(url=self.url, data=post_data, headers=headers)
|
|
106
115
|
elif self.product in ["iris-detect-manage-watchlist-domains"]:
|
|
107
116
|
patch_data = self.kwargs.copy()
|
|
108
117
|
patch_data.update(self.api.extra_request_params)
|
|
109
|
-
return session.patch(url=self.url, json=patch_data)
|
|
110
|
-
elif self.product in FEEDS_PRODUCTS_LIST:
|
|
111
|
-
session_params = self._get_session_params()
|
|
112
|
-
parameters = session_params.get("parameters")
|
|
113
|
-
headers = session_params.get("headers")
|
|
114
|
-
return session.get(url=self.url, params=parameters, headers=headers, **self.api.extra_request_params)
|
|
118
|
+
return session.patch(url=self.url, json=patch_data, headers=headers)
|
|
115
119
|
else:
|
|
116
|
-
|
|
120
|
+
parameters = session_params_and_headers.get("parameters")
|
|
121
|
+
return session.get(
|
|
122
|
+
url=self.url,
|
|
123
|
+
params=parameters,
|
|
124
|
+
headers=headers,
|
|
125
|
+
**self.api.extra_request_params,
|
|
126
|
+
)
|
|
117
127
|
|
|
118
128
|
def _get_results(self):
|
|
119
129
|
wait_for = self._wait_time()
|
|
@@ -152,7 +162,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
152
162
|
def check_limit_exceeded(self):
|
|
153
163
|
limit_exceeded, reason = False, ""
|
|
154
164
|
if isinstance(self._data, dict) and (
|
|
155
|
-
"response" in self._data
|
|
165
|
+
"response" in self._data
|
|
166
|
+
and "limit_exceeded" in self._data["response"]
|
|
167
|
+
and self._data["response"]["limit_exceeded"] is True
|
|
156
168
|
):
|
|
157
169
|
limit_exceeded, reason = True, self._data["response"]["message"]
|
|
158
170
|
elif "response" in self._data and "limit_exceeded" in self._data:
|
|
@@ -163,14 +175,14 @@ class Results(MutableMapping, MutableSequence):
|
|
|
163
175
|
|
|
164
176
|
@property
|
|
165
177
|
def status(self):
|
|
166
|
-
if not getattr(self, "_status", None):
|
|
178
|
+
if not getattr(self, "_status", None) and not self.product in RTTF_PRODUCTS_LIST:
|
|
167
179
|
self._status = self._get_results().status_code
|
|
168
180
|
|
|
169
181
|
return self._status
|
|
170
182
|
|
|
171
|
-
def setStatus(self, code, response=None):
|
|
183
|
+
def setStatus(self, code, response=None, reason_text=None):
|
|
172
184
|
self._status = code
|
|
173
|
-
if code == 200 or (self.product in
|
|
185
|
+
if code == 200 or (self.product in RTTF_PRODUCTS_LIST and code == 206):
|
|
174
186
|
return
|
|
175
187
|
|
|
176
188
|
reason = None
|
|
@@ -181,6 +193,9 @@ class Results(MutableMapping, MutableSequence):
|
|
|
181
193
|
reason = response.text
|
|
182
194
|
if callable(reason):
|
|
183
195
|
reason = reason()
|
|
196
|
+
else: # optionally pass a customize reason of error for better traceback
|
|
197
|
+
if reason_text is not None:
|
|
198
|
+
reason = reason_text
|
|
184
199
|
|
|
185
200
|
if code in (400, 422):
|
|
186
201
|
raise BadRequestException(code, reason)
|
|
@@ -330,4 +345,8 @@ class Results(MutableMapping, MutableSequence):
|
|
|
330
345
|
return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
|
|
331
346
|
|
|
332
347
|
def __str__(self):
|
|
333
|
-
return str(
|
|
348
|
+
return str(
|
|
349
|
+
json.dumps(self.data(), indent=4, separators=(",", ": "))
|
|
350
|
+
if self.kwargs.get("format", "json") == "json"
|
|
351
|
+
else self.data()
|
|
352
|
+
)
|
|
@@ -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,
|
|
12
|
+
from domaintools.constants import Endpoint, RTTF_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
|
|
@@ -58,7 +58,9 @@ class DTCLICommand:
|
|
|
58
58
|
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
|
|
59
59
|
return value
|
|
60
60
|
except:
|
|
61
|
-
raise typer.BadParameter(
|
|
61
|
+
raise typer.BadParameter(
|
|
62
|
+
f"{value} is neither an integer or a valid ISO 8601 datetime string in UTC form"
|
|
63
|
+
)
|
|
62
64
|
|
|
63
65
|
@staticmethod
|
|
64
66
|
def validate_source_file_extension(value: str):
|
|
@@ -78,7 +80,9 @@ class DTCLICommand:
|
|
|
78
80
|
ext = get_file_extension(value)
|
|
79
81
|
|
|
80
82
|
if ext.lower() not in VALID_EXTENSIONS:
|
|
81
|
-
raise typer.BadParameter(
|
|
83
|
+
raise typer.BadParameter(
|
|
84
|
+
f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}"
|
|
85
|
+
)
|
|
82
86
|
|
|
83
87
|
return value
|
|
84
88
|
|
|
@@ -110,8 +114,8 @@ class DTCLICommand:
|
|
|
110
114
|
def _get_formatted_output(cls, cmd_name: str, response, out_format: str = "json"):
|
|
111
115
|
if cmd_name in ("available_api_calls",):
|
|
112
116
|
return "\n".join(response)
|
|
113
|
-
if response.product in
|
|
114
|
-
|
|
117
|
+
if response.product in RTTF_PRODUCTS_LIST:
|
|
118
|
+
pass # do nothing
|
|
115
119
|
return str(getattr(response, out_format) if out_format != "list" else response.as_list())
|
|
116
120
|
|
|
117
121
|
@classmethod
|
|
@@ -180,6 +184,7 @@ class DTCLICommand:
|
|
|
180
184
|
out_file = params.pop("out_file", sys.stdout)
|
|
181
185
|
verify_ssl = params.pop("no_verify_ssl", False)
|
|
182
186
|
always_sign_api_key = params.pop("no_sign_api_key", False)
|
|
187
|
+
header_authentication = params.pop("no_header_authentication", False)
|
|
183
188
|
source = None
|
|
184
189
|
|
|
185
190
|
if "src_file" in params:
|
|
@@ -202,7 +207,7 @@ class DTCLICommand:
|
|
|
202
207
|
transient=True,
|
|
203
208
|
) as progress:
|
|
204
209
|
|
|
205
|
-
progress.add_task(
|
|
210
|
+
task_id = progress.add_task(
|
|
206
211
|
description=f"Using api credentials with a username of: [cyan]{user}[/cyan]\nExecuting [green]{name}[/green] api call...",
|
|
207
212
|
total=None,
|
|
208
213
|
)
|
|
@@ -214,29 +219,39 @@ class DTCLICommand:
|
|
|
214
219
|
verify_ssl=verify_ssl,
|
|
215
220
|
rate_limit=rate_limit,
|
|
216
221
|
always_sign_api_key=always_sign_api_key,
|
|
222
|
+
header_authentication=header_authentication,
|
|
217
223
|
)
|
|
218
224
|
dt_api_func = getattr(dt_api, name)
|
|
219
|
-
|
|
220
225
|
params = params | kwargs
|
|
221
226
|
|
|
222
227
|
response = dt_api_func(**params)
|
|
223
|
-
progress.
|
|
228
|
+
progress.update(
|
|
229
|
+
task_id,
|
|
224
230
|
description=f"Preparing results with format of {response_format}...",
|
|
225
|
-
total=None,
|
|
226
231
|
)
|
|
227
232
|
|
|
228
|
-
output = cls._get_formatted_output(
|
|
233
|
+
output = cls._get_formatted_output(
|
|
234
|
+
cmd_name=name, response=response, out_format=response_format
|
|
235
|
+
)
|
|
229
236
|
|
|
230
237
|
if isinstance(out_file, _io.TextIOWrapper):
|
|
238
|
+
progress.update(
|
|
239
|
+
task_id,
|
|
240
|
+
description=f"Printing the results with format of {response_format}...",
|
|
241
|
+
)
|
|
231
242
|
# use rich `print` command to prettify the ouput in sys.stdout
|
|
232
|
-
if response.product in
|
|
233
|
-
|
|
243
|
+
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
|
|
244
|
+
for feeds in response.response():
|
|
245
|
+
print(feeds)
|
|
234
246
|
else:
|
|
235
247
|
print(response)
|
|
236
248
|
else:
|
|
249
|
+
progress.update(
|
|
250
|
+
task_id,
|
|
251
|
+
description=f"Writing results to {out_file}",
|
|
252
|
+
)
|
|
237
253
|
# if it's a file then write
|
|
238
254
|
out_file.write(output if output.endswith("\n") else output + "\n")
|
|
239
|
-
time.sleep(0.25)
|
|
240
255
|
except Exception as e:
|
|
241
256
|
if isinstance(e, ServiceException):
|
|
242
257
|
code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
|