domaintools-api 2.0.0__tar.gz → 2.1.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.0.0/domaintools_api.egg-info → domaintools_api-2.1.0}/PKG-INFO +1 -1
- domaintools_api-2.1.0/VERSION +1 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/_version.py +1 -1
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/api.py +29 -2
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/base_results.py +123 -51
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/api.py +6 -4
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/__init__.py +1 -0
- domaintools_api-2.1.0/domaintools/cli/commands/feeds.py +138 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/constants.py +4 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/utils.py +2 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/utils.py +7 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0/domaintools_api.egg-info}/PKG-INFO +1 -1
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/SOURCES.txt +1 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/setup.py +1 -1
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_api.py +34 -2
- domaintools_api-2.0.0/VERSION +0 -1
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/LICENSE +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/README.md +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/__init__.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/domains.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/iris.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/phisheye.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/main.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/exceptions.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/filters.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/results.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/entry_points.txt +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/requires.txt +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/top_level.txt +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_async/__init__.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/pyproject.toml +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/setup.cfg +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_async.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_cli.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_filters.py +0 -0
- {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_utils.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.0
|
|
@@ -1,7 +1,6 @@
|
|
|
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
|
-
from types import MethodType
|
|
5
4
|
import re
|
|
6
5
|
|
|
7
6
|
from domaintools._version import current as version
|
|
@@ -51,7 +50,7 @@ class API(object):
|
|
|
51
50
|
verify_ssl=True,
|
|
52
51
|
rate_limit=True,
|
|
53
52
|
proxy_url=None,
|
|
54
|
-
always_sign_api_key=
|
|
53
|
+
always_sign_api_key=True,
|
|
55
54
|
key_sign_hash="sha256",
|
|
56
55
|
app_name="python_wrapper",
|
|
57
56
|
app_version=version,
|
|
@@ -1120,3 +1119,31 @@ class API(object):
|
|
|
1120
1119
|
response_path=(),
|
|
1121
1120
|
**kwargs,
|
|
1122
1121
|
)
|
|
1122
|
+
|
|
1123
|
+
def nod(self, **kwargs):
|
|
1124
|
+
"""Returns back list of the newly observed domains feed"""
|
|
1125
|
+
sessionID = kwargs.get("sessionID")
|
|
1126
|
+
after = kwargs.get("after")
|
|
1127
|
+
if not (sessionID or after):
|
|
1128
|
+
raise ValueError("sessionID or after (can be both) must be defined")
|
|
1129
|
+
|
|
1130
|
+
return self._results(
|
|
1131
|
+
"newly-observed-domains-feed-(api)",
|
|
1132
|
+
"v1/feed/nod/",
|
|
1133
|
+
response_path=(),
|
|
1134
|
+
**kwargs,
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
def nad(self, **kwargs):
|
|
1138
|
+
"""Returns back list of the newly active domains feed"""
|
|
1139
|
+
sessionID = kwargs.get("sessionID")
|
|
1140
|
+
after = kwargs.get("after")
|
|
1141
|
+
if not (sessionID or after):
|
|
1142
|
+
raise ValueError("sessionID or after (can be both) must be defined")
|
|
1143
|
+
|
|
1144
|
+
return self._results(
|
|
1145
|
+
"newly-active-domains-feed-(api)",
|
|
1146
|
+
"v1/feed/nad/",
|
|
1147
|
+
response_path=(),
|
|
1148
|
+
**kwargs,
|
|
1149
|
+
)
|
|
@@ -1,27 +1,46 @@
|
|
|
1
1
|
"""Defines the base result object - which specifies how DomainTools API endpoints will be interacted with"""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
import time
|
|
6
6
|
import logging
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
|
|
9
|
-
from domaintools.exceptions import (
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
from domaintools.exceptions import (
|
|
10
|
+
BadRequestException,
|
|
11
|
+
InternalServerErrorException,
|
|
12
|
+
NotAuthorizedException,
|
|
13
|
+
NotFoundException,
|
|
14
|
+
ServiceException,
|
|
15
|
+
ServiceUnavailableException,
|
|
16
|
+
IncompleteResponseException,
|
|
17
|
+
RequestUriTooLongException,
|
|
18
|
+
)
|
|
19
|
+
from domaintools.utils import get_feeds_products_list
|
|
20
|
+
|
|
12
21
|
from httpx import Client
|
|
13
22
|
|
|
14
|
-
try:
|
|
23
|
+
try: # pragma: no cover
|
|
15
24
|
from collections.abc import MutableMapping, MutableSequence
|
|
16
|
-
except ImportError:
|
|
25
|
+
except ImportError: # pragma: no cover
|
|
17
26
|
from collections import MutableMapping, MutableSequence
|
|
18
27
|
|
|
19
28
|
log = logging.getLogger(__name__)
|
|
20
29
|
|
|
30
|
+
|
|
21
31
|
class Results(MutableMapping, MutableSequence):
|
|
22
32
|
"""The base (abstract) DomainTools result definition"""
|
|
23
33
|
|
|
24
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api,
|
|
37
|
+
product,
|
|
38
|
+
url,
|
|
39
|
+
items_path=(),
|
|
40
|
+
response_path=("response",),
|
|
41
|
+
proxy_url=None,
|
|
42
|
+
**kwargs,
|
|
43
|
+
):
|
|
25
44
|
self.api = api
|
|
26
45
|
self.product = product
|
|
27
46
|
self.url = url
|
|
@@ -41,51 +60,67 @@ class Results(MutableMapping, MutableSequence):
|
|
|
41
60
|
|
|
42
61
|
now = datetime.now()
|
|
43
62
|
limit = self.api.limits[self.product]
|
|
44
|
-
if
|
|
45
|
-
limit[
|
|
63
|
+
if "last_scheduled" not in limit:
|
|
64
|
+
limit["last_scheduled"] = now
|
|
46
65
|
return None
|
|
47
66
|
|
|
48
|
-
safe_after = limit[
|
|
67
|
+
safe_after = limit["last_scheduled"] + limit["interval"]
|
|
49
68
|
wait_for = 0
|
|
50
69
|
if now < safe_after:
|
|
51
70
|
wait_for = safe_after - now
|
|
52
|
-
wait_for = float(wait_for.seconds) + (
|
|
53
|
-
|
|
71
|
+
wait_for = float(wait_for.seconds) + (
|
|
72
|
+
float(wait_for.microseconds) / 1000000.0
|
|
73
|
+
)
|
|
74
|
+
limit["last_scheduled"] = safe_after
|
|
54
75
|
else:
|
|
55
|
-
limit[
|
|
76
|
+
limit["last_scheduled"] = now
|
|
56
77
|
|
|
57
78
|
return wait_for
|
|
58
79
|
|
|
59
80
|
def _make_request(self):
|
|
60
81
|
|
|
61
|
-
with Client(
|
|
62
|
-
|
|
82
|
+
with Client(
|
|
83
|
+
verify=self.api.verify_ssl, proxies=self.api.proxy_url, timeout=None
|
|
84
|
+
) as session:
|
|
85
|
+
if self.product in [
|
|
86
|
+
"iris-investigate",
|
|
87
|
+
"iris-enrich",
|
|
88
|
+
"iris-detect-escalate-domains",
|
|
89
|
+
]:
|
|
63
90
|
post_data = self.kwargs.copy()
|
|
64
91
|
post_data.update(self.api.extra_request_params)
|
|
65
92
|
return session.post(url=self.url, data=post_data)
|
|
66
|
-
elif self.product in [
|
|
93
|
+
elif self.product in ["iris-detect-manage-watchlist-domains"]:
|
|
67
94
|
patch_data = self.kwargs.copy()
|
|
68
95
|
patch_data.update(self.api.extra_request_params)
|
|
69
96
|
return session.patch(url=self.url, json=patch_data)
|
|
70
97
|
else:
|
|
71
|
-
return session.get(
|
|
98
|
+
return session.get(
|
|
99
|
+
url=self.url, params=self.kwargs, **self.api.extra_request_params
|
|
100
|
+
)
|
|
72
101
|
|
|
73
102
|
def _get_results(self):
|
|
74
103
|
wait_for = self._wait_time()
|
|
75
|
-
if self.api.rate_limit and (
|
|
104
|
+
if self.api.rate_limit and (
|
|
105
|
+
wait_for is None or self.product == "account-information"
|
|
106
|
+
):
|
|
76
107
|
data = self._make_request()
|
|
77
|
-
if data.status_code == 503:
|
|
108
|
+
if data.status_code == 503: # pragma: no cover
|
|
78
109
|
sleeptime = 60
|
|
79
|
-
log.info(
|
|
80
|
-
|
|
110
|
+
log.info(
|
|
111
|
+
"503 encountered for [%s] - sleeping [%s] seconds before retrying request.",
|
|
112
|
+
self.product,
|
|
113
|
+
sleeptime,
|
|
114
|
+
)
|
|
81
115
|
time.sleep(sleeptime)
|
|
82
116
|
self._wait_time()
|
|
83
117
|
data = self._make_request()
|
|
84
118
|
return data
|
|
85
119
|
|
|
86
120
|
if wait_for > 0:
|
|
87
|
-
log.info(
|
|
88
|
-
|
|
121
|
+
log.info(
|
|
122
|
+
"Sleeping for [%s] prior to requesting [%s].", wait_for, self.product
|
|
123
|
+
)
|
|
89
124
|
time.sleep(wait_for)
|
|
90
125
|
return self._make_request()
|
|
91
126
|
|
|
@@ -93,7 +128,11 @@ class Results(MutableMapping, MutableSequence):
|
|
|
93
128
|
if self._data is None:
|
|
94
129
|
results = self._get_results()
|
|
95
130
|
self.setStatus(results.status_code, results)
|
|
96
|
-
if
|
|
131
|
+
if (
|
|
132
|
+
self.kwargs.get("format", "json") == "json"
|
|
133
|
+
and self.product
|
|
134
|
+
not in get_feeds_products_list() # Special handling of feeds products' data to preserve the result in jsonline format
|
|
135
|
+
):
|
|
97
136
|
self._data = results.json()
|
|
98
137
|
else:
|
|
99
138
|
self._data = results.text
|
|
@@ -104,16 +143,20 @@ class Results(MutableMapping, MutableSequence):
|
|
|
104
143
|
self._limit_exceeded_message = message
|
|
105
144
|
|
|
106
145
|
if self._limit_exceeded is True:
|
|
107
|
-
raise ServiceException(
|
|
146
|
+
raise ServiceException(
|
|
147
|
+
503, "Limit Exceeded{}".format(self._limit_exceeded_message)
|
|
148
|
+
)
|
|
108
149
|
else:
|
|
109
150
|
return self._data
|
|
110
151
|
|
|
111
152
|
def check_limit_exceeded(self):
|
|
112
|
-
if self.kwargs.get(
|
|
113
|
-
if (
|
|
114
|
-
"
|
|
115
|
-
self._data[
|
|
116
|
-
|
|
153
|
+
if self.kwargs.get("format", "json") == "json":
|
|
154
|
+
if (
|
|
155
|
+
"response" in self._data
|
|
156
|
+
and "limit_exceeded" in self._data["response"]
|
|
157
|
+
and self._data["response"]["limit_exceeded"] is True
|
|
158
|
+
):
|
|
159
|
+
return True, self._data["response"]["message"]
|
|
117
160
|
# TODO: handle html, xml response errors better.
|
|
118
161
|
elif "response" in self._data and "limit_exceeded" in self._data:
|
|
119
162
|
return True, "limit exceeded"
|
|
@@ -121,7 +164,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
121
164
|
|
|
122
165
|
@property
|
|
123
166
|
def status(self):
|
|
124
|
-
if not getattr(self,
|
|
167
|
+
if not getattr(self, "_status", None):
|
|
125
168
|
self._status = self._get_results().status_code
|
|
126
169
|
|
|
127
170
|
return self._status
|
|
@@ -135,7 +178,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
135
178
|
if response is not None:
|
|
136
179
|
try:
|
|
137
180
|
reason = response.json()
|
|
138
|
-
except Exception:
|
|
181
|
+
except Exception: # pragma: no cover
|
|
139
182
|
reason = response.text
|
|
140
183
|
if callable(reason):
|
|
141
184
|
reason = reason()
|
|
@@ -146,16 +189,16 @@ class Results(MutableMapping, MutableSequence):
|
|
|
146
189
|
raise NotAuthorizedException(code, reason)
|
|
147
190
|
elif code == 404:
|
|
148
191
|
raise NotFoundException(code, reason)
|
|
149
|
-
elif code == 500:
|
|
192
|
+
elif code == 500: # pragma: no cover
|
|
150
193
|
raise InternalServerErrorException(code, reason)
|
|
151
|
-
elif code == 503:
|
|
194
|
+
elif code == 503: # pragma: no cover
|
|
152
195
|
raise ServiceUnavailableException(code, reason)
|
|
153
|
-
elif code == 206:
|
|
196
|
+
elif code == 206: # pragma: no cover
|
|
154
197
|
raise IncompleteResponseException(code, reason)
|
|
155
|
-
elif code == 414:
|
|
198
|
+
elif code == 414: # pragma: no cover
|
|
156
199
|
raise RequestUriTooLongException(code, reason)
|
|
157
|
-
else:
|
|
158
|
-
raise ServiceException(code,
|
|
200
|
+
else: # pragma: no cover
|
|
201
|
+
raise ServiceException(code, "Unknown Exception")
|
|
159
202
|
|
|
160
203
|
def response(self):
|
|
161
204
|
if self._response is None:
|
|
@@ -171,7 +214,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
171
214
|
|
|
172
215
|
def emails(self):
|
|
173
216
|
"""Find and returns all emails mentioned in the response"""
|
|
174
|
-
return set(re.findall(r
|
|
217
|
+
return set(re.findall(r"[\w\.-]+@[\w\.-]+", str(self.response())))
|
|
175
218
|
|
|
176
219
|
def _items(self):
|
|
177
220
|
if self._items_list is None:
|
|
@@ -221,25 +264,54 @@ class Results(MutableMapping, MutableSequence):
|
|
|
221
264
|
|
|
222
265
|
@property
|
|
223
266
|
def json(self):
|
|
224
|
-
self.kwargs.pop(
|
|
225
|
-
return self.__class__(
|
|
226
|
-
|
|
267
|
+
self.kwargs.pop("format", None)
|
|
268
|
+
return self.__class__(
|
|
269
|
+
format="json",
|
|
270
|
+
product=self.product,
|
|
271
|
+
url=self.url,
|
|
272
|
+
items_path=self.items_path,
|
|
273
|
+
response_path=self.response_path,
|
|
274
|
+
api=self.api,
|
|
275
|
+
**self.kwargs,
|
|
276
|
+
)
|
|
227
277
|
|
|
228
278
|
@property
|
|
229
279
|
def xml(self):
|
|
230
|
-
self.kwargs.pop(
|
|
231
|
-
return self.__class__(
|
|
232
|
-
|
|
280
|
+
self.kwargs.pop("format", None)
|
|
281
|
+
return self.__class__(
|
|
282
|
+
format="xml",
|
|
283
|
+
product=self.product,
|
|
284
|
+
url=self.url,
|
|
285
|
+
items_path=self.items_path,
|
|
286
|
+
response_path=self.response_path,
|
|
287
|
+
api=self.api,
|
|
288
|
+
**self.kwargs,
|
|
289
|
+
)
|
|
233
290
|
|
|
234
291
|
@property
|
|
235
292
|
def html(self):
|
|
236
|
-
self.kwargs.pop(
|
|
237
|
-
return self.__class__(
|
|
238
|
-
|
|
293
|
+
self.kwargs.pop("format", None)
|
|
294
|
+
return self.__class__(
|
|
295
|
+
api=self.api,
|
|
296
|
+
product=self.product,
|
|
297
|
+
url=self.url,
|
|
298
|
+
items_path=self.items_path,
|
|
299
|
+
response_path=self.response_path,
|
|
300
|
+
format="html",
|
|
301
|
+
**self.kwargs,
|
|
302
|
+
)
|
|
239
303
|
|
|
240
304
|
def as_list(self):
|
|
241
|
-
return
|
|
305
|
+
return "\n".join(
|
|
306
|
+
[
|
|
307
|
+
json.dumps(item, indent=4, separators=(",", ": "))
|
|
308
|
+
for item in self._items()
|
|
309
|
+
]
|
|
310
|
+
)
|
|
242
311
|
|
|
243
312
|
def __str__(self):
|
|
244
|
-
return str(
|
|
245
|
-
|
|
313
|
+
return str(
|
|
314
|
+
json.dumps(self.data(), indent=4, separators=(",", ": "))
|
|
315
|
+
if self.kwargs.get("format", "json") == "json"
|
|
316
|
+
else self.data()
|
|
317
|
+
)
|
|
@@ -148,10 +148,11 @@ class DTCLICommand:
|
|
|
148
148
|
kwargs (Optional[Dict], optional): The command available kwargs to pass in domaintools API
|
|
149
149
|
"""
|
|
150
150
|
try:
|
|
151
|
-
rate_limit = params.pop("rate_limit"
|
|
152
|
-
response_format = params.pop("format"
|
|
153
|
-
out_file = params.pop("out_file"
|
|
154
|
-
verify_ssl = params.pop("no_verify_ssl"
|
|
151
|
+
rate_limit = params.pop("rate_limit", False)
|
|
152
|
+
response_format = params.pop("format", "json")
|
|
153
|
+
out_file = params.pop("out_file", sys.stdout)
|
|
154
|
+
verify_ssl = params.pop("no_verify_ssl", False)
|
|
155
|
+
always_sign_api_key = params.pop("no_sign_api_key", False)
|
|
155
156
|
source = None
|
|
156
157
|
|
|
157
158
|
if "src_file" in params:
|
|
@@ -185,6 +186,7 @@ class DTCLICommand:
|
|
|
185
186
|
app_name=cls.APP_PARTNER_NAME,
|
|
186
187
|
verify_ssl=verify_ssl,
|
|
187
188
|
rate_limit=rate_limit,
|
|
189
|
+
always_sign_api_key=always_sign_api_key,
|
|
188
190
|
)
|
|
189
191
|
dt_api_func = getattr(dt_api, name)
|
|
190
192
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from domaintools.cli.main import dt_cli
|
|
6
|
+
from domaintools.cli.api import DTCLICommand
|
|
7
|
+
from domaintools.cli.utils import get_cli_helptext_by_name
|
|
8
|
+
from domaintools.cli import constants as c
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dt_cli.command(
|
|
12
|
+
name=c.FEEDS_NAD,
|
|
13
|
+
help=get_cli_helptext_by_name(command_name=c.FEEDS_NAD),
|
|
14
|
+
)
|
|
15
|
+
def feeds_nad(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
|
|
18
|
+
key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
|
|
19
|
+
creds_file: str = typer.Option(
|
|
20
|
+
"~/.dtapi",
|
|
21
|
+
"-c",
|
|
22
|
+
"--credfile",
|
|
23
|
+
help="Optional file with API username and API key, one per line.",
|
|
24
|
+
),
|
|
25
|
+
rate_limit: bool = typer.Option(
|
|
26
|
+
False,
|
|
27
|
+
"-l",
|
|
28
|
+
"--rate-limit",
|
|
29
|
+
help="Rate limit API calls against the API based on per minute limits.",
|
|
30
|
+
),
|
|
31
|
+
format: str = typer.Option(
|
|
32
|
+
"json",
|
|
33
|
+
"-f",
|
|
34
|
+
"--format",
|
|
35
|
+
help="Output format in {'list', 'json', 'xml', 'html'}",
|
|
36
|
+
callback=DTCLICommand.validate_format_input,
|
|
37
|
+
),
|
|
38
|
+
out_file: typer.FileTextWrite = typer.Option(
|
|
39
|
+
sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
|
|
40
|
+
),
|
|
41
|
+
no_verify_ssl: bool = typer.Option(
|
|
42
|
+
False,
|
|
43
|
+
"--no-verify-ssl",
|
|
44
|
+
help="Skip verification of SSL certificate when making HTTPs API calls",
|
|
45
|
+
),
|
|
46
|
+
no_sign_api_key: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--no-sign-api-key",
|
|
49
|
+
help="Skip signing of api key",
|
|
50
|
+
),
|
|
51
|
+
sessionID: str = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--session-id",
|
|
54
|
+
help="Unique identifier for the session",
|
|
55
|
+
),
|
|
56
|
+
after: str = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--after",
|
|
59
|
+
help="Start of the time window, relative to the current time in seconds, for which data will be provided",
|
|
60
|
+
),
|
|
61
|
+
domain: str = typer.Option(
|
|
62
|
+
None,
|
|
63
|
+
"-d",
|
|
64
|
+
"--domain",
|
|
65
|
+
help="A string value used to filter feed results",
|
|
66
|
+
),
|
|
67
|
+
top: str = typer.Option(
|
|
68
|
+
None,
|
|
69
|
+
"--top",
|
|
70
|
+
help="Number of results to return in the response payload",
|
|
71
|
+
),
|
|
72
|
+
):
|
|
73
|
+
DTCLICommand.run(name=c.FEEDS_NAD, params=ctx.params)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dt_cli.command(
|
|
77
|
+
name=c.FEEDS_NOD,
|
|
78
|
+
help=get_cli_helptext_by_name(command_name=c.FEEDS_NOD),
|
|
79
|
+
)
|
|
80
|
+
def feeds_nod(
|
|
81
|
+
ctx: typer.Context,
|
|
82
|
+
user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
|
|
83
|
+
key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
|
|
84
|
+
creds_file: str = typer.Option(
|
|
85
|
+
"~/.dtapi",
|
|
86
|
+
"-c",
|
|
87
|
+
"--credfile",
|
|
88
|
+
help="Optional file with API username and API key, one per line.",
|
|
89
|
+
),
|
|
90
|
+
rate_limit: bool = typer.Option(
|
|
91
|
+
False,
|
|
92
|
+
"-l",
|
|
93
|
+
"--rate-limit",
|
|
94
|
+
help="Rate limit API calls against the API based on per minute limits.",
|
|
95
|
+
),
|
|
96
|
+
format: str = typer.Option(
|
|
97
|
+
"json",
|
|
98
|
+
"-f",
|
|
99
|
+
"--format",
|
|
100
|
+
help="Output format in {'list', 'json', 'xml', 'html'}",
|
|
101
|
+
callback=DTCLICommand.validate_format_input,
|
|
102
|
+
),
|
|
103
|
+
out_file: typer.FileTextWrite = typer.Option(
|
|
104
|
+
sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
|
|
105
|
+
),
|
|
106
|
+
no_verify_ssl: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--no-verify-ssl",
|
|
109
|
+
help="Skip verification of SSL certificate when making HTTPs API calls",
|
|
110
|
+
),
|
|
111
|
+
no_sign_api_key: bool = typer.Option(
|
|
112
|
+
False,
|
|
113
|
+
"--no-sign-api-key",
|
|
114
|
+
help="Skip signing of api key",
|
|
115
|
+
),
|
|
116
|
+
sessionID: str = typer.Option(
|
|
117
|
+
None,
|
|
118
|
+
"--session-id",
|
|
119
|
+
help="Unique identifier for the session",
|
|
120
|
+
),
|
|
121
|
+
after: str = typer.Option(
|
|
122
|
+
None,
|
|
123
|
+
"--after",
|
|
124
|
+
help="Start of the time window, relative to the current time in seconds, for which data will be provided",
|
|
125
|
+
),
|
|
126
|
+
domain: str = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
"-d",
|
|
129
|
+
"--domain",
|
|
130
|
+
help="A string value used to filter feed results",
|
|
131
|
+
),
|
|
132
|
+
top: str = typer.Option(
|
|
133
|
+
None,
|
|
134
|
+
"--top",
|
|
135
|
+
help="Number of results to return in the response payload",
|
|
136
|
+
),
|
|
137
|
+
):
|
|
138
|
+
DTCLICommand.run(name=c.FEEDS_NOD, params=ctx.params)
|
|
@@ -81,6 +81,8 @@ _command_help_mapper = {
|
|
|
81
81
|
c.IRIS_DETECT_MANAGE_WATCHLIST_DOMAINS: "Changes the watch state of a list of domains by their Iris Detect domain ID.",
|
|
82
82
|
c.IRIS_DETECT_ESCALATE_DOMAINS: "Changes the escalation type of a list of domains by their Iris Detect domain ID.",
|
|
83
83
|
c.IRIS_DETECT_IGNORED_DOMAINS: "Returns back a list of ignored domains in Iris Detect based on the provided filters.",
|
|
84
|
+
c.FEEDS_NAD: "Returns back newly active domains feed.",
|
|
85
|
+
c.FEEDS_NOD: "Returns back newly observed domains feed.",
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
|
|
@@ -179,3 +179,10 @@ def convert_str_to_dateobj(
|
|
|
179
179
|
string_date: str, date_format: Optional[str] = "%Y-%m-%d"
|
|
180
180
|
) -> datetime:
|
|
181
181
|
return datetime.strptime(string_date, date_format)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_feeds_products_list():
|
|
185
|
+
return [
|
|
186
|
+
"newly-active-domains-feed-(api)",
|
|
187
|
+
"newly-observed-domains-feed-(api)",
|
|
188
|
+
]
|
|
@@ -21,6 +21,7 @@ domaintools/cli/commands/__init__.py
|
|
|
21
21
|
domaintools/cli/commands/accounts.py
|
|
22
22
|
domaintools/cli/commands/detects.py
|
|
23
23
|
domaintools/cli/commands/domains.py
|
|
24
|
+
domaintools/cli/commands/feeds.py
|
|
24
25
|
domaintools/cli/commands/ips.py
|
|
25
26
|
domaintools/cli/commands/iris.py
|
|
26
27
|
domaintools/cli/commands/phisheye.py
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Tests the Python interface for DomainTools APIs"""
|
|
2
2
|
|
|
3
3
|
from os import environ
|
|
4
|
-
from datetime import datetime
|
|
5
4
|
|
|
5
|
+
import json
|
|
6
6
|
import pytest
|
|
7
|
-
from domaintools import API, exceptions
|
|
8
7
|
|
|
8
|
+
from domaintools import API, exceptions
|
|
9
9
|
from tests.settings import api, vcr
|
|
10
10
|
|
|
11
11
|
|
|
@@ -513,3 +513,35 @@ def test_limit_exceeded():
|
|
|
513
513
|
with pytest.raises(exceptions.ServiceException):
|
|
514
514
|
response = api.iris_investigate(ip="8.8.8.8")
|
|
515
515
|
response.response()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@vcr.use_cassette
|
|
519
|
+
def test_newly_observed_domains_feed():
|
|
520
|
+
results = api.nod(after="-60")
|
|
521
|
+
response = results.response()
|
|
522
|
+
rows = response.strip().split("\n")
|
|
523
|
+
|
|
524
|
+
assert response is not None
|
|
525
|
+
assert results.status == 200
|
|
526
|
+
assert len(rows) >= 1
|
|
527
|
+
|
|
528
|
+
for row in rows:
|
|
529
|
+
feed_result = json.loads(row)
|
|
530
|
+
assert "timestamp" in feed_result.keys()
|
|
531
|
+
assert "domain" in feed_result.keys()
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@vcr.use_cassette
|
|
535
|
+
def test_newly_active_domains_feed():
|
|
536
|
+
results = api.nad(after="-60")
|
|
537
|
+
response = results.response()
|
|
538
|
+
rows = response.strip().split("\n")
|
|
539
|
+
|
|
540
|
+
assert response is not None
|
|
541
|
+
assert results.status == 200
|
|
542
|
+
assert len(rows) >= 1
|
|
543
|
+
|
|
544
|
+
for row in rows:
|
|
545
|
+
feed_result = json.loads(row)
|
|
546
|
+
assert "timestamp" in feed_result.keys()
|
|
547
|
+
assert "domain" in feed_result.keys()
|
domaintools_api-2.0.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.0.0
|
|
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.0.0 → domaintools_api-2.1.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
|