domaintools-api-test-upload 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_test_upload-2.7.4/domaintools_api_test_upload.egg-info → domaintools_api_test_upload-2.8.0}/PKG-INFO +46 -5
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/README.md +41 -4
- domaintools_api_test_upload-2.8.0/VERSION +1 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/_version.py +1 -1
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/api.py +82 -33
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/base_results.py +13 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/api.py +10 -17
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/domains.py +46 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/iris.py +5 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/constants.py +1 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/utils.py +1 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/decorators.py +4 -4
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0/domaintools_api_test_upload.egg-info}/PKG-INFO +46 -5
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/requires.txt +4 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_async/__init__.py +15 -3
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/pyproject.toml +4 -1
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_api.py +86 -4
- domaintools_api_test_upload-2.8.0/tests/test_async.py +91 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_cli.py +8 -1
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_decorators.py +32 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_filters.py +5 -12
- domaintools_api_test_upload-2.7.4/VERSION +0 -1
- domaintools_api_test_upload-2.7.4/tests/test_async.py +0 -50
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/LICENSE +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/__init__.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/__init__.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/__init__.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/accounts.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/detects.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/feeds.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/commands/ips.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/main.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/constants.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/docstring_patcher.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/exceptions.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/filters.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/request_validator.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/results.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/specs/iris-openapi.yaml +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/utils.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/SOURCES.txt +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/dependency_links.txt +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/entry_points.txt +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools_api_test_upload.egg-info/top_level.txt +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/setup.cfg +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/setup.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_docstring_patcher.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_request_validator.py +0 -0
- {domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api_test_upload
|
|
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
|
|
@@ -128,11 +128,7 @@ class API(object):
|
|
|
128
128
|
print(f"Error loading {specs_file_path}: {e}")
|
|
129
129
|
|
|
130
130
|
def _get_ssl_default_context(self, verify_ssl: Union[str, bool]):
|
|
131
|
-
return (
|
|
132
|
-
ssl.create_default_context(cafile=verify_ssl)
|
|
133
|
-
if isinstance(verify_ssl, str)
|
|
134
|
-
else verify_ssl
|
|
135
|
-
)
|
|
131
|
+
return ssl.create_default_context(cafile=verify_ssl) if isinstance(verify_ssl, str) else verify_ssl
|
|
136
132
|
|
|
137
133
|
def _build_api_url(self, api_url=None, api_port=None):
|
|
138
134
|
"""Build the API url based on the given url and port. Defaults to `https://api.domaintools.com`"""
|
|
@@ -160,18 +156,11 @@ class API(object):
|
|
|
160
156
|
hours = limit_hours and 3600 / float(limit_hours)
|
|
161
157
|
minutes = limit_minutes and 60 / float(limit_minutes)
|
|
162
158
|
|
|
163
|
-
self.limits[product["id"]] = {
|
|
164
|
-
"interval": timedelta(seconds=minutes or hours or default)
|
|
165
|
-
}
|
|
159
|
+
self.limits[product["id"]] = {"interval": timedelta(seconds=minutes or hours or default)}
|
|
166
160
|
|
|
167
161
|
def _results(self, product, path, cls=Results, **kwargs):
|
|
168
162
|
"""Returns _results for the specified API path with the specified **kwargs parameters"""
|
|
169
|
-
if
|
|
170
|
-
product != "account-information"
|
|
171
|
-
and self.rate_limit
|
|
172
|
-
and not self.limits_set
|
|
173
|
-
and not self.limits
|
|
174
|
-
):
|
|
163
|
+
if product != "account-information" and self.rate_limit and not self.limits_set and not self.limits:
|
|
175
164
|
always_sign_api_key_previous_value = self.always_sign_api_key
|
|
176
165
|
header_authentication_previous_value = self.header_authentication
|
|
177
166
|
self._rate_limit(product)
|
|
@@ -215,9 +204,7 @@ class API(object):
|
|
|
215
204
|
else:
|
|
216
205
|
raise ValueError(
|
|
217
206
|
"Invalid value '{0}' for 'key_sign_hash'. "
|
|
218
|
-
"Values available are {1}".format(
|
|
219
|
-
self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES)
|
|
220
|
-
)
|
|
207
|
+
"Values available are {1}".format(self.key_sign_hash, ",".join(AVAILABLE_KEY_SIGN_HASHES))
|
|
221
208
|
)
|
|
222
209
|
|
|
223
210
|
parameters["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -229,9 +216,7 @@ class API(object):
|
|
|
229
216
|
|
|
230
217
|
def account_information(self, **kwargs):
|
|
231
218
|
"""Provides a snapshot of your accounts current API usage"""
|
|
232
|
-
return self._results(
|
|
233
|
-
"account-information", "/v1/account", items_path=("products",), **kwargs
|
|
234
|
-
)
|
|
219
|
+
return self._results("account-information", "/v1/account", items_path=("products",), **kwargs)
|
|
235
220
|
|
|
236
221
|
def available_api_calls(self):
|
|
237
222
|
"""Provides a list of api calls that you can use based on your account information."""
|
|
@@ -290,6 +275,57 @@ class API(object):
|
|
|
290
275
|
"""Returns a profile for the specified domain name"""
|
|
291
276
|
return self._results("domain-profile", "/v1/{0}".format(query))
|
|
292
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
|
+
|
|
293
329
|
def domain_search(
|
|
294
330
|
self,
|
|
295
331
|
query,
|
|
@@ -434,9 +470,7 @@ class API(object):
|
|
|
434
470
|
|
|
435
471
|
def reverse_ip(self, domain=None, limit=None, **kwargs):
|
|
436
472
|
"""Pass in a domain name."""
|
|
437
|
-
return self._results(
|
|
438
|
-
"reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs
|
|
439
|
-
)
|
|
473
|
+
return self._results("reverse-ip", "/v1/{0}/reverse-ip".format(domain), limit=limit, **kwargs)
|
|
440
474
|
|
|
441
475
|
def host_domains(self, ip=None, limit=None, **kwargs):
|
|
442
476
|
"""Pass in an IP address."""
|
|
@@ -610,12 +644,8 @@ class API(object):
|
|
|
610
644
|
younger_than_date = kwargs.pop("younger_than_date", {}) or None
|
|
611
645
|
older_than_date = kwargs.pop("older_than_date", {}) or None
|
|
612
646
|
updated_after = kwargs.pop("updated_after", {}) or None
|
|
613
|
-
include_domains_with_missing_field = (
|
|
614
|
-
|
|
615
|
-
)
|
|
616
|
-
exclude_domains_with_missing_field = (
|
|
617
|
-
kwargs.pop("exclude_domains_with_missing_field", {}) or None
|
|
618
|
-
)
|
|
647
|
+
include_domains_with_missing_field = kwargs.pop("include_domains_with_missing_field", {}) or None
|
|
648
|
+
exclude_domains_with_missing_field = kwargs.pop("exclude_domains_with_missing_field", {}) or None
|
|
619
649
|
|
|
620
650
|
filtered_results = DTResultFilter(result_set=results).by(
|
|
621
651
|
[
|
|
@@ -677,19 +707,20 @@ class API(object):
|
|
|
677
707
|
create_date=None,
|
|
678
708
|
active=None,
|
|
679
709
|
search_hash=None,
|
|
680
|
-
|
|
710
|
+
risk_score_threshold=None,
|
|
681
711
|
younger_than_date=None,
|
|
682
712
|
older_than_date=None,
|
|
683
713
|
updated_after=None,
|
|
684
714
|
include_domains_with_missing_field=None,
|
|
685
715
|
exclude_domains_with_missing_field=None,
|
|
716
|
+
irisql=None,
|
|
686
717
|
**kwargs,
|
|
687
718
|
):
|
|
688
719
|
"""Returns back a list of domains based on the provided filters.
|
|
689
720
|
|
|
690
721
|
You can loop over results of your investigation as if it was a native Python list:
|
|
691
722
|
|
|
692
|
-
for result in api.iris_investigate(ip='199.30.228.112'):
|
|
723
|
+
for result in api.iris_investigate(ip='199.30.228.112'):
|
|
693
724
|
|
|
694
725
|
api.iris_investigate(QUERY)['results_count'] Returns the number of results returned with this request
|
|
695
726
|
api.iris_investigate(QUERY)['total_count'] Returns the number of results available within Iris
|
|
@@ -698,9 +729,27 @@ class API(object):
|
|
|
698
729
|
api.iris_investigate(QUERY)['position'] Returns the position key that can be used to retrieve the next page:
|
|
699
730
|
next_page = api.iris_investigate(QUERY, position=api.iris_investigate(QUERY)['position'])
|
|
700
731
|
|
|
701
|
-
|
|
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')
|
|
702
740
|
|
|
703
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
|
+
|
|
704
753
|
# We put search_hash in the signature definition so the CLI can see it as a valid arg
|
|
705
754
|
if search_hash:
|
|
706
755
|
kwargs["search_hash"] = search_hash
|
|
@@ -732,7 +781,7 @@ class API(object):
|
|
|
732
781
|
|
|
733
782
|
filtered_results = DTResultFilter(result_set=results).by(
|
|
734
783
|
[
|
|
735
|
-
filter_by_riskscore(threshold=
|
|
784
|
+
filter_by_riskscore(threshold=risk_score_threshold),
|
|
736
785
|
filter_by_expire_date(date=younger_than_date, lookup_type="before"),
|
|
737
786
|
filter_by_expire_date(date=older_than_date, lookup_type="after"),
|
|
738
787
|
filter_by_date_updated_after(date=updated_after),
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/base_results.py
RENAMED
|
@@ -108,6 +108,17 @@ class Results(MutableMapping, MutableSequence):
|
|
|
108
108
|
"iris-enrich",
|
|
109
109
|
"iris-detect-escalate-domains",
|
|
110
110
|
]:
|
|
111
|
+
if self.product == "iris-investigate" and "irisql" in self.kwargs:
|
|
112
|
+
irisql_query = self.kwargs["irisql"]
|
|
113
|
+
auth_keys = {"api_username", "timestamp", "signature", "api_key"}
|
|
114
|
+
query_params = {k: v for k, v in self.kwargs.items() if k != "irisql" and k not in auth_keys}
|
|
115
|
+
query_params.update(self.api.extra_request_params)
|
|
116
|
+
return session.post(
|
|
117
|
+
url=self.url,
|
|
118
|
+
content=irisql_query,
|
|
119
|
+
params=query_params,
|
|
120
|
+
headers={**headers, "Content-Type": "text/plain", "X-Api-Key": self.api.key},
|
|
121
|
+
)
|
|
111
122
|
post_data = self.kwargs.copy()
|
|
112
123
|
post_data.update(self.api.extra_request_params)
|
|
113
124
|
return session.post(url=self.url, data=post_data, headers=headers)
|
|
@@ -277,6 +288,8 @@ class Results(MutableMapping, MutableSequence):
|
|
|
277
288
|
|
|
278
289
|
@property
|
|
279
290
|
def json(self):
|
|
291
|
+
if self._data is not None:
|
|
292
|
+
return self
|
|
280
293
|
self.kwargs.pop("format", None)
|
|
281
294
|
return self.__class__(
|
|
282
295
|
format="json",
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/api.py
RENAMED
|
@@ -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",
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/utils.py
RENAMED
|
@@ -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.",
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/decorators.py
RENAMED
|
@@ -51,9 +51,7 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
|
|
|
51
51
|
|
|
52
52
|
if "domains" in arguments.keys():
|
|
53
53
|
domains = arguments.pop("domains")
|
|
54
|
-
arguments["domain"] = (
|
|
55
|
-
",".join(domains) if isinstance(domains, (list, tuple)) else domains
|
|
56
|
-
)
|
|
54
|
+
arguments["domain"] = ",".join(domains) if isinstance(domains, (list, tuple)) else domains
|
|
57
55
|
|
|
58
56
|
if spec:
|
|
59
57
|
# Determine which HTTP method is currently being executed.
|
|
@@ -61,6 +59,8 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
|
|
|
61
59
|
# Otherwise, default to the first method defined in the decorator.
|
|
62
60
|
current_method = kwargs.get("method", normalized_methods[0])
|
|
63
61
|
|
|
62
|
+
filtered_arguments = {k: v for k, v in arguments.items() if v is not None}
|
|
63
|
+
|
|
64
64
|
# Run Validation
|
|
65
65
|
# This will raise a ValueError and stop execution if validation fails.
|
|
66
66
|
try:
|
|
@@ -68,7 +68,7 @@ def api_endpoint(spec_name: str, path: str, methods: Union[str, List[str]]):
|
|
|
68
68
|
spec=spec,
|
|
69
69
|
path=path,
|
|
70
70
|
method=current_method,
|
|
71
|
-
parameters=
|
|
71
|
+
parameters=filtered_arguments,
|
|
72
72
|
)
|
|
73
73
|
except ValueError as e:
|
|
74
74
|
print(f"[Validation Error] {e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: domaintools_api_test_upload
|
|
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")
|
|
@@ -143,7 +161,6 @@ def test_name_server_monitor():
|
|
|
143
161
|
def test_parsed_whois():
|
|
144
162
|
api_call = api.parsed_whois("google.com")
|
|
145
163
|
with api_call as result:
|
|
146
|
-
assert "registrant" in result
|
|
147
164
|
assert "registration" in result
|
|
148
165
|
assert "name_servers" in result
|
|
149
166
|
assert "whois" in result
|
|
@@ -291,8 +308,6 @@ def test_whois():
|
|
|
291
308
|
assert "whois" in whois
|
|
292
309
|
assert "record_source" in whois
|
|
293
310
|
|
|
294
|
-
assert "abusecomplaints@markmonitor.com" in api_call.emails()
|
|
295
|
-
|
|
296
311
|
|
|
297
312
|
@vcr.use_cassette
|
|
298
313
|
def test_whois_history():
|
|
@@ -450,6 +465,73 @@ def test_iris_investigate():
|
|
|
450
465
|
assert result["domain"] in ["amazon.com", "google.com"]
|
|
451
466
|
|
|
452
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
|
+
|
|
453
535
|
@vcr.use_cassette
|
|
454
536
|
def test_iris_detect_monitors():
|
|
455
537
|
with pytest.raises(ValueError):
|
|
@@ -476,7 +558,7 @@ def test_iris_detect_watched_domains():
|
|
|
476
558
|
detect_results = api.iris_detect_watched_domains(
|
|
477
559
|
monitor_id="nAwmQg2pqg", sort=["risk_score"], order="desc"
|
|
478
560
|
)
|
|
479
|
-
assert len(detect_results["watchlist_domains"]) ==
|
|
561
|
+
assert len(detect_results["watchlist_domains"]) == 5
|
|
480
562
|
|
|
481
563
|
detect_results = api.iris_detect_watched_domains(escalation_types="blocked")
|
|
482
564
|
assert detect_results["count"] == 1
|
|
@@ -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_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_decorators.py
RENAMED
|
@@ -217,3 +217,35 @@ class TestApiEndpointDecorator:
|
|
|
217
217
|
call_kwargs = mock_validate.call_args[1]
|
|
218
218
|
|
|
219
219
|
assert call_kwargs.get("parameters") == {"name": "test-name"}
|
|
220
|
+
|
|
221
|
+
def test_none_arguments_are_filtered_before_validation(self, mock_client):
|
|
222
|
+
"""
|
|
223
|
+
Test that parameters with None values are excluded from the arguments
|
|
224
|
+
passed to the validator.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
@api_endpoint(spec_name="v1", path="/users", methods="POST")
|
|
228
|
+
def create_user(name=None, status=None):
|
|
229
|
+
return "Success"
|
|
230
|
+
|
|
231
|
+
with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
|
|
232
|
+
create_user(mock_client, name="Alice", status=None)
|
|
233
|
+
|
|
234
|
+
call_kwargs = mock_validate.call_args[1]
|
|
235
|
+
assert call_kwargs.get("parameters") == {"name": "Alice"}
|
|
236
|
+
assert "status" not in call_kwargs.get("parameters", {})
|
|
237
|
+
|
|
238
|
+
def test_all_none_arguments_passes_empty_parameters(self, mock_client):
|
|
239
|
+
"""
|
|
240
|
+
Test that when all arguments are None, an empty dict is passed to the validator.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
@api_endpoint(spec_name="v1", path="/users", methods="POST")
|
|
244
|
+
def create_user(name=None, body=None):
|
|
245
|
+
return "Success"
|
|
246
|
+
|
|
247
|
+
with patch("domaintools.request_validator.RequestValidator.validate") as mock_validate:
|
|
248
|
+
create_user(mock_client, name=None, body=None)
|
|
249
|
+
|
|
250
|
+
call_kwargs = mock_validate.call_args[1]
|
|
251
|
+
assert call_kwargs.get("parameters") == {}
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/tests/test_filters.py
RENAMED
|
@@ -16,20 +16,17 @@ class DTFiltersTest(unittest.TestCase):
|
|
|
16
16
|
domaintools_iris_result = iris_investigate_data.domaintools()
|
|
17
17
|
int_chase_iris_result = iris_investigate_data.int_chase()
|
|
18
18
|
|
|
19
|
-
test_results = {
|
|
20
|
-
"results": domaintools_iris_result["results"]
|
|
21
|
-
+ int_chase_iris_result["results"]
|
|
22
|
-
}
|
|
19
|
+
test_results = {"results": domaintools_iris_result["results"] + int_chase_iris_result["results"]}
|
|
23
20
|
|
|
24
21
|
self.dt_res_filter = DTResultFilter(result_set=test_results)
|
|
25
22
|
|
|
26
23
|
def test_filter_by_riskscore(self):
|
|
27
24
|
"""Test domaintools filter by riskcore"""
|
|
28
|
-
|
|
25
|
+
risk_score_threshold = 69
|
|
29
26
|
|
|
30
27
|
result = self.dt_res_filter.by(
|
|
31
28
|
[
|
|
32
|
-
filter_by_riskscore(threshold=
|
|
29
|
+
filter_by_riskscore(threshold=risk_score_threshold),
|
|
33
30
|
]
|
|
34
31
|
)
|
|
35
32
|
|
|
@@ -77,9 +74,7 @@ class DTFiltersTest(unittest.TestCase):
|
|
|
77
74
|
|
|
78
75
|
results = self.dt_res_filter.by(
|
|
79
76
|
[
|
|
80
|
-
filter_by_field(
|
|
81
|
-
field=include_domains_with_missing_field, filter_type="include"
|
|
82
|
-
),
|
|
77
|
+
filter_by_field(field=include_domains_with_missing_field, filter_type="include"),
|
|
83
78
|
]
|
|
84
79
|
)
|
|
85
80
|
|
|
@@ -90,9 +85,7 @@ class DTFiltersTest(unittest.TestCase):
|
|
|
90
85
|
|
|
91
86
|
results = self.dt_res_filter.by(
|
|
92
87
|
[
|
|
93
|
-
filter_by_field(
|
|
94
|
-
field=exclude_domains_with_missing_field, filter_type="exclude"
|
|
95
|
-
),
|
|
88
|
+
filter_by_field(field=exclude_domains_with_missing_field, filter_type="exclude"),
|
|
96
89
|
]
|
|
97
90
|
)
|
|
98
91
|
|
|
@@ -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
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/__init__.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/cli/main.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/exceptions.py
RENAMED
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/filters.py
RENAMED
|
File without changes
|
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/results.py
RENAMED
|
File without changes
|
|
File without changes
|
{domaintools_api_test_upload-2.7.4 → domaintools_api_test_upload-2.8.0}/domaintools/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|