-
Notifications
You must be signed in to change notification settings - Fork 348
Surface Web TLD Domain Enumerate
Xiè Jí'ān Jieyab 谢吉 edited this page Aug 19, 2026
·
1 revision
Python
python -m http.server 8080 Library
requests
dnspython
Script Python
#!/usr/bin/env python3
"""
Domain TLD Enumerator
======================
Example:
python3 domain_tld_enum.py --name contohbrand
python3 domain_tld_enum.py --name contohbrand --tlds com,id,su,st,cx
python3 domain_tld_enum.py --name contohbrand --brute --brute-max-len 3
python3 domain_tld_enum.py --name contohbrand --brute --no-common --brute-max-len 2
# Brute-force the SECOND extension behind a fixed first suffix
# e.g. --brute2-suffix1 co -> abc.co.aa, abc.co.ab, ... abc.co.zz
python3 domain_tld_enum.py --name contohbrand --brute2 --brute2-suffix1 co
# Multiple first suffixes at once, each gets its own brute-forced second extension
# e.g. abc.co.aa..zz, abc.go.aa..zz, abc.web.aa..zz
python3 domain_tld_enum.py --name contohbrand --brute2 --brute2-suffix1 co,go,web
# Longer second extension (2-3 chars), combined with common TLDs
python3 domain_tld_enum.py --name contohbrand --brute2 --brute2-suffix1 co --brute2-max-len 3
"""
import argparse
import concurrent.futures
import itertools
import json
import os
import re
import signal
import socket
import string
import sys
import time
import requests
import dns.resolver
try:
from shutil import get_terminal_size
except ImportError:
get_terminal_size = None
requests.packages.urllib3.disable_warnings()
DEFAULT_TIMEOUT = 6
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240"
class Colors:
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BOLD = "\033[1m"
RESET = "\033[0m"
@staticmethod
def disable():
Colors.GREEN = Colors.RED = Colors.YELLOW = Colors.BOLD = Colors.RESET = ""
def green(text):
return f"{Colors.GREEN}{text}{Colors.RESET}"
# Wordlist (TLD + ccTLD + gTLD + vanity)
COMMON_TLDS = [
# All region goverment
"gov","mil","go.id","mil.id","gov.au","gov.br",
"gov.cn","gov.hk","gov.in","gov.ir","gov.my","gov.pk",
"gov.pl","gov.ro","gov.ru","gov.sg","gov.tr","gov.uk",
"gob.ar","gob.bo","gob.cl","gob.ec","gob.es","gob.gt",
"gob.hn","gob.mx","gob.ni","gob.pa","gob.pe","gob.sv",
"gob.ve","govt.nz","gouv.fr","gc.ca","admin.ch","bund.de",
"go.jp","go.kr","go.th","gov.ph","gov.vn","gov.za","gov.ng",
"gov.eg","gov.il","gov.sa","gov.ae","gov.qa","gov.kw",
"gov.om","gov.bh","gov.lk","gov.bd","gov.np","gov.mm",
"gov.kh","gov.la","gov.tw","gov.mo","gov.fj","gov.ws",
"gov.pg","gov.sb","gov.to","gov.vu","gov.mt","gov.cy",
"gov.gr","gov.pt","gov.ie","gov.is","gov.no","gov.se",
"gov.fi","gov.dk","gov.ee","gov.lv","gov.lt","gov.cz",
"gov.sk","gov.hu","gov.si","gov.hr","gov.ba","gov.rs",
"gov.me","gov.mk","gov.al","gov.bg","gov.md","gov.ua",
"gov.by","gov.kz","gov.uz","gov.tm","gov.kg","gov.tj",
"gov.mn","gov.ge","gov.am","gov.az",
# int (international) or secret service
"int",
# Core
"com","net","org","info","biz","name","pro",
# Startup
"io","ai","app","dev","tech","cloud",
"software","digital","network",
"security","email","tools",
# Business
"company","business","finance","capital",
"ventures","partners","consulting",
"solutions","services","support",
# Content
"blog","news","media","press",
"wiki","community","forum",
# Commerce
"shop","store","market","shopping",
"sale","deals",
# Branding
"xyz","online","site","website",
"space","world","live","today",
"agency","studio","design",
"group","life","center",
# Indonesia
"id","co.id","web.id","or.id",
"ac.id","sch.id","my.id",
# Malaysia
"com.my","org.my","edu.my","net.my",
# Southeast Asia
"sg","my","th","vn","ph",
"bn","kh","mm","cn",
# East Asia
"jp","kr","tw","hk","mo",
# South Asia
"in","pk","bd","lk","np",
# Europe
"uk","co.uk","de","fr",
"it","es","pt","pl","ru",
"se","no","fi","dk","ch",
"at","be","cz","ro","hu",
"sk","si","bg","ua","ie",
"lv","lt","lu","mt","hr",
"rs","ba","al","mk",
"gr","cy","nl",
# Americas
"us","ca","mx","br","ar",
# Millitary
"mil","mil.id","mil.kr","mil.pk","mil.ru","mil.ng",
"mil.za","mil.nz","mil.ph","mil.bd","mil.lk","mil.my",
"mil.br","mil.ar","mil.pe","mil.bo","mil.py","mil.uy",
"mil.ve","mil.ec","mil.co","mil.gt","mil.hn","mil.ni",
"mil.sv","mil.pa","mil.do","mil.cu","mil.mx","mil.cl",
# Oceania
"au","nz",
# Middle East
"ae","sa","qa","tr",
# Vanity
"cc","tv","gg","vc","me",
"fm","am","ws","to","sh",
"is","ly","la","so","im",
"bz","li","sc","ms","gd",
"top","vip","icu","monster",
"buzz","click","link","win",
"fun","quest","su","st","cx",
"ax","mu","as","pet","town",
"sc","tk","ml","ga","cf","gq",
"nu","cool","re","wf","tf",
"pm","yt","nf","hn","moe","sx",
"bf","co","pw","work","club",
"one","ltd","sec","download",
"host","lol","ng","wtf","xxx",
"ee","party","bot","ooo","cat","tx",
"ovh","codes","trade","cfd","men","do",
"best","ninja","pp.ua","city",
"rocks","bar","dog","run","red","ink",
"service","services","family","works","work",
"gay","buz","coffe","bio","toys",
"money","cafe","love","fyi","pub","cash",
"ne","page","domains","directory","tet",
"tel","local","university","cheap","chruch",
"inc","pizza","blue","legal","rentals","review",
"taxi","casa","md","ma","rip","tours","capital",
"recipes","audio","help","land","coach","guide",
"foundation","er","watch","delivery","fund","gold",
"cyou","computer","express","institute","reviews",
"ventures","date","loan","ci","casino","band",
"hg","ag","bet","ing","racing","game","kim","rest","vet",
"af","tips","tax","wine","cv","cards","pink","earth","sex",
"pics","cam","parts","part","fail","ge","ski","fe","mom",
"eco","law","gy","baby","porn","vg","sucks","mc","duns",
"srt","sbs","atas","zip","surf","llc","ao","black","moda",
"sexy","gratis","claims","voyage","ke","gl","camp","exposed",
"diamonds","ht","je","lc","wang","meme","box","moi","spot",
"sb","lst","ug","gs","by","actor","aco","ad",
# China
"ren","shouji","tushu","wanggou","weibo","xihuan","xin",
# Israel
"il"
# Additional modern gTLD
"team", "global", "care", "social",
"video", "chat", "academy", "training",
"events", "marketing", "exchange",
"international", "technology","rock","art",
"stream","games","sciene","mobi","guru",
"com.au","bid","travel","plus","systems","edu",
"co.in","domain","photography","expert","tube",
"arpa","abc","asia","domains","boats","bike",
"faith","fish","bingo","irish","homes","solar",
"flights","industries","fan","gmbh","rich","hiv",
"nexus","mex.com","lotto","co.gg","rsvp","whoswho",
"info.ec","google","co.ve","kiwi.nz","idv.tw",
"law.pro","reit","jur.pro","bar.pro","zuerich",
"acct.pro","nom.pe","waw.pl","pro.ec","web.ve",
"arte","clinique","luxe","able","co.zm","com.zm",
"org.zm","co.za","net.za","org.za","web.za","zippo"
# JP
"みんな","jpn.com",
# Hongkong
"网络",
# TEST ASIA
"ac","az","bh","bn","bt",
"iq","ir","jo","kg","kp",
"kw","kz","lb","mn","mv","om",
"ps","sy","tj","tl","tm","uz","ye",
# IANA
# https://www.icann.org/en/contracted-parties/registry-operators/resources/listings
"aaa","aarp","abb","abbott","abbvie","abc","able","abogado",
"abudhabi","ac","academy","accenture","accountant","accountants","aco",
"actor","ad","ads","adult","ae","aeg","aero","aetna","af","afl","africa",
"ag","agakhan","agency","ai","aig","airbus","airforce","airtel","akdn","al",
"alibaba","alipay","allfinanz","allstate","ally","alsace","alstom","am","amazon",
"americanexpress","americanfamily","amex","amfam","amica","amsterdam","analytics",
"android","anquan","anz","ao","aol","apartments","app","apple","aq","aquarelle","ar",
"arab","aramco","archi","army","arpa","art","arte","as","asda","asia","associates","at",
"athleta","attorney","au","auction","audi","audible","audio","auspost","author","auto","autos",
"aw","aws","ax","axa","az","azure","ba","baby","baidu","banamex","band","bank","bar","barcelona",
"barclaycard","barclays","barefoot","bargains","baseball","basketball","bauhaus","bayern","bb","bbc",
"bbt","bbva","bcg","bcn","bd","be","beats","beauty","beer","berlin","best","bestbuy","bet","bf","bg","bh",
"bharti","bi","bible","bid","bike","bing","bingo","bio","biz","bj","black","blackfriday","blockbuster","blog",
"bloomberg","blue","bm","bms","bmw","bn","bnpparibas","bo","boats","boehringer","bofa","bom","bond","boo","book",
"booking","bosch","bostik","boston","bot","boutique","box","br","bradesco","bridgestone","broadway","broker","brother",
"brussels","bs","bt","build","builders","business","buy","buzz","bv","bw","by","bz","bzh","ca","cab","cafe","cal","call",
"calvinklein","cam","camera","camp","canon","capetown","capital","capitalone","car","caravan","cards","care","career","careers",
"cars","casa","case","cash","casino","cat","catering","catholic","cba","cbn","cbre","cc","cd","center","ceo","cern","cf","cfa","cfd",
"cg","ch","chanel","channel","charity","chase","chat","cheap","chintai","christmas","chrome","church","ci","cipriani","circle","cisco",
"citadel","citi","citic","city","ck","cl","claims","cleaning","click","clinic","clinique","clothing","cloud","club","clubmed","cm","cn","co",
"coach","codes","coffee","college","cologne","com","commbank","community","company","compare","computer","comsec","condos","construction",
"consulting","contact","contractors","cooking","cool","coop","corsica","country","coupon","coupons","courses","cpa","cr","credit","creditcard",
"creditunion","cricket","crown","crs","cruise","cruises","cu","cuisinella","cv","cw","cx","cy","cymru","cyou","cz","dad","dance","data","date",
"dating","datsun","day","dclk","dds","de","deal","dealer","deals","degree","delivery","dell","deloitte","delta","democrat","dental","dentist",
"desi","design","dev","dhl","diamonds","diet","digital","direct","directory","discount","discover","dish","diy","dj","dk","dm","dnp","do","docs",
"doctor","dog","domains","dot","download","drive","dtv","dubai","dupont","durban","dvag","dvr","dz","earth","eat","ec","eco","edeka","edu","education",
"ee","eg","email","emerck","energy","engineer","engineering","enterprises","epson",
"equipment","er","ericsson","erni","es","esq","estate","et","eu","eurovision","eus",
"events","exchange","expert","exposed","express","extraspace","fage","fail","fairwinds",
"faith","family","fan","fans","farm","farmers","fashion","fast","fedex","feedback",
"ferrari","ferrero","fi","fidelity","fido","film","final","finance","financial","fire",
"firestone","firmdale","fish","fishing","fit","fitness","fj","fk","flickr","flights","flir",
"florist","flowers","fly","fm","fo","foo","food","football","ford","forex","forsale","forum",
"foundation","fox","fr","free","fresenius","frl","frogans","frontier","ftr","fujitsu","fun",
"fund","furniture","futbol","fyi","ga","gal","gallery","gallo","gallup","game","games","gap",
"garden","gay","gb","gbiz","gd","gdn","ge","gea","gent","genting","george","gf","gg","ggee","gh",
"gi","gift","gifts","gives","giving","gl","glass","gle","global","globo","gm","gmail","gmbh","gmo",
"gmx","gn","godaddy","gold","goldpoint","golf","goodyear","goog","google","gop","got","gov","gp","gq",
"gr","grainger","graphics","gratis","green","gripe","grocery","group","gs","gt","gu","gucci","guge","guide",
"guitars","guru","gw","gy","hair","hamburg","hangout","haus","hbo","hdfc","hdfcbank","health","healthcare","help",
"helsinki","here","hermes","hiphop","hisamitsu","hitachi","hiv","hk","hkt","hm","hn","hockey","holdings","holiday",
"homedepot","homegoods","homes","homesense","honda","horse","hospital","host","hosting","hot","hotels","hotmail","house",
"how","hr","hsbc","ht","hu","hughes","hyatt","hyundai","ibm","icbc","ice","icu","id","ie","ieee","ifm","ikano","il","im",
"imamat","imdb","immo","immobilien","in","inc","industries","infiniti","info","ing","ink","institute","insurance","insure",
"intuit","investments","io","ipiranga","iq","ir","irish","is","ismaili","ist","istanbul","it","itau","itv","jaguar","java","jcb","je",
"jeep","jetzt","jewelry","jio","jll","jm","jmp","jnj","jo","jobs","joburg","jot","joy","jp","jpmorgan","jprs","juegos","juniper","kaufen",
"kddi","ke","kerryhotels","kerryproperties","kfh","kg","kh","ki","kia","kids","kim","kindle","kitchen","kiwi","km","kn","koeln","komatsu","kosher",
"kp","kpmg","kpn","kr","krd","kred","kuokgroup","kw","ky","kyoto","kz","la","lacaixa","lamborghini","lamer","land","landrover","lanxess","lasalle",
"lat","latino","latrobe","law","lawyer","lb","lc","lds","lease","leclerc","lefrak","legal","lego","lexus","lgbt","li","lidl","life","lifeinsurance",
"lifestyle","lighting","like","lilly","limited","limo","lincoln","link","live","living","lk","llc","llp","loan","loans","locker","locus","lol",
"london","lotte","lotto","love","lpl","lplfinancial","lr","ls","lt","ltd","ltda","lu","lundbeck","luxe","luxury","lv","ly","ma","madrid",
"maif","maison","makeup","man","management","mango","map","market","marketing","markets","marriott","marshalls","mattel","mba","mc","mckinsey",
"md","me","med","media","meet","melbourne","meme","memorial","men","menu","merck","merckmsd","mg","mh","miami","microsoft","mil","mini","mint",
"mit","mitsubishi","mk","ml","mlb","mls","mm","mma","mn","mo","mobi","mobile","moda","moe","moi","mom","monash","money","monster","mormon",
"mortgage","moscow","moto","motorcycles","mov","movie","mp","mq","mr","ms","msd","mt","mtn","mtr","mu","museum","music","mv","mw","mx","my",
"mz","na","nab","nagoya","name","navy","nba","nc","ne","nec","net","netbank","netflix","network","neustar","new","news","next","nextdirect","nexus",
"nf","nfl","ng","ngo","nhk","ni","nico","nike","nikon","ninja","nissan","nissay","nl","no","nokia","norton","now","nowruz","nowtv","np","nr","nra",
"nrw","ntt","nu","nyc","nz","obi","observer","office","okinawa","olayan","olayangroup","ollo","om","omega","one","ong","onl","online","ooo","open",
"oracle","orange","org","organic","origins","osaka","otsuka","ott","ovh","pa","page","panasonic","paris","pars","partners","parts","party","pay",
"pccw","pe","pet","pf","pfizer","pg","ph","pharmacy","phd","philips","phone","photo","photography","photos","physio","pics","pictet","pictures",
"pid","pin","ping","pink","pioneer","pizza","pk","pl","place","play","playstation","plumbing","plus","pm","pn","pnc","pohl","poker","politie",
"porn","post","pr","praxi","press","prime","pro","prod","productions","prof","progressive","promo","properties","property","protection","pru",
"prudential","ps","pt","pub","pw","pwc","py","qa","qpon","quebec","quest","racing","radio","re","read","realestate","realtor","realty",
"recipes","redumbrella","rehab","reise","reisen","reit","reliance","ren","rent","rentals","repair","report","republican","rest",
"restaurant","review","reviews","rexroth","rich","richardli","ricoh","ril","rio","rip","rodeo","rogers","room",
"rs","rsvp","ru","rugby","ruhr","run","rw","rwe","ryukyu","sa","saarland","safe","safety","sakura","sale","salon","samsclub",
"samsung","sandvik","sandvikcoromant","sanofi","sap","sarl","sas","save","saxo","sb","sbi","sbs","sc","scb","schaeffler",
"schmidt","scholarships","school","schule","schwarz","science","scot","sd","se","search","seat","secure","security","seek",
"select","sener","services","seven","sew","sex","sexy","sfr","sg","sh","shangrila","sharp","shell","shia","shiksha","shoes",
"shop","shopping","shouji","show","si","silk","sina","singles","site","sj","sk","ski","skin","sky","skype","sl","sling","sm","smart",
"smile","sn","sncf","so","soccer","social","softbank","software","sohu","solar","solutions","song","sony","soy","spa","space","sport",
"spot","sr","srl","ss","st","stada","staples","star","statebank","statefarm","stc","stcgroup","stockholm","storage","store","stream",
"studio","study","style","su","sucks","supplies","supply","support","surf","surgery","suzuki","sv","swatch","swiss","sx","sy","sydney","systems",
"sz","tab","taipei","talk","taobao","target","tatamotors","tatar","tattoo","tax","taxi","tc","tci","td","tdk","team","tech","technology",
"tel","temasek","tennis","teva","tf","tg","th","thd","theater","theatre","tiaa","tickets","tienda","tips","tires","tirol","tj","tjmaxx",
"tjx","tk","tkmaxx","tl","tm","tmall","tn","to","today","tokyo","tools","top","toray","toshiba","total","tours","town","toyota","toys",
"tr","trade","trading","training","travel","travelers","travelersinsurance","trust","trv","tt","tube","tui","tunes","tushu","tv","tvs",
"tw","tz","ua","ubank","ubs","ug","uk","unicom","university","uno","uol","ups","us","uy","uz","va","vacations","vana","vanguard",
"ve","vegas","ventures","verisign","versicherung","vet","vg","vi","viajes","video","vig","viking","villas","vin","vip","virgin","visa",
"vision","viva","vivo","vlaanderen","vn","vodka","volvo","vote","voting","voto","voyage","vu","wales","walmart","walter","wang","wanggou",
"watch","watches","weather","weatherchannel","webcam","weber","website","wed","wedding","weibo","weir","wf","whoswho","wien",
"wiki","williamhill","win","windows","wine","winners","wme","woodside","work","works","world","wow","ws","wtc","wtf","xbox",
"xerox","xihuan","xin","xn--11b4c3d","xn--1ck2e1b","xn--1qqw23a","xn--2scrj9c","xn--30rr7y","xn--3bst00m","xn--3ds443g",
"xn--3e0b707e","xn--3hcrj9c","xn--3pxu8k","xn--42c2d9a","xn--45br5cyl","xn--45brj9c","xn--45q11c","xn--4dbrk0ce","xn--4gbrim",
"xn--54b7fta0cc","xn--55qw42g","xn--55qx5d","xn--5su34j936bgsg","xn--5tzm5g","xn--6frz82g","xn--6qq986b3xl","xn--80adxhks",
"xn--80ao21a","xn--80aqecdr1a","xn--80asehdb","xn--80aswg","xn--8y0a063a","xn--90a3ac","xn--90ae","xn--90ais","xn--9dbq2a",
"xn--9et52u","xn--9krt00a","xn--b4w605ferd","xn--bck1b9a5dre4c","xn--c1avg","xn--c2br7g","xn--cck2b3b","xn--cckwcxetd",
"xn--cg4bki","xn--clchc0ea0b2g2a9gcd","xn--czr694b","xn--czrs0t","xn--czru2d","xn--d1acj3b","xn--d1alf","xn--e1a4c",
"xn--eckvdtc9d","xn--efvy88h","xn--fct429k","xn--fhbei","xn--fiq228c5hs","xn--fiq64b","xn--fiqs8s","xn--fiqz9s","xn--fjq720a","xn--flw351e",
"xn--fpcrj9c3d","xn--fzc2c9e2c","xn--fzys8d69uvgm","xn--g2xx48c","xn--gckr3f0f","xn--gecrj9c","xn--gk3at1e","xn--h2breg3eve",
"xn--h2brj9c","xn--h2brj9c8c","xn--hxt814e","xn--i1b6b1a6a2e","xn--imr513n","xn--io0a7i","xn--j1aef","xn--j1amh","xn--j6w193g",
"xn--jlq480n2rg","xn--jvr189m","xn--kcrx77d1x4a","xn--kprw13d","xn--kpry57d","xn--kput3i","xn--l1acc","xn--lgbbat1ad8j","xn--mgb9awbf",
"xn--mgba3a3ejt","xn--mgba3a4f16a","xn--mgba7c0bbn0a","xn--mgbaam7a8h","xn--mgbab2bd","xn--mgbah1a3hjkrd","xn--mgbai9azgqp6j","xn--mgbayh7gpa",
"xn--mgbbh1a","xn--mgbbh1a71e","xn--mgbc0a9azcg","xn--mgbca7dzdo","xn--mgbcpq6gpa1a","xn--mgberp4a5d4ar","xn--mgbgu82a","xn--mgbi4ecexp",
"xn--mgbpl2fh","xn--mgbt3dhd","xn--mgbtx2b","xn--mgbx4cd0ab","xn--mix891f","xn--mk1bu44c","xn--mxtq1m","xn--ngbc5azd","xn--ngbe9e0a","xn--ngbrx",
"xn--node","xn--nqv7f","xn--nqv7fs00ema","xn--nyqy26a","xn--o3cw4h","xn--ogbpf8fl","xn--otu796d","xn--p1acf","xn--p1ai","xn--pgbs0dh","xn--pssy2u",
"xn--q7ce6a","xn--q9jyb4c","xn--qcka1pmc","xn--qxa6a","xn--qxam","xn--rhqv96g","xn--rovu88b","xn--rvc1e0am3e","xn--s9brj9c","xn--ses554g",
"xn--t60b56a","xn--tckwe","xn--tiq49xqyj","xn--unup4y","xn--vermgensberater-ctb","xn--vermgensberatung-pwb","xn--vhquv","xn--vuq861b",
"xn--w4r85el8fhu5dnra","xn--w4rs40l","xn--wgbh1c","xn--wgbl6a","xn--xhq521b","xn--xkc2al3hye2a","xn--xkc2dl3a5ee0h","xn--y9a3aq","xn--yfro4i67o",
"xn--ygbi2ammx","xn--zfr164b","xxx","xyz","yachts","yahoo","yamaxun","yandex","ye","yodobashi","yoga","yokohama","you","youtube","yun","za",
"zappos","zara","zero","zm","zone","zuerich","zw",
]
def generate_brute_tlds(max_len=3, min_len=1):
"""
Generate a-z string combinations with lengths ranging from min_len to max_len.
WARNING:
The number of combinations grows exponentially (26^n).
Avoid setting max_len too high (>4) unless you are prepared for
long execution times and possible DNS rate limiting.
len 1 -> 26
len 2 -> 676
len 3 -> 17,576
len 4 -> 456,976
len 5 -> 11,881,376
"""
letters = string.ascii_lowercase
for length in range(min_len, max_len + 1):
for combo in itertools.product(letters, repeat=length):
yield "".join(combo)
def generate_brute_tlds_2level(suffixes, max_len=2, min_len=1):
"""
Brute-force the SECOND extension only, behind a fixed first suffix.
`suffixes` is the list of first-level suffixes the user provides (e.g. ["co"]).
Result example with suffixes=["co"]: co.aa, co.ab, co.ac, ... co.zz
Combined with --name later as {name}.{suffix}.{brute} -> abc.co.aa, abc.co.ab, ...
"""
letters = string.ascii_lowercase
for suffix in suffixes:
suffix = suffix.strip().lstrip(".")
if not suffix:
continue
for length in range(min_len, max_len + 1):
for combo in itertools.product(letters, repeat=length):
yield f"{suffix}.{''.join(combo)}"
def build_tld_list(args):
"""
Build the TLD list according to the following priority order:
1. Custom TLDs provided via --tlds (if specified)
2. Common TLDs (unless --no-common is enabled)
3. Brute-force generated a-z combinations (if --brute is enabled)
The order is preserved to ensure that commonly used and
high-value TLDs are processed first, followed by brute-force
generated candidates.
"""
tlds = []
if args.tlds:
tlds.extend([t.strip().lstrip(".") for t in args.tlds.split(",") if t.strip()])
if not args.no_common:
tlds.extend(COMMON_TLDS)
if args.brute:
brute_list = list(generate_brute_tlds(max_len=args.brute_max_len, min_len=args.brute_min_len))
tlds.extend(brute_list)
if args.brute2:
suffixes = [s.strip() for s in args.brute2_suffix1.split(",") if s.strip()]
brute2_list = list(generate_brute_tlds_2level(
suffixes, max_len=args.brute2_max_len, min_len=args.brute2_min_len
))
tlds.extend(brute2_list)
# dedupe, keep order (common tetap di depan, brute di belakang)
seen = set()
final = []
for t in tlds:
if t not in seen:
seen.add(t)
final.append(t)
return final
def resolve_dns(domain):
"""Resolve A record. Return list IP or None"""
try:
answers = dns.resolver.resolve(domain, "A", lifetime=DEFAULT_TIMEOUT)
return [str(r) for r in answers]
except Exception:
# fallback ke socket sebagai cadangan resolver
try:
ip = socket.gethostbyname(domain)
return [ip]
except Exception:
return None
def resolve_ns(domain):
"""Resolve NS record. Return list nameserver or None."""
try:
answers = dns.resolver.resolve(domain, "NS", lifetime=DEFAULT_TIMEOUT)
return [str(r).rstrip(".") for r in answers]
except Exception:
return None
def extract_title(html_text):
if not html_text:
return None
match = re.search(r"<title[^>]*>(.*?)</title>", html_text, re.IGNORECASE | re.DOTALL)
if match:
title = re.sub(r"\s+", " ", match.group(1)).strip()
return title[:200] if title else None
return None
def check_http(url):
"""Request HTTP(S), return dict info"""
try:
resp = requests.get(
url,
timeout=DEFAULT_TIMEOUT,
headers={"User-Agent": USER_AGENT},
allow_redirects=True,
verify=False,
)
return {
"status_code": resp.status_code,
"length": len(resp.content),
"title": extract_title(resp.text),
"server": resp.headers.get("Server"),
"content_type": resp.headers.get("Content-Type"),
"final_url": resp.url,
"redirected": resp.url != url,
}
except requests.exceptions.SSLError:
return {"error": "ssl_error"}
except requests.exceptions.ConnectTimeout:
return {"error": "connect_timeout"}
except requests.exceptions.ConnectionError:
return {"error": "connection_error"}
except requests.exceptions.RequestException as e:
return {"error": str(e)[:120]}
def enumerate_one(name, tld):
domain = f"{name}.{tld}"
result = {
"domain": domain,
"tld": tld,
"resolved": False,
"ip": None,
"ns": None,
"http": None,
"https": None,
}
ips = resolve_dns(domain)
ns_records = resolve_ns(domain)
result["ns"] = ns_records
if ips or ns_records:
result["resolved"] = True
result["available_guess"] = False
else:
result["available_guess"] = True
if not ips:
return result
result["ip"] = ips
https_info = check_http(f"https://{domain}")
result["https"] = https_info
http_info = check_http(f"http://{domain}")
result["http"] = http_info
return result
def truncate(text, width):
if text is None:
return "-"
text = str(text)
if len(text) <= width:
return text
return text[: width - 1] + "…"
def get_combined_status(result):
parts = []
for scheme in ("https", "http"):
info = result.get(scheme)
if info is None:
continue
if "status_code" in info:
parts.append(f"{scheme[0].upper()}:{info['status_code']}")
elif "error" in info:
parts.append(f"{scheme[0].upper()}:ERR")
return ", ".join(parts) if parts else "-"
def get_combined_title(result):
for scheme in ("https", "http"):
info = result.get(scheme)
if info and info.get("title"):
return info["title"]
return None
def get_combined_length(result):
for scheme in ("https", "http"):
info = result.get(scheme)
if info and "length" in info:
return info["length"]
return None
def get_https_service(result):
info = result.get("https")
if info is None:
return "-"
if "status_code" in info:
return f"yes ({info['status_code']})"
if "error" in info:
return f"no ({info['error']})"
return "-"
def has_status_200(result):
for scheme in ("https", "http"):
info = result.get(scheme)
if info and info.get("status_code") == 200:
return True
return False
def colorize_status(code_str, has_200):
if has_200:
return green(code_str)
return code_str
def print_table_header(widths):
headers = ["DOMAIN", "TLD", "HTTP/HTTPS CODE", "TITLE", "NS", "DNS", "HTTPS", "LENGTH"]
def fmt_row(cells):
return " | ".join(truncate(c, w).ljust(w) for c, w in zip(cells, widths))
sep = "-+-".join("-" * w for w in widths)
print(fmt_row(headers))
print(sep)
TABLE_WIDTHS = [28, 8, 18, 26, 22, 8, 14, 8]
def format_table_row(r, widths=TABLE_WIDTHS, use_color=True):
domain = r["domain"]
tld = r["tld"]
code = get_combined_status(r)
title = get_combined_title(r)
ns_list = r.get("ns")
ns_str = ns_list[0] if ns_list else "-"
if ns_list and len(ns_list) > 1:
ns_str += f" (+{len(ns_list)-1})"
is_up = r.get("resolved")
dns_status = "UP" if is_up else "down"
https_status = get_https_service(r)
length = get_combined_length(r)
has_200 = has_status_200(r)
cells = [domain, tld, code, title, ns_str, dns_status, https_status, length]
out = []
for i, (c, w) in enumerate(zip(cells, widths)):
text = truncate(c, w).ljust(w)
if use_color:
if i == 2 and has_200: # HTTP/HTTPS CODE
text = green(truncate(c, w)) + " " * (w - len(truncate(c, w)))
elif i == 5 and is_up: # DNS
text = green(truncate(c, w)) + " " * (w - len(truncate(c, w)))
out.append(text)
return " | ".join(out)
def print_table(results, use_color=True):
print_table_header(TABLE_WIDTHS)
for r in results:
print(format_table_row(r, use_color=use_color))
def state_path_for(output_path):
base, _ = os.path.splitext(output_path)
return f"{base}.state.json"
def load_state(state_file, name, planned_tlds):
"""Load checkpoint kalau cocok target_name dan TLD plan-nya sama. Return (results, done_tlds_set) atau (None, None) kalau tidak valid/tidak ada."""
if not os.path.isfile(state_file):
return None, None
try:
with open(state_file, "r", encoding="utf-8") as f:
state = json.load(f)
except Exception:
return None, None
if state.get("target_name") != name:
return None, None
if state.get("planned_tlds") != planned_tlds:
return None, None
results = state.get("results", [])
done_tlds = {r["tld"] for r in results}
return results, done_tlds
def save_state(state_file, name, planned_tlds, results):
state = {
"target_name": name,
"planned_tlds": planned_tlds,
"results": results,
}
tmp_file = f"{state_file}.tmp"
with open(tmp_file, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False)
os.replace(tmp_file, state_file)
def clear_state(state_file):
try:
os.remove(state_file)
except FileNotFoundError:
pass
def main():
parser = argparse.ArgumentParser(
description="Enumerate domain availability and metadata across multiple TLDs"
)
parser.add_argument(
"--name",
required=True,
help="Domain name without TLD (e.g. examplebrand)"
)
parser.add_argument(
"--tlds",
help="Custom TLD list separated by commas (e.g. com,id,su,st,cx)"
)
parser.add_argument(
"--no-common",
action="store_true",
help="Skip the built-in common TLD list"
)
parser.add_argument(
"--brute",
action="store_true",
help="Enable brute-force generation of alphabetic TLD combinations (a-z)"
)
parser.add_argument(
"--brute-min-len",
type=int,
default=1,
help="Minimum brute-force TLD length (default: 1)"
)
parser.add_argument(
"--brute-max-len",
type=int,
default=3,
help=(
"Maximum brute-force TLD length (default: 3). "
"WARNING: combinations grow exponentially (26^n). "
"len=3 -> 17,576 | len=4 -> 456,976 | "
"len=5 -> 11.8 million. Recommended maximum: 3-4."
),
)
parser.add_argument(
"--brute2",
action="store_true",
help=(
"Enable brute-force for the SECOND extension, behind a fixed first "
"suffix given via --brute2-suffix1 (e.g. --brute2-suffix1 co -> "
"abc.co.aa, abc.co.ab, ... abc.co.zz)"
)
)
parser.add_argument(
"--brute2-suffix1",
help=(
"Required if --brute2 is set. First-level suffix(es), comma-separated "
"(e.g. co or co,go,web). The second extension is brute-forced a-z "
"behind each one: --brute2-suffix1 co -> abc.co.aa, abc.co.ab, ..."
),
)
parser.add_argument(
"--brute2-min-len",
type=int,
default=1,
help="Minimum length of the brute-forced second extension for --brute2 (default: 1)"
)
parser.add_argument(
"--brute2-max-len",
type=int,
default=2,
help=(
"Maximum length of the brute-forced second extension for --brute2 (default: 2). "
"WARNING: grows as 26^n * number of --brute2-suffix1 entries. "
"Recommended maximum: 2-3."
),
)
parser.add_argument(
"--threads",
type=int,
default=30,
help="Number of worker threads (default: 30)"
)
parser.add_argument(
"--output",
default="Results-tld-domain.json",
help="Output JSON file path"
)
parser.add_argument(
"--only-resolved",
action="store_true",
help="Only save domains that successfully resolve to an IP address"
)
parser.add_argument(
"--only-200",
action="store_true",
help="Only save domains returning HTTP status 200"
)
parser.add_argument(
"--no-color",
action="store_true",
help="Disable colored CLI output"
)
args = parser.parse_args()
if args.no_color:
Colors.disable()
if args.brute2 and not args.brute2_suffix1:
print(
"[!] --brute2 requires --brute2-suffix1 (e.g. --brute2-suffix1 co "
"or --brute2-suffix1 co,go,web)."
)
sys.exit(1)
tlds = build_tld_list(args)
if not tlds:
print(
"[!] No TLDs selected. Use --tlds or do not enable --no-common."
)
sys.exit(1)
# Calculate how many TLDs come from the common list vs brute force
n_common = 0 if args.no_common else len(COMMON_TLDS)
n_custom = len(args.tlds.split(",")) if args.tlds else 0
n_brute = len(tlds) - n_common - n_custom
print(f"[*] Target name: {args.name}")
print(
f"[*] Total TLDs to check: {len(tlds)} "
f"(common: {n_common}, brute-force: {max(n_brute, 0)})"
)
print(f"[*] Worker threads: {args.threads}")
print()
if args.brute:
print(
f"[*] Brute-force TLD length range: "
f"{args.brute_min_len}-{args.brute_max_len} characters"
)
if args.brute2:
print(
f"[*] Brute-force 2nd extension behind suffix(es) [{args.brute2_suffix1}], "
f"length range: {args.brute2_min_len}-{args.brute2_max_len} characters"
)
results = []
start = time.time()
# IMPORTANT:
# Process common TLDs first (DNS + HTTP + title + NS lookup),
# then continue with brute-force generated TLDs.
# This ensures high-value and commonly used TLDs are prioritized.
common_set = set(COMMON_TLDS) if not args.no_common else set()
ordered_tlds = sorted(
tlds,
key=lambda t: (t not in common_set, tlds.index(t))
)
state_file = state_path_for(args.output)
prev_results, done_tlds = load_state(state_file, args.name, ordered_tlds)
if prev_results:
results = prev_results
remaining_tlds = [t for t in ordered_tlds if t not in done_tlds]
print(f"[*] Resuming from checkpoint: {len(done_tlds)} already checked, {len(remaining_tlds)} remaining")
print()
else:
remaining_tlds = ordered_tlds
print_table_header(TABLE_WIDTHS)
for r in results:
print(format_table_row(r, use_color=not args.no_color))
pause_requested = {"flag": False}
def handle_sigint(signum, frame):
pause_requested["flag"] = True
old_handler = signal.signal(signal.SIGINT, handle_sigint)
aborted = False
if remaining_tlds:
try:
with concurrent.futures.ThreadPoolExecutor(
max_workers=args.threads
) as executor:
# Submit all jobs up-front so lookups still run in parallel,
# but iterate in submit order (a-z) instead of as_completed(),
# so results print/save strictly in the original sequence.
ordered_futures = [
(tld, executor.submit(enumerate_one, args.name, tld))
for tld in remaining_tlds
]
futures = {f: tld for tld, f in ordered_futures}
for tld, future in ordered_futures:
try:
res = future.result()
except Exception as e:
res = {
"domain": f"{args.name}.{tld}",
"tld": tld,
"resolved": False,
"error": str(e),
}
results.append(res)
print(format_table_row(res, use_color=not args.no_color))
if pause_requested["flag"]:
pause_requested["flag"] = False
signal.signal(signal.SIGINT, old_handler)
save_state(state_file, args.name, ordered_tlds, results)
choice = input(
f"\n[!] Paused. {len(results)}/{len(tlds)} checked, progress saved to {state_file}.\n"
f" [c]ontinue / [a]bort: "
).strip().lower()
if choice == "a":
aborted = True
for f in futures:
f.cancel()
break
signal.signal(signal.SIGINT, handle_sigint)
except KeyboardInterrupt:
save_state(state_file, args.name, ordered_tlds, results)
aborted = True
print(f"\n[!] Interrupted. {len(results)}/{len(tlds)} checked, progress saved to {state_file}.")
signal.signal(signal.SIGINT, old_handler)
def write_output(results_to_write, partial=False):
if args.only_resolved:
results_to_write = [r for r in results_to_write if r.get("resolved")]
if args.only_200:
results_to_write = [r for r in results_to_write if has_status_200(r)]
results_to_write = sorted(
results_to_write,
key=lambda r: (
not has_status_200(r),
not r.get("resolved"),
r["tld"]
)
)
data = {
"target_name": args.name,
"partial": partial,
"total_planned": len(tlds),
"total_checked": len(results),
"total_resolved": sum(1 for r in results_to_write if r.get("resolved")),
"total_status_200": sum(1 for r in results_to_write if has_status_200(r)),
"common_tlds_checked": n_common,
"brute_force_checked": max(n_brute, 0),
"elapsed_seconds": round(time.time() - start, 2),
"results": results_to_write,
}
with open(args.output, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return data
if aborted:
partial_data = write_output(results, partial=True)
print(
f"[*] Aborted. {len(results)}/{len(tlds)} checked so far — "
f"partial results saved to: {args.output}"
)
print(
f"[*] Checkpoint saved to: {state_file} "
f"(resume later with the same --output to continue)"
)
return
clear_state(state_file)
print()
output_data = write_output(results, partial=False)
print(
f"\n[+] Completed in "
f"{output_data['elapsed_seconds']}s"
)
print(
f"[+] {output_data['total_resolved']} / "
f"{output_data['total_checked']} domains resolved"
)
print(
f"[+] {output_data['total_status_200']} "
f"domains returned HTTP 200"
)
print(
f"[+] Results saved to: "
f"{args.output}"
)
if __name__ == "__main__":
main()Viewer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jieyab89 Domain Enum TLD Viewer</title>
<style>
:root {
--bg: #0a0e14;
--panel: #11161f;
--panel-2: #151b26;
--border: #1c2433;
--text: #e8ecf2;
--muted: #6b7686;
--muted-2: #8b96a8;
--green: #3ddc84;
--green-dim: #1f5e3d;
--red: #ff5c5c;
--red-dim: #5e2222;
--yellow: #e0c068;
--accent: #5b8cff;
--mono: "JetBrains Mono", "IBM Plex Mono", "SF Mono", Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--mono);
font-size: 13px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ---- top bar ---- */
.topbar {
border-bottom: 1px solid var(--border);
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--panel);
}
.topbar .brand {
color: var(--green);
font-weight: 600;
letter-spacing: 0.5px;
}
.topbar .brand::before {
content: "❯ ";
color: var(--muted);
}
.topbar .meta {
color: var(--muted);
font-size: 12px;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* ---- loader ---- */
.loader {
display: flex;
align-items: center;
gap: 14px;
background: var(--panel);
border: 1px dashed var(--border);
padding: 14px 16px;
margin-bottom: 20px;
}
.file-btn {
background: var(--panel-2);
border: 1px solid var(--border);
color: var(--green);
padding: 8px 16px;
font-size: 12px;
font-family: var(--mono);
cursor: pointer;
white-space: nowrap;
transition: border-color .12s, background .12s;
}
.file-btn:hover {
border-color: var(--green);
background: rgba(61, 220, 132, 0.06);
}
.loader-hint {
color: var(--muted);
font-size: 12px;
}
.loader-hint.ok { color: var(--green); }
.loader-hint.err { color: var(--red); }
/* ---- stats ---- */
.stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
margin-bottom: 20px;
}
.stat {
background: var(--panel);
padding: 14px 16px;
}
.stat .num {
font-size: 22px;
font-weight: 700;
color: var(--text);
}
.stat .label {
color: var(--muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
margin-top: 4px;
}
.stat.resolved .num { color: var(--green); }
.stat.status200 .num { color: var(--accent); }
/* ---- search ---- */
.search-box {
display: flex;
align-items: center;
gap: 10px;
background: var(--panel);
border: 1px solid var(--border);
padding: 12px 16px;
margin-bottom: 14px;
}
.search-box .prompt {
color: var(--green);
font-weight: 700;
}
.search-box input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text);
font-family: var(--mono);
font-size: 14px;
caret-color: var(--green);
}
.search-box input::placeholder { color: var(--muted); }
.search-box .count {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
/* ---- filters ---- */
.filters {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.chip {
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted-2);
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
user-select: none;
transition: border-color .12s, color .12s;
}
.chip:hover { border-color: var(--muted); color: var(--text); }
.chip.active {
border-color: var(--green);
color: var(--green);
background: rgba(61, 220, 132, 0.08);
}
.chip.active[data-filter="down"] {
border-color: var(--red);
color: var(--red);
background: rgba(255, 92, 92, 0.08);
}
/* ---- table ---- */
.table-wrap {
border: 1px solid var(--border);
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 980px;
}
thead th {
text-align: left;
background: var(--panel-2);
color: var(--muted-2);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
white-space: nowrap;
}
tbody td {
padding: 9px 14px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
tbody tr { background: var(--panel); }
tbody tr:hover { background: var(--panel-2); }
tbody tr.no-match { display: none; }
.domain-cell { color: var(--text); font-weight: 600; }
.domain-cell mark, td mark {
background: var(--green);
color: #061109;
padding: 0 2px;
}
.tld-tag {
color: var(--accent);
}
.badge {
display: inline-block;
padding: 1px 7px;
border-radius: 2px;
font-size: 11px;
font-weight: 700;
}
.badge.up { background: rgba(61,220,132,0.12); color: var(--green); }
.badge.down { background: rgba(255,92,92,0.10); color: var(--red); }
.code-200 { color: var(--green); font-weight: 700; }
.code-other { color: var(--yellow); }
.code-err { color: var(--muted); }
.title-cell { color: var(--muted-2); }
.empty-dash { color: var(--muted); }
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--muted);
}
.empty-state .glyph {
font-size: 28px;
color: var(--border);
margin-bottom: 10px;
}
footer {
text-align: center;
color: var(--muted);
font-size: 11px;
margin-top: 18px;
}
::-webkit-scrollbar { height: 8px; width: 8px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); }
@media (max-width: 700px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div class="topbar">
<span class="brand">Jieyab89 Domain TLD Reccon</span>
<span class="meta" id="targetMeta">target: -</span>
</div>
<div class="wrap">
<div class="loader" id="loaderBox">
<label class="file-btn" for="fileInput">📂 load JSON output</label>
<input type="file" id="fileInput" accept=".json" hidden>
<span class="loader-hint" id="loaderHint">no file loaded — choose Results-tld-domain.json</span>
</div>
<div class="stats" id="statsRow"></div>
<div class="search-box">
<span class="prompt">grep></span>
<input id="searchInput" type="text" placeholder="search domain, tld, title, ip, server, ns..." autofocus>
<span class="count" id="resultCount">0 / 0</span>
</div>
<div class="filters" id="filterRow">
<span class="chip active" data-filter="all">all</span>
<span class="chip" data-filter="up">dns: up</span>
<span class="chip" data-filter="down">dns: down</span>
<span class="chip" data-filter="200">http 200</span>
</div>
<div class="table-wrap">
<table id="resultTable">
<thead>
<tr>
<th>domain</th>
<th>tld</th>
<th>dns</th>
<th>ip</th>
<th>http/https</th>
<th>title</th>
<th>server</th>
<th>ns</th>
</tr>
</thead>
<tbody id="resultBody"></tbody>
</table>
<div class="empty-state" id="emptyState" style="display:none;">
<div class="glyph">∅</div>
no_match_found
</div>
</div>
<footer id="footerInfo"></footer>
</div>
<script>
let DATA = { target_name: "-", total_checked: 0, total_resolved: 0, total_status_200: 0, common_tlds_checked: 0, brute_force_checked: 0, elapsed_seconds: 0, results: [] };
function getCombinedStatus(r) {
const parts = [];
for (const scheme of ["https", "http"]) {
const info = r[scheme];
if (!info) continue;
if (info.status_code !== undefined) parts.push(`${scheme[0].toUpperCase()}:${info.status_code}`);
else if (info.error) parts.push(`${scheme[0].toUpperCase()}:ERR`);
}
return parts.length ? parts.join(", ") : "-";
}
function getCombinedTitle(r) {
for (const scheme of ["https", "http"]) {
const info = r[scheme];
if (info && info.title) return info.title;
}
return null;
}
function getCombinedServer(r) {
for (const scheme of ["https", "http"]) {
const info = r[scheme];
if (info && info.server) return info.server;
}
return null;
}
function hasStatus200(r) {
for (const scheme of ["https", "http"]) {
const info = r[scheme];
if (info && info.status_code === 200) return true;
}
return false;
}
function escapeHtml(str) {
if (str === null || str === undefined) return "";
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
function highlight(text, query) {
const safe = escapeHtml(text);
if (!query) return safe;
try {
const re = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig");
return safe.replace(re, "<mark>$1</mark>");
} catch (e) {
return safe;
}
}
let results = [];
let rowEls = [];
let activeFilter = "all";
function renderStats() {
const stats = [
{ label: "target", value: DATA.target_name, plain: true },
{ label: "checked", value: DATA.total_checked },
{ label: "resolved", value: DATA.total_resolved, cls: "resolved" },
{ label: "http 200", value: DATA.total_status_200, cls: "status200" },
{ label: "elapsed", value: DATA.elapsed_seconds + "s" }
];
document.getElementById("statsRow").innerHTML = stats.map(s => `
<div class="stat ${s.cls || ""}">
<div class="num">${escapeHtml(s.value)}</div>
<div class="label">${s.label}</div>
</div>
`).join("");
document.getElementById("targetMeta").textContent = `target: ${DATA.target_name} · ${DATA.total_checked} tld checked`;
document.getElementById("footerInfo").textContent =
`${results.length} entries loaded · common_tlds: ${DATA.common_tlds_checked} · brute_force: ${DATA.brute_force_checked}`;
}
function rowMatchesFilter(r) {
if (activeFilter === "all") return true;
if (activeFilter === "up") return !!r.resolved;
if (activeFilter === "down") return !r.resolved;
if (activeFilter === "200") return hasStatus200(r);
return true;
}
function rowMatchesSearch(r, q) {
if (!q) return true;
const haystack = [
r.domain,
r.tld,
getCombinedTitle(r),
getCombinedServer(r),
...(r.ip || []),
...(r.ns || [])
].filter(Boolean).join(" ").toLowerCase();
return haystack.includes(q.toLowerCase());
}
function buildRow(r) {
const tr = document.createElement("tr");
tr.dataset.domain = r.domain.toLowerCase();
tr.dataset.tld = r.tld.toLowerCase();
tr.dataset.title = (getCombinedTitle(r) || "").toLowerCase();
tr.dataset.server = (getCombinedServer(r) || "").toLowerCase();
tr.dataset.ip = (r.ip || []).join(" ").toLowerCase();
tr.dataset.ns = (r.ns || []).join(" ").toLowerCase();
tr.dataset.resolved = r.resolved ? "1" : "0";
tr.dataset.s200 = hasStatus200(r) ? "1" : "0";
const status = getCombinedStatus(r);
const statusCls = hasStatus200(r) ? "code-200" : (status === "-" ? "code-err" : "code-other");
const title = getCombinedTitle(r);
const server = getCombinedServer(r);
const ip = (r.ip || []).join(", ");
const ns = (r.ns || []).join(", ");
const dnsBadge = r.resolved
? `<span class="badge up">UP</span>`
: `<span class="badge down">DOWN</span>`;
tr.innerHTML = `
<td class="domain-cell" data-field="domain">${escapeHtml(r.domain)}</td>
<td class="tld-tag" data-field="tld">${escapeHtml(r.tld)}</td>
<td data-field="dns">${dnsBadge}</td>
<td data-field="ip">${ip ? escapeHtml(ip) : '<span class="empty-dash">-</span>'}</td>
<td class="${statusCls}" data-field="status">${escapeHtml(status)}</td>
<td class="title-cell" data-field="title">${title ? escapeHtml(title) : '<span class="empty-dash">-</span>'}</td>
<td data-field="server">${server ? escapeHtml(server) : '<span class="empty-dash">-</span>'}</td>
<td data-field="ns">${ns ? escapeHtml(ns) : '<span class="empty-dash">-</span>'}</td>
`;
return tr;
}
const tbody = document.getElementById("resultBody");
function renderTable() {
tbody.innerHTML = "";
rowEls = results.map(r => {
const el = buildRow(r);
tbody.appendChild(el);
return { el, r };
});
}
function applyFilters() {
const q = document.getElementById("searchInput").value.trim();
let visibleCount = 0;
rowEls.forEach(({ el, r }) => {
const matches = rowMatchesFilter(r) && rowMatchesSearch(r, q);
el.classList.toggle("no-match", !matches);
if (matches) {
visibleCount++;
// re-render highlighted cells
el.querySelector('[data-field="domain"]').innerHTML = highlight(r.domain, q);
el.querySelector('[data-field="tld"]').innerHTML = highlight(r.tld, q);
const titleVal = getCombinedTitle(r);
el.querySelector('[data-field="title"]').innerHTML = titleVal ? highlight(titleVal, q) : '<span class="empty-dash">-</span>';
const serverVal = getCombinedServer(r);
el.querySelector('[data-field="server"]').innerHTML = serverVal ? highlight(serverVal, q) : '<span class="empty-dash">-</span>';
const ipVal = (r.ip || []).join(", ");
el.querySelector('[data-field="ip"]').innerHTML = ipVal ? highlight(ipVal, q) : '<span class="empty-dash">-</span>';
const nsVal = (r.ns || []).join(", ");
el.querySelector('[data-field="ns"]').innerHTML = nsVal ? highlight(nsVal, q) : '<span class="empty-dash">-</span>';
}
});
document.getElementById("resultCount").textContent = `${visibleCount} / ${results.length}`;
document.getElementById("emptyState").style.display = visibleCount === 0 ? "block" : "none";
document.getElementById("resultTable").style.display = visibleCount === 0 ? "none" : "table";
}
function loadJsonFile(file) {
const hint = document.getElementById("loaderHint");
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
let parsed;
try {
parsed = JSON.parse(e.target.result);
} catch (err) {
hint.textContent = `failed to parse JSON: ${err.message}`;
hint.className = "loader-hint err";
return;
}
if (!parsed || !Array.isArray(parsed.results)) {
hint.textContent = `invalid format — field "results" (array) not found`;
hint.className = "loader-hint err";
return;
}
DATA = parsed;
results = DATA.results;
hint.textContent = `loaded: ${file.name} (${results.length} entries)`;
hint.className = "loader-hint ok";
renderStats();
renderTable();
applyFilters();
};
reader.onerror = () => {
hint.textContent = "failed to read file";
hint.className = "loader-hint err";
};
reader.readAsText(file);
}
document.getElementById("fileInput").addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) loadJsonFile(file);
});
// drag & drop support
const loaderBox = document.getElementById("loaderBox");
["dragover", "dragenter"].forEach(evt =>
loaderBox.addEventListener(evt, (e) => { e.preventDefault(); loaderBox.style.borderColor = "var(--green)"; })
);
["dragleave", "drop"].forEach(evt =>
loaderBox.addEventListener(evt, (e) => { e.preventDefault(); loaderBox.style.borderColor = "var(--border)"; })
);
loaderBox.addEventListener("drop", (e) => {
const file = e.dataTransfer.files[0];
if (file) loadJsonFile(file);
});
document.getElementById("searchInput").addEventListener("input", applyFilters);
document.getElementById("filterRow").addEventListener("click", (e) => {
const chip = e.target.closest(".chip");
if (!chip) return;
document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));
chip.classList.add("active");
activeFilter = chip.dataset.filter;
applyFilters();
});
renderStats();
renderTable();
applyFilters();
</script>
</body>
</html>