16 KiB
16 KiB
| name | description |
|---|---|
| domain-intel | Passive domain reconnaissance using Python stdlib. Use this skill for subdomain discovery, SSL certificate inspection, WHOIS lookups, DNS records, domain availability checks, and bulk multi-domain analysis. No API keys required. Triggers on requests like "find subdomains", "check ssl cert", "whois lookup", "is this domain available", "bulk check these domains". |
Domain Intelligence — Passive OSINT
Passive domain reconnaissance using only Python stdlib and public data sources.
Zero dependencies. Zero API keys. Works out of the box.
Data Sources
- crt.sh — Certificate Transparency logs (subdomain discovery)
- WHOIS servers — Direct TCP queries to 100+ authoritative TLD servers
- Google DNS-over-HTTPS — MX/NS/TXT/CNAME resolution
- System DNS — A/AAAA record resolution
Usage
When the user asks about a domain, use the terminal tool to run the appropriate Python snippet below.
All functions print structured JSON. Parse and summarize results for the user.
1. Subdomain Discovery (crt.sh)
import json, urllib.request, urllib.parse
from datetime import datetime, timezone
def subdomains(domain, include_expired=False, limit=200):
url = f"https://crt.sh/?q=%25.{urllib.parse.quote(domain)}&output=json"
req = urllib.request.Request(url, headers={"User-Agent": "domain-intel-skill/1.0", "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=15) as r:
entries = json.loads(r.read().decode())
seen, results = set(), []
for e in entries:
not_after = e.get("not_after", "")
if not include_expired and not_after:
try:
dt = datetime.strptime(not_after[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
if dt <= datetime.now(timezone.utc):
continue
except ValueError:
pass
for name in e.get("name_value", "").splitlines():
name = name.strip().lower()
if name and name not in seen:
seen.add(name)
results.append({"subdomain": name, "issuer": e.get("issuer_name",""), "not_after": not_after})
results.sort(key=lambda r: (r["subdomain"].startswith("*"), r["subdomain"]))
results = results[:limit]
print(json.dumps({"domain": domain, "count": len(results), "subdomains": results}, indent=2))
subdomains("DOMAIN_HERE")
Example: Replace DOMAIN_HERE with example.com
2. SSL Certificate Inspection
import json, ssl, socket
from datetime import datetime, timezone
def check_ssl(host, port=443, timeout=10):
def flat(rdns):
r = {}
for rdn in rdns:
for item in rdn:
if isinstance(item, (list,tuple)) and len(item)==2:
r[item[0]] = item[1]
return r
def extract_uris(entries):
return [e[-1] if isinstance(e,(list,tuple)) else str(e) for e in entries]
def parse_date(s):
for fmt in ("%b %d %H:%M:%S %Y %Z", "%b %d %H:%M:%S %Y %Z"):
try: return datetime.strptime(s, fmt).replace(tzinfo=timezone.utc)
except ValueError: pass
return None
warning = None
try:
ctx = ssl.create_default_context()
with socket.create_connection((host, port), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as s:
cert, cipher, proto = s.getpeercert(), s.cipher(), s.version()
except ssl.SSLCertVerificationError as e:
warning = str(e)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
with socket.create_connection((host, port), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as s:
cert, cipher, proto = s.getpeercert(), s.cipher(), s.version()
not_after = parse_date(cert.get("notAfter",""))
not_before = parse_date(cert.get("notBefore",""))
now = datetime.now(timezone.utc)
days = (not_after - now).days if not_after else None
is_expired = days is not None and days < 0
if is_expired: status = f"EXPIRED ({abs(days)} days ago)"
elif days is not None and days <= 14: status = f"CRITICAL — {days} day(s) left"
elif days is not None and days <= 30: status = f"WARNING — {days} day(s) left"
else: status = f"OK — {days} day(s) remaining" if days is not None else "unknown"
print(json.dumps({
"host": host, "port": port,
"subject": flat(cert.get("subject",[])),
"issuer": flat(cert.get("issuer",[])),
"subject_alt_names": [f"{t}:{v}" for t,v in cert.get("subjectAltName",[])],
"not_before": not_before.isoformat() if not_before else "",
"not_after": not_after.isoformat() if not_after else "",
"days_remaining": days, "is_expired": is_expired, "expiry_status": status,
"tls_version": proto, "cipher_suite": cipher[0] if cipher else None,
"serial_number": cert.get("serialNumber",""),
"ocsp_urls": extract_uris(cert.get("OCSP",[])),
"ca_issuers": extract_uris(cert.get("caIssuers",[])),
"verification_warning": warning,
}, indent=2))
check_ssl("DOMAIN_HERE")
3. WHOIS Lookup (100+ TLDs)
import json, socket, re
from datetime import datetime, timezone
WHOIS_SERVERS = {
"com":"whois.verisign-grs.com","net":"whois.verisign-grs.com","org":"whois.pir.org",
"io":"whois.nic.io","co":"whois.nic.co","ai":"whois.nic.ai","dev":"whois.nic.google",
"app":"whois.nic.google","tech":"whois.nic.tech","shop":"whois.nic.shop",
"store":"whois.nic.store","online":"whois.nic.online","site":"whois.nic.site",
"cloud":"whois.nic.cloud","digital":"whois.nic.digital","media":"whois.nic.media",
"blog":"whois.nic.blog","info":"whois.afilias.net","biz":"whois.biz",
"me":"whois.nic.me","tv":"whois.nic.tv","cc":"whois.nic.cc","ws":"whois.website.ws",
"uk":"whois.nic.uk","co.uk":"whois.nic.uk","de":"whois.denic.de","nl":"whois.domain-registry.nl",
"fr":"whois.nic.fr","it":"whois.nic.it","es":"whois.nic.es","pl":"whois.dns.pl",
"ru":"whois.tcinet.ru","se":"whois.iis.se","no":"whois.norid.no","fi":"whois.fi",
"ch":"whois.nic.ch","at":"whois.nic.at","be":"whois.dns.be","cz":"whois.nic.cz",
"br":"whois.registro.br","ca":"whois.cira.ca","mx":"whois.mx","au":"whois.auda.org.au",
"jp":"whois.jprs.jp","cn":"whois.cnnic.cn","in":"whois.inregistry.net","kr":"whois.kr",
"sg":"whois.sgnic.sg","hk":"whois.hkirc.hk","tr":"whois.nic.tr","ae":"whois.aeda.net.ae",
"za":"whois.registry.net.za","ng":"whois.nic.net.ng","ly":"whois.nic.ly",
"space":"whois.nic.space","zone":"whois.nic.zone","ninja":"whois.nic.ninja",
"guru":"whois.nic.guru","rocks":"whois.nic.rocks","social":"whois.nic.social",
"network":"whois.nic.network","global":"whois.nic.global","design":"whois.nic.design",
"studio":"whois.nic.studio","agency":"whois.nic.agency","finance":"whois.nic.finance",
"legal":"whois.nic.legal","health":"whois.nic.health","green":"whois.nic.green",
"city":"whois.nic.city","land":"whois.nic.land","live":"whois.nic.live",
"game":"whois.nic.game","games":"whois.nic.games","pw":"whois.nic.pw",
"mn":"whois.nic.mn","sh":"whois.nic.sh","gg":"whois.gg","im":"whois.nic.im",
}
def whois_query(domain, server, port=43):
with socket.create_connection((server, port), timeout=10) as s:
s.sendall((domain+"\r\n").encode())
chunks = []
while True:
c = s.recv(4096)
if not c: break
chunks.append(c)
return b"".join(chunks).decode("utf-8", errors="replace")
def parse_iso(s):
if not s: return None
for fmt in ("%Y-%m-%dT%H:%M:%S","%Y-%m-%dT%H:%M:%SZ","%Y-%m-%d %H:%M:%S","%Y-%m-%d"):
try: return datetime.strptime(s[:19],fmt).replace(tzinfo=timezone.utc)
except ValueError: pass
return None
def whois(domain):
parts = domain.split(".")
server = WHOIS_SERVERS.get(".".join(parts[-2:])) or WHOIS_SERVERS.get(parts[-1])
if not server:
print(json.dumps({"error": f"No WHOIS server for .{parts[-1]}"}))
return
try:
raw = whois_query(domain, server)
except Exception as e:
print(json.dumps({"error": str(e)}))
return
patterns = {
"registrar": r"(?:Registrar|registrar):\s*(.+)",
"creation_date": r"(?:Creation Date|Created|created):\s*(.+)",
"expiration_date": r"(?:Registry Expiry Date|Expiration Date|Expiry Date):\s*(.+)",
"updated_date": r"(?:Updated Date|Last Modified):\s*(.+)",
"name_servers": r"(?:Name Server|nserver):\s*(.+)",
"status": r"(?:Domain Status|status):\s*(.+)",
"dnssec": r"DNSSEC:\s*(.+)",
}
result = {"domain": domain, "whois_server": server}
for key, pat in patterns.items():
matches = re.findall(pat, raw, re.IGNORECASE)
if matches:
if key in ("name_servers","status"):
result[key] = list(dict.fromkeys(m.strip().lower() for m in matches))
else:
result[key] = matches[0].strip()
for field in ("creation_date","expiration_date","updated_date"):
if field in result:
dt = parse_iso(result[field][:19])
if dt:
result[field] = dt.isoformat()
if field == "expiration_date":
days = (dt - datetime.now(timezone.utc)).days
result["expiration_days_remaining"] = days
result["is_expired"] = days < 0
print(json.dumps(result, indent=2))
whois("DOMAIN_HERE")
4. DNS Records
import json, socket, urllib.request, urllib.parse
def dns(domain, types=None):
if not types: types = ["A","AAAA","MX","NS","TXT","CNAME"]
records = {}
for qtype in types:
if qtype == "A":
try: records["A"] = list(dict.fromkeys(i[4][0] for i in socket.getaddrinfo(domain,None,socket.AF_INET)))
except: records["A"] = []
elif qtype == "AAAA":
try: records["AAAA"] = list(dict.fromkeys(i[4][0] for i in socket.getaddrinfo(domain,None,socket.AF_INET6)))
except: records["AAAA"] = []
else:
url = f"https://dns.google/resolve?name={urllib.parse.quote(domain)}&type={qtype}"
try:
req = urllib.request.Request(url, headers={"User-Agent":"domain-intel-skill/1.0"})
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
records[qtype] = [a.get("data","").strip().rstrip(".") for a in data.get("Answer",[]) if a.get("data")]
except:
records[qtype] = []
print(json.dumps({"domain": domain, "records": records}, indent=2))
dns("DOMAIN_HERE")
5. Domain Availability Check
import json, socket, ssl
def available(domain):
import urllib.request, urllib.parse, re
from datetime import datetime, timezone
signals = {}
# DNS check
try: a = [i[4][0] for i in socket.getaddrinfo(domain,None,socket.AF_INET)]
except: a = []
try: ns_url = f"https://dns.google/resolve?name={urllib.parse.quote(domain)}&type=NS"
req = urllib.request.Request(ns_url, headers={"User-Agent":"domain-intel-skill/1.0"})
with urllib.request.urlopen(req, timeout=10) as r:
ns = [x.get("data","") for x in json.loads(r.read()).get("Answer",[])]
except: ns = []
signals["dns_a"] = a
signals["dns_ns"] = ns
dns_exists = bool(a or ns)
# SSL check
ssl_up = False
try:
ctx = ssl.create_default_context()
ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
with socket.create_connection((domain,443),timeout=3) as s:
with ctx.wrap_socket(s, server_hostname=domain): ssl_up = True
except: pass
signals["ssl_reachable"] = ssl_up
# WHOIS check (simple)
WHOIS = {"com":"whois.verisign-grs.com","net":"whois.verisign-grs.com","org":"whois.pir.org",
"io":"whois.nic.io","co":"whois.nic.co","ai":"whois.nic.ai","dev":"whois.nic.google",
"me":"whois.nic.me","app":"whois.nic.google","tech":"whois.nic.tech"}
tld = domain.rsplit(".",1)[-1]
whois_avail = None
whois_note = ""
server = WHOIS.get(tld)
if server:
try:
with socket.create_connection((server,43),timeout=10) as s:
s.sendall((domain+"\r\n").encode())
raw = b""
while True:
c = s.recv(4096)
if not c: break
raw += c
raw = raw.decode("utf-8",errors="replace").lower()
if any(p in raw for p in ["no match","not found","no data found","status: free"]):
whois_avail = True; whois_note = "WHOIS: not found"
elif "registrar:" in raw or "creation date:" in raw:
whois_avail = False; whois_note = "WHOIS: registered"
else: whois_note = "WHOIS: inconclusive"
except Exception as e: whois_note = f"WHOIS error: {e}"
signals["whois_available"] = whois_avail
signals["whois_note"] = whois_note
if not dns_exists and whois_avail is True: verdict,conf = "LIKELY AVAILABLE","high"
elif dns_exists or whois_avail is False or ssl_up: verdict,conf = "REGISTERED / IN USE","high"
elif not dns_exists and whois_avail is None: verdict,conf = "POSSIBLY AVAILABLE","medium"
else: verdict,conf = "UNCERTAIN","low"
print(json.dumps({"domain":domain,"verdict":verdict,"confidence":conf,"signals":signals},indent=2))
available("DOMAIN_HERE")
6. Bulk Analysis (Multiple Domains in Parallel)
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
# Paste any of the functions above (check_ssl, whois, dns, available, subdomains)
# then use this runner:
def bulk_check(domains, checks=None, max_workers=5):
if not checks: checks = ["ssl", "whois", "dns", "available"]
def run_one(domain):
result = {"domain": domain}
# Import/define individual functions above, then:
if "ssl" in checks:
try: result["ssl"] = json.loads(check_ssl_json(domain))
except Exception as e: result["ssl"] = {"error": str(e)}
if "whois" in checks:
try: result["whois"] = json.loads(whois_json(domain))
except Exception as e: result["whois"] = {"error": str(e)}
if "dns" in checks:
try: result["dns"] = json.loads(dns_json(domain))
except Exception as e: result["dns"] = {"error": str(e)}
if "available" in checks:
try: result["available"] = json.loads(available_json(domain))
except Exception as e: result["available"] = {"error": str(e)}
return result
results = []
with ThreadPoolExecutor(max_workers=min(max_workers,10)) as ex:
futures = {ex.submit(run_one, d): d for d in domains[:20]}
for f in as_completed(futures):
results.append(f.result())
print(json.dumps({"total": len(results), "checks": checks, "results": results}, indent=2))
Quick Reference
| Task | What to run |
|---|---|
| Find subdomains | Snippet 1 — replace DOMAIN_HERE |
| Check SSL cert | Snippet 2 — replace DOMAIN_HERE |
| WHOIS lookup | Snippet 3 — replace DOMAIN_HERE |
| DNS records | Snippet 4 — replace DOMAIN_HERE |
| Is domain available? | Snippet 5 — replace DOMAIN_HERE |
| Bulk check 20 domains | Snippet 6 |
Notes
- All requests are passive — no active scanning, no packets sent to target hosts (except SSL check which makes a TCP connection)
subdomainsonly queries crt.sh — the target domain is never contacted- WHOIS queries go to registrar servers, not the target
- Results are structured JSON — summarize key findings for the user
- For expired cert warnings or WHOIS redaction, mention these to the user as notable findings