domaintools-api 2.5.3__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.3/domaintools_api.egg-info → domaintools_api-2.6.0}/PKG-INFO +5 -1
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/README.md +4 -0
- domaintools_api-2.6.0/VERSION +1 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/_version.py +1 -1
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/api.py +85 -17
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/base_results.py +27 -8
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/api.py +24 -11
- domaintools_api-2.6.0/domaintools/constants.py +58 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/results.py +121 -30
- {domaintools_api-2.5.3 → domaintools_api-2.6.0/domaintools_api.egg-info}/PKG-INFO +5 -1
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/pyproject.toml +3 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_api.py +68 -70
- domaintools_api-2.5.3/VERSION +0 -1
- domaintools_api-2.5.3/domaintools/constants.py +0 -41
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/LICENSE +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/__init__.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/constants.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/main.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/cli/utils.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/exceptions.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/filters.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools/utils.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/entry_points.txt +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/requires.txt +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/top_level.txt +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_async/__init__.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/setup.cfg +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/setup.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_async.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_cli.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.0}/tests/test_filters.py +0 -0
- {domaintools_api-2.5.3 → domaintools_api-2.6.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.
|
|
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)
|
|
@@ -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.
|
|
@@ -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,
|
|
@@ -92,7 +98,9 @@ class API(object):
|
|
|
92
98
|
self._build_api_url(api_url, api_port)
|
|
93
99
|
|
|
94
100
|
if not https:
|
|
95
|
-
raise Exception(
|
|
101
|
+
raise Exception(
|
|
102
|
+
"The DomainTools API endpoints no longer support http traffic. Please make sure https=True."
|
|
103
|
+
)
|
|
96
104
|
if proxy_url and not isinstance(proxy_url, str):
|
|
97
105
|
raise Exception("Proxy URL must be a string. For example: '127.0.0.1:8888'")
|
|
98
106
|
|
|
@@ -110,8 +118,12 @@ class API(object):
|
|
|
110
118
|
|
|
111
119
|
self._rest_api_url = rest_api_url
|
|
112
120
|
|
|
113
|
-
def _rate_limit(self):
|
|
121
|
+
def _rate_limit(self, product):
|
|
114
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
|
+
|
|
115
127
|
self.limits_set = True
|
|
116
128
|
for product in self.account_information():
|
|
117
129
|
limit_minutes = product["per_minute_limit"] or None
|
|
@@ -128,8 +140,9 @@ class API(object):
|
|
|
128
140
|
if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
|
|
129
141
|
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
130
142
|
header_authentication_previous_value = self.header_authentication
|
|
131
|
-
self._rate_limit()
|
|
132
|
-
# Reset always_sign_api_key and header_authentication to its original
|
|
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
|
|
133
146
|
self.always_sign_api_key = always_sign_api_key_previous_value
|
|
134
147
|
self.header_authentication = header_authentication_previous_value
|
|
135
148
|
|
|
@@ -139,7 +152,13 @@ class API(object):
|
|
|
139
152
|
is_rttf_product = product in RTTF_PRODUCTS_LIST
|
|
140
153
|
self._handle_api_key_parameters(is_rttf_product)
|
|
141
154
|
self.handle_api_key(is_rttf_product, path, parameters)
|
|
142
|
-
parameters.update(
|
|
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
|
+
)
|
|
143
162
|
|
|
144
163
|
return cls(self, product, uri, **parameters)
|
|
145
164
|
|
|
@@ -189,8 +208,30 @@ class API(object):
|
|
|
189
208
|
string[1:],
|
|
190
209
|
)
|
|
191
210
|
|
|
192
|
-
api_calls = tuple(
|
|
193
|
-
|
|
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)
|
|
194
235
|
|
|
195
236
|
def brand_monitor(self, query, exclude=None, domain_status=None, days_back=None, **kwargs):
|
|
196
237
|
"""Pass in one or more terms as a list or separated by the pipe character ( | )"""
|
|
@@ -445,7 +486,16 @@ class API(object):
|
|
|
445
486
|
"""Performs a search for the provided search terms ANDed together,
|
|
446
487
|
returning the pivot engine row data for the resulting domains.
|
|
447
488
|
"""
|
|
448
|
-
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
|
+
):
|
|
449
499
|
raise ValueError("At least one search term must be specified")
|
|
450
500
|
|
|
451
501
|
return self._results(
|
|
@@ -1069,7 +1119,10 @@ class API(object):
|
|
|
1069
1119
|
validate_feeds_parameters(kwargs)
|
|
1070
1120
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1071
1121
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint)
|
|
1072
|
-
if
|
|
1122
|
+
if (
|
|
1123
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1124
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1125
|
+
):
|
|
1073
1126
|
# headers param is allowed only in Feed API and CSV format
|
|
1074
1127
|
kwargs.pop("headers", None)
|
|
1075
1128
|
|
|
@@ -1101,7 +1154,10 @@ class API(object):
|
|
|
1101
1154
|
validate_feeds_parameters(kwargs)
|
|
1102
1155
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1103
1156
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1104
|
-
if
|
|
1157
|
+
if (
|
|
1158
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1159
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1160
|
+
):
|
|
1105
1161
|
# headers param is allowed only in Feed API and CSV format
|
|
1106
1162
|
kwargs.pop("headers", None)
|
|
1107
1163
|
|
|
@@ -1162,7 +1218,10 @@ class API(object):
|
|
|
1162
1218
|
validate_feeds_parameters(kwargs)
|
|
1163
1219
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1164
1220
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1165
|
-
if
|
|
1221
|
+
if (
|
|
1222
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1223
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1224
|
+
):
|
|
1166
1225
|
# headers param is allowed only in Feed API and CSV format
|
|
1167
1226
|
kwargs.pop("headers", None)
|
|
1168
1227
|
|
|
@@ -1194,7 +1253,10 @@ class API(object):
|
|
|
1194
1253
|
validate_feeds_parameters(kwargs)
|
|
1195
1254
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1196
1255
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1197
|
-
if
|
|
1256
|
+
if (
|
|
1257
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1258
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1259
|
+
):
|
|
1198
1260
|
# headers param is allowed only in Feed API and CSV format
|
|
1199
1261
|
kwargs.pop("headers", None)
|
|
1200
1262
|
|
|
@@ -1225,12 +1287,15 @@ class API(object):
|
|
|
1225
1287
|
validate_feeds_parameters(kwargs)
|
|
1226
1288
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1227
1289
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1228
|
-
if
|
|
1290
|
+
if (
|
|
1291
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1292
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1293
|
+
):
|
|
1229
1294
|
# headers param is allowed only in Feed API and CSV format
|
|
1230
1295
|
kwargs.pop("headers", None)
|
|
1231
1296
|
|
|
1232
1297
|
return self._results(
|
|
1233
|
-
f"domain-risk-
|
|
1298
|
+
f"real-time-domain-risk-({source})",
|
|
1234
1299
|
f"v1/{endpoint}/domainrisk/",
|
|
1235
1300
|
response_path=(),
|
|
1236
1301
|
cls=FeedsResults,
|
|
@@ -1256,12 +1321,15 @@ class API(object):
|
|
|
1256
1321
|
validate_feeds_parameters(kwargs)
|
|
1257
1322
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
1258
1323
|
source = ENDPOINT_TO_SOURCE_MAP.get(endpoint).value
|
|
1259
|
-
if
|
|
1324
|
+
if (
|
|
1325
|
+
endpoint == Endpoint.DOWNLOAD.value
|
|
1326
|
+
or kwargs.get("output_format", OutputFormat.JSONL.value) != OutputFormat.CSV.value
|
|
1327
|
+
):
|
|
1260
1328
|
# headers param is allowed only in Feed API and CSV format
|
|
1261
1329
|
kwargs.pop("headers", None)
|
|
1262
1330
|
|
|
1263
1331
|
return self._results(
|
|
1264
|
-
f"domain-hotlist-
|
|
1332
|
+
f"real-time-domain-hotlist-({source})",
|
|
1265
1333
|
f"v1/{endpoint}/domainhotlist/",
|
|
1266
1334
|
response_path=(),
|
|
1267
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:
|
|
@@ -92,10 +97,10 @@ class Results(MutableMapping, MutableSequence):
|
|
|
92
97
|
header_key_for_api_key = "X-Api-Key" if is_rttf_product else "X-API-Key"
|
|
93
98
|
headers[header_key_for_api_key] = self.api.key
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
session_param_and_headers = {"parameters": parameters, "headers": headers}
|
|
101
|
+
return session_param_and_headers
|
|
96
102
|
|
|
97
103
|
def _make_request(self):
|
|
98
|
-
|
|
99
104
|
with Client(verify=self.api.verify_ssl, proxy=self.api.proxy_url, timeout=None) as session:
|
|
100
105
|
session_params_and_headers = self._get_session_params_and_headers()
|
|
101
106
|
headers = session_params_and_headers.get("headers")
|
|
@@ -113,7 +118,12 @@ class Results(MutableMapping, MutableSequence):
|
|
|
113
118
|
return session.patch(url=self.url, json=patch_data, headers=headers)
|
|
114
119
|
else:
|
|
115
120
|
parameters = session_params_and_headers.get("parameters")
|
|
116
|
-
return session.get(
|
|
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,12 +175,12 @@ 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
185
|
if code == 200 or (self.product in RTTF_PRODUCTS_LIST and code == 206):
|
|
174
186
|
return
|
|
@@ -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
|
+
)
|
|
@@ -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
|
|
|
@@ -111,7 +115,7 @@ class DTCLICommand:
|
|
|
111
115
|
if cmd_name in ("available_api_calls",):
|
|
112
116
|
return "\n".join(response)
|
|
113
117
|
if response.product in RTTF_PRODUCTS_LIST:
|
|
114
|
-
|
|
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
|
|
@@ -203,7 +207,7 @@ class DTCLICommand:
|
|
|
203
207
|
transient=True,
|
|
204
208
|
) as progress:
|
|
205
209
|
|
|
206
|
-
progress.add_task(
|
|
210
|
+
task_id = progress.add_task(
|
|
207
211
|
description=f"Using api credentials with a username of: [cyan]{user}[/cyan]\nExecuting [green]{name}[/green] api call...",
|
|
208
212
|
total=None,
|
|
209
213
|
)
|
|
@@ -218,27 +222,36 @@ class DTCLICommand:
|
|
|
218
222
|
header_authentication=header_authentication,
|
|
219
223
|
)
|
|
220
224
|
dt_api_func = getattr(dt_api, name)
|
|
221
|
-
|
|
222
225
|
params = params | kwargs
|
|
223
226
|
|
|
224
227
|
response = dt_api_func(**params)
|
|
225
|
-
progress.
|
|
228
|
+
progress.update(
|
|
229
|
+
task_id,
|
|
226
230
|
description=f"Preparing results with format of {response_format}...",
|
|
227
|
-
total=None,
|
|
228
231
|
)
|
|
229
232
|
|
|
230
|
-
output = cls._get_formatted_output(
|
|
233
|
+
output = cls._get_formatted_output(
|
|
234
|
+
cmd_name=name, response=response, out_format=response_format
|
|
235
|
+
)
|
|
231
236
|
|
|
232
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
|
+
)
|
|
233
242
|
# use rich `print` command to prettify the ouput in sys.stdout
|
|
234
|
-
if response.product in RTTF_PRODUCTS_LIST:
|
|
235
|
-
|
|
243
|
+
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
|
|
244
|
+
for feeds in response.response():
|
|
245
|
+
print(feeds)
|
|
236
246
|
else:
|
|
237
247
|
print(response)
|
|
238
248
|
else:
|
|
249
|
+
progress.update(
|
|
250
|
+
task_id,
|
|
251
|
+
description=f"Writing results to {out_file}",
|
|
252
|
+
)
|
|
239
253
|
# if it's a file then write
|
|
240
254
|
out_file.write(output if output.endswith("\n") else output + "\n")
|
|
241
|
-
time.sleep(0.25)
|
|
242
255
|
except Exception as e:
|
|
243
256
|
if isinstance(e, ServiceException):
|
|
244
257
|
code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Endpoint(Enum):
|
|
5
|
+
FEED = "feed"
|
|
6
|
+
DOWNLOAD = "download"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Source(Enum):
|
|
10
|
+
API = "api"
|
|
11
|
+
S3 = "s3"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OutputFormat(Enum):
|
|
15
|
+
JSONL = "jsonl"
|
|
16
|
+
CSV = "csv"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
HEADER_ACCEPT_KEY_CSV_FORMAT = "text/csv"
|
|
20
|
+
|
|
21
|
+
ENDPOINT_TO_SOURCE_MAP = {
|
|
22
|
+
Endpoint.FEED.value: Source.API,
|
|
23
|
+
Endpoint.DOWNLOAD.value: Source.S3,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
RTTF_PRODUCTS_LIST = [
|
|
27
|
+
"newly-active-domains-feed-(api)",
|
|
28
|
+
"newly-active-domains-feed-(s3)",
|
|
29
|
+
"newly-observed-domains-feed-(api)",
|
|
30
|
+
"newly-observed-domains-feed-(s3)",
|
|
31
|
+
"newly-observed-hosts-feed-(api)",
|
|
32
|
+
"newly-observed-hosts-feed-(s3)",
|
|
33
|
+
"real-time-domain-hotlist-(api)",
|
|
34
|
+
"real-time-domain-hotlist-(s3)",
|
|
35
|
+
"domain-registration-data-access-protocol-feed-(api)",
|
|
36
|
+
"domain-registration-data-access-protocol-feed-(s3)",
|
|
37
|
+
"real-time-domain-risk-(api)",
|
|
38
|
+
"real-time-domain-risk-(s3)",
|
|
39
|
+
"real-time-domain-discovery-feed-(api)",
|
|
40
|
+
"real-time-domain-discovery-feed-(s3)",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
RTTF_PRODUCTS_CMD_MAPPING = {
|
|
44
|
+
"newly-active-domains-feed-(api)": "nad",
|
|
45
|
+
"newly-active-domains-feed-(s3)": "nad",
|
|
46
|
+
"newly-observed-domains-feed-(api)": "nod",
|
|
47
|
+
"newly-observed-domains-feed-(s3)": "nod",
|
|
48
|
+
"newly-observed-hosts-feed-(api)": "noh",
|
|
49
|
+
"newly-observed-hosts-feed-(s3)": "noh",
|
|
50
|
+
"real-time-domain-hotlist-(api)": "domainhotlist",
|
|
51
|
+
"real-time-domain-hotlist-(s3)": "domainhotlist",
|
|
52
|
+
"domain-registration-data-access-protocol-feed-(api)": "domainrdap",
|
|
53
|
+
"domain-registration-data-access-protocol-feed-(s3)": "domainrdap",
|
|
54
|
+
"real-time-domain-risk-(api)": "realtime_domain_risk",
|
|
55
|
+
"real-time-domain-risk-(s3)": "realtime_domain_risk",
|
|
56
|
+
"real-time-domain-discovery-feed-(api)": "domaindiscovery",
|
|
57
|
+
"real-time-domain-discovery-feed-(s3)": "domaindiscovery",
|
|
58
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
Defines the used Result object based on the current versions and/or features available to Python runtime
|
|
3
3
|
Additionally, defines any custom result objects that may be used to enable more Pythonic interaction with endpoints.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import logging
|
|
7
|
+
from itertools import zip_longest, chain
|
|
8
|
+
from typing import Generator
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
7
11
|
|
|
8
12
|
try: # pragma: no cover
|
|
9
13
|
from collections import OrderedDict
|
|
10
14
|
except ImportError: # pragma: no cover
|
|
11
15
|
from ordereddict import OrderedDict
|
|
12
16
|
|
|
13
|
-
from itertools import zip_longest
|
|
14
|
-
from typing import Generator
|
|
15
17
|
|
|
16
18
|
from domaintools_async import AsyncResults as Results
|
|
17
19
|
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
class Reputation(Results):
|
|
20
24
|
"""Returns the reputation results in a format that can quickly be converted into floats / ints"""
|
|
@@ -32,7 +36,11 @@ class GroupedIterable(Results):
|
|
|
32
36
|
def _items(self):
|
|
33
37
|
if self._items_list is None:
|
|
34
38
|
self._items_list = chain(
|
|
35
|
-
*[
|
|
39
|
+
*[
|
|
40
|
+
zip_longest([], value, fillvalue=key)
|
|
41
|
+
for key, value in self.response().items()
|
|
42
|
+
if type(value) in (list, tuple)
|
|
43
|
+
]
|
|
36
44
|
)
|
|
37
45
|
|
|
38
46
|
return self._items_list
|
|
@@ -45,13 +53,27 @@ class ParsedWhois(Results):
|
|
|
45
53
|
"""Returns a flattened version of the parsed whois data"""
|
|
46
54
|
parsed = self["parsed_whois"]
|
|
47
55
|
flat = OrderedDict()
|
|
48
|
-
for key in (
|
|
56
|
+
for key in (
|
|
57
|
+
"domain",
|
|
58
|
+
"created_date",
|
|
59
|
+
"updated_date",
|
|
60
|
+
"expired_date",
|
|
61
|
+
"statuses",
|
|
62
|
+
"name_servers",
|
|
63
|
+
):
|
|
49
64
|
if key in parsed:
|
|
50
65
|
value = parsed[key]
|
|
51
66
|
flat[key] = " | ".join(value) if type(value) in (list, tuple) else value
|
|
52
67
|
|
|
53
68
|
registrar = parsed.get("registrar", {})
|
|
54
|
-
for key in (
|
|
69
|
+
for key in (
|
|
70
|
+
"name",
|
|
71
|
+
"abuse_contact_phone",
|
|
72
|
+
"abuse_contact_email",
|
|
73
|
+
"iana_id",
|
|
74
|
+
"url",
|
|
75
|
+
"whois_server",
|
|
76
|
+
):
|
|
55
77
|
if key in registrar:
|
|
56
78
|
flat["registrar_{0}".format(key)] = registrar[key]
|
|
57
79
|
|
|
@@ -59,10 +81,24 @@ class ParsedWhois(Results):
|
|
|
59
81
|
networks = parsed.get("networks")
|
|
60
82
|
for network in networks:
|
|
61
83
|
id = network.get("id")
|
|
62
|
-
for key in (
|
|
84
|
+
for key in (
|
|
85
|
+
"range",
|
|
86
|
+
"asn",
|
|
87
|
+
"org",
|
|
88
|
+
"parent",
|
|
89
|
+
"customer",
|
|
90
|
+
"country",
|
|
91
|
+
"phone",
|
|
92
|
+
"status",
|
|
93
|
+
"source",
|
|
94
|
+
"updated_date",
|
|
95
|
+
"created_date",
|
|
96
|
+
):
|
|
63
97
|
if key in network:
|
|
64
98
|
value = network[key]
|
|
65
|
-
flat["network_{0}".format(id)] =
|
|
99
|
+
flat["network_{0}".format(id)] = (
|
|
100
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
101
|
+
)
|
|
66
102
|
|
|
67
103
|
if "contacts" in parsed:
|
|
68
104
|
contacts = parsed.get("contacts")
|
|
@@ -86,15 +122,30 @@ class ParsedWhois(Results):
|
|
|
86
122
|
):
|
|
87
123
|
if key in contact:
|
|
88
124
|
value = contact[key]
|
|
89
|
-
flat["{0}_{1}".format(contact_type, key)] =
|
|
125
|
+
flat["{0}_{1}".format(contact_type, key)] = (
|
|
126
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
127
|
+
)
|
|
90
128
|
|
|
91
129
|
elif type(contacts) is dict:
|
|
92
130
|
for contact_type in ("registrant", "admin", "tech", "billing"):
|
|
93
131
|
contact = contacts.get(contact_type, {})
|
|
94
|
-
for key in (
|
|
132
|
+
for key in (
|
|
133
|
+
"name",
|
|
134
|
+
"email",
|
|
135
|
+
"org",
|
|
136
|
+
"street",
|
|
137
|
+
"city",
|
|
138
|
+
"state",
|
|
139
|
+
"postal",
|
|
140
|
+
"country",
|
|
141
|
+
"phone",
|
|
142
|
+
"fax",
|
|
143
|
+
):
|
|
95
144
|
if key in contact:
|
|
96
145
|
value = contact[key]
|
|
97
|
-
flat["{0}_{1}".format(contact_type, key)] =
|
|
146
|
+
flat["{0}_{1}".format(contact_type, key)] = (
|
|
147
|
+
" ".join(value) if type(value) in (list, tuple) else value
|
|
148
|
+
)
|
|
98
149
|
|
|
99
150
|
return flat
|
|
100
151
|
|
|
@@ -130,7 +181,9 @@ class ParsedDomainRdap(Results):
|
|
|
130
181
|
for i, contact in enumerate(registrar_value, start=1):
|
|
131
182
|
for contact_key, contact_value in contact.items():
|
|
132
183
|
flat[f"registrar_contacts_{contact_key}"] = (
|
|
133
|
-
" | ".join(contact_value)
|
|
184
|
+
" | ".join(contact_value)
|
|
185
|
+
if type(contact_value) in (list, tuple)
|
|
186
|
+
else contact_value
|
|
134
187
|
)
|
|
135
188
|
|
|
136
189
|
continue
|
|
@@ -140,31 +193,69 @@ class ParsedDomainRdap(Results):
|
|
|
140
193
|
if contacts:
|
|
141
194
|
for i, contact in enumerate(contacts, start=1):
|
|
142
195
|
for contact_key, contact_value in contact.items():
|
|
143
|
-
flat[f"contact_{contact_key}_{i}"] =
|
|
196
|
+
flat[f"contact_{contact_key}_{i}"] = (
|
|
197
|
+
" | ".join(contact_value) if type(contact_value) in (list, tuple) else contact_value
|
|
198
|
+
)
|
|
144
199
|
|
|
145
200
|
return flat
|
|
146
201
|
|
|
147
202
|
|
|
148
203
|
class FeedsResults(Results):
|
|
149
|
-
"""
|
|
204
|
+
"""
|
|
205
|
+
Real Time Threat Feeds (RTTF) returns an application/ndjson stream.
|
|
206
|
+
With this we use httpx stream to process each JSON object efficiently.
|
|
207
|
+
|
|
208
|
+
Highlevel process:
|
|
209
|
+
|
|
210
|
+
httpx stream -> check status code -> yield back data to client -> repeat if 206
|
|
211
|
+
|
|
212
|
+
Returns the generator object for feeds results.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def _make_request(self) -> Generator:
|
|
216
|
+
"""
|
|
217
|
+
Creates and manages the httpx stream request, yielding data line by line.
|
|
218
|
+
This is the core generator that communicates with the DT frontend API server.
|
|
219
|
+
"""
|
|
220
|
+
session_info = self._get_session_params_and_headers()
|
|
221
|
+
headers = session_info.get("headers")
|
|
222
|
+
parameters = session_info.get("parameters")
|
|
223
|
+
|
|
224
|
+
with httpx.stream(
|
|
225
|
+
"GET",
|
|
226
|
+
self.url,
|
|
227
|
+
headers=headers,
|
|
228
|
+
params=parameters,
|
|
229
|
+
verify=self.api.verify_ssl,
|
|
230
|
+
proxy=self.api.proxy_url,
|
|
231
|
+
timeout=None,
|
|
232
|
+
) as response:
|
|
233
|
+
# set the status already
|
|
234
|
+
error_text = ""
|
|
235
|
+
status_code = response.status_code
|
|
236
|
+
if status_code not in [200, 206]:
|
|
237
|
+
response.read()
|
|
238
|
+
error_text = response.text
|
|
239
|
+
|
|
240
|
+
self.setStatus(status_code, reason_text=error_text)
|
|
241
|
+
|
|
242
|
+
for line in response.iter_lines():
|
|
243
|
+
yield line
|
|
244
|
+
|
|
245
|
+
def data(self) -> Generator:
|
|
246
|
+
self._data = self._make_request()
|
|
247
|
+
return self._data
|
|
150
248
|
|
|
151
249
|
def response(self) -> Generator:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
resp_data = self.data()
|
|
155
|
-
status_code = self.status
|
|
156
|
-
yield resp_data
|
|
250
|
+
while self.status != 200:
|
|
251
|
+
yield from self.data()
|
|
157
252
|
|
|
158
|
-
self._data = None # clear the data here
|
|
159
253
|
if not self.kwargs.get("sessionID"):
|
|
160
254
|
# we'll only do iterative request for queries that has sessionID.
|
|
161
|
-
# Otherwise, we will have an infinite request if sessionID was not provided
|
|
255
|
+
# Otherwise, we will have an infinite request if sessionID was not provided
|
|
256
|
+
# but the required data asked is more than the maximum (1 hour of data)
|
|
162
257
|
break
|
|
258
|
+
self._status = None
|
|
163
259
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
self.setStatus(results.status_code, results)
|
|
167
|
-
self._data = results.text
|
|
168
|
-
self.check_limit_exceeded()
|
|
169
|
-
|
|
170
|
-
return self._data
|
|
260
|
+
def __str__(self):
|
|
261
|
+
return f"{self.__class__.__name__} - {self.product}"
|
|
@@ -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)
|
|
@@ -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.
|
|
@@ -473,7 +473,9 @@ def test_iris_detect_watched_domains():
|
|
|
473
473
|
detect_results = api.iris_detect_watched_domains()
|
|
474
474
|
assert detect_results["count"] >= 0
|
|
475
475
|
|
|
476
|
-
detect_results = api.iris_detect_watched_domains(
|
|
476
|
+
detect_results = api.iris_detect_watched_domains(
|
|
477
|
+
monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc"
|
|
478
|
+
)
|
|
477
479
|
assert len(detect_results["watchlist_domains"]) == 3
|
|
478
480
|
|
|
479
481
|
detect_results = api.iris_detect_watched_domains(escalation_types="blocked")
|
|
@@ -482,17 +484,23 @@ def test_iris_detect_watched_domains():
|
|
|
482
484
|
|
|
483
485
|
@vcr.use_cassette
|
|
484
486
|
def test_iris_detect_manage_watchlist_domains():
|
|
485
|
-
detect_results = api.iris_detect_manage_watchlist_domains(
|
|
487
|
+
detect_results = api.iris_detect_manage_watchlist_domains(
|
|
488
|
+
watchlist_domain_ids=["gae08rdVWG"], state="watched"
|
|
489
|
+
)
|
|
486
490
|
assert detect_results["watchlist_domains"][0]["state"] == "watched"
|
|
487
491
|
|
|
488
492
|
|
|
489
493
|
@vcr.use_cassette
|
|
490
494
|
def test_iris_detect_escalate_domains():
|
|
491
495
|
# If you rerun this test without VCR, it will fail because the domain is already escalated
|
|
492
|
-
detect_results = api.iris_detect_escalate_domains(
|
|
496
|
+
detect_results = api.iris_detect_escalate_domains(
|
|
497
|
+
watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="blocked"
|
|
498
|
+
)
|
|
493
499
|
assert detect_results["escalations"][0]["escalation_type"] == "blocked"
|
|
494
500
|
|
|
495
|
-
detect_results = api.iris_detect_escalate_domains(
|
|
501
|
+
detect_results = api.iris_detect_escalate_domains(
|
|
502
|
+
watchlist_domain_ids=["OWxzqKqQEY"], escalation_type="google_safe"
|
|
503
|
+
)
|
|
496
504
|
assert detect_results["escalations"][0]["escalation_type"] == "google_safe"
|
|
497
505
|
|
|
498
506
|
|
|
@@ -515,34 +523,40 @@ def test_limit_exceeded():
|
|
|
515
523
|
@vcr.use_cassette
|
|
516
524
|
def test_newly_observed_domains_feed():
|
|
517
525
|
results = feeds_api.nod(after="-60", top=5)
|
|
526
|
+
total_count = 0
|
|
518
527
|
for response in results.response():
|
|
519
528
|
assert results.status == 200
|
|
520
529
|
|
|
521
530
|
rows = response.strip().split("\n")
|
|
522
531
|
assert response is not None
|
|
523
|
-
|
|
532
|
+
total_count += 1
|
|
524
533
|
|
|
525
534
|
for row in rows:
|
|
526
535
|
feed_result = json.loads(row)
|
|
527
536
|
assert "timestamp" in feed_result.keys()
|
|
528
537
|
assert "domain" in feed_result.keys()
|
|
529
538
|
|
|
539
|
+
assert total_count == 5
|
|
540
|
+
|
|
530
541
|
|
|
531
542
|
@vcr.use_cassette
|
|
532
543
|
def test_newly_observed_hosts_feed():
|
|
533
544
|
results = feeds_api.noh(after="-60", top=5)
|
|
545
|
+
total_count = 0
|
|
534
546
|
for response in results.response():
|
|
535
547
|
assert results.status == 200
|
|
536
548
|
|
|
537
549
|
rows = response.strip().split("\n")
|
|
538
550
|
assert response is not None
|
|
539
|
-
|
|
551
|
+
total_count += 1
|
|
540
552
|
|
|
541
553
|
for row in rows:
|
|
542
554
|
feed_result = json.loads(row)
|
|
543
555
|
assert "timestamp" in feed_result.keys()
|
|
544
556
|
assert "domain" in feed_result.keys()
|
|
545
557
|
|
|
558
|
+
assert total_count == 5
|
|
559
|
+
|
|
546
560
|
|
|
547
561
|
@vcr.use_cassette
|
|
548
562
|
@pytest.mark.skip(reason="Failing test due to unknown reason. Still investigating the root-cause.")
|
|
@@ -553,16 +567,14 @@ def test_newly_observed_domains_feed_pagination():
|
|
|
553
567
|
results = feeds_api.nod(sessionID="integrations-testing", after=after)
|
|
554
568
|
page_count = 0
|
|
555
569
|
for response in results.response():
|
|
556
|
-
|
|
570
|
+
response = response.strip()
|
|
557
571
|
assert response is not None
|
|
558
|
-
assert len(rows) >= 1
|
|
559
572
|
|
|
560
573
|
page_count += 1
|
|
561
574
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
assert "domain" in feed_result.keys()
|
|
575
|
+
feed_result = json.loads(response)
|
|
576
|
+
assert "timestamp" in feed_result.keys()
|
|
577
|
+
assert "domain" in feed_result.keys()
|
|
566
578
|
|
|
567
579
|
assert page_count >= 1
|
|
568
580
|
|
|
@@ -573,14 +585,12 @@ def test_newly_active_domains_feed():
|
|
|
573
585
|
for response in results.response():
|
|
574
586
|
assert results.status == 200
|
|
575
587
|
|
|
576
|
-
|
|
588
|
+
response = response.strip()
|
|
577
589
|
assert response is not None
|
|
578
|
-
assert len(rows) >= 1
|
|
579
590
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
assert "domain" in feed_result.keys()
|
|
591
|
+
feed_result = json.loads(response)
|
|
592
|
+
assert "timestamp" in feed_result.keys()
|
|
593
|
+
assert "domain" in feed_result.keys()
|
|
584
594
|
|
|
585
595
|
|
|
586
596
|
@vcr.use_cassette
|
|
@@ -589,19 +599,17 @@ def test_domainrdap_feed():
|
|
|
589
599
|
for response in results.response():
|
|
590
600
|
assert results.status == 200
|
|
591
601
|
|
|
592
|
-
|
|
602
|
+
response = response.strip()
|
|
593
603
|
|
|
594
604
|
assert response is not None
|
|
595
|
-
assert len(rows) == 2
|
|
596
605
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
606
|
+
feed_result = json.loads(response)
|
|
607
|
+
assert "timestamp" in feed_result.keys()
|
|
608
|
+
assert "domain" in feed_result.keys()
|
|
609
|
+
assert "parsed_record" in feed_result.keys()
|
|
610
|
+
assert "domain" in feed_result["parsed_record"]["parsed_fields"]
|
|
611
|
+
assert "emails" in feed_result["parsed_record"]["parsed_fields"]
|
|
612
|
+
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
605
613
|
|
|
606
614
|
|
|
607
615
|
@vcr.use_cassette
|
|
@@ -610,14 +618,12 @@ def test_domain_discovery_feed():
|
|
|
610
618
|
for response in results.response():
|
|
611
619
|
assert results.status == 200
|
|
612
620
|
|
|
613
|
-
|
|
621
|
+
response = response.strip()
|
|
614
622
|
assert response is not None
|
|
615
|
-
assert len(rows) >= 1
|
|
616
623
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
assert "domain" in feed_result.keys()
|
|
624
|
+
feed_result = json.loads(response)
|
|
625
|
+
assert "timestamp" in feed_result.keys()
|
|
626
|
+
assert "domain" in feed_result.keys()
|
|
621
627
|
|
|
622
628
|
|
|
623
629
|
@vcr.use_cassette
|
|
@@ -626,19 +632,17 @@ def test_domainrdap_feed_not_api_header_auth():
|
|
|
626
632
|
for response in results.response():
|
|
627
633
|
assert results.status == 200
|
|
628
634
|
|
|
629
|
-
|
|
635
|
+
response = response.strip()
|
|
630
636
|
|
|
631
637
|
assert response is not None
|
|
632
|
-
assert len(rows) == 5
|
|
633
638
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
639
|
+
feed_result = json.loads(response)
|
|
640
|
+
assert "timestamp" in feed_result.keys()
|
|
641
|
+
assert "domain" in feed_result.keys()
|
|
642
|
+
assert "parsed_record" in feed_result.keys()
|
|
643
|
+
assert "domain" in feed_result["parsed_record"]["parsed_fields"]
|
|
644
|
+
assert "emails" in feed_result["parsed_record"]["parsed_fields"]
|
|
645
|
+
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
642
646
|
|
|
643
647
|
|
|
644
648
|
@vcr.use_cassette
|
|
@@ -654,14 +658,12 @@ def test_feeds_endpoint_should_non_header_auth_be_the_default():
|
|
|
654
658
|
for response in results.response():
|
|
655
659
|
assert results.status == 200
|
|
656
660
|
|
|
657
|
-
|
|
661
|
+
response = response.strip()
|
|
658
662
|
assert response is not None
|
|
659
|
-
assert len(rows) >= 1
|
|
660
663
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
assert "files" in feed_result["response"].keys()
|
|
664
|
+
feed_result = json.loads(response)
|
|
665
|
+
assert "download_name" in feed_result["response"].keys()
|
|
666
|
+
assert "files" in feed_result["response"].keys()
|
|
665
667
|
|
|
666
668
|
|
|
667
669
|
@vcr.use_cassette
|
|
@@ -686,18 +688,16 @@ def test_realtime_domain_risk():
|
|
|
686
688
|
for response in results.response():
|
|
687
689
|
assert results.status == 200
|
|
688
690
|
|
|
689
|
-
|
|
691
|
+
response = response.strip()
|
|
690
692
|
assert response is not None
|
|
691
|
-
assert len(rows) >= 1
|
|
692
693
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
assert "overall_risk" in feed_result.keys()
|
|
694
|
+
feed_result = json.loads(response)
|
|
695
|
+
assert "timestamp" in feed_result.keys()
|
|
696
|
+
assert "domain" in feed_result.keys()
|
|
697
|
+
assert "phishing_risk" in feed_result.keys()
|
|
698
|
+
assert "malware_risk" in feed_result.keys()
|
|
699
|
+
assert "proximity_risk" in feed_result.keys()
|
|
700
|
+
assert "overall_risk" in feed_result.keys()
|
|
701
701
|
|
|
702
702
|
|
|
703
703
|
@vcr.use_cassette
|
|
@@ -706,18 +706,16 @@ def test_domain_hotlist():
|
|
|
706
706
|
for response in results.response():
|
|
707
707
|
assert results.status == 200
|
|
708
708
|
|
|
709
|
-
|
|
709
|
+
response = response.strip()
|
|
710
710
|
assert response is not None
|
|
711
|
-
assert len(rows) >= 1
|
|
712
711
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
assert "overall_risk" in feed_result.keys()
|
|
712
|
+
feed_result = json.loads(response)
|
|
713
|
+
assert "timestamp" in feed_result.keys()
|
|
714
|
+
assert "domain" in feed_result.keys()
|
|
715
|
+
assert "phishing_risk" in feed_result.keys()
|
|
716
|
+
assert "malware_risk" in feed_result.keys()
|
|
717
|
+
assert "proximity_risk" in feed_result.keys()
|
|
718
|
+
assert "overall_risk" in feed_result.keys()
|
|
721
719
|
|
|
722
720
|
|
|
723
721
|
@vcr.use_cassette
|
domaintools_api-2.5.3/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.5.3
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Endpoint(Enum):
|
|
5
|
-
FEED = "feed"
|
|
6
|
-
DOWNLOAD = "download"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Source(Enum):
|
|
10
|
-
API = "api"
|
|
11
|
-
S3 = "s3"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class OutputFormat(Enum):
|
|
15
|
-
JSONL = "jsonl"
|
|
16
|
-
CSV = "csv"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
HEADER_ACCEPT_KEY_CSV_FORMAT = "text/csv"
|
|
20
|
-
|
|
21
|
-
ENDPOINT_TO_SOURCE_MAP = {
|
|
22
|
-
Endpoint.FEED.value: Source.API,
|
|
23
|
-
Endpoint.DOWNLOAD.value: Source.S3,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
RTTF_PRODUCTS_LIST = [
|
|
27
|
-
"newly-active-domains-feed-(api)",
|
|
28
|
-
"newly-active-domains-feed-(s3)",
|
|
29
|
-
"newly-observed-domains-feed-(api)",
|
|
30
|
-
"newly-observed-domains-feed-(s3)",
|
|
31
|
-
"newly-observed-hosts-feed-(api)",
|
|
32
|
-
"newly-observed-hosts-feed-(s3)",
|
|
33
|
-
"domain-hotlist-feed-(api)",
|
|
34
|
-
"domain-hotlist-feed-(s3)",
|
|
35
|
-
"domain-registration-data-access-protocol-feed-(api)",
|
|
36
|
-
"domain-registration-data-access-protocol-feed-(s3)",
|
|
37
|
-
"domain-risk-feed-(api)",
|
|
38
|
-
"domain-risk-feed-(s3)",
|
|
39
|
-
"real-time-domain-discovery-feed-(api)",
|
|
40
|
-
"real-time-domain-discovery-feed-(s3)",
|
|
41
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{domaintools_api-2.5.3 → domaintools_api-2.6.0}/domaintools_api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|