Skip to content

Commit 390a27d

Browse files
elhoimclaude
andcommitted
[medium] Add parking-domain-ns warninglist generator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 99b7e88 commit 390a27d

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

generate_all.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ python3 generate-leaseweb.py
3333
python3 generate_phone_numbers.py
3434
#python3 generate-stackpath.py # source https://k3t9x2h3.map2.ssl.hwcdn.net/ipblocks.txt is dead (NXDOMAIN); StackPath wound down its CDN and hwcdn.net is now a parked domain-for-sale page
3535
python3 generate-tlds.py
36+
python3 generate-parking-domain-ns.py
3637
python3 generate-salesforce.py
3738
python3 generate-github.py
3839
python3 generate-vultr.py
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Generate the parking-domain name server warninglist.
5+
6+
Source: https://github.com/tma22-parking/tma22-parking.github.io
7+
(parking_services.json) -- the indicator set published alongside "Domain
8+
Parking: Largely Present, Rarely Considered!" (Zirngibl, Deusch, Sattler,
9+
Aulbach, Carle and Jonker, TMA 2022). It enumerates 82 parking services and,
10+
for each, the NS, A, AAAA and CNAME records that identify a domain parked with
11+
it. Only the NS indicators are used here, which is exactly what this list
12+
holds.
13+
14+
Why this source: it is the only *published, structured, peer-reviewed*
15+
enumeration of parking-service name servers found. The alternatives are
16+
personal gists covering roughly ten services -- far less than the 129 entries
17+
already committed here.
18+
19+
**Its limitation, stated plainly: it is a 2022 research artefact, not a live
20+
feed.** It does not grow when a new parking service appears. Running this
21+
generator therefore reconciles the list against a fixed, citable body of
22+
evidence and adds what is missing; it does not keep the list current on its
23+
own. That is still worth automating -- it replaces hand-transcription with a
24+
reproducible import -- but a maintainer should not read a successful run as
25+
"the list is now complete".
26+
27+
**This generator unions, it never replaces.** Entries committed here that the
28+
paper does not cover are kept untouched.
29+
30+
The NS indicators come in two shapes: literal host names ("ns1.dan.com") and
31+
SQL LIKE patterns ("ns%.parkingcrew.net."). Both are reduced to the
32+
registrable domain this list stores ("dan.com", "parkingcrew.net"), because
33+
that is the convention of the committed entries -- "bodis.com", not
34+
"ns1.bodis.com". A pattern whose wildcard falls inside the *domain* rather
35+
than the host label (e.g. "%.parking%.com") is skipped rather than guessed at:
36+
expanding it would claim domains nobody has shown to be parking services.
37+
"""
38+
39+
import json
40+
import logging
41+
import re
42+
43+
from generator import download, get_abspath_list_file, get_version, write_to_file
44+
45+
URL = (
46+
"https://raw.githubusercontent.com/tma22-parking/"
47+
"tma22-parking.github.io/main/parking_services.json"
48+
)
49+
50+
DST = "parking-domain-ns"
51+
52+
# A host name made only of ordinary labels: no SQL wildcard anywhere.
53+
CLEAN_HOST = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$")
54+
55+
56+
def registrable_domain(host):
57+
"""Reduce a name-server host name to the domain this list stores.
58+
59+
"ns1.bodis.com" -> "bodis.com". Deliberately the last two labels rather
60+
than a public-suffix lookup: every entry in the committed list is a plain
61+
two-label domain, the source's name servers are all under ordinary gTLDs,
62+
and adding a Public Suffix List fetch for this would be a dependency the
63+
result does not need. A name under a multi-part suffix (".co.uk") would be
64+
over-reduced, so those are rejected below instead of being guessed at.
65+
"""
66+
labels = host.strip(".").split(".")
67+
if len(labels) < 2:
68+
return None
69+
return ".".join(labels[-2:])
70+
71+
72+
# Suffixes where the last two labels are not a registrable domain. The source
73+
# uses none of these today; the guard exists so that if one is ever added it
74+
# is skipped loudly rather than reduced to a public suffix such as "co.uk",
75+
# which as a warninglist entry would match a large part of a national TLD.
76+
MULTIPART_SUFFIXES = (
77+
".co.uk", ".org.uk", ".ac.uk", ".gov.uk", ".co.jp", ".com.au", ".co.nz",
78+
".com.br", ".co.za", ".com.cn", ".net.cn", ".org.cn", ".co.in", ".com.mx",
79+
)
80+
81+
82+
def extract_ns_domains(services):
83+
domains = set()
84+
for key, service in services.items():
85+
records = (service or {}).get("record_types") or {}
86+
for indicator in records.get("ns") or []:
87+
if isinstance(indicator, dict):
88+
pattern = indicator.get("ilike")
89+
if not pattern:
90+
continue
91+
value = pattern.strip().lower().rstrip(".")
92+
# Accept a wildcard only in the leading host label
93+
# ("ns%.bodis.com"): the rest must be a literal domain.
94+
head, _, tail = value.partition(".")
95+
if "%" in tail or "_" in tail or not tail:
96+
logging.warning(
97+
"%s: skipping NS pattern with a wildcard in the domain: %s",
98+
key, pattern,
99+
)
100+
continue
101+
candidate = tail
102+
else:
103+
value = str(indicator).strip().lower().rstrip(".")
104+
if not value or "%" in value or "_" in value:
105+
logging.warning("%s: skipping NS indicator: %s", key, indicator)
106+
continue
107+
candidate = value
108+
109+
if not CLEAN_HOST.match(candidate):
110+
logging.warning("%s: skipping unparseable NS host: %s", key, indicator)
111+
continue
112+
if candidate.endswith(MULTIPART_SUFFIXES):
113+
logging.warning(
114+
"%s: skipping %s -- under a multi-part public suffix, "
115+
"reducing it would produce an over-broad entry", key, candidate,
116+
)
117+
continue
118+
119+
domain = registrable_domain(candidate)
120+
if domain:
121+
domains.add(domain)
122+
return domains
123+
124+
125+
def existing_warninglist():
126+
try:
127+
with open(get_abspath_list_file(DST)) as data_file:
128+
return json.load(data_file)
129+
except (IOError, OSError, ValueError):
130+
return None
131+
132+
133+
def main():
134+
response = download(URL)
135+
response.raise_for_status()
136+
137+
services = response.json()
138+
if not isinstance(services, dict) or not services:
139+
raise Exception("Unexpected upstream shape: expected a JSON object of services")
140+
141+
fetched = extract_ns_domains(services)
142+
if not fetched:
143+
raise Exception(
144+
"No parking name servers found upstream, refusing to write an empty list"
145+
)
146+
147+
warninglist = existing_warninglist()
148+
if warninglist is None:
149+
raise Exception(
150+
"lists/{}/list.json is missing; this generator maintains an "
151+
"existing curated list and will not create one from scratch".format(DST)
152+
)
153+
154+
committed = set(warninglist.get("list", []))
155+
merged = committed.union(fetched)
156+
logging.info(
157+
"parking-domain-ns: %d services, %d committed + %d fetched -> %d "
158+
"after union (%d new)",
159+
len(services), len(committed), len(fetched), len(merged),
160+
len(merged) - len(committed),
161+
)
162+
163+
warninglist["list"] = sorted(merged)
164+
warninglist["version"] = get_version()
165+
write_to_file(warninglist, DST)
166+
167+
168+
if __name__ == "__main__":
169+
main()

0 commit comments

Comments
 (0)