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.
Files changed (41) hide show
  1. {domaintools_api-2.0.0/domaintools_api.egg-info → domaintools_api-2.1.0}/PKG-INFO +1 -1
  2. domaintools_api-2.1.0/VERSION +1 -0
  3. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/_version.py +1 -1
  4. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/api.py +29 -2
  5. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/base_results.py +123 -51
  6. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/api.py +6 -4
  7. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/__init__.py +1 -0
  8. domaintools_api-2.1.0/domaintools/cli/commands/feeds.py +138 -0
  9. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/constants.py +4 -0
  10. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/utils.py +2 -0
  11. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/utils.py +7 -0
  12. {domaintools_api-2.0.0 → domaintools_api-2.1.0/domaintools_api.egg-info}/PKG-INFO +1 -1
  13. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/SOURCES.txt +1 -0
  14. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/setup.py +1 -1
  15. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_api.py +34 -2
  16. domaintools_api-2.0.0/VERSION +0 -1
  17. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/LICENSE +0 -0
  18. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/README.md +0 -0
  19. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/__init__.py +0 -0
  20. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/__init__.py +0 -0
  21. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/accounts.py +0 -0
  22. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/detects.py +0 -0
  23. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/domains.py +0 -0
  24. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/ips.py +0 -0
  25. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/iris.py +0 -0
  26. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/commands/phisheye.py +0 -0
  27. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/cli/main.py +0 -0
  28. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/exceptions.py +0 -0
  29. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/filters.py +0 -0
  30. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools/results.py +0 -0
  31. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
  32. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/entry_points.txt +0 -0
  33. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/requires.txt +0 -0
  34. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_api.egg-info/top_level.txt +0 -0
  35. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/domaintools_async/__init__.py +0 -0
  36. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/pyproject.toml +0 -0
  37. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/setup.cfg +0 -0
  38. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_async.py +0 -0
  39. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_cli.py +0 -0
  40. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_filters.py +0 -0
  41. {domaintools_api-2.0.0 → domaintools_api-2.1.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: domaintools_api
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -0,0 +1 @@
1
+ 2.1.0
@@ -20,4 +20,4 @@ OTHER DEALINGS IN THE SOFTWARE.
20
20
 
21
21
  """
22
22
 
23
- current = "2.0.0"
23
+ current = "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=False,
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
- import collections
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 (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
+
12
21
  from httpx import Client
13
22
 
14
- try: # pragma: no cover
23
+ try: # pragma: no cover
15
24
  from collections.abc import MutableMapping, MutableSequence
16
- except ImportError: # pragma: no cover
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__(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
+ ):
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 'last_scheduled' not in limit:
45
- limit['last_scheduled'] = now
63
+ if "last_scheduled" not in limit:
64
+ limit["last_scheduled"] = now
46
65
  return None
47
66
 
48
- safe_after = limit['last_scheduled'] + limit['interval']
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) + (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
54
75
  else:
55
- limit['last_scheduled'] = now
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(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
+ ]:
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 ['iris-detect-manage-watchlist-domains']:
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(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
+ )
72
101
 
73
102
  def _get_results(self):
74
103
  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
+ ):
76
107
  data = self._make_request()
77
- if data.status_code == 503: # pragma: no cover
108
+ if data.status_code == 503: # pragma: no cover
78
109
  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
+ )
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('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
+ )
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 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
+ ):
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(503, "Limit Exceeded{}".format(self._limit_exceeded_message))
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('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"]
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, '_status', None):
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: # pragma: no cover
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: # pragma: no cover
192
+ elif code == 500: # pragma: no cover
150
193
  raise InternalServerErrorException(code, reason)
151
- elif code == 503: # pragma: no cover
194
+ elif code == 503: # pragma: no cover
152
195
  raise ServiceUnavailableException(code, reason)
153
- elif code == 206: # pragma: no cover
196
+ elif code == 206: # pragma: no cover
154
197
  raise IncompleteResponseException(code, reason)
155
- elif code == 414: # pragma: no cover
198
+ elif code == 414: # pragma: no cover
156
199
  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")
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'[\w\.-]+@[\w\.-]+', str(self.response())))
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('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
+ )
227
277
 
228
278
  @property
229
279
  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
+ )
233
290
 
234
291
  @property
235
292
  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
+ )
239
303
 
240
304
  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
+ )
242
311
 
243
312
  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
+ )
@@ -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") 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)
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
 
@@ -4,3 +4,4 @@ from .domains import *
4
4
  from .ips import *
5
5
  from .phisheye import *
6
6
  from .detects import *
7
+ from .feeds import *
@@ -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)
@@ -41,3 +41,7 @@ IRIS_DETECT_IGNORED_DOMAINS = "iris_detect_ignored_domains"
41
41
  # phisheye
42
42
  PHISHEYE = "phisheye"
43
43
  PHISHEYE_TERM_LIST = "phisheye_term_list"
44
+
45
+ # feeds
46
+ FEEDS_NAD = "nad"
47
+ FEEDS_NOD = "nod"
@@ -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
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: domaintools_api
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: DomainTools Official Python API
5
5
  Author-email: DomainTools <integrations@domaintools.com>
6
6
  License: The MIT License (MIT)
@@ -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
@@ -60,7 +60,7 @@ class PyTest(TestCommand):
60
60
  cmdclass["test"] = PyTest
61
61
 
62
62
  setup(
63
+ name="domaintools_api",
63
64
  cmdclass=cmdclass,
64
65
  ext_modules=ext_modules,
65
- **PyTest.extra_kwargs,
66
66
  )
@@ -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()
@@ -1 +0,0 @@
1
- 2.0.0
File without changes