Skip to content

Commit ec0a211

Browse files
authored
Merge pull request #136 from DomainTools/IDEV-1877-1886-add-nod-and-nad-implemention
IDEV-1877 and IDEV-1886: Add nod and nad implemention
2 parents 7842374 + 4534947 commit ec0a211

File tree

14 files changed

+20501
-67
lines changed

14 files changed

+20501
-67
lines changed

domaintools/api.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from datetime import datetime, timedelta, timezone
22
from hashlib import sha1, sha256, md5
33
from hmac import new as hmac
4-
from types import MethodType
54
import re
65

76
from domaintools._version import current as version
@@ -1120,3 +1119,23 @@ def iris_detect_ignored_domains(
11201119
response_path=(),
11211120
**kwargs,
11221121
)
1122+
1123+
def newly_observed_domains_feed(self, **kwargs):
1124+
"""Returns back list of the newly observed domains feed"""
1125+
return self._results(
1126+
"newly-observed-domains-feed-(api)",
1127+
"v1/feed/nod/",
1128+
response_path=(),
1129+
after="-60",
1130+
**kwargs,
1131+
)
1132+
1133+
def newly_active_domains_feed(self, **kwargs):
1134+
"""Returns back list of the newly active domains feed"""
1135+
return self._results(
1136+
"newly-active-domains-feed-(api)",
1137+
"v1/feed/nad/",
1138+
response_path=(),
1139+
after="-60",
1140+
**kwargs,
1141+
)

domaintools/base_results.py

Lines changed: 123 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,46 @@
11
"""Defines the base result object - which specifies how DomainTools API endpoints will be interacted with"""
2-
import collections
2+
33
import json
44
import re
55
import time
66
import logging
77
from datetime import datetime
88

9-
from domaintools.exceptions import (BadRequestException, InternalServerErrorException, NotAuthorizedException,
10-
NotFoundException, ServiceException, ServiceUnavailableException,
11-
IncompleteResponseException, RequestUriTooLongException)
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+
1221
from httpx import Client
1322

14-
try: # pragma: no cover
23+
try: # pragma: no cover
1524
from collections.abc import MutableMapping, MutableSequence
16-
except ImportError: # pragma: no cover
25+
except ImportError: # pragma: no cover
1726
from collections import MutableMapping, MutableSequence
1827

1928
log = logging.getLogger(__name__)
2029

30+
2131
class Results(MutableMapping, MutableSequence):
2232
"""The base (abstract) DomainTools result definition"""
2333

24-
def __init__(self, api, product, url, items_path=(), response_path=('response', ), proxy_url=None, **kwargs):
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+
):
2544
self.api = api
2645
self.product = product
2746
self.url = url
@@ -41,59 +60,79 @@ def _wait_time(self):
4160

4261
now = datetime.now()
4362
limit = self.api.limits[self.product]
44-
if 'last_scheduled' not in limit:
45-
limit['last_scheduled'] = now
63+
if "last_scheduled" not in limit:
64+
limit["last_scheduled"] = now
4665
return None
4766

48-
safe_after = limit['last_scheduled'] + limit['interval']
67+
safe_after = limit["last_scheduled"] + limit["interval"]
4968
wait_for = 0
5069
if now < safe_after:
5170
wait_for = safe_after - now
52-
wait_for = float(wait_for.seconds) + (float(wait_for.microseconds) / 1000000.0)
53-
limit['last_scheduled'] = safe_after
71+
wait_for = float(wait_for.seconds) + (
72+
float(wait_for.microseconds) / 1000000.0
73+
)
74+
limit["last_scheduled"] = safe_after
5475
else:
55-
limit['last_scheduled'] = now
76+
limit["last_scheduled"] = now
5677

5778
return wait_for
5879

5980
def _make_request(self):
6081

61-
with Client(verify=self.api.verify_ssl, proxies=self.api.proxy_url, timeout=None) as session:
62-
if self.product in ['iris-investigate', 'iris-enrich', 'iris-detect-escalate-domains']:
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+
]:
6390
post_data = self.kwargs.copy()
6491
post_data.update(self.api.extra_request_params)
6592
return session.post(url=self.url, data=post_data)
66-
elif self.product in ['iris-detect-manage-watchlist-domains']:
93+
elif self.product in ["iris-detect-manage-watchlist-domains"]:
6794
patch_data = self.kwargs.copy()
6895
patch_data.update(self.api.extra_request_params)
6996
return session.patch(url=self.url, json=patch_data)
7097
else:
71-
return session.get(url=self.url, params=self.kwargs, **self.api.extra_request_params)
98+
return session.get(
99+
url=self.url, params=self.kwargs, **self.api.extra_request_params
100+
)
72101

73102
def _get_results(self):
74103
wait_for = self._wait_time()
75-
if self.api.rate_limit and (wait_for is None or self.product == 'account-information'):
104+
if self.api.rate_limit and (
105+
wait_for is None or self.product == "account-information"
106+
):
76107
data = self._make_request()
77-
if data.status_code == 503: # pragma: no cover
108+
if data.status_code == 503: # pragma: no cover
78109
sleeptime = 60
79-
log.info('503 encountered for [%s] - sleeping [%s] seconds before retrying request.',
80-
self.product, sleeptime)
110+
log.info(
111+
"503 encountered for [%s] - sleeping [%s] seconds before retrying request.",
112+
self.product,
113+
sleeptime,
114+
)
81115
time.sleep(sleeptime)
82116
self._wait_time()
83117
data = self._make_request()
84118
return data
85119

86120
if wait_for > 0:
87-
log.info('Sleeping for [%s] prior to requesting [%s].',
88-
wait_for, self.product)
121+
log.info(
122+
"Sleeping for [%s] prior to requesting [%s].", wait_for, self.product
123+
)
89124
time.sleep(wait_for)
90125
return self._make_request()
91126

92127
def data(self):
93128
if self._data is None:
94129
results = self._get_results()
95130
self.setStatus(results.status_code, results)
96-
if self.kwargs.get('format', 'json') == 'json':
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+
):
97136
self._data = results.json()
98137
else:
99138
self._data = results.text
@@ -104,24 +143,28 @@ def data(self):
104143
self._limit_exceeded_message = message
105144

106145
if self._limit_exceeded is True:
107-
raise ServiceException(503, "Limit Exceeded{}".format(self._limit_exceeded_message))
146+
raise ServiceException(
147+
503, "Limit Exceeded{}".format(self._limit_exceeded_message)
148+
)
108149
else:
109150
return self._data
110151

111152
def check_limit_exceeded(self):
112-
if self.kwargs.get('format', 'json') == 'json':
113-
if ("response" in self._data and
114-
"limit_exceeded" in self._data['response'] and
115-
self._data['response']['limit_exceeded'] is True):
116-
return True, self._data['response']['message']
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"]
117160
# TODO: handle html, xml response errors better.
118161
elif "response" in self._data and "limit_exceeded" in self._data:
119162
return True, "limit exceeded"
120163
return False, ""
121164

122165
@property
123166
def status(self):
124-
if not getattr(self, '_status', None):
167+
if not getattr(self, "_status", None):
125168
self._status = self._get_results().status_code
126169

127170
return self._status
@@ -135,7 +178,7 @@ def setStatus(self, code, response=None):
135178
if response is not None:
136179
try:
137180
reason = response.json()
138-
except Exception: # pragma: no cover
181+
except Exception: # pragma: no cover
139182
reason = response.text
140183
if callable(reason):
141184
reason = reason()
@@ -146,16 +189,16 @@ def setStatus(self, code, response=None):
146189
raise NotAuthorizedException(code, reason)
147190
elif code == 404:
148191
raise NotFoundException(code, reason)
149-
elif code == 500: # pragma: no cover
192+
elif code == 500: # pragma: no cover
150193
raise InternalServerErrorException(code, reason)
151-
elif code == 503: # pragma: no cover
194+
elif code == 503: # pragma: no cover
152195
raise ServiceUnavailableException(code, reason)
153-
elif code == 206: # pragma: no cover
196+
elif code == 206: # pragma: no cover
154197
raise IncompleteResponseException(code, reason)
155-
elif code == 414: # pragma: no cover
198+
elif code == 414: # pragma: no cover
156199
raise RequestUriTooLongException(code, reason)
157-
else: # pragma: no cover
158-
raise ServiceException(code, 'Unknown Exception')
200+
else: # pragma: no cover
201+
raise ServiceException(code, "Unknown Exception")
159202

160203
def response(self):
161204
if self._response is None:
@@ -171,7 +214,7 @@ def items(self):
171214

172215
def emails(self):
173216
"""Find and returns all emails mentioned in the response"""
174-
return set(re.findall(r'[\w\.-]+@[\w\.-]+', str(self.response())))
217+
return set(re.findall(r"[\w\.-]+@[\w\.-]+", str(self.response())))
175218

176219
def _items(self):
177220
if self._items_list is None:
@@ -221,25 +264,54 @@ def __exit__(self, *args):
221264

222265
@property
223266
def json(self):
224-
self.kwargs.pop('format', None)
225-
return self.__class__(format='json', product=self.product, url=self.url, items_path=self.items_path,
226-
response_path=self.response_path, api=self.api, **self.kwargs)
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+
)
227277

228278
@property
229279
def xml(self):
230-
self.kwargs.pop('format', None)
231-
return self.__class__(format='xml', product=self.product, url=self.url, items_path=self.items_path,
232-
response_path=self.response_path, api=self.api, **self.kwargs)
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+
)
233290

234291
@property
235292
def html(self):
236-
self.kwargs.pop('format', None)
237-
return self.__class__(api=self.api, product=self.product, url=self.url, items_path=self.items_path,
238-
response_path=self.response_path, format='html', **self.kwargs)
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+
)
239303

240304
def as_list(self):
241-
return '\n'.join([json.dumps(item, indent=4, separators=(',', ': ')) for item in self._items()])
305+
return "\n".join(
306+
[
307+
json.dumps(item, indent=4, separators=(",", ": "))
308+
for item in self._items()
309+
]
310+
)
242311

243312
def __str__(self):
244-
return str(json.dumps(self.data(), indent=4,
245-
separators=(',', ': ')) if self.kwargs.get('format', 'json') == 'json' else self.data())
313+
return str(
314+
json.dumps(self.data(), indent=4, separators=(",", ": "))
315+
if self.kwargs.get("format", "json") == "json"
316+
else self.data()
317+
)

domaintools/cli/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,11 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
148148
kwargs (Optional[Dict], optional): The command available kwargs to pass in domaintools API
149149
"""
150150
try:
151-
rate_limit = params.pop("rate_limit") or False
152-
response_format = params.pop("format") or "json"
153-
out_file = params.pop("out_file") or sys.stdout
154-
verify_ssl = params.pop("no_verify_ssl") or False
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)
155156
source = None
156157

157158
if "src_file" in params:
@@ -185,6 +186,7 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
185186
app_name=cls.APP_PARTNER_NAME,
186187
verify_ssl=verify_ssl,
187188
rate_limit=rate_limit,
189+
always_sign_api_key=always_sign_api_key,
188190
)
189191
dt_api_func = getattr(dt_api, name)
190192

domaintools/cli/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .ips import *
55
from .phisheye import *
66
from .detects import *
7+
from .feeds import *

0 commit comments

Comments
 (0)