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:
24
skills/domain/DESCRIPTION.md
Normal file
24
skills/domain/DESCRIPTION.md
Normal 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
|
||||
392
skills/domain/domain-intel/SKILL.md
Normal file
392
skills/domain/domain-intel/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user