domaintools-api-test-upload 2.2.1__tar.gz → 2.2.2__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_test_upload-2.2.1/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.2.2}/PKG-INFO +39 -4
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/README.md +38 -3
- domaintools_api_test_upload-2.2.2/VERSION +1 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/_version.py +1 -1
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/api.py +10 -4
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/base_results.py +35 -37
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/api.py +7 -2
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/utils.py +1 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/results.py +27 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/utils.py +6 -2
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2/domaintools_api_test_upload.egg-info}/PKG-INFO +39 -4
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_async/__init__.py +4 -18
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/setup.py +2 -1
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_api.py +115 -61
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_utils.py +52 -52
- domaintools_api_test_upload-2.2.1/VERSION +0 -1
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/LICENSE +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/__init__.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/commands/phisheye.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/constants.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/constants.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/exceptions.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_api_test_upload.egg-info/requires.txt +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/pyproject.toml +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_async.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_cli.py +0 -0
- {domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_filters.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: domaintools_api_test_upload
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -272,16 +272,51 @@ Real-Time Threat Intelligence Feeds provide data on the different stages of the
|
|
|
272
272
|
Custom parameters aside from the common `GET` Request parameters:
|
|
273
273
|
- `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
|
|
274
274
|
```python
|
|
275
|
-
api = API(USERNAME, KEY)
|
|
275
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
276
276
|
api.nod(endpoint="feed", **kwargs)
|
|
277
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
279
|
```python
|
|
280
|
-
api = API(USERNAME, KEY)
|
|
280
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
281
281
|
api.nod(header_authentication=False, **kwargs)
|
|
282
282
|
```
|
|
283
283
|
- `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
284
|
```python
|
|
285
|
-
api = API(USERNAME, KEY)
|
|
285
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
286
286
|
api.nod(output_format="csv", **kwargs)
|
|
287
287
|
```
|
|
288
|
+
|
|
289
|
+
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:
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
- Each subsequent call to the API using your `sessionID` will return all data since the last.
|
|
293
|
+
- 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.
|
|
294
|
+
2. Or, specify the time range in one of two ways:
|
|
295
|
+
- Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
|
|
296
|
+
- 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)
|
|
297
|
+
|
|
298
|
+
## Handling iterative response from RTUF endpoints:
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
### Single request because the requested data is within the maximum result:
|
|
303
|
+
```python
|
|
304
|
+
from domaintools import API
|
|
305
|
+
|
|
306
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
307
|
+
results = api.nod(sessionID="my-session-id", after=-60)
|
|
308
|
+
|
|
309
|
+
for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
|
|
310
|
+
# do things to result
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Multiple requests because the requested data is more than the maximum result per request:
|
|
314
|
+
```python
|
|
315
|
+
from domaintools import API
|
|
316
|
+
|
|
317
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
318
|
+
results = api.nod(sessionID="my-session-id", after=-7200)
|
|
319
|
+
|
|
320
|
+
for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
|
|
321
|
+
# do things to partial_result
|
|
322
|
+
```
|
|
@@ -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
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.
|
|
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.2.2
|
|
@@ -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,
|
|
@@ -1065,7 +1067,7 @@ class API(object):
|
|
|
1065
1067
|
**kwargs,
|
|
1066
1068
|
)
|
|
1067
1069
|
|
|
1068
|
-
def nod(self, **kwargs):
|
|
1070
|
+
def nod(self, **kwargs) -> FeedsResults:
|
|
1069
1071
|
"""Returns back list of the newly observed domains feed"""
|
|
1070
1072
|
validate_feeds_parameters(kwargs)
|
|
1071
1073
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
@@ -1078,10 +1080,11 @@ class API(object):
|
|
|
1078
1080
|
f"newly-observed-domains-feed-({source.value})",
|
|
1079
1081
|
f"v1/{endpoint}/nod/",
|
|
1080
1082
|
response_path=(),
|
|
1083
|
+
cls=FeedsResults,
|
|
1081
1084
|
**kwargs,
|
|
1082
1085
|
)
|
|
1083
1086
|
|
|
1084
|
-
def nad(self, **kwargs):
|
|
1087
|
+
def nad(self, **kwargs) -> FeedsResults:
|
|
1085
1088
|
"""Returns back list of the newly active domains feed"""
|
|
1086
1089
|
validate_feeds_parameters(kwargs)
|
|
1087
1090
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
@@ -1094,10 +1097,11 @@ class API(object):
|
|
|
1094
1097
|
f"newly-active-domains-feed-({source})",
|
|
1095
1098
|
f"v1/{endpoint}/nad/",
|
|
1096
1099
|
response_path=(),
|
|
1100
|
+
cls=FeedsResults,
|
|
1097
1101
|
**kwargs,
|
|
1098
1102
|
)
|
|
1099
1103
|
|
|
1100
|
-
def domainrdap(self, **kwargs):
|
|
1104
|
+
def domainrdap(self, **kwargs) -> FeedsResults:
|
|
1101
1105
|
"""Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP)"""
|
|
1102
1106
|
validate_feeds_parameters(kwargs)
|
|
1103
1107
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
@@ -1107,10 +1111,11 @@ class API(object):
|
|
|
1107
1111
|
f"domain-registration-data-access-protocol-feed-({source})",
|
|
1108
1112
|
f"v1/{endpoint}/domainrdap/",
|
|
1109
1113
|
response_path=(),
|
|
1114
|
+
cls=FeedsResults,
|
|
1110
1115
|
**kwargs,
|
|
1111
1116
|
)
|
|
1112
1117
|
|
|
1113
|
-
def domaindiscovery(self, **kwargs):
|
|
1118
|
+
def domaindiscovery(self, **kwargs) -> FeedsResults:
|
|
1114
1119
|
"""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
1120
|
validate_feeds_parameters(kwargs)
|
|
1116
1121
|
endpoint = kwargs.pop("endpoint", Endpoint.FEED.value)
|
|
@@ -1123,5 +1128,6 @@ class API(object):
|
|
|
1123
1128
|
f"real-time-domain-discovery-feed-({source})",
|
|
1124
1129
|
f"v1/{endpoint}/domaindiscovery/",
|
|
1125
1130
|
response_path=(),
|
|
1131
|
+
cls=FeedsResults,
|
|
1126
1132
|
**kwargs,
|
|
1127
1133
|
)
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/base_results.py
RENAMED
|
@@ -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
|
-
|
|
97
|
-
parameters.
|
|
98
|
-
|
|
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
|
-
|
|
149
|
-
self._limit_exceeded = True
|
|
150
|
-
self._limit_exceeded_message = message
|
|
148
|
+
self.check_limit_exceeded()
|
|
151
149
|
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
185
|
+
if code in (400, 422):
|
|
188
186
|
raise BadRequestException(code, reason)
|
|
189
|
-
elif code
|
|
187
|
+
elif code in (401, 403):
|
|
190
188
|
raise NotAuthorizedException(code, reason)
|
|
191
189
|
elif code == 404:
|
|
192
190
|
raise NotFoundException(code, reason)
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/api.py
RENAMED
|
@@ -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
|
-
|
|
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")
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/utils.py
RENAMED
|
@@ -85,6 +85,7 @@ _command_help_mapper = {
|
|
|
85
85
|
c.FEEDS_NAD: "Returns back newly active domains feed.",
|
|
86
86
|
c.FEEDS_NOD: "Returns back newly observed domains feed.",
|
|
87
87
|
c.FEEDS_DOMAINRDAP: "Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP).",
|
|
88
|
+
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
89
|
}
|
|
89
90
|
|
|
90
91
|
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/results.py
RENAMED
|
@@ -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
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/utils.py
RENAMED
|
@@ -177,8 +177,12 @@ 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
|
|
180
|
+
raise ValueError("sessionID or after or before must be provided")
|
|
181
181
|
|
|
182
182
|
format = params.get("output_format")
|
|
183
|
-
|
|
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
|
+
raise ValueError(f"{Endpoint.DOWNLOAD.value} API does not support header authentication. Provide api_key in the parameter")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: domaintools_api_test_upload
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -272,16 +272,51 @@ Real-Time Threat Intelligence Feeds provide data on the different stages of the
|
|
|
272
272
|
Custom parameters aside from the common `GET` Request parameters:
|
|
273
273
|
- `endpoint` (choose either `download` or `feed` API endpoint - default is `feed`)
|
|
274
274
|
```python
|
|
275
|
-
api = API(USERNAME, KEY)
|
|
275
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
276
276
|
api.nod(endpoint="feed", **kwargs)
|
|
277
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
279
|
```python
|
|
280
|
-
api = API(USERNAME, KEY)
|
|
280
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
281
281
|
api.nod(header_authentication=False, **kwargs)
|
|
282
282
|
```
|
|
283
283
|
- `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
284
|
```python
|
|
285
|
-
api = API(USERNAME, KEY)
|
|
285
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
286
286
|
api.nod(output_format="csv", **kwargs)
|
|
287
287
|
```
|
|
288
|
+
|
|
289
|
+
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:
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
- Each subsequent call to the API using your `sessionID` will return all data since the last.
|
|
293
|
+
- 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.
|
|
294
|
+
2. Or, specify the time range in one of two ways:
|
|
295
|
+
- Either an `after=-60` query parameter, where (in this example) -60 indicates the previous 60 seconds.
|
|
296
|
+
- 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)
|
|
297
|
+
|
|
298
|
+
## Handling iterative response from RTUF endpoints:
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
### Single request because the requested data is within the maximum result:
|
|
303
|
+
```python
|
|
304
|
+
from domaintools import API
|
|
305
|
+
|
|
306
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
307
|
+
results = api.nod(sessionID="my-session-id", after=-60)
|
|
308
|
+
|
|
309
|
+
for result in results.response() # generator that holds NOD feeds data for the past 60 seconds and is expected to request only once
|
|
310
|
+
# do things to result
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Multiple requests because the requested data is more than the maximum result per request:
|
|
314
|
+
```python
|
|
315
|
+
from domaintools import API
|
|
316
|
+
|
|
317
|
+
api = API(USERNAME, KEY, always_sign_api_key=False)
|
|
318
|
+
results = api.nod(sessionID="my-session-id", after=-7200)
|
|
319
|
+
|
|
320
|
+
for partial_result in results.response() # generator that holds NOD feeds data for the past 2 hours and is expected to request multiple times
|
|
321
|
+
# do things to partial_result
|
|
322
|
+
```
|
|
@@ -52,20 +52,9 @@ class AsyncResults(Results):
|
|
|
52
52
|
patch_data.update(self.api.extra_request_params)
|
|
53
53
|
results = await session.patch(url=self.url, json=patch_data)
|
|
54
54
|
elif self.product in FEEDS_PRODUCTS_LIST:
|
|
55
|
-
|
|
56
|
-
parameters.
|
|
57
|
-
|
|
58
|
-
"format", None
|
|
59
|
-
) # 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.
|
|
60
|
-
headers = {}
|
|
61
|
-
if self.kwargs.get("output_format", OutputFormat.JSONL.value) == OutputFormat.CSV.value:
|
|
62
|
-
parameters["headers"] = int(bool(self.kwargs.get("headers", False)))
|
|
63
|
-
headers["accept"] = HEADER_ACCEPT_KEY_CSV_FORMAT
|
|
64
|
-
|
|
65
|
-
header_api_key = parameters.pop("X-Api-Key", None)
|
|
66
|
-
if header_api_key:
|
|
67
|
-
headers["X-Api-Key"] = header_api_key
|
|
68
|
-
|
|
55
|
+
session_params = self._get_session_params()
|
|
56
|
+
parameters = session_params.get("parameters")
|
|
57
|
+
headers = session_params.get("headers")
|
|
69
58
|
results = await session.get(url=self.url, params=parameters, headers=headers, **self.api.extra_request_params)
|
|
70
59
|
else:
|
|
71
60
|
results = await session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
|
|
@@ -75,11 +64,8 @@ class AsyncResults(Results):
|
|
|
75
64
|
self._data = results.json()
|
|
76
65
|
else:
|
|
77
66
|
self._data = results.text()
|
|
78
|
-
limit_exceeded, message = self.check_limit_exceeded()
|
|
79
67
|
|
|
80
|
-
|
|
81
|
-
self._limit_exceeded = True
|
|
82
|
-
self._limit_exceeded_message = message
|
|
68
|
+
self.check_limit_exceeded()
|
|
83
69
|
|
|
84
70
|
async def __awaitable__(self):
|
|
85
71
|
if self._data is None:
|
|
@@ -37,7 +37,8 @@ if CYTHON:
|
|
|
37
37
|
return module_names
|
|
38
38
|
|
|
39
39
|
ext_modules = [
|
|
40
|
-
Extension("domaintools." + ext, [path.join("domaintools", ext + ".py")])
|
|
40
|
+
Extension("domaintools." + ext, [path.join("domaintools", ext + ".py")])
|
|
41
|
+
for ext in list_modules(path.join(MYDIR, "domaintools"))
|
|
41
42
|
]
|
|
42
43
|
cmdclass["build_ext"] = build_ext
|
|
43
44
|
|
|
@@ -5,8 +5,10 @@ from os import environ
|
|
|
5
5
|
import json
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
+
from inspect import isgenerator
|
|
9
|
+
|
|
8
10
|
from domaintools import API, exceptions
|
|
9
|
-
from tests.settings import api, vcr
|
|
11
|
+
from tests.settings import api, feeds_api, vcr
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@vcr.use_cassette
|
|
@@ -529,87 +531,139 @@ def test_limit_exceeded():
|
|
|
529
531
|
|
|
530
532
|
@vcr.use_cassette
|
|
531
533
|
def test_newly_observed_domains_feed():
|
|
532
|
-
results =
|
|
533
|
-
response
|
|
534
|
-
|
|
534
|
+
results = feeds_api.nod(after="-60", header_authentication=False)
|
|
535
|
+
for response in results.response():
|
|
536
|
+
assert results.status == 200
|
|
537
|
+
|
|
538
|
+
rows = response.strip().split("\n")
|
|
539
|
+
assert response is not None
|
|
540
|
+
assert len(rows) >= 1
|
|
541
|
+
|
|
542
|
+
for row in rows:
|
|
543
|
+
feed_result = json.loads(row)
|
|
544
|
+
assert "timestamp" in feed_result.keys()
|
|
545
|
+
assert "domain" in feed_result.keys()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@vcr.use_cassette
|
|
549
|
+
def test_newly_observed_domains_feed_pagination():
|
|
550
|
+
results = feeds_api.nod(sessionID="integrations-testing", after="2025-01-16T10:20:00Z")
|
|
551
|
+
page_count = 0
|
|
552
|
+
for response in results.response():
|
|
553
|
+
rows = response.strip().split("\n")
|
|
554
|
+
assert response is not None
|
|
555
|
+
assert len(rows) >= 1
|
|
556
|
+
|
|
557
|
+
page_count += 1
|
|
535
558
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
559
|
+
for row in rows:
|
|
560
|
+
feed_result = json.loads(row)
|
|
561
|
+
assert "timestamp" in feed_result.keys()
|
|
562
|
+
assert "domain" in feed_result.keys()
|
|
539
563
|
|
|
540
|
-
|
|
541
|
-
feed_result = json.loads(row)
|
|
542
|
-
assert "timestamp" in feed_result.keys()
|
|
543
|
-
assert "domain" in feed_result.keys()
|
|
564
|
+
assert page_count > 1
|
|
544
565
|
|
|
545
566
|
|
|
546
567
|
@vcr.use_cassette
|
|
547
568
|
def test_newly_active_domains_feed():
|
|
548
|
-
results =
|
|
549
|
-
response
|
|
550
|
-
|
|
569
|
+
results = feeds_api.nad(after="-60", header_authentication=False)
|
|
570
|
+
for response in results.response():
|
|
571
|
+
assert results.status == 200
|
|
551
572
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
573
|
+
rows = response.strip().split("\n")
|
|
574
|
+
assert response is not None
|
|
575
|
+
assert len(rows) >= 1
|
|
555
576
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
577
|
+
for row in rows:
|
|
578
|
+
feed_result = json.loads(row)
|
|
579
|
+
assert "timestamp" in feed_result.keys()
|
|
580
|
+
assert "domain" in feed_result.keys()
|
|
560
581
|
|
|
561
582
|
|
|
562
583
|
@vcr.use_cassette
|
|
563
584
|
def test_domainrdap_feed():
|
|
564
|
-
results =
|
|
565
|
-
response
|
|
566
|
-
|
|
585
|
+
results = feeds_api.domainrdap(after="-60", top=2, header_authenticationn=False)
|
|
586
|
+
for response in results.response():
|
|
587
|
+
assert results.status == 200
|
|
567
588
|
|
|
568
|
-
|
|
569
|
-
assert results.status == 200
|
|
570
|
-
assert len(rows) == 5
|
|
589
|
+
rows = response.strip().split("\n")
|
|
571
590
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
591
|
+
assert response is not None
|
|
592
|
+
assert len(rows) == 2
|
|
593
|
+
|
|
594
|
+
for row in rows:
|
|
595
|
+
feed_result = json.loads(row)
|
|
596
|
+
assert "timestamp" in feed_result.keys()
|
|
597
|
+
assert "domain" in feed_result.keys()
|
|
598
|
+
assert "parsed_record" in feed_result.keys()
|
|
599
|
+
assert "domain" in feed_result["parsed_record"]["parsed_fields"]
|
|
600
|
+
assert "emails" in feed_result["parsed_record"]["parsed_fields"]
|
|
601
|
+
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
580
602
|
|
|
581
603
|
|
|
582
604
|
@vcr.use_cassette
|
|
583
605
|
def test_domain_discovery_feed():
|
|
584
|
-
results =
|
|
585
|
-
response
|
|
586
|
-
|
|
606
|
+
results = feeds_api.domaindiscovery(after="-60", header_authentication=False)
|
|
607
|
+
for response in results.response():
|
|
608
|
+
assert results.status == 200
|
|
587
609
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
610
|
+
rows = response.strip().split("\n")
|
|
611
|
+
assert response is not None
|
|
612
|
+
assert len(rows) >= 1
|
|
591
613
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
614
|
+
for row in rows:
|
|
615
|
+
feed_result = json.loads(row)
|
|
616
|
+
assert "timestamp" in feed_result.keys()
|
|
617
|
+
assert "domain" in feed_result.keys()
|
|
596
618
|
|
|
597
619
|
|
|
598
620
|
@vcr.use_cassette
|
|
599
621
|
def test_domainrdap_feed_not_api_header_auth():
|
|
600
|
-
results =
|
|
601
|
-
response
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
622
|
+
results = feeds_api.domainrdap(after="-60", sessiondID="integrations-testing", top=5, header_authenticationn=False)
|
|
623
|
+
for response in results.response():
|
|
624
|
+
assert results.status == 200
|
|
625
|
+
|
|
626
|
+
rows = response.strip().split("\n")
|
|
627
|
+
|
|
628
|
+
assert response is not None
|
|
629
|
+
assert len(rows) == 5
|
|
630
|
+
|
|
631
|
+
for row in rows:
|
|
632
|
+
feed_result = json.loads(row)
|
|
633
|
+
assert "timestamp" in feed_result.keys()
|
|
634
|
+
assert "domain" in feed_result.keys()
|
|
635
|
+
assert "parsed_record" in feed_result.keys()
|
|
636
|
+
assert "domain" in feed_result["parsed_record"]["parsed_fields"]
|
|
637
|
+
assert "emails" in feed_result["parsed_record"]["parsed_fields"]
|
|
638
|
+
assert "contacts" in feed_result["parsed_record"]["parsed_fields"]
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@vcr.use_cassette
|
|
642
|
+
def test_verify_response_is_a_generator():
|
|
643
|
+
results = feeds_api.domaindiscovery(after="-60", header_authenticationn=False)
|
|
644
|
+
|
|
645
|
+
assert isgenerator(results.response())
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@vcr.use_cassette
|
|
649
|
+
def test_feeds_endpoint_should_raise_error_if_download_api_using_header_auth():
|
|
650
|
+
with pytest.raises(ValueError) as excinfo:
|
|
651
|
+
feeds_api.domaindiscovery(after="-60", endpoint="download")
|
|
652
|
+
|
|
653
|
+
assert str(excinfo.value) == "download API does not support header authentication. Provide api_key in the parameter"
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@vcr.use_cassette
|
|
657
|
+
def test_feeds_endpoint_should_raise_error_if_no_required_params():
|
|
658
|
+
with pytest.raises(ValueError) as excinfo:
|
|
659
|
+
feeds_api.domaindiscovery()
|
|
660
|
+
|
|
661
|
+
assert str(excinfo.value) == "sessionID or after or before must be provided"
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@vcr.use_cassette
|
|
665
|
+
def test_feeds_endpoint_should_raise_error_if_asked_csv_format_for_download_api():
|
|
666
|
+
with pytest.raises(ValueError) as excinfo:
|
|
667
|
+
feeds_api.domaindiscovery(after="-60", output_format="csv", endpoint="download")
|
|
668
|
+
|
|
669
|
+
assert str(excinfo.value) == "csv format is not available in download API."
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
2
4
|
from datetime import datetime, timedelta
|
|
3
5
|
|
|
4
6
|
from tests.responses import iris_investigate_data
|
|
@@ -15,24 +17,18 @@ def test_get_domain_age():
|
|
|
15
17
|
def test_get_threat_component():
|
|
16
18
|
threat_components = [
|
|
17
19
|
{"name": "threat_profile_malware", "risk_score": 31},
|
|
18
|
-
{"name": "threat_profile_spam", "risk_score": 73, "threats": ["spam"]}
|
|
20
|
+
{"name": "threat_profile_spam", "risk_score": 73, "threats": ["spam"]},
|
|
19
21
|
]
|
|
20
22
|
result = utils.get_threat_component(threat_components, "threat_profile_malware")
|
|
21
23
|
assert result.get("risk_score") == 31
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def test_investigate_average_risk_score():
|
|
25
|
-
domains = [
|
|
26
|
-
{"domain_risk": {"risk_score": 25}},
|
|
27
|
-
{"domain_risk": {"risk_score": 27}}
|
|
28
|
-
]
|
|
27
|
+
domains = [{"domain_risk": {"risk_score": 25}}, {"domain_risk": {"risk_score": 27}}]
|
|
29
28
|
result = utils.get_average_risk_score(domains)
|
|
30
29
|
assert result == 26
|
|
31
30
|
|
|
32
|
-
domains = [
|
|
33
|
-
{"domain_risk": {"risk_score": 25}},
|
|
34
|
-
{}
|
|
35
|
-
]
|
|
31
|
+
domains = [{"domain_risk": {"risk_score": 25}}, {}]
|
|
36
32
|
result = utils.get_average_risk_score(domains)
|
|
37
33
|
assert result == 25
|
|
38
34
|
|
|
@@ -42,17 +38,11 @@ def test_investigate_average_risk_score():
|
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
def test_detect_average_risk_score():
|
|
45
|
-
domains = [
|
|
46
|
-
{"risk_score": 25},
|
|
47
|
-
{"risk_score": 27}
|
|
48
|
-
]
|
|
41
|
+
domains = [{"risk_score": 25}, {"risk_score": 27}]
|
|
49
42
|
result = utils.get_average_risk_score(domains)
|
|
50
43
|
assert result == 26
|
|
51
44
|
|
|
52
|
-
domains = [
|
|
53
|
-
{"risk_score": 25},
|
|
54
|
-
{"risk_score": None}
|
|
55
|
-
]
|
|
45
|
+
domains = [{"risk_score": 25}, {"risk_score": None}]
|
|
56
46
|
result = utils.get_average_risk_score(domains)
|
|
57
47
|
assert result == 25
|
|
58
48
|
|
|
@@ -65,24 +55,15 @@ def test_investigate_average_age():
|
|
|
65
55
|
two_days_ago = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d")
|
|
66
56
|
five_days_ago = (datetime.now() - timedelta(days=5)).strftime("%Y-%m-%d")
|
|
67
57
|
|
|
68
|
-
domains = [
|
|
69
|
-
{"create_date": {"value": two_days_ago}},
|
|
70
|
-
{"create_date": {"value": five_days_ago}}
|
|
71
|
-
]
|
|
58
|
+
domains = [{"create_date": {"value": two_days_ago}}, {"create_date": {"value": five_days_ago}}]
|
|
72
59
|
result = utils.get_average_age(domains)
|
|
73
60
|
assert result == 3
|
|
74
61
|
|
|
75
|
-
domains = [
|
|
76
|
-
{"create_date": {"value": two_days_ago}},
|
|
77
|
-
{}
|
|
78
|
-
]
|
|
62
|
+
domains = [{"create_date": {"value": two_days_ago}}, {}]
|
|
79
63
|
result = utils.get_average_age(domains)
|
|
80
64
|
assert result == 2
|
|
81
65
|
|
|
82
|
-
domains = [
|
|
83
|
-
{"create_date": {"value": two_days_ago}},
|
|
84
|
-
{"create_date": {"value": ""}}
|
|
85
|
-
]
|
|
66
|
+
domains = [{"create_date": {"value": two_days_ago}}, {"create_date": {"value": ""}}]
|
|
86
67
|
result = utils.get_average_age(domains)
|
|
87
68
|
assert result == 2
|
|
88
69
|
|
|
@@ -94,17 +75,11 @@ def test_investigate_average_age():
|
|
|
94
75
|
def test_detect_average_age():
|
|
95
76
|
two_days_ago = int((datetime.now() - timedelta(days=2)).strftime("%Y%m%d"))
|
|
96
77
|
five_days_ago = int((datetime.now() - timedelta(days=5)).strftime("%Y%m%d"))
|
|
97
|
-
domains = [
|
|
98
|
-
{"create_date": two_days_ago},
|
|
99
|
-
{"create_date": five_days_ago}
|
|
100
|
-
]
|
|
78
|
+
domains = [{"create_date": two_days_ago}, {"create_date": five_days_ago}]
|
|
101
79
|
result = utils.get_average_age(domains)
|
|
102
80
|
assert result == 3
|
|
103
81
|
|
|
104
|
-
domains = [
|
|
105
|
-
{"create_date": two_days_ago},
|
|
106
|
-
{"create_date": None}
|
|
107
|
-
]
|
|
82
|
+
domains = [{"create_date": two_days_ago}, {"create_date": None}]
|
|
108
83
|
result = utils.get_average_age(domains)
|
|
109
84
|
assert result == 2
|
|
110
85
|
|
|
@@ -121,26 +96,51 @@ def test_data_prune():
|
|
|
121
96
|
|
|
122
97
|
def test_find_emails():
|
|
123
98
|
emails = utils.find_emails(json.dumps(iris_investigate_data.domaintools()))
|
|
124
|
-
assert emails == {
|
|
99
|
+
assert emails == {"abuse@enom.com", "hostmaster@nsone.net"}
|
|
125
100
|
|
|
126
101
|
|
|
127
102
|
def test_find_ips():
|
|
128
103
|
ips = utils.find_ips(json.dumps(iris_investigate_data.domaintools()))
|
|
129
|
-
assert ips == {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
104
|
+
assert ips == {
|
|
105
|
+
"142.250.115.26",
|
|
106
|
+
"142.250.141.27",
|
|
107
|
+
"198.51.44.4",
|
|
108
|
+
"198.51.44.68",
|
|
109
|
+
"198.51.45.4",
|
|
110
|
+
"198.51.45.68",
|
|
111
|
+
"199.30.228.112",
|
|
112
|
+
"64.233.171.26",
|
|
113
|
+
"74.125.142.26",
|
|
114
|
+
}
|
|
115
|
+
|
|
138
116
|
|
|
139
117
|
def test_get_pivots():
|
|
140
118
|
pivots = utils.get_pivots(iris_investigate_data.domaintools().get("results"), "")
|
|
141
|
-
assert pivots == [
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
]
|
|
119
|
+
assert pivots == [["IP ADDRESS", ("199.30.228.112", 4)], ["IP ASN", (17318, 111)], ["IP ISP", ("DomainTools LLC", 222)]]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_validate_feeds_parameters_should_raise_error_if_download_api_using_header_auth(test_feeds_params):
|
|
123
|
+
test_feeds_params["output_format"] = "jsonl"
|
|
124
|
+
|
|
125
|
+
with pytest.raises(ValueError) as excinfo:
|
|
126
|
+
utils.validate_feeds_parameters(test_feeds_params)
|
|
127
|
+
|
|
128
|
+
assert str(excinfo.value) == "download API does not support header authentication. Provide api_key in the parameter"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_validate_feeds_parameters_should_raise_error_if_no_required_params(test_feeds_params):
|
|
132
|
+
test_feeds_params.pop("sessionID", None)
|
|
133
|
+
test_feeds_params.pop("after", None)
|
|
134
|
+
test_feeds_params.pop("before", None)
|
|
135
|
+
|
|
136
|
+
with pytest.raises(ValueError) as excinfo:
|
|
137
|
+
utils.validate_feeds_parameters(test_feeds_params)
|
|
138
|
+
|
|
139
|
+
assert str(excinfo.value) == "sessionID or after or before must be provided"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_validate_feeds_parameters_should_raise_error_if_asked_csv_format_for_download_api(test_feeds_params):
|
|
143
|
+
with pytest.raises(ValueError) as excinfo:
|
|
144
|
+
utils.validate_feeds_parameters(test_feeds_params)
|
|
146
145
|
|
|
146
|
+
assert str(excinfo.value) == "csv format is not available in download API."
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.2.1
|
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/__init__.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/__init__.py
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
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/constants.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/cli/main.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/constants.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/exceptions.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/domaintools/filters.py
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
|
{domaintools_api_test_upload-2.2.1 → domaintools_api_test_upload-2.2.2}/tests/test_filters.py
RENAMED
|
File without changes
|