Skip to content

Commit f081335

Browse files
authored
Merge pull request #143 from DomainTools/IDEV-1995-implement-parsed-domain-rdap-feed
IDEV-1995: implement parsed domain rdap feed
2 parents 48da7e0 + 95ae2cc commit f081335

File tree

9 files changed

+434
-63
lines changed

9 files changed

+434
-63
lines changed

domaintools/api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,3 +1085,18 @@ def nad(self, **kwargs):
10851085
response_path=(),
10861086
**kwargs,
10871087
)
1088+
1089+
def domainrdap(self, **kwargs):
1090+
"""Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP)"""
1091+
sessionID = kwargs.get("sessionID")
1092+
after = kwargs.get("after")
1093+
before = kwargs.get("before")
1094+
if not (sessionID or after or before):
1095+
raise ValueError("sessionID or after or before must be defined")
1096+
1097+
return self._results(
1098+
"domain-registration-data-access-protocol-feed-(api)",
1099+
"v1/feed/domainrdap/",
1100+
response_path=(),
1101+
**kwargs,
1102+
)

domaintools/base_results.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def data(self):
138138
return self._data
139139

140140
def check_limit_exceeded(self):
141-
if self.kwargs.get("format", "json") == "json":
141+
if self.kwargs.get("format", "json") == "json" and self.product not in get_feeds_products_list():
142142
if "response" in self._data and "limit_exceeded" in self._data["response"] and self._data["response"]["limit_exceeded"] is True:
143143
return True, self._data["response"]["message"]
144144
# TODO: handle html, xml response errors better.

domaintools/cli/api.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import _io
66

7+
from datetime import datetime
78
from typing import Optional, Dict, Tuple
89
from rich.progress import Progress, SpinnerColumn, TextColumn
910

@@ -28,11 +29,21 @@ def print_api_version(value: bool):
2829
def validate_format_input(value: str):
2930
VALID_FORMATS = ("list", "json", "xml", "html")
3031
if value not in VALID_FORMATS:
31-
raise typer.BadParameter(
32-
f"{value} is not in available formats: {VALID_FORMATS}"
33-
)
32+
raise typer.BadParameter(f"{value} is not in available formats: {VALID_FORMATS}")
3433
return value
3534

35+
@staticmethod
36+
def validate_after_or_before_input(value: str):
37+
if value is None or value.replace("-", "").isdigit():
38+
return value
39+
40+
# Checks if value is a valid ISO 8601 datetime string in UTC form
41+
try:
42+
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
43+
return value
44+
except:
45+
raise typer.BadParameter(f"{value} is neither an integer or a valid ISO 8601 datetime string in UTC form")
46+
3647
@staticmethod
3748
def validate_source_file_extension(value: str):
3849
"""Validates source file extension.
@@ -51,9 +62,7 @@ def validate_source_file_extension(value: str):
5162
ext = get_file_extension(value)
5263

5364
if ext.lower() not in VALID_EXTENSIONS:
54-
raise typer.BadParameter(
55-
f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}"
56-
)
65+
raise typer.BadParameter(f"{value} is not in valid extensions. Valid file extensions: {VALID_EXTENSIONS}")
5766

5867
return value
5968

@@ -85,11 +94,7 @@ def args_to_dict(*args) -> Dict:
8594
def _get_formatted_output(cls, cmd_name: str, response, out_format: str = "json"):
8695
if cmd_name in ("available_api_calls",):
8796
return "\n".join(response)
88-
return str(
89-
getattr(response, out_format)
90-
if out_format != "list"
91-
else response.as_list()
92-
)
97+
return str(getattr(response, out_format) if out_format != "list" else response.as_list())
9398

9499
@classmethod
95100
def _get_credentials(cls, params: Optional[Dict] = {}) -> Tuple[str]:
@@ -106,9 +111,7 @@ def _get_credentials(cls, params: Optional[Dict] = {}) -> Tuple[str]:
106111
with open(creds_file, "r") as cf:
107112
user, key = cf.readline().strip(), cf.readline().strip()
108113
except FileNotFoundError as e:
109-
raise typer.BadParameter(
110-
f"{str(e)}. Please create one first and try again."
111-
)
114+
raise typer.BadParameter(f"{str(e)}. Please create one first and try again.")
112115

113116
return user, key
114117

@@ -198,9 +201,7 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
198201
total=None,
199202
)
200203

201-
output = cls._get_formatted_output(
202-
cmd_name=name, response=response, out_format=response_format
203-
)
204+
output = cls._get_formatted_output(cmd_name=name, response=response, out_format=response_format)
204205

205206
if isinstance(out_file, _io.TextIOWrapper):
206207
# use rich `print` command to prettify the ouput in sys.stdout
@@ -215,10 +216,7 @@ def run(cls, name: str, params: Optional[Dict] = {}, **kwargs):
215216
_reason = getattr(e, "reason", {})
216217
# check data type first as some of the reasons is just plain text
217218
if isinstance(_reason, dict):
218-
_reason = (
219-
_reason.get("error", {}).get("message")
220-
or "Unknown Error occured."
221-
)
219+
_reason = _reason.get("error", {}).get("message") or "Unknown Error occured."
222220

223221
reason = typer.style(_reason, bg=typer.colors.RED)
224222

domaintools/cli/commands/feeds.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ def feeds_nad(
3535
help="Output format in {'list', 'json', 'xml', 'html'}",
3636
callback=DTCLICommand.validate_format_input,
3737
),
38-
out_file: typer.FileTextWrite = typer.Option(
39-
sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
40-
),
38+
out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
4139
no_verify_ssl: bool = typer.Option(
4240
False,
4341
"--no-verify-ssl",
@@ -100,9 +98,7 @@ def feeds_nod(
10098
help="Output format in {'list', 'json', 'xml', 'html'}",
10199
callback=DTCLICommand.validate_format_input,
102100
),
103-
out_file: typer.FileTextWrite = typer.Option(
104-
sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"
105-
),
101+
out_file: typer.FileTextWrite = typer.Option(sys.stdout, "-o", "--out-file", help="Output file (defaults to stdout)"),
106102
no_verify_ssl: bool = typer.Option(
107103
False,
108104
"--no-verify-ssl",
@@ -136,3 +132,59 @@ def feeds_nod(
136132
),
137133
):
138134
DTCLICommand.run(name=c.FEEDS_NOD, params=ctx.params)
135+
136+
137+
@dt_cli.command(
138+
name=c.FEEDS_DOMAINRDAP,
139+
help=get_cli_helptext_by_name(command_name=c.FEEDS_DOMAINRDAP),
140+
)
141+
def feeds_domainrdap(
142+
ctx: typer.Context,
143+
user: str = typer.Option(None, "-u", "--user", help="Domaintools API Username."),
144+
key: str = typer.Option(None, "-k", "--key", help="DomainTools API key"),
145+
creds_file: str = typer.Option(
146+
"~/.dtapi",
147+
"-c",
148+
"--credfile",
149+
help="Optional file with API username and API key, one per line.",
150+
),
151+
no_verify_ssl: bool = typer.Option(
152+
False,
153+
"--no-verify-ssl",
154+
help="Skip verification of SSL certificate when making HTTPs API calls",
155+
),
156+
no_sign_api_key: bool = typer.Option(
157+
False,
158+
"--no-sign-api-key",
159+
help="Skip signing of api key",
160+
),
161+
sessionID: str = typer.Option(
162+
None,
163+
"--session-id",
164+
help="Unique identifier for the session",
165+
),
166+
after: str = typer.Option(
167+
None,
168+
"--after",
169+
help="Start of the time window, relative to the current time in seconds, for which data will be provided",
170+
callback=DTCLICommand.validate_after_or_before_input,
171+
),
172+
before: str = typer.Option(
173+
None,
174+
"--before",
175+
help="The end of the query window in seconds, relative to the current time, inclusive",
176+
callback=DTCLICommand.validate_after_or_before_input,
177+
),
178+
domain: str = typer.Option(
179+
None,
180+
"-d",
181+
"--domain",
182+
help="A string value used to filter feed results",
183+
),
184+
top: str = typer.Option(
185+
None,
186+
"--top",
187+
help="Number of results to return in the response payload",
188+
),
189+
):
190+
DTCLICommand.run(name=c.FEEDS_DOMAINRDAP, params=ctx.params)

domaintools/cli/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@
4646
# feeds
4747
FEEDS_NAD = "nad"
4848
FEEDS_NOD = "nod"
49+
FEEDS_DOMAINRDAP = "domainrdap"

domaintools/cli/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def _phisheye_termlist():
8484
c.IRIS_DETECT_IGNORED_DOMAINS: "Returns back a list of ignored domains in Iris Detect based on the provided filters.",
8585
c.FEEDS_NAD: "Returns back newly active domains feed.",
8686
c.FEEDS_NOD: "Returns back newly observed domains feed.",
87+
c.FEEDS_DOMAINRDAP: "Returns changes to global domain registration information, populated by the Registration Data Access Protocol (RDAP).",
8788
}
8889

8990

domaintools/utils.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ def get_domain_age(create_date):
1919
try:
2020
create_date = datetime.strptime(create_date, "%Y%m%d")
2121
except ValueError:
22-
raise ValueError(
23-
"Invalid date format. Supported formats are %Y-%m-%d and %Y%m%d."
24-
)
22+
raise ValueError("Invalid date format. Supported formats are %Y-%m-%d and %Y%m%d.")
2523

2624
time_diff = datetime.now() - create_date
2725

@@ -110,11 +108,7 @@ def prune_data(data_obj):
110108
prune_data(item)
111109
if not isinstance(item, int) and not item:
112110
items_to_prune.append(index)
113-
data_obj[:] = [
114-
item
115-
for index, item in enumerate(data_obj)
116-
if index not in items_to_prune and len(item)
117-
]
111+
data_obj[:] = [item for index, item in enumerate(data_obj) if index not in items_to_prune and len(item)]
118112

119113

120114
def find_emails(data_str):
@@ -151,9 +145,7 @@ def get_pivots(data_obj, name, return_data=None, count=0, pivot_threshold=500):
151145
for k, v in data_obj.items():
152146
if isinstance(data_obj[k], (dict, list)):
153147
name = "{}_{}".format(name, k)
154-
temp_data = get_pivots(
155-
data_obj[k], name, return_data, count, pivot_threshold
156-
)
148+
temp_data = get_pivots(data_obj[k], name, return_data, count, pivot_threshold)
157149
if temp_data:
158150
return_data.append([name[1:].upper().replace("_", " "), temp_data])
159151
name = temp_name
@@ -175,14 +167,13 @@ def get_pivots(data_obj, name, return_data=None, count=0, pivot_threshold=500):
175167
return return_data
176168

177169

178-
def convert_str_to_dateobj(
179-
string_date: str, date_format: Optional[str] = "%Y-%m-%d"
180-
) -> datetime:
170+
def convert_str_to_dateobj(string_date: str, date_format: Optional[str] = "%Y-%m-%d") -> datetime:
181171
return datetime.strptime(string_date, date_format)
182172

183173

184174
def get_feeds_products_list():
185175
return [
186176
"newly-active-domains-feed-(api)",
187177
"newly-observed-domains-feed-(api)",
178+
"domain-registration-data-access-protocol-feed-(api)",
188179
]

0 commit comments

Comments
 (0)