domaintools-api 2.7.4__tar.gz → 2.8.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.7.4/domaintools_api.egg-info → domaintools_api-2.8.0}/PKG-INFO +46 -5
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/README.md +41 -4
- domaintools_api-2.8.0/VERSION +1 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/_version.py +1 -1
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/api.py +72 -2
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/base_results.py +24 -6
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/api.py +10 -17
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/domains.py +46 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/iris.py +5 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/constants.py +1 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/utils.py +1 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0/domaintools_api.egg-info}/PKG-INFO +46 -5
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_api.egg-info/requires.txt +4 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_async/__init__.py +15 -3
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/pyproject.toml +4 -1
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_api.py +113 -0
- domaintools_api-2.8.0/tests/test_async.py +91 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_cli.py +8 -1
- domaintools_api-2.7.4/VERSION +0 -1
- domaintools_api-2.7.4/tests/test_async.py +0 -50
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/LICENSE +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/__init__.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/cli/main.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/constants.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/decorators.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/docstring_patcher.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/exceptions.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/filters.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/request_validator.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/results.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/specs/iris-openapi.yaml +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools/utils.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_api.egg-info/SOURCES.txt +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_api.egg-info/dependency_links.txt +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_api.egg-info/entry_points.txt +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/domaintools_api.egg-info/top_level.txt +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/setup.cfg +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/setup.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_decorators.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_docstring_patcher.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_filters.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_request_validator.py +0 -0
- {domaintools_api-2.7.4 → domaintools_api-2.8.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -50,6 +50,10 @@ Requires-Dist: typer
|
|
|
50
50
|
Provides-Extra: test
|
|
51
51
|
Requires-Dist: pytest; extra == "test"
|
|
52
52
|
Requires-Dist: mock; extra == "test"
|
|
53
|
+
Requires-Dist: vcrpy; extra == "test"
|
|
54
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
56
|
+
Requires-Dist: yarl; extra == "test"
|
|
53
57
|
Dynamic: license-file
|
|
54
58
|
|
|
55
59
|

|
|
@@ -199,6 +203,32 @@ You can get the status code of a response outside of exception handling by doing
|
|
|
199
203
|
api.domain_profile('google.com').status == 200
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
IrisQL
|
|
207
|
+
===================
|
|
208
|
+
|
|
209
|
+
IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
query = """# IrisQL-1.0
|
|
213
|
+
DOMAIN CONTAINS "phishing"
|
|
214
|
+
AND
|
|
215
|
+
RISK_SCORE GREATER_THAN 85
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
results = api.iris_investigate(irisql=query)
|
|
219
|
+
print(results["results_count"])
|
|
220
|
+
for domain in results:
|
|
221
|
+
print(domain["domain"])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
|
|
231
|
+
|
|
202
232
|
Using the API Asynchronously
|
|
203
233
|
===================
|
|
204
234
|
|
|
@@ -255,6 +285,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
|
|
|
255
285
|
domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
|
|
256
286
|
```
|
|
257
287
|
|
|
288
|
+
IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Pagination parameters can be passed alongside the IrisQL query:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
|
|
298
|
+
```
|
|
299
|
+
|
|
258
300
|
To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
|
|
259
301
|
|
|
260
302
|
```bash
|
|
@@ -343,12 +385,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
343
385
|
source venv/bin/activate
|
|
344
386
|
```
|
|
345
387
|
|
|
346
|
-
- Install dependencies
|
|
388
|
+
- Install dependencies (with test extras):
|
|
347
389
|
```bash
|
|
348
|
-
pip install -
|
|
390
|
+
pip install -e ".[test]"
|
|
349
391
|
```
|
|
350
|
-
|
|
351
|
-
- From the python_api project root directory, install the package.
|
|
392
|
+
Or without test dependencies:
|
|
352
393
|
```bash
|
|
353
394
|
pip install -e .
|
|
354
395
|
```
|
|
@@ -145,6 +145,32 @@ You can get the status code of a response outside of exception handling by doing
|
|
|
145
145
|
api.domain_profile('google.com').status == 200
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
+
IrisQL
|
|
149
|
+
===================
|
|
150
|
+
|
|
151
|
+
IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
query = """# IrisQL-1.0
|
|
155
|
+
DOMAIN CONTAINS "phishing"
|
|
156
|
+
AND
|
|
157
|
+
RISK_SCORE GREATER_THAN 85
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
results = api.iris_investigate(irisql=query)
|
|
161
|
+
print(results["results_count"])
|
|
162
|
+
for domain in results:
|
|
163
|
+
print(domain["domain"])
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
|
|
173
|
+
|
|
148
174
|
Using the API Asynchronously
|
|
149
175
|
===================
|
|
150
176
|
|
|
@@ -201,6 +227,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
|
|
|
201
227
|
domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
|
|
202
228
|
```
|
|
203
229
|
|
|
230
|
+
IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Pagination parameters can be passed alongside the IrisQL query:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
|
|
240
|
+
```
|
|
241
|
+
|
|
204
242
|
To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
|
|
205
243
|
|
|
206
244
|
```bash
|
|
@@ -289,12 +327,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
289
327
|
source venv/bin/activate
|
|
290
328
|
```
|
|
291
329
|
|
|
292
|
-
- Install dependencies
|
|
330
|
+
- Install dependencies (with test extras):
|
|
293
331
|
```bash
|
|
294
|
-
pip install -
|
|
332
|
+
pip install -e ".[test]"
|
|
295
333
|
```
|
|
296
|
-
|
|
297
|
-
- From the python_api project root directory, install the package.
|
|
334
|
+
Or without test dependencies:
|
|
298
335
|
```bash
|
|
299
336
|
pip install -e .
|
|
300
337
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.8.0
|
|
@@ -275,6 +275,57 @@ class API(object):
|
|
|
275
275
|
"""Returns a profile for the specified domain name"""
|
|
276
276
|
return self._results("domain-profile", "/v1/{0}".format(query))
|
|
277
277
|
|
|
278
|
+
def domain_history(
|
|
279
|
+
self,
|
|
280
|
+
query,
|
|
281
|
+
include_fields=None,
|
|
282
|
+
exclude_fields=None,
|
|
283
|
+
page_size=None,
|
|
284
|
+
offset=None,
|
|
285
|
+
next=None,
|
|
286
|
+
parsed_whois=None,
|
|
287
|
+
parsed_domain_rdap=None,
|
|
288
|
+
**kwargs,
|
|
289
|
+
):
|
|
290
|
+
"""Returns the history of changes for a given domain name.
|
|
291
|
+
|
|
292
|
+
Results are returned in reverse chronological order. Each change event includes
|
|
293
|
+
a timestamp, the field that changed, and the complete before/after domain state.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
query: The apex domain name to retrieve history for (e.g. "domaintools.com").
|
|
297
|
+
include_fields: Comma-separated list of exact field names. Only change events
|
|
298
|
+
matching these fields appear in results. Cannot be combined with
|
|
299
|
+
exclude_fields. Supports aggregate prefixes (e.g. "all_ssl", "all_ip").
|
|
300
|
+
Example: "ip,registrar,all_ssl"
|
|
301
|
+
exclude_fields: Comma-separated list of exact field names. Change events
|
|
302
|
+
matching these fields are omitted. Cannot be combined with include_fields.
|
|
303
|
+
Example: "all_web_trackers,all_ssl"
|
|
304
|
+
page_size: Number of change events per page. Maximum and default is 100.
|
|
305
|
+
offset: 0-indexed starting point for pagination. Increment by page_size for
|
|
306
|
+
each subsequent page.
|
|
307
|
+
next: When True, includes a next URL in the response for cursor-based
|
|
308
|
+
pagination. Auth parameters must still be included when following it.
|
|
309
|
+
parsed_whois: When True, includes the full parsed WHOIS record in the
|
|
310
|
+
before/after objects of each change event.
|
|
311
|
+
parsed_domain_rdap: When True, includes the full parsed Domain RDAP record
|
|
312
|
+
in the before/after objects of each change event.
|
|
313
|
+
"""
|
|
314
|
+
return self._results(
|
|
315
|
+
"domain-history",
|
|
316
|
+
"/v1/domain-history",
|
|
317
|
+
domain=query,
|
|
318
|
+
include_fields=include_fields,
|
|
319
|
+
exclude_fields=exclude_fields,
|
|
320
|
+
page_size=page_size,
|
|
321
|
+
offset=offset,
|
|
322
|
+
next=next,
|
|
323
|
+
parsed_whois=parsed_whois,
|
|
324
|
+
parsed_domain_rdap=parsed_domain_rdap,
|
|
325
|
+
items_path=("changes",),
|
|
326
|
+
**kwargs,
|
|
327
|
+
)
|
|
328
|
+
|
|
278
329
|
def domain_search(
|
|
279
330
|
self,
|
|
280
331
|
query,
|
|
@@ -662,13 +713,14 @@ class API(object):
|
|
|
662
713
|
updated_after=None,
|
|
663
714
|
include_domains_with_missing_field=None,
|
|
664
715
|
exclude_domains_with_missing_field=None,
|
|
716
|
+
irisql=None,
|
|
665
717
|
**kwargs,
|
|
666
718
|
):
|
|
667
719
|
"""Returns back a list of domains based on the provided filters.
|
|
668
720
|
|
|
669
721
|
You can loop over results of your investigation as if it was a native Python list:
|
|
670
722
|
|
|
671
|
-
for result in api.iris_investigate(ip='199.30.228.112'):
|
|
723
|
+
for result in api.iris_investigate(ip='199.30.228.112'):
|
|
672
724
|
|
|
673
725
|
api.iris_investigate(QUERY)['results_count'] Returns the number of results returned with this request
|
|
674
726
|
api.iris_investigate(QUERY)['total_count'] Returns the number of results available within Iris
|
|
@@ -677,9 +729,27 @@ class API(object):
|
|
|
677
729
|
api.iris_investigate(QUERY)['position'] Returns the position key that can be used to retrieve the next page:
|
|
678
730
|
next_page = api.iris_investigate(QUERY, position=api.iris_investigate(QUERY)['position'])
|
|
679
731
|
|
|
680
|
-
|
|
732
|
+
IrisQL mode (mutually exclusive with all other search parameters):
|
|
733
|
+
|
|
734
|
+
irisql: str: A raw IrisQL query string. Must begin with '# IrisQL-1.0'.
|
|
735
|
+
Sent as a raw POST body (text/plain). When set, all domain/filter params are ignored.
|
|
736
|
+
Pagination params (page_size, sort_by, position) are still supported via **kwargs.
|
|
737
|
+
|
|
738
|
+
Example:
|
|
739
|
+
api.iris_investigate(irisql='# IrisQL-1.0\\nDOMAIN CONTAINS "phishing"', page_size=50, sort_by='risk_score')
|
|
681
740
|
|
|
682
741
|
"""
|
|
742
|
+
if irisql is not None:
|
|
743
|
+
if domains:
|
|
744
|
+
print("Warning: irisql is set — ignoring 'domains' and other search parameters. IrisQL query takes precedence.")
|
|
745
|
+
return self._results(
|
|
746
|
+
"iris-investigate",
|
|
747
|
+
"/v1/iris-investigate/",
|
|
748
|
+
items_path=("results",),
|
|
749
|
+
irisql=irisql,
|
|
750
|
+
**kwargs,
|
|
751
|
+
)
|
|
752
|
+
|
|
683
753
|
# We put search_hash in the signature definition so the CLI can see it as a valid arg
|
|
684
754
|
if search_hash:
|
|
685
755
|
kwargs["search_hash"] = search_hash
|
|
@@ -25,7 +25,6 @@ from domaintools.exceptions import (
|
|
|
25
25
|
RequestUriTooLongException,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
try: # pragma: no cover
|
|
30
29
|
from collections.abc import MutableMapping, MutableSequence
|
|
31
30
|
except ImportError: # pragma: no cover
|
|
@@ -108,6 +107,19 @@ class Results(MutableMapping, MutableSequence):
|
|
|
108
107
|
"iris-enrich",
|
|
109
108
|
"iris-detect-escalate-domains",
|
|
110
109
|
]:
|
|
110
|
+
if self.product == "iris-investigate" and "irisql" in self.kwargs:
|
|
111
|
+
irisql_query = self.kwargs["irisql"]
|
|
112
|
+
auth_keys = {"api_username", "timestamp", "signature", "api_key"}
|
|
113
|
+
query_params = {
|
|
114
|
+
k: v for k, v in self.kwargs.items() if k != "irisql" and k not in auth_keys
|
|
115
|
+
}
|
|
116
|
+
query_params.update(self.api.extra_request_params)
|
|
117
|
+
return session.post(
|
|
118
|
+
url=self.url,
|
|
119
|
+
content=irisql_query,
|
|
120
|
+
params=query_params,
|
|
121
|
+
headers={**headers, "Content-Type": "text/plain", "X-Api-Key": self.api.key},
|
|
122
|
+
)
|
|
111
123
|
post_data = self.kwargs.copy()
|
|
112
124
|
post_data.update(self.api.extra_request_params)
|
|
113
125
|
return session.post(url=self.url, data=post_data, headers=headers)
|
|
@@ -153,13 +165,13 @@ class Results(MutableMapping, MutableSequence):
|
|
|
153
165
|
self._data = results.json()
|
|
154
166
|
else:
|
|
155
167
|
self._data = results.text
|
|
156
|
-
|
|
157
168
|
self.check_limit_exceeded()
|
|
158
169
|
|
|
159
170
|
return self._data
|
|
160
171
|
|
|
161
172
|
def check_limit_exceeded(self):
|
|
162
173
|
limit_exceeded, reason = False, ""
|
|
174
|
+
|
|
163
175
|
if isinstance(self._data, dict) and (
|
|
164
176
|
"response" in self._data
|
|
165
177
|
and "limit_exceeded" in self._data["response"]
|
|
@@ -167,7 +179,13 @@ class Results(MutableMapping, MutableSequence):
|
|
|
167
179
|
):
|
|
168
180
|
limit_exceeded, reason = True, self._data["response"]["message"]
|
|
169
181
|
elif "response" in self._data and "limit_exceeded" in self._data:
|
|
170
|
-
|
|
182
|
+
# check for xml format, and return the actual error message
|
|
183
|
+
if self.kwargs.get("format") == "xml" and isinstance(self._data, str):
|
|
184
|
+
if re.search(r"<limit_exceeded>1</limit_exceeded>", self._data):
|
|
185
|
+
msg = re.search(r"<message>(.*?)</message>", self._data)
|
|
186
|
+
limit_exceeded, reason = True, msg.group(1) if msg else ""
|
|
187
|
+
else:
|
|
188
|
+
limit_exceeded = True
|
|
171
189
|
|
|
172
190
|
if limit_exceeded:
|
|
173
191
|
raise ServiceException(503, f"Limit Exceeded {reason}")
|
|
@@ -277,6 +295,8 @@ class Results(MutableMapping, MutableSequence):
|
|
|
277
295
|
|
|
278
296
|
@property
|
|
279
297
|
def json(self):
|
|
298
|
+
if self._data is not None:
|
|
299
|
+
return self
|
|
280
300
|
self.kwargs.pop("format", None)
|
|
281
301
|
return self.__class__(
|
|
282
302
|
format="json",
|
|
@@ -341,9 +361,7 @@ class Results(MutableMapping, MutableSequence):
|
|
|
341
361
|
)
|
|
342
362
|
|
|
343
363
|
def as_list(self):
|
|
344
|
-
return "\n".join(
|
|
345
|
-
[json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()]
|
|
346
|
-
)
|
|
364
|
+
return "\n".join([json.dumps(item, indent=4, separators=(",", ": ")) for item in self._items()])
|
|
347
365
|
|
|
348
366
|
def __str__(self):
|
|
349
367
|
return str(
|
|
@@ -230,28 +230,21 @@ class DTCLICommand:
|
|
|
230
230
|
description=f"Preparing results with format of {response_format}...",
|
|
231
231
|
)
|
|
232
232
|
|
|
233
|
+
if name not in ("available_api_calls",) and not getattr(response, "product", None) in RTTF_PRODUCTS_LIST:
|
|
234
|
+
response.data()
|
|
235
|
+
|
|
233
236
|
output = cls._get_formatted_output(
|
|
234
237
|
cmd_name=name, response=response, out_format=response_format
|
|
235
238
|
)
|
|
236
239
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
# use rich `print` command to prettify the ouput in sys.stdout
|
|
243
|
-
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
|
|
244
|
-
for feeds in response.response():
|
|
245
|
-
print(feeds)
|
|
246
|
-
else:
|
|
247
|
-
print(response)
|
|
240
|
+
if isinstance(out_file, _io.TextIOWrapper):
|
|
241
|
+
if name not in ("available_api_calls",) and response.product in RTTF_PRODUCTS_LIST:
|
|
242
|
+
for feeds in response.response():
|
|
243
|
+
print(feeds)
|
|
248
244
|
else:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
253
|
-
# if it's a file then write
|
|
254
|
-
out_file.write(output if output.endswith("\n") else output + "\n")
|
|
245
|
+
print(output)
|
|
246
|
+
else:
|
|
247
|
+
out_file.write(output if output.endswith("\n") else output + "\n")
|
|
255
248
|
except Exception as e:
|
|
256
249
|
if isinstance(e, ServiceException):
|
|
257
250
|
code = typer.style(getattr(e, "code", 400), fg=typer.colors.BRIGHT_RED)
|
|
@@ -87,6 +87,51 @@ def domain_profile(
|
|
|
87
87
|
DTCLICommand.run(name=c.DOMAIN_PROFILE, params=ctx.params)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
@dt_cli.command(
|
|
91
|
+
name=c.DOMAIN_HISTORY,
|
|
92
|
+
help=get_cli_helptext_by_name(command_name=c.DOMAIN_HISTORY),
|
|
93
|
+
)
|
|
94
|
+
def domain_history(
|
|
95
|
+
ctx: typer.Context,
|
|
96
|
+
query: str = typer.Option(..., "-q", "--query", help="The apex domain name to retrieve history for (e.g. domaintools.com)."),
|
|
97
|
+
include_fields: str = typer.Option(None, "--include-fields", help="Comma-separated list of exact field names. Only change events matching these fields appear in results. Cannot be combined with --exclude-fields. Example: ip,registrar,all_ssl"),
|
|
98
|
+
exclude_fields: str = typer.Option(None, "--exclude-fields", help="Comma-separated list of exact field names. Change events matching these fields are omitted. Cannot be combined with --include-fields. Example: all_web_trackers,all_ssl"),
|
|
99
|
+
page_size: int = typer.Option(None, "--page-size", help="Number of change events per page. Maximum is 100 (default: 100)."),
|
|
100
|
+
offset: int = typer.Option(None, "--offset", help="0-indexed starting point for pagination. Increment by page-size for each subsequent page."),
|
|
101
|
+
next: bool = typer.Option(None, "--next", help="When true, includes a next URL in the response for cursor-based pagination."),
|
|
102
|
+
parsed_whois: bool = typer.Option(None, "--parsed-whois", help="When true, includes the full parsed WHOIS record in the before/after objects of each change event."),
|
|
103
|
+
parsed_domain_rdap: bool = typer.Option(None, "--parsed-domain-rdap", help="When true, includes the full parsed Domain RDAP record in the before/after objects of each change event."),
|
|
104
|
+
user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
|
|
105
|
+
key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
|
|
106
|
+
creds_file: str = typer.Option(
|
|
107
|
+
"~/.dtapi",
|
|
108
|
+
"-c",
|
|
109
|
+
"--credfile",
|
|
110
|
+
help="Optional file with API username and API key, one per line.",
|
|
111
|
+
),
|
|
112
|
+
rate_limit: bool = typer.Option(
|
|
113
|
+
False,
|
|
114
|
+
"-l",
|
|
115
|
+
"--rate-limit",
|
|
116
|
+
help="Rate limit API calls against the API based on per minute limits.",
|
|
117
|
+
),
|
|
118
|
+
format: str = typer.Option(
|
|
119
|
+
"json",
|
|
120
|
+
"-f",
|
|
121
|
+
"--format",
|
|
122
|
+
help="Output format in {'list', 'json', 'xml', 'html'}",
|
|
123
|
+
callback=DTCLICommand.validate_format_input,
|
|
124
|
+
),
|
|
125
|
+
out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
|
|
126
|
+
no_verify_ssl: bool = typer.Option(
|
|
127
|
+
False,
|
|
128
|
+
"--no-verify-ssl",
|
|
129
|
+
help="Skip verification of SSL certificate when making HTTPs API calls",
|
|
130
|
+
),
|
|
131
|
+
):
|
|
132
|
+
DTCLICommand.run(name=c.DOMAIN_HISTORY, params=ctx.params)
|
|
133
|
+
|
|
134
|
+
|
|
90
135
|
@dt_cli.command(
|
|
91
136
|
name=c.DOMAIN_SEARCH,
|
|
92
137
|
help=get_cli_helptext_by_name(command_name=c.DOMAIN_SEARCH),
|
|
@@ -666,6 +711,7 @@ def risk_evidence(
|
|
|
666
711
|
|
|
667
712
|
__all__ = [
|
|
668
713
|
"brand_monitor",
|
|
714
|
+
"domain_history",
|
|
669
715
|
"domain_profile",
|
|
670
716
|
"domain_search",
|
|
671
717
|
"name_server_monitor",
|
|
@@ -23,6 +23,11 @@ def iris_investigate(
|
|
|
23
23
|
create_date: str = typer.Option(None, "--create-date", help="The create date."),
|
|
24
24
|
active: bool = typer.Option(None, "--active", help="The domains that are in active state"),
|
|
25
25
|
search_hash: str = typer.Option(None, "--search-hash", help="The search hash to use"),
|
|
26
|
+
irisql: str = typer.Option(
|
|
27
|
+
None,
|
|
28
|
+
"--irisql",
|
|
29
|
+
help="IrisQL query string (must begin with '# IrisQL-1.0'). Mutually exclusive with domain/filter params. Pagination kwargs (--page-size, --sort-by, --position) are still supported.",
|
|
30
|
+
),
|
|
26
31
|
src_file: str = typer.Option(
|
|
27
32
|
None,
|
|
28
33
|
"-s",
|
|
@@ -35,6 +35,7 @@ _command_help_mapper = {
|
|
|
35
35
|
c.IRIS_INVESTIGATE: _iris_investigate_helptext(),
|
|
36
36
|
c.IRIS_ENRICH: "Returns back enriched data related to the specified domains using our Iris Enrich service.",
|
|
37
37
|
c.BRAND_MONITOR: "Pass in one or more terms as a list or separated by the pipe character ( | )",
|
|
38
|
+
c.DOMAIN_HISTORY: "Returns the history of changes for a given domain name.",
|
|
38
39
|
c.DOMAIN_PROFILE: "Returns a profile for the specified domain name",
|
|
39
40
|
c.DOMAIN_SEARCH: """Each term in the query string must be at least three characters long. Pass in a list or use spaces to separate multiple terms.""",
|
|
40
41
|
c.HOSTING_HISTORY: "Returns the hosting history from the given domain name.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: DomainTools Official Python API
|
|
5
5
|
Author-email: DomainTools <integrations@domaintools.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -50,6 +50,10 @@ Requires-Dist: typer
|
|
|
50
50
|
Provides-Extra: test
|
|
51
51
|
Requires-Dist: pytest; extra == "test"
|
|
52
52
|
Requires-Dist: mock; extra == "test"
|
|
53
|
+
Requires-Dist: vcrpy; extra == "test"
|
|
54
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
56
|
+
Requires-Dist: yarl; extra == "test"
|
|
53
57
|
Dynamic: license-file
|
|
54
58
|
|
|
55
59
|

|
|
@@ -199,6 +203,32 @@ You can get the status code of a response outside of exception handling by doing
|
|
|
199
203
|
api.domain_profile('google.com').status == 200
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
IrisQL
|
|
207
|
+
===================
|
|
208
|
+
|
|
209
|
+
IrisQL is a query language for Iris Investigate that lets you express complex, multi-field searches in a single request. Pass the query as a raw string via the `irisql` parameter. The query must begin with `# IrisQL-1.0`.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
query = """# IrisQL-1.0
|
|
213
|
+
DOMAIN CONTAINS "phishing"
|
|
214
|
+
AND
|
|
215
|
+
RISK_SCORE GREATER_THAN 85
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
results = api.iris_investigate(irisql=query)
|
|
219
|
+
print(results["results_count"])
|
|
220
|
+
for domain in results:
|
|
221
|
+
print(domain["domain"])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Pagination parameters (`page_size`, `sort_by`, `position`) are supported alongside IrisQL via `**kwargs`:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
results = api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
When `irisql` is set, any domain or filter parameters passed alongside it are silently ignored. IrisQL uses header-based authentication (`X-Api-Key`) automatically.
|
|
231
|
+
|
|
202
232
|
Using the API Asynchronously
|
|
203
233
|
===================
|
|
204
234
|
|
|
@@ -255,6 +285,18 @@ Optionally, you can specify the desired format (html, xml, json, or list) of the
|
|
|
255
285
|
domaintools domain_search google --max_length 10 -u $TEST_USER -k $TEST_KEY -f html
|
|
256
286
|
```
|
|
257
287
|
|
|
288
|
+
IrisQL queries are supported via the `--irisql` flag on `iris_investigate`. The query must begin with `# IrisQL-1.0` on its own line:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' -u $TEST_USER -k $TEST_KEY
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Pagination parameters can be passed alongside the IrisQL query:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
domaintools iris_investigate --irisql $'# IrisQL-1.0\nDOMAIN CONTAINS "phishing"' --page-size 50 --sort-by risk_score -u $TEST_USER -k $TEST_KEY
|
|
298
|
+
```
|
|
299
|
+
|
|
258
300
|
To avoid having to type in your API key repeatedly, you can specify them in `~/.dtapi` separated by a new line:
|
|
259
301
|
|
|
260
302
|
```bash
|
|
@@ -343,12 +385,11 @@ To add more e2e tests, put these in the `../tests/e2e` folder.
|
|
|
343
385
|
source venv/bin/activate
|
|
344
386
|
```
|
|
345
387
|
|
|
346
|
-
- Install dependencies
|
|
388
|
+
- Install dependencies (with test extras):
|
|
347
389
|
```bash
|
|
348
|
-
pip install -
|
|
390
|
+
pip install -e ".[test]"
|
|
349
391
|
```
|
|
350
|
-
|
|
351
|
-
- From the python_api project root directory, install the package.
|
|
392
|
+
Or without test dependencies:
|
|
352
393
|
```bash
|
|
353
394
|
pip install -e .
|
|
354
395
|
```
|
|
@@ -46,9 +46,21 @@ class AsyncResults(Results):
|
|
|
46
46
|
session_params_and_headers = self._get_session_params_and_headers()
|
|
47
47
|
headers = session_params_and_headers.get("headers")
|
|
48
48
|
if self.product in ["iris-investigate", "iris-enrich", "iris-detect-escalate-domains"]:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if self.product == "iris-investigate" and "irisql" in self.kwargs:
|
|
50
|
+
irisql_query = self.kwargs["irisql"]
|
|
51
|
+
auth_keys = {"api_username", "timestamp", "signature", "api_key"}
|
|
52
|
+
query_params = {k: v for k, v in self.kwargs.items() if k != "irisql" and k not in auth_keys}
|
|
53
|
+
query_params.update(self.api.extra_request_params)
|
|
54
|
+
results = await session.post(
|
|
55
|
+
url=self.url,
|
|
56
|
+
content=irisql_query,
|
|
57
|
+
params=query_params,
|
|
58
|
+
headers={**headers, "Content-Type": "text/plain", "X-Api-Key": self.api.key},
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
post_data = self.kwargs.copy()
|
|
62
|
+
post_data.update(self.api.extra_request_params)
|
|
63
|
+
results = await session.post(url=self.url, data=post_data, headers=headers)
|
|
52
64
|
elif self.product in ["iris-detect-manage-watchlist-domains"]:
|
|
53
65
|
patch_data = self.kwargs.copy()
|
|
54
66
|
patch_data.update(self.api.extra_request_params, headers=headers)
|
|
@@ -41,7 +41,7 @@ Homepage = "https://github.com/domaintools/python_api"
|
|
|
41
41
|
domaintools = "domaintools.cli:run"
|
|
42
42
|
|
|
43
43
|
[project.optional-dependencies]
|
|
44
|
-
test = ["pytest", "mock"]
|
|
44
|
+
test = ["pytest", "mock", "vcrpy", "pytest-asyncio", "pytest-cov", "yarl"]
|
|
45
45
|
|
|
46
46
|
[tool.setuptools]
|
|
47
47
|
packages = [
|
|
@@ -58,5 +58,8 @@ domaintools = ["specs/*.yaml"]
|
|
|
58
58
|
[tool.setuptools.dynamic]
|
|
59
59
|
version = { "file" = "VERSION" }
|
|
60
60
|
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
|
|
61
64
|
[tool.black]
|
|
62
65
|
line-length = 110
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests the Python interface for DomainTools APIs"""
|
|
2
2
|
|
|
3
3
|
from os import environ
|
|
4
|
+
from unittest.mock import MagicMock, patch, call
|
|
4
5
|
|
|
5
6
|
import json
|
|
6
7
|
import pytest
|
|
@@ -10,6 +11,7 @@ from inspect import isgenerator
|
|
|
10
11
|
|
|
11
12
|
from domaintools import API, exceptions
|
|
12
13
|
from tests.settings import api, feeds_api, vcr
|
|
14
|
+
from tests.responses.iris_investigate_data import domaintools as iris_investigate_fixture
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
@vcr.use_cassette
|
|
@@ -68,6 +70,22 @@ def test_domain_profile():
|
|
|
68
70
|
assert "ip_address" in history
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
@vcr.use_cassette
|
|
74
|
+
def test_domain_history():
|
|
75
|
+
api_call = api.domain_history("chat.com", include_fields="registrar")
|
|
76
|
+
with api_call as result:
|
|
77
|
+
assert "domain" in result
|
|
78
|
+
assert "count" in result
|
|
79
|
+
assert "registration_source" in result
|
|
80
|
+
assert "changes" in result
|
|
81
|
+
|
|
82
|
+
for change in api_call:
|
|
83
|
+
assert "timestamp" in change
|
|
84
|
+
assert "field" in change
|
|
85
|
+
assert "before" in change
|
|
86
|
+
assert "after" in change
|
|
87
|
+
|
|
88
|
+
|
|
71
89
|
@vcr.use_cassette
|
|
72
90
|
def test_domain_search():
|
|
73
91
|
api_call = api.domain_search("google")
|
|
@@ -447,6 +465,73 @@ def test_iris_investigate():
|
|
|
447
465
|
assert result["domain"] in ["amazon.com", "google.com"]
|
|
448
466
|
|
|
449
467
|
|
|
468
|
+
def test_iris_investigate_irisql_calls_results_with_irisql():
|
|
469
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
470
|
+
with patch.object(api, "_results") as mock_results:
|
|
471
|
+
api.iris_investigate(irisql=query)
|
|
472
|
+
mock_results.assert_called_once_with(
|
|
473
|
+
"iris-investigate",
|
|
474
|
+
"/v1/iris-investigate/",
|
|
475
|
+
items_path=("results",),
|
|
476
|
+
irisql=query,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_iris_investigate_irisql_with_pagination_kwargs():
|
|
481
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
482
|
+
with patch.object(api, "_results") as mock_results:
|
|
483
|
+
api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score", position=0)
|
|
484
|
+
mock_results.assert_called_once_with(
|
|
485
|
+
"iris-investigate",
|
|
486
|
+
"/v1/iris-investigate/",
|
|
487
|
+
items_path=("results",),
|
|
488
|
+
irisql=query,
|
|
489
|
+
page_size=50,
|
|
490
|
+
sort_by="risk_score",
|
|
491
|
+
position=0,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def test_iris_investigate_irisql_warns_and_ignores_domains(capsys):
|
|
496
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
497
|
+
with patch.object(api, "_results"):
|
|
498
|
+
api.iris_investigate(irisql=query, domains=["google.com"])
|
|
499
|
+
captured = capsys.readouterr()
|
|
500
|
+
assert "Warning" in captured.out
|
|
501
|
+
assert "irisql" in captured.out
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_iris_investigate_irisql_does_not_require_other_params():
|
|
505
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
506
|
+
with patch.object(api, "_results"):
|
|
507
|
+
api.iris_investigate(irisql=query)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def test_iris_investigate_irisql_request_uses_raw_body():
|
|
511
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
512
|
+
mock_response = MagicMock()
|
|
513
|
+
mock_response.status_code = 200
|
|
514
|
+
mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
|
|
515
|
+
|
|
516
|
+
with patch("domaintools.base_results.Client") as mock_client:
|
|
517
|
+
mock_session = MagicMock()
|
|
518
|
+
mock_client.return_value.__enter__.return_value = mock_session
|
|
519
|
+
mock_session.post.return_value = mock_response
|
|
520
|
+
|
|
521
|
+
result = api.iris_investigate(irisql=query)
|
|
522
|
+
result.data()
|
|
523
|
+
|
|
524
|
+
_, kwargs = mock_session.post.call_args
|
|
525
|
+
assert kwargs.get("content") == query
|
|
526
|
+
assert "data" not in kwargs
|
|
527
|
+
assert kwargs.get("headers", {}).get("Content-Type") == "text/plain"
|
|
528
|
+
assert kwargs.get("headers", {}).get("X-Api-Key") == api.key
|
|
529
|
+
for auth_key in ("api_username", "timestamp", "signature", "api_key"):
|
|
530
|
+
assert auth_key not in kwargs.get("params", {})
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
|
|
450
535
|
@vcr.use_cassette
|
|
451
536
|
def test_iris_detect_monitors():
|
|
452
537
|
with pytest.raises(ValueError):
|
|
@@ -517,6 +602,34 @@ def test_limit_exceeded():
|
|
|
517
602
|
response.response()
|
|
518
603
|
|
|
519
604
|
|
|
605
|
+
def test_limit_exceeded_xml():
|
|
606
|
+
xml_response = """<response>
|
|
607
|
+
<error>
|
|
608
|
+
<code>413</code>
|
|
609
|
+
<message>Maximum 10000 returned - you may need to refine your query.</message>
|
|
610
|
+
</error>
|
|
611
|
+
<limit_exceeded>1</limit_exceeded>
|
|
612
|
+
<has_more_results>1</has_more_results>
|
|
613
|
+
<message>Maximum 10000 returned - you may need to refine your query.</message>
|
|
614
|
+
<missing_domains/>
|
|
615
|
+
</response>"""
|
|
616
|
+
|
|
617
|
+
mock_response = MagicMock()
|
|
618
|
+
mock_response.status_code = 200
|
|
619
|
+
mock_response.text = xml_response
|
|
620
|
+
|
|
621
|
+
with patch("domaintools.base_results.Client") as mock_client:
|
|
622
|
+
mock_session = MagicMock()
|
|
623
|
+
mock_client.return_value.__enter__.return_value = mock_session
|
|
624
|
+
mock_session.post.return_value = mock_response
|
|
625
|
+
|
|
626
|
+
with pytest.raises(exceptions.ServiceException) as exc_info:
|
|
627
|
+
result = api.iris_investigate(ip="8.8.8.8", format="xml")
|
|
628
|
+
result.data()
|
|
629
|
+
|
|
630
|
+
assert "Maximum 10000 returned" in str(exc_info.value)
|
|
631
|
+
|
|
632
|
+
|
|
520
633
|
@vcr.use_cassette
|
|
521
634
|
def test_newly_observed_domains_feed():
|
|
522
635
|
results = feeds_api.nod(after="-60", top=5)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Tests async interaction support for DomainTools APIs"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import MagicMock, AsyncMock, patch
|
|
6
|
+
|
|
7
|
+
from tests.settings import api, vcr
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@vcr.use_cassette
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_async_iteration():
|
|
13
|
+
results = await api.domain_search("google")
|
|
14
|
+
assert results
|
|
15
|
+
|
|
16
|
+
list_of_domains = []
|
|
17
|
+
for domain in results:
|
|
18
|
+
list_of_domains += domain
|
|
19
|
+
|
|
20
|
+
for domain in list_of_domains:
|
|
21
|
+
assert type(domain) == str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@vcr.use_cassette
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_async_context_manager():
|
|
27
|
+
results = await api.domain_search("google")
|
|
28
|
+
assert results
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@vcr.use_cassette
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_async_simple_await():
|
|
34
|
+
results = await api.domain_search("google")
|
|
35
|
+
assert results
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@vcr.use_cassette
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_async_simple_await_post():
|
|
41
|
+
investigation_results = await api.iris_investigate(domains=["amazon.com", "google.com"])
|
|
42
|
+
assert investigation_results["results_count"]
|
|
43
|
+
for result in investigation_results:
|
|
44
|
+
assert result["domain"] in ["amazon.com", "google.com"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@vcr.use_cassette
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
async def test_async_simple_await_patch():
|
|
50
|
+
detect_results = await api.iris_detect_manage_watchlist_domains(watchlist_domain_ids=["gae08rdVWG"], state="watched")
|
|
51
|
+
assert detect_results["watchlist_domains"][0]["state"] == "watched"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_async_irisql_uses_raw_body():
|
|
56
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
57
|
+
mock_response = MagicMock()
|
|
58
|
+
mock_response.status_code = 200
|
|
59
|
+
mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
|
|
60
|
+
|
|
61
|
+
with patch("domaintools_async.AsyncClient") as mock_client:
|
|
62
|
+
mock_session = AsyncMock()
|
|
63
|
+
mock_client.return_value.__aenter__.return_value = mock_session
|
|
64
|
+
mock_session.post.return_value = mock_response
|
|
65
|
+
|
|
66
|
+
result = await api.iris_investigate(irisql=query)
|
|
67
|
+
|
|
68
|
+
_, kwargs = mock_session.post.call_args
|
|
69
|
+
assert kwargs.get("content") == query
|
|
70
|
+
assert "data" not in kwargs
|
|
71
|
+
assert kwargs.get("headers", {}).get("Content-Type") == "text/plain"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_async_irisql_with_pagination_kwargs():
|
|
76
|
+
query = "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\""
|
|
77
|
+
mock_response = MagicMock()
|
|
78
|
+
mock_response.status_code = 200
|
|
79
|
+
mock_response.json.return_value = {"response": {"results": [], "results_count": 0}}
|
|
80
|
+
|
|
81
|
+
with patch("domaintools_async.AsyncClient") as mock_client:
|
|
82
|
+
mock_session = AsyncMock()
|
|
83
|
+
mock_client.return_value.__aenter__.return_value = mock_session
|
|
84
|
+
mock_session.post.return_value = mock_response
|
|
85
|
+
|
|
86
|
+
result = await api.iris_investigate(irisql=query, page_size=50, sort_by="risk_score")
|
|
87
|
+
|
|
88
|
+
_, kwargs = mock_session.post.call_args
|
|
89
|
+
assert kwargs.get("content") == query
|
|
90
|
+
assert "page_size" in kwargs.get("params", {})
|
|
91
|
+
assert "sort_by" in kwargs.get("params", {})
|
|
@@ -27,9 +27,16 @@ def test_valid_command():
|
|
|
27
27
|
|
|
28
28
|
def test_invalid_command():
|
|
29
29
|
result = runner.invoke(dt_cli, ["test_invalid_command"])
|
|
30
|
-
assert "No such command 'test_invalid_command'." in result.
|
|
30
|
+
assert "No such command 'test_invalid_command'." in result.output
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def test_no_creds_file_not_found():
|
|
34
34
|
result = runner.invoke(dt_cli, ["iris_investigate", "--domain", "domaintools.com"])
|
|
35
35
|
assert "No such file or directory" in result.stdout
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_iris_investigate_irisql_flag_accepted():
|
|
39
|
+
result = runner.invoke(dt_cli, ["iris_investigate", "--irisql", "# IrisQL-1.0\nDOMAIN CONTAINS \"phishing\"", "--help"])
|
|
40
|
+
assert "--irisql" in result.stdout
|
|
41
|
+
|
|
42
|
+
|
domaintools_api-2.7.4/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.7.4
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"""Tests async interaction support for DomainTools APIs"""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from tests.settings import api, vcr
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@vcr.use_cassette
|
|
10
|
-
@pytest.mark.asyncio
|
|
11
|
-
async def test_async_iteration():
|
|
12
|
-
results = await api.domain_search("google")
|
|
13
|
-
assert results
|
|
14
|
-
|
|
15
|
-
list_of_domains = []
|
|
16
|
-
for domain in results:
|
|
17
|
-
list_of_domains += domain
|
|
18
|
-
|
|
19
|
-
for domain in list_of_domains:
|
|
20
|
-
assert type(domain) == str
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@vcr.use_cassette
|
|
24
|
-
@pytest.mark.asyncio
|
|
25
|
-
async def test_async_context_manager():
|
|
26
|
-
results = await api.domain_search("google")
|
|
27
|
-
assert results
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@vcr.use_cassette
|
|
31
|
-
@pytest.mark.asyncio
|
|
32
|
-
async def test_async_simple_await():
|
|
33
|
-
results = await api.domain_search("google")
|
|
34
|
-
assert results
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@vcr.use_cassette
|
|
38
|
-
@pytest.mark.asyncio
|
|
39
|
-
async def test_async_simple_await_post():
|
|
40
|
-
investigation_results = await api.iris_investigate(domains=["amazon.com", "google.com"])
|
|
41
|
-
assert investigation_results["results_count"]
|
|
42
|
-
for result in investigation_results:
|
|
43
|
-
assert result["domain"] in ["amazon.com", "google.com"]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@vcr.use_cassette
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_async_simple_await_patch():
|
|
49
|
-
detect_results = await api.iris_detect_manage_watchlist_domains(watchlist_domain_ids=["gae08rdVWG"], state="watched")
|
|
50
|
-
assert detect_results["watchlist_domains"][0]["state"] == "watched"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{domaintools_api-2.7.4 → domaintools_api-2.8.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
|