Merge pull request #136 from FurkanL0/feat/domain-intel-skill

feat(skills): add passive domain intelligence skill — subdomains, SSL, WHOIS, DNS, availability
This commit is contained in:
Teknium
2026-02-27 23:47:50 -08:00
committed by GitHub
2 changed files with 416 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
---
name: domain-intel
description: 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".
license: MIT
---
Passive domain intelligence using only Python stdlib and public data sources.
Zero dependencies. Zero API keys. Works out of the box.
## Capabilities
- Subdomain discovery via crt.sh certificate transparency logs
- Live SSL/TLS certificate inspection (expiry, cipher, SANs, TLS version)
- WHOIS lookup — supports 100+ TLDs via direct TCP queries
- DNS records: A, AAAA, MX, NS, TXT, CNAME
- Domain availability check (DNS + WHOIS + SSL signals)
- Bulk multi-domain analysis in parallel (up to 20 domains)
## Data Sources
- crt.sh — Certificate Transparency logs
- WHOIS servers — Direct TCP to 100+ authoritative TLD servers
- Google DNS-over-HTTPS — MX/NS/TXT/CNAME resolution
- System DNS — A/AAAA records

View File

@@ -0,0 +1,392 @@
---
name: domain-intel
description: 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)
```python
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
```python
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)
```python
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
```python
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
```python
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)
```python
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)
- `subdomains` only 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