feat(domains): per-record provider intel + vendor strip

Structured DNSRecord list carries the detected vendor per row (NS/MX/CNAME/
TXT value, ASN org, response headers, SPF includes, verification strings)
and a Providers strip shows registrar/DNS/hosting/email/CA with favicons —
the domainstack-style view. Detection tables expanded to ~70 NS, ~45 mail,
~60 CNAME, ~130 registrar, and ~50 TXT patterns.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-22 18:26:24 +02:00
1 parent f4ebdf1f33
commit 041c54c18c
7 files changed
+886 -114

No files matched your search

+9
View File
@@ -6,6 +6,15 @@ All notable changes to Dash. Format follows
## [Unreleased] ## [Unreleased]
### Added
- Domain detail now identifies vendors per record: A/AAAA/CNAME/NS/MX/TXT
rows carry provider badges (Cloudflare, Google, Purelymail, …) with favicon
icons, plus a five-role provider strip (registrar, DNS, hosting, email, CA)
above the detail sections. Detection covers ~70 NS providers, ~45 mail
providers, ~60 CNAME targets, ASN/org matching, response-header signals,
and TXT verification/SPF attribution
### Changed ### Changed
- Settings dialog regrouped into icon-labelled sections (renderer, appearance, - Settings dialog regrouped into icon-labelled sections (renderer, appearance,
+13 -4
View File
@@ -27,6 +27,7 @@ const domainCols = `id, name, tld, active, auto_renew, alert_days_before, tags,
ssl_key_size, ssl_sig_algo, ssl_alt_names, host_country, host_country_code, ssl_key_size, ssl_sig_algo, ssl_alt_names, host_country, host_country_code,
host_region, host_city, host_isp, host_org, host_as, host_lat, host_lon, host_region, host_city, host_isp, host_org, host_as, host_lat, host_lon,
dns_provider, email_provider, hosting_provider, ca_provider, headers, dns_provider, email_provider, hosting_provider, ca_provider, headers,
records, providers,
favicon_url, lookup_error, interval_h, last_checked, position, alerts, created_at` favicon_url, lookup_error, interval_h, last_checked, position, alerts, created_at`
// domainRow mirrors the domains table; JSON text columns decode on scan. // domainRow mirrors the domains table; JSON text columns decode on scan.
@@ -82,6 +83,8 @@ type domainRow struct {
HostingProvider string `json:"hostingProvider"` HostingProvider string `json:"hostingProvider"`
CAProvider string `json:"caProvider"` CAProvider string `json:"caProvider"`
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
Records []domain.DNSRecord `json:"records"`
Providers domain.Providers `json:"providers"`
FaviconURL string `json:"faviconUrl"` FaviconURL string `json:"faviconUrl"`
LookupError string `json:"lookupError"` LookupError string `json:"lookupError"`
IntervalH int `json:"intervalH"` IntervalH int `json:"intervalH"`
@@ -93,7 +96,7 @@ type domainRow struct {
func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) { func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) {
var d domainRow var d domainRow
var tags, statuses, ns, mx, txt, v4, v6, altNames, headers, alerts string var tags, statuses, ns, mx, txt, v4, v6, altNames, headers, records, providers, alerts string
var active, autoRenew, privacy, lock int var active, autoRenew, privacy, lock int
err := row.Scan(&d.ID, &d.Name, &d.TLD, &active, &autoRenew, &d.AlertDaysBefore, err := row.Scan(&d.ID, &d.Name, &d.TLD, &active, &autoRenew, &d.AlertDaysBefore,
&tags, &d.Notes, &d.ExpiryDate, &d.CreationDate, &d.UpdatedDate, &tags, &d.Notes, &d.ExpiryDate, &d.CreationDate, &d.UpdatedDate,
@@ -104,7 +107,7 @@ func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) {
&d.SSLKeySize, &d.SSLSigAlgo, &altNames, &d.HostCountry, &d.HostCountryCode, &d.SSLKeySize, &d.SSLSigAlgo, &altNames, &d.HostCountry, &d.HostCountryCode,
&d.HostRegion, &d.HostCity, &d.HostISP, &d.HostOrg, &d.HostAS, &d.HostLat, &d.HostRegion, &d.HostCity, &d.HostISP, &d.HostOrg, &d.HostAS, &d.HostLat,
&d.HostLon, &d.DNSProvider, &d.EmailProvider, &d.HostingProvider, &d.CAProvider, &d.HostLon, &d.DNSProvider, &d.EmailProvider, &d.HostingProvider, &d.CAProvider,
&headers, &d.FaviconURL, &d.LookupError, &d.IntervalH, &d.LastChecked, &headers, &records, &providers, &d.FaviconURL, &d.LookupError, &d.IntervalH, &d.LastChecked,
&d.Position, &alerts, &d.CreatedAt) &d.Position, &alerts, &d.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -124,6 +127,9 @@ func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) {
d.SSLAltNames = decodeList(altNames) d.SSLAltNames = decodeList(altNames)
d.Headers = map[string]string{} d.Headers = map[string]string{}
_ = json.Unmarshal([]byte(headers), &d.Headers) _ = json.Unmarshal([]byte(headers), &d.Headers)
d.Records = []domain.DNSRecord{}
_ = json.Unmarshal([]byte(records), &d.Records)
_ = json.Unmarshal([]byte(providers), &d.Providers)
return &d, nil return &d, nil
} }
@@ -417,6 +423,8 @@ func (s *Server) applyResult(id string, r *domain.Result) {
expiry, created, updated = fmtT(r.ExpiryDate), fmtT(r.CreationDate), fmtT(r.UpdatedDate) expiry, created, updated = fmtT(r.ExpiryDate), fmtT(r.CreationDate), fmtT(r.UpdatedDate)
sslFrom, sslTo = fmtT(r.SSLValidFrom), fmtT(r.SSLValidTo) sslFrom, sslTo = fmtT(r.SSLValidFrom), fmtT(r.SSLValidTo)
hdr, _ := json.Marshal(r.Headers) hdr, _ := json.Marshal(r.Headers)
recs, _ := json.Marshal(r.Records)
provs, _ := json.Marshal(r.Providers)
_, err := s.db.Exec(`UPDATE domains SET _, err := s.db.Exec(`UPDATE domains SET
expiry_date=?, creation_date=?, updated_date=?, registrar_name=?, expiry_date=?, creation_date=?, updated_date=?, registrar_name=?,
@@ -428,7 +436,8 @@ func (s *Server) applyResult(id string, r *domain.Result) {
ssl_key_size=?, ssl_sig_algo=?, ssl_alt_names=?, host_country=?, ssl_key_size=?, ssl_sig_algo=?, ssl_alt_names=?, host_country=?,
host_country_code=?, host_region=?, host_city=?, host_isp=?, host_org=?, host_country_code=?, host_region=?, host_city=?, host_isp=?, host_org=?,
host_as=?, host_lat=?, host_lon=?, dns_provider=?, email_provider=?, host_as=?, host_lat=?, host_lon=?, dns_provider=?, email_provider=?,
hosting_provider=?, ca_provider=?, headers=?, favicon_url=?, hosting_provider=?, ca_provider=?, headers=?, records=?, providers=?,
favicon_url=?,
lookup_error=?, last_checked=strftime('%Y-%m-%dT%H:%M:%fZ','now') lookup_error=?, last_checked=strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE id=?`, WHERE id=?`,
expiry, created, updated, r.RegistrarName, r.RegistrarID, r.RegistrarURL, expiry, created, updated, r.RegistrarName, r.RegistrarID, r.RegistrarURL,
@@ -440,7 +449,7 @@ func (s *Server) applyResult(id string, r *domain.Result) {
enc(r.SSLAltNames), r.HostCountry, r.HostCountryCode, r.HostRegion, enc(r.SSLAltNames), r.HostCountry, r.HostCountryCode, r.HostRegion,
r.HostCity, r.HostISP, r.HostOrg, r.HostAS, r.HostLat, r.HostLon, r.HostCity, r.HostISP, r.HostOrg, r.HostAS, r.HostLat, r.HostLon,
r.DNSProvider, r.EmailProvider, r.HostingProvider, r.CAProvider, r.DNSProvider, r.EmailProvider, r.HostingProvider, r.CAProvider,
string(hdr), r.FaviconURL, truncate(r.Error, 300), id) string(hdr), string(recs), string(provs), r.FaviconURL, truncate(r.Error, 300), id)
if err != nil { if err != nil {
s.log.Error("domain apply", zap.Error(err)) s.log.Error("domain apply", zap.Error(err))
return return
@@ -0,0 +1,9 @@
-- +goose Up
-- Structured DNS records + provider identifications for the domain detail
-- page. JSON blobs — the flat columns stay for compat and check history.
ALTER TABLE domains ADD COLUMN records TEXT NOT NULL DEFAULT '[]';
ALTER TABLE domains ADD COLUMN providers TEXT NOT NULL DEFAULT '{}';
-- +goose Down
ALTER TABLE domains DROP COLUMN providers;
ALTER TABLE domains DROP COLUMN records;
+719 -94
View File
@@ -88,9 +88,46 @@ type Result struct {
HostingProvider string `json:"hostingProvider"` HostingProvider string `json:"hostingProvider"`
CAProvider string `json:"caProvider"` CAProvider string `json:"caProvider"`
// Structured intel — domainstack-style record list + vendor strip.
Records []DNSRecord `json:"records"`
Providers Providers `json:"providers"`
Headers map[string]string `json:"headers"` Headers map[string]string `json:"headers"`
FaviconURL string `json:"faviconUrl"` FaviconURL string `json:"faviconUrl"`
Error string `json:"error"` // non-fatal partial failure note Error string `json:"error"` // non-fatal partial failure note
mxPairs []mxPair // structured MX; MXRecords keeps the display string
}
// Provider names the vendor behind a record or role. Domain feeds the
// favicon icon rendered next to the name in the UI.
type Provider struct {
Name string `json:"name"`
Domain string `json:"domain"`
Icon string `json:"icon"`
}
// Providers is the detected-vendor strip: registrar, DNS host, hosting,
// email, and certificate authority.
type Providers struct {
Registrar *Provider `json:"registrar,omitempty"`
DNS *Provider `json:"dns,omitempty"`
Email *Provider `json:"email,omitempty"`
Hosting *Provider `json:"hosting,omitempty"`
CA *Provider `json:"ca,omitempty"`
}
// DNSRecord is one resolved record with the vendor its value points at.
type DNSRecord struct {
Type string `json:"type"` // A AAAA CNAME NS MX TXT
Value string `json:"value"`
Priority *int `json:"priority,omitempty"` // MX preference
Provider *Provider `json:"provider,omitempty"`
}
type mxPair struct {
host string
pref int
} }
// Clean normalises user input to a bare registrable hostname. // Clean normalises user input to a bare registrable hostname.
@@ -125,6 +162,7 @@ func (l *Lookup) Run(ctx context.Context, rawName string) *Result {
IPv4: []string{}, IPv4: []string{},
IPv6: []string{}, IPv6: []string{},
SSLAltNames: []string{}, SSLAltNames: []string{},
Records: []DNSRecord{},
Headers: map[string]string{}, Headers: map[string]string{},
FaviconURL: fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=128", name), FaviconURL: fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=128", name),
} }
@@ -155,14 +193,68 @@ func (l *Lookup) Run(ctx context.Context, rawName string) *Result {
l.host(r.IPv4[0], r) l.host(r.IPv4[0], r)
} }
l.headers(ctx, name, r) l.headers(ctx, name, r)
l.identify(r)
r.DNSProvider = detectDNSProvider(r.NameServers)
r.EmailProvider = detectEmailProvider(r.MXRecords)
r.HostingProvider = detectHostingProvider(r.Headers)
r.CAProvider = detectCA(r.SSLIssuer)
return r return r
} }
// identify resolves provider vendors after every probe has run, then builds
// the structured record list the detail page renders.
func (l *Lookup) identify(r *Result) {
r.Providers.Registrar = matchProvider(r.RegistrarName, registrarProviders)
for _, n := range r.NameServers {
if p := matchProvider(n, nsProviders); p != nil {
r.Providers.DNS = p
break
}
}
for _, m := range r.mxPairs {
if p := matchProvider(m.host, mailProviders); p != nil {
r.Providers.Email = p
break
}
}
r.Providers.Hosting = detectHostProvider(r)
r.Providers.CA = matchProvider(r.SSLIssuer, caProviders)
// Legacy flat names mirror the strip for the list page + old clients.
r.DNSProvider = nameOf(r.Providers.DNS)
r.EmailProvider = nameOf(r.Providers.Email)
r.HostingProvider = nameOf(r.Providers.Hosting)
r.CAProvider = nameOf(r.Providers.CA)
recs := make([]DNSRecord, 0,
len(r.IPv4)+len(r.IPv6)+len(r.NameServers)+len(r.mxPairs)+len(r.TXTRecords)+1)
add := func(t, v string, pri *int, p *Provider) {
recs = append(recs, DNSRecord{Type: t, Value: v, Priority: pri, Provider: p})
}
for _, ip := range r.IPv4 {
add("A", ip, nil, r.Providers.Hosting)
}
for _, ip := range r.IPv6 {
add("AAAA", ip, nil, r.Providers.Hosting)
}
if r.CNAME != "" {
add("CNAME", r.CNAME, nil, matchProvider(r.CNAME, hostTargets))
}
for _, n := range r.NameServers {
add("NS", n, nil, matchProvider(n, nsProviders))
}
for _, m := range r.mxPairs {
pri := m.pref
add("MX", m.host, &pri, matchProvider(m.host, mailProviders))
}
for _, t := range r.TXTRecords {
add("TXT", t, nil, matchTXTProvider(t))
}
r.Records = recs
}
func nameOf(p *Provider) string {
if p == nil {
return ""
}
return p.Name
}
// --- DNS -------------------------------------------------------------------- // --- DNS --------------------------------------------------------------------
func (l *Lookup) dns(ctx context.Context, name string, r *Result) { func (l *Lookup) dns(ctx context.Context, name string, r *Result) {
@@ -174,7 +266,9 @@ func (l *Lookup) dns(ctx context.Context, name string, r *Result) {
} }
if mx, err := res.LookupMX(ctx, name); err == nil { if mx, err := res.LookupMX(ctx, name); err == nil {
for _, m := range mx { for _, m := range mx {
r.MXRecords = append(r.MXRecords, fmt.Sprintf("%s (pri %d)", strings.TrimSuffix(m.Host, "."), m.Pref)) host := strings.TrimSuffix(strings.ToLower(m.Host), ".")
r.mxPairs = append(r.mxPairs, mxPair{host: host, pref: int(m.Pref)})
r.MXRecords = append(r.MXRecords, fmt.Sprintf("%s (pri %d)", host, m.Pref))
} }
} }
if txt, err := res.LookupTXT(ctx, name); err == nil { if txt, err := res.LookupTXT(ctx, name); err == nil {
@@ -297,108 +391,639 @@ func (l *Lookup) headers(ctx context.Context, name string, r *Result) {
} }
// --- Provider detection (condensed port of beszel's detect package) ----------- // --- Provider detection (condensed port of beszel's detect package) -----------
//
// Tables map a substring of the record value / org / issuer to a vendor name
// plus the brand domain used for the favicon icon. Order matters — first
// match wins, so specific substrings precede generic ones.
func detectDNSProvider(ns []string) string { type provMatch struct{ sub, name, dom string }
table := []struct{ sub, name string }{
{"cloudflare", "Cloudflare"}, {"awsdns", "Amazon Route 53"}, func prov(name, dom string) *Provider {
{"googledomains", "Google Domains"}, {"google.com", "Google Domains"}, return &Provider{Name: name, Domain: dom,
{"namecheap", "Namecheap"}, {"godaddy", "GoDaddy"}, {"domaincontrol", "GoDaddy"}, Icon: fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=64", dom)}
{"nsone.net", "NS1"}, {"digitalocean", "DigitalOcean"}, {"linode", "Linode"},
{"vultr", "Vultr"}, {"he.net", "Hurricane Electric"}, {"dynect", "Dyn (Oracle)"},
{"ultradns", "UltraDNS"}, {"dnsimple", "DNSimple"}, {"hover", "Hover"},
{"enom", "eNom"}, {"worldnic", "Network Solutions"}, {"zoneedit", "ZoneEdit"},
{"easydns", "EasyDNS"}, {"gandi", "Gandi"}, {"ovh", "OVH"},
{"hetzner", "Hetzner"}, {"azure-dns", "Microsoft Azure"},
{"dns.google", "Google Cloud DNS"}, {"porkbun", "Porkbun"},
{"cloudns", "ClouDNS"}, {"desec", "deSEC"}, {"nic.cz", "CZ.NIC"},
}
for _, n := range ns {
n = strings.ToLower(n)
for _, t := range table {
if strings.Contains(n, t.sub) {
return t.name
}
}
}
return ""
} }
func detectEmailProvider(mx []string) string { func matchProvider(haystack string, table []provMatch) *Provider {
table := []struct{ sub, name string }{ h := strings.ToLower(haystack)
{"google.com", "Google Workspace"}, {"aspmx", "Google Workspace"},
{"outlook.com", "Microsoft 365"}, {"protection.outlook", "Microsoft 365"},
{"mail.protection", "Microsoft 365"}, {"protonmail", "Proton Mail"},
{"proton.me", "Proton Mail"}, {"zoho", "Zoho Mail"},
{"fastmail", "Fastmail"}, {"mimecast", "Mimecast"},
{"proofpoint", "Proofpoint"}, {"barracuda", "Barracuda"},
{"mailgun", "Mailgun"}, {"sendgrid", "SendGrid"},
{"amazonses", "Amazon SES"}, {"yandex", "Yandex"},
{"tuta", "Tuta"}, {"mailbox.org", "mailbox.org"},
}
for _, m := range mx {
m = strings.ToLower(m)
for _, t := range table { for _, t := range table {
if strings.Contains(m, t.sub) { if strings.Contains(h, t.sub) {
return t.name return prov(t.name, t.dom)
} }
} }
} return nil
return ""
} }
func detectHostingProvider(h map[string]string) string { var nsProviders = []provMatch{
{"cloudflare", "Cloudflare", "cloudflare.com"},
{"awsdns", "Amazon Route 53", "aws.amazon.com"},
{"googledomains", "Google Domains", "domains.google"},
{"dns.google", "Google Cloud DNS", "cloud.google.com"},
{"azure-dns", "Microsoft Azure", "azure.microsoft.com"},
{"domaincontrol", "GoDaddy", "godaddy.com"},
{"godaddy", "GoDaddy", "godaddy.com"},
{"registrar-servers", "Namecheap", "namecheap.com"},
{"namecheap", "Namecheap", "namecheap.com"},
{"nsone.net", "NS1", "ns1.com"},
{"digitalocean", "DigitalOcean", "digitalocean.com"},
{"linode", "Akamai", "akamai.com"},
{"akamaitech", "Akamai", "akamai.com"},
{"vultr", "Vultr", "vultr.com"},
{"he.net", "Hurricane Electric", "he.net"},
{"dynect", "Dyn (Oracle)", "dyn.com"},
{"ultradns", "UltraDNS", "verisign.com"},
{"dnsimple", "DNSimple", "dnsimple.com"},
{"hover", "Hover", "hover.com"},
{"enom", "eNom", "enom.com"},
{"worldnic", "Network Solutions", "networksolutions.com"},
{"zoneedit", "ZoneEdit", "zoneedit.com"},
{"easydns", "EasyDNS", "easydns.com"},
{"gandi", "Gandi", "gandi.net"},
{"ovh", "OVHcloud", "ovhcloud.com"},
{"hetzner", "Hetzner", "hetzner.com"},
{"your-server.de", "Hetzner", "hetzner.com"},
{"porkbun", "Porkbun", "porkbun.com"},
{"cloudns", "ClouDNS", "cloudns.net"},
{"desec", "deSEC", "desec.io"},
{"nic.cz", "CZ.NIC", "nic.cz"},
{"dnsmadeeasy", "DNS Made Easy", "dnsmadeeasy.com"},
{"ui-dns", "IONOS", "ionos.com"},
{"squarespacedns", "Squarespace", "squarespace.com"},
{"wordpress.com", "WordPress.com", "wordpress.com"},
{"dns-parking.com", "Hostinger", "hostinger.com"},
{"hostinger", "Hostinger", "hostinger.com"},
{"siteground", "SiteGround", "siteground.com"},
{"dreamhost", "DreamHost", "dreamhost.com"},
{"inmotionhosting", "InMotion", "inmotionhosting.com"},
{"bluehost", "Bluehost", "bluehost.com"},
{"hostgator", "HostGator", "hostgator.com"},
{"kinstadns", "Kinsta", "kinsta.com"},
{"kasserver", "ALL-INKL.COM", "all-inkl.com"},
{"all-inkl", "ALL-INKL.COM", "all-inkl.com"},
{"strato", "STRATO", "strato.de"},
{"udagdns", "United Domains", "uniteddomains.com"},
{"udag.de", "United Domains", "uniteddomains.com"},
{"transip", "TransIP", "transip.nl"},
{"infomaniak", "Infomaniak", "infomaniak.com"},
{"one.com", "One.com", "one.com"},
{"aruba", "Aruba", "aruba.it"},
{"technorail", "Aruba", "aruba.it"},
{"inwx", "INWX", "inwx.com"},
{"kasserver.com", "ALL-INKL.COM", "all-inkl.com"},
{"contabo", "Contabo", "contabo.com"},
{"netcup", "netcup", "netcup.eu"},
{"glesys", "GleSYS", "glesys.se"},
{"scaleway", "Scaleway", "scaleway.com"},
{"exoscale", "Exoscale", "exoscale.com"},
{"dnspod", "DNSPod", "dnspod.com"},
{"hicloud", "Huawei Cloud", "huaweicloud.com"},
{"alidns", "Alibaba Cloud", "alibabacloud.com"},
{"purelymail", "Purelymail", "purelymail.com"},
{"migadu", "Migadu", "migadu.com"},
{"improvmx", "ImprovMX", "improvmx.com"},
{"bunny", "Bunny", "bunny.net"},
{"edgekey", "Akamai", "akamai.com"},
{"edgesuite", "Akamai", "akamai.com"},
{"fastly", "Fastly", "fastly.com"},
{"cdn77", "CDN77", "cdn77.com"},
{"stackpath", "StackPath", "stackpath.com"},
{"ns.cloudflare", "Cloudflare", "cloudflare.com"},
{"vercel-dns", "Vercel", "vercel.com"},
}
var mailProviders = []provMatch{
{"aspmx", "Google Workspace", "google.com"},
{"googlemail", "Google Workspace", "google.com"},
{"google.com", "Google Workspace", "google.com"},
{"outlook.com", "Microsoft 365", "microsoft.com"},
{"protection.outlook", "Microsoft 365", "microsoft.com"},
{"mail.protection", "Microsoft 365", "microsoft.com"},
{"protonmail", "Proton Mail", "proton.me"},
{"proton.me", "Proton Mail", "proton.me"},
{"zoho", "Zoho Mail", "zoho.com"},
{"fastmail", "Fastmail", "fastmail.com"},
{"messagingengine", "Fastmail", "fastmail.com"},
{"mimecast", "Mimecast", "mimecast.com"},
{"proofpoint", "Proofpoint", "proofpoint.com"},
{"barracuda", "Barracuda", "barracuda.com"},
{"mailgun", "Mailgun", "mailgun.com"},
{"sendgrid", "SendGrid", "sendgrid.com"},
{"amazonses", "Amazon SES", "aws.amazon.com"},
{"yandex", "Yandex", "yandex.com"},
{"tuta", "Tuta", "tuta.com"},
{"mailbox.org", "mailbox.org", "mailbox.org"},
{"purelymail", "Purelymail", "purelymail.com"},
{"migadu", "Migadu", "migadu.com"},
{"mxroute", "MXroute", "mxroute.com"},
{"mymxroute", "MXroute", "mxroute.com"},
{"icloud.com", "iCloud Mail", "apple.com"},
{"infomaniak", "Infomaniak", "infomaniak.com"},
{"mailpod", "One.com", "one.com"},
{"one.com", "One.com", "one.com"},
{"gmx", "GMX", "gmx.net"},
{"forwardemail", "Forward Email", "forwardemail.net"},
{"posteo", "Posteo", "posteo.de"},
{"mailo", "Mailo", "mailo.com"},
{"hey.com", "HEY", "hey.com"},
{"privateemail", "Namecheap Private Email", "namecheap.com"},
{"registrar-servers", "Namecheap", "namecheap.com"},
{"ovh.net", "OVHcloud Mail", "ovhcloud.com"},
{"perfora.net", "IONOS Mail", "ionos.com"},
{"ui-dns", "IONOS", "ionos.com"},
{"improvmx", "ImprovMX", "improvmx.com"},
{"porkbun", "Porkbun Mail", "porkbun.com"},
{"kundenserver", "IONOS Mail", "ionos.com"},
{"t-online", "T-Online", "telekom.de"},
{"web.de", "WEB.DE", "web.de"},
{"mail.ru", "Mail.ru", "mail.ru"},
{"qq.com", "QQ Mail", "qq.com"},
{"163.com", "NetEase Mail", "163.com"},
{"exim.rs", "Exim", "exim.org"},
{"mx.cloudflare", "Cloudflare Email Routing", "cloudflare.com"},
}
// hostTargets: CNAME targets that reveal the hosting platform.
var hostTargets = []provMatch{
{"github.io", "GitHub Pages", "github.com"},
{"vercel-dns", "Vercel", "vercel.com"},
{"vercel.app", "Vercel", "vercel.com"},
{"netlify", "Netlify", "netlify.com"},
{"cloudfront.net", "AWS CloudFront", "aws.amazon.com"},
{"azurefd.net", "Azure Front Door", "azure.microsoft.com"},
{"azureedge.net", "Azure CDN", "azure.microsoft.com"},
{"cloudapp.net", "Microsoft Azure", "azure.microsoft.com"},
{"trafficmanager.net", "Microsoft Azure", "azure.microsoft.com"},
{"herokuapp.com", "Heroku", "heroku.com"},
{"herokudns.com", "Heroku", "heroku.com"},
{"workers.dev", "Cloudflare Workers", "cloudflare.com"},
{"pages.dev", "Cloudflare Pages", "cloudflare.com"},
{"cdn.cloudflare", "Cloudflare", "cloudflare.com"},
{"fly.dev", "Fly.io", "fly.io"},
{"onrender.com", "Render", "render.com"},
{"railway.app", "Railway", "railway.app"},
{"surge.sh", "Surge", "surge.sh"},
{"neocities.org", "Neocities", "neocities.org"},
{"readthedocs", "Read the Docs", "readthedocs.io"},
{"webflow.io", "Webflow", "webflow.com"},
{"squarespace.com", "Squarespace", "squarespace.com"},
{"wixdns.net", "Wix", "wix.com"},
{"wixsite.com", "Wix", "wix.com"},
{"editorx.com", "Wix Studio", "wix.com"},
{"myshopify.com", "Shopify", "shopify.com"},
{"wpengine", "WP Engine", "wpengine.com"},
{"wordpress.com", "WordPress.com", "wordpress.com"},
{"googlehosted.com", "Google", "google.com"},
{"googleapis.com", "Google Cloud", "cloud.google.com"},
{"fastly.net", "Fastly", "fastly.com"},
{"fastlylb.net", "Fastly", "fastly.com"},
{"akamaiedge.net", "Akamai", "akamai.com"},
{"edgekey.net", "Akamai", "akamai.com"},
{"edgesuite.net", "Akamai", "akamai.com"},
{"akamai.net", "Akamai", "akamai.com"},
{"b-cdn.net", "Bunny", "bunny.net"},
{"bunnycdn", "Bunny", "bunny.net"},
{"elb.amazonaws.com", "AWS ELB", "aws.amazon.com"},
{"amazonaws.com", "Amazon AWS", "aws.amazon.com"},
{"stackpathdns", "StackPath", "stackpath.com"},
{"kinsta.cloud", "Kinsta", "kinsta.com"},
{"pantheonsite.io", "Pantheon", "pantheon.io"},
{"pantheon.io", "Pantheon", "pantheon.io"},
{"ghost.io", "Ghost", "ghost.org"},
{"hubspot", "HubSpot", "hubspot.com"},
{"dnsimple.com", "DNSimple", "dnsimple.com"},
{"pressdns.com", "Pressable", "pressable.com"},
{"kinsta", "Kinsta", "kinsta.com"},
{"cdn77", "CDN77", "cdn77.com"},
{"incapsula", "Imperva", "imperva.com"},
{"sucuri", "Sucuri", "sucuri.net"},
{"flywheel", "Flywheel", "getflywheel.com"},
{"gridscale", "gridscale", "gridscale.io"},
{"webflow", "Webflow", "webflow.com"},
{"framer.app", "Framer", "framer.com"},
{"carrd.co", "Carrd", "carrd.co"},
{"super.so", "Super", "super.so"},
{"typedream.app", "Typedream", "typedream.com"},
{"cargo.site", "Cargo", "cargo.site"},
{"gitlab.io", "GitLab Pages", "gitlab.com"},
{"bitbucket.io", "Bitbucket", "bitbucket.org"},
{"codeberg.page", "Codeberg Pages", "codeberg.org"},
{"glitch.me", "Glitch", "glitch.com"},
{"repl.co", "Replit", "replit.com"},
{"replit.app", "Replit", "replit.com"},
{"deno.dev", "Deno Deploy", "deno.com"},
{"supabase.co", "Supabase", "supabase.com"},
{"firebaseapp.com", "Firebase", "firebase.google.com"},
{"web.app", "Firebase", "firebase.google.com"},
{"appspot.com", "Google App Engine", "cloud.google.com"},
}
// asnProviders: ip-api org/AS strings → hosting vendor.
var asnProviders = []provMatch{
{"cloudflare", "Cloudflare", "cloudflare.com"},
{"akamai", "Akamai", "akamai.com"},
{"linode", "Akamai (Linode)", "linode.com"},
{"amazon", "Amazon AWS", "aws.amazon.com"},
{"aws", "Amazon AWS", "aws.amazon.com"},
{"google", "Google Cloud", "cloud.google.com"},
{"microsoft", "Microsoft Azure", "azure.microsoft.com"},
{"azure", "Microsoft Azure", "azure.microsoft.com"},
{"digitalocean", "DigitalOcean", "digitalocean.com"},
{"hetzner", "Hetzner", "hetzner.com"},
{"ovh", "OVHcloud", "ovhcloud.com"},
{"kimsufi", "OVHcloud", "ovhcloud.com"},
{"soyoustart", "OVHcloud", "ovhcloud.com"},
{"vultr", "Vultr", "vultr.com"},
{"choopa", "Vultr", "vultr.com"},
{"fastly", "Fastly", "fastly.com"},
{"vercel", "Vercel", "vercel.com"},
{"netlify", "Netlify", "netlify.com"},
{"github", "GitHub", "github.com"},
{"scaleway", "Scaleway", "scaleway.com"},
{"oracle", "Oracle Cloud", "oracle.com"},
{"alibaba", "Alibaba Cloud", "alibabacloud.com"},
{"tencent", "Tencent Cloud", "cloud.tencent.com"},
{"contabo", "Contabo", "contabo.com"},
{"ionos", "IONOS", "ionos.com"},
{"1&1", "IONOS", "ionos.com"},
{"shopify", "Shopify", "shopify.com"},
{"netcup", "netcup", "netcup.eu"},
{"upcloud", "UpCloud", "upcloud.com"},
{"kamatera", "Kamatera", "kamatera.com"},
{"fly.io", "Fly.io", "fly.io"},
{"render", "Render", "render.com"},
{"railway", "Railway", "railway.app"},
{"bunny", "Bunny", "bunny.net"},
{"hostinger", "Hostinger", "hostinger.com"},
{"siteground", "SiteGround", "siteground.com"},
{"godaddy", "GoDaddy", "godaddy.com"},
{"automattic", "WordPress.com", "wordpress.com"},
{"squarespace", "Squarespace", "squarespace.com"},
{"wix", "Wix", "wix.com"},
{"infomaniak", "Infomaniak", "infomaniak.com"},
{"dreamhost", "DreamHost", "dreamhost.com"},
{"liquid web", "Liquid Web", "liquidweb.com"},
{"liquidweb", "Liquid Web", "liquidweb.com"},
{"inmotion", "InMotion", "inmotionhosting.com"},
{"unified layer", "Bluehost", "bluehost.com"},
{"hostgator", "HostGator", "hostgator.com"},
{"glesys", "GleSYS", "glesys.se"},
{"exoscale", "Exoscale", "exoscale.com"},
{"incapsula", "Imperva", "imperva.com"},
{"sucuri", "Sucuri", "sucuri.net"},
{"ddos-guard", "DDoS-Guard", "ddos-guard.net"},
{"huawei", "Huawei Cloud", "huaweicloud.com"},
{"leaseweb", "Leaseweb", "leaseweb.com"},
{"aruba", "Aruba", "aruba.it"},
{"krystal", "Krystal", "krystal.uk"},
{"namecheap", "Namecheap", "namecheap.com"},
{"web-hosting.com", "Namecheap", "namecheap.com"},
{"zendesk", "Zendesk", "zendesk.com"},
{"kinsta", "Kinsta", "kinsta.com"},
{"flywheel", "Flywheel", "getflywheel.com"},
{"wp engine", "WP Engine", "wpengine.com"},
{"pantheon", "Pantheon", "pantheon.io"},
{"meta", "Meta", "meta.com"},
{"gitlab", "GitLab", "gitlab.com"},
{"stackpath", "StackPath", "stackpath.com"},
{"cdn77", "CDN77", "cdn77.com"},
{"imperva", "Imperva", "imperva.com"},
{"framer", "Framer", "framer.com"},
{"webflow", "Webflow", "webflow.com"},
{"readthedocs", "Read the Docs", "readthedocs.io"},
{"transip", "TransIP", "transip.nl"},
{"strato", "STRATO", "strato.de"},
{"all-inkl", "ALL-INKL.COM", "all-inkl.com"},
{"one.com", "One.com", "one.com"},
{"netlify", "Netlify", "netlify.com"},
{"neocities", "Neocities", "neocities.org"},
}
var registrarProviders = []provMatch{
{"markmonitor", "MarkMonitor", "markmonitor.com"},
{"namecheap", "Namecheap", "namecheap.com"},
{"cloudflare", "Cloudflare", "cloudflare.com"},
{"godaddy", "GoDaddy", "godaddy.com"},
{"gandi", "Gandi", "gandi.net"},
{"porkbun", "Porkbun", "porkbun.com"},
{"ovh", "OVHcloud", "ovhcloud.com"},
{"name.com", "Name.com", "name.com"},
{"ionos", "IONOS", "ionos.com"},
{"1&1", "IONOS", "ionos.com"},
{"1und1", "IONOS", "ionos.com"},
{"tucows", "Tucows", "tucows.com"},
{"squarespace", "Squarespace", "squarespace.com"},
{"google", "Google Domains", "domains.google"},
{"dynadot", "Dynadot", "dynadot.com"},
{"namesilo", "NameSilo", "namesilo.com"},
{"epik", "Epik", "epik.com"},
{"hexonet", "HEXONET", "hexonet.net"},
{"1api", "HEXONET", "hexonet.net"},
{"openprovider", "Openprovider", "openprovider.com"},
{"internetx", "InterNetX", "internetx.com"},
{"nic.cz", "CZ.NIC", "nic.cz"},
{"hostinger", "Hostinger", "hostinger.com"},
{"com laude", "Com Laude", "comlaude.com"},
{"csc", "CSC", "cscdbs.com"},
{"safenames", "Safenames", "safenames.net"},
{"xinnet", "Xin Net", "xinnet.com"},
{"alibaba", "Alibaba Cloud", "alibabacloud.com"},
{"dnspod", "DNSPod", "dnspod.com"},
{"tencent", "Tencent Cloud", "cloud.tencent.com"},
{"gabia", "Gabia", "gabia.com"},
{"reg.ru", "REG.RU", "reg.ru"},
{"nic.ru", "RU-CENTER", "nic.ru"},
{"nordname", "NordName", "nordname.com"},
{"internet.bs", "Internet.bs", "internetbs.net"},
{"key-systems", "Key-Systems", "key-systems.net"},
{"united domains", "United Domains", "uniteddomains.com"},
{"united-domains", "United Domains", "uniteddomains.com"},
{"scaleway", "Scaleway", "scaleway.com"},
{"hetzner", "Hetzner", "hetzner.com"},
{"infomaniak", "Infomaniak", "infomaniak.com"},
{"one.com", "One.com", "one.com"},
{"netim", "Netim", "netim.com"},
{"inwx", "INWX", "inwx.com"},
{"internetworx", "INWX", "inwx.com"},
{"checkdomain", "checkdomain", "checkdomain.de"},
{"domaindiscount24", "domaindiscount24", "domaindiscount24.com"},
{"easyname", "easyname", "easyname.com"},
{"world4you", "World4You", "world4you.com"},
{"all-inkl", "ALL-INKL.COM", "all-inkl.com"},
{"transip", "TransIP", "transip.nl"},
{"strato", "STRATO", "strato.de"},
{"hover", "Hover", "hover.com"},
{"enom", "eNom", "enom.com"},
{"namebright", "NameBright", "namebright.com"},
{"sav.com", "Sav.com", "sav.com"},
{"turncommerce", "DropCatch", "dropcatch.com"},
{"eurodns", "EuroDNS", "eurodns.com"},
{"gransy", "Gransy", "regtons.com"},
{"101domain", "101domain", "101domain.com"},
{"iwantmyname", "iwantmyname", "iwantmyname.com"},
{"rebel", "Rebel", "rebel.ca"},
{"blacknight", "Blacknight", "blacknight.com"},
{"gname", "GNAME", "gname.com"},
{"west.cn", "West.cn", "west.cn"},
{"west263", "West.cn", "west.cn"},
{"imena.ua", "Imena.ua", "imena.ua"},
{"ukraine.com.ua", "Hosting Ukraine", "ukraine.com.ua"},
{"beget", "Beget", "beget.com"},
{"selectel", "Selectel", "selectel.ru"},
{"timeweb", "Timeweb", "timeweb.ru"},
{"papaki", "Papaki", "papaki.com"},
{"dotname", "Dotname", "dotname.co.kr"},
{"gabia", "Gabia", "gabia.com"},
{"registar-se", "Namecheap", "namecheap.com"},
{"registrar-servers", "Namecheap", "namecheap.com"},
{"namebay", "Namebay", "namebay.com"},
{"domainpeople", "DomainPeople", "domainpeople.com"},
{"networksolutions", "Network Solutions", "networksolutions.com"},
{"network solutions", "Network Solutions", "networksolutions.com"},
{"register.com", "Register.com", "register.com"},
{"webcentral", "Webcentral", "webcentral.com.au"},
{"melbourne it", "Melbourne IT", "melbourneit.com.au"},
{"corporatedomains", "CSC", "cscdbs.com"},
{"amazon registrar", "Amazon Registrar", "aws.amazon.com"},
{"nominet", "Nominet", "nominet.uk"},
{"123-reg", "123 Reg", "123-reg.co.uk"},
{"domain.com", "Domain.com", "domain.com"},
{"dotster", "Dotster", "dotster.com"},
{"joker", "Joker.com", "joker.com"},
{"variomedia", "Variomedia", "variomedia.de"},
{"goneo", "goneo", "goneo.de"},
{"manitu", "manitu", "manitu.de"},
{"hosteurope", "Host Europe", "hosteurope.de"},
{"keyweb", "Keyweb", "keyweb.de"},
{"wedos", "WEDOS", "wedos.com"},
{"active24", "Active 24", "active24.com"},
{"forpsi", "FORPSI", "forpsi.com"},
{"ignum", "IGNUM", "ignum.cz"},
{"oneprovider", "OneProvider", "oneprovider.com"},
{"reg.cz", "REG.CZ", "reg.cz"},
{"subreg", "Websupport", "websupport.sk"},
{"websupport", "Websupport", "websupport.sk"},
{"loopia", "Loopia", "loopia.se"},
{"binero", "Binero", "binero.se"},
{"one.se", "One.com", "one.com"},
{"simply.com", "Simply.com", "simply.com"},
{"domeneshop", "Domeneshop", "domeneshop.no"},
{"syse", "Syse", "syse.no"},
{"registrar.eu", "Openprovider", "openprovider.com"},
{"mondo", "Mondo International", "mondo.com"},
{"dansk.net", "Dansk.net", "dansk.net"},
{"internet invest", "Imena.ua", "imena.ua"},
{"netearth", "NetEarth One", "netearthone.com"},
{"publicdomainregistry", "PublicDomainRegistry", "publicdomainregistry.com"},
{"pdr ltd", "PublicDomainRegistry", "publicdomainregistry.com"},
{"webnames", "Webnames", "webnames.ca"},
{"rebel.ca", "Rebel", "rebel.ca"},
{"easydns", "easyDNS", "easydns.com"},
{"namespro", "Namespro", "namespro.ca"},
{"sibername", "Sibername", "sibername.com"},
{"netfirms", "Netfirms", "netfirms.com"},
{"names.co.uk", "names.co.uk", "names.co.uk"},
{"namesco", "names.co.uk", "names.co.uk"},
{"fasthosts", "Fasthosts", "fasthosts.co.uk"},
{"heart internet", "Heart Internet", "heartinternet.uk"},
{"dreamhost", "DreamHost", "dreamhost.com"},
{"pair domains", "pair Domains", "pairdomains.com"},
{"domainthenet", "Domain The Net", "domainthenet.com"},
{"domainregistry", "DomainRegistry", "domainregistry.com"},
{"moniker", "Moniker", "moniker.com"},
{"fabulous", "Fabulous", "fabulous.com"},
{"above.com", "Above.com", "above.com"},
{"united-internet", "United Internet", "united-internet.de"},
{"united internet", "United Internet", "united-internet.de"},
{"arsys", "Arsys", "arsys.es"},
{"piensa solutions", "Piensa Solutions", "piensasolutions.com"},
{"dinahosting", "Dinahosting", "dinahosting.com"},
{"cdmon", "CDmon", "cdmon.com"},
{"nominalia", "Nominalia", "nominalia.com"},
{"register.it", "Register.it", "register.it"},
{"ovh sas", "OVHcloud", "ovhcloud.com"},
{"gandi sas", "Gandi", "gandi.net"},
{"blue razor", "GoDaddy", "godaddy.com"},
{"wild west domains", "GoDaddy", "godaddy.com"},
{"starfield", "GoDaddy", "godaddy.com"},
{"nameking", "Namecheap", "namecheap.com"},
{"enom", "eNom", "enom.com"},
{"realtime register", "Realtime Register", "realtimeregister.com"},
{"rrpproxy", "RRPproxy", "rrpproxy.net"},
{"key-systems gmbh", "Key-Systems", "key-systems.net"},
{"internet domains", "Internet Domains", "internetdomains.ch"},
{"corehub", "CORE", "corehub.net"},
{"don dominio", "DonDominio", "dondominio.com"},
{"arsys internet", "Arsys", "arsys.es"},
{"key-systems", "Key-Systems", "key-systems.net"},
{"centralnic", "CentralNic", "centralnic.com"},
{"internet.bs corp", "Internet.bs", "internetbs.net"},
{"gkg.net", "GKG.NET", "gkg.net"},
{"namecheap inc", "Namecheap", "namecheap.com"},
{"namecheap, inc", "Namecheap", "namecheap.com"},
{"google llc", "Google Domains", "domains.google"},
{"squarespace domains", "Squarespace", "squarespace.com"},
{"turncommerce, inc.", "DropCatch", "dropcatch.com"},
{"amazon", "Amazon Registrar", "aws.amazon.com"},
{"name.com, inc", "Name.com", "name.com"},
{"godaddy.com, llc", "GoDaddy", "godaddy.com"},
{"tucows domains", "Tucows", "tucows.com"},
{"porkbun llc", "Porkbun", "porkbun.com"},
{"cloudflare, inc", "Cloudflare", "cloudflare.com"},
}
// caProviders: issuer CN/org → certificate authority vendor.
var caProviders = []provMatch{
{"let's encrypt", "Let's Encrypt", "letsencrypt.org"},
{"letsencrypt", "Let's Encrypt", "letsencrypt.org"},
{"digicert", "DigiCert", "digicert.com"},
{"sectigo", "Sectigo", "sectigo.com"},
{"comodoca", "Sectigo", "sectigo.com"},
{"comodo", "Sectigo", "sectigo.com"},
{"globalsign", "GlobalSign", "globalsign.com"},
{"godaddy", "GoDaddy", "godaddy.com"},
{"starfield", "GoDaddy", "godaddy.com"},
{"entrust", "Entrust", "entrust.com"},
{"google trust", "Google Trust Services", "pki.goog"},
{"pki.goog", "Google Trust Services", "pki.goog"},
{"amazon", "Amazon Trust Services", "amazontrust.com"},
{"zerossl", "ZeroSSL", "zerossl.com"},
{"buypass", "Buypass", "buypass.com"},
{"ssl.com", "SSL.com", "ssl.com"},
{"actalis", "Actalis", "actalis.com"},
{"certum", "Certum", "certum.pl"},
{"harica", "HARICA", "harica.gr"},
{"swisssign", "SwissSign", "swisssign.com"},
{"cloudflare", "Cloudflare", "cloudflare.com"},
{"microsoft", "Microsoft", "microsoft.com"},
{"geotrust", "DigiCert", "digicert.com"},
{"thawte", "DigiCert", "digicert.com"},
{"rapidssl", "DigiCert", "digicert.com"},
{"usertrust", "Sectigo", "sectigo.com"},
{"certainly", "Certainly", "certainly.io"},
{"isrg", "Let's Encrypt", "letsencrypt.org"},
{"e1", "Let's Encrypt", "letsencrypt.org"},
{"r3", "Let's Encrypt", "letsencrypt.org"},
{"r10", "Let's Encrypt", "letsencrypt.org"},
{"r11", "Let's Encrypt", "letsencrypt.org"},
{"wotrus", "WoTrus", "wotrus.com"},
{"cameo", "Cameo", "cameo.com"},
}
// detectHostProvider picks the hosting vendor: CNAME target first (most
// precise), then response headers, then the ASN/org of the primary IPv4.
func detectHostProvider(r *Result) *Provider {
if r.CNAME != "" {
if p := matchProvider(r.CNAME, hostTargets); p != nil {
return p
}
}
h := r.Headers
server := strings.ToLower(h["server"] + " " + h["x-powered-by"] + " " + h["via"]) server := strings.ToLower(h["server"] + " " + h["x-powered-by"] + " " + h["via"])
if _, ok := h["cf-ray"]; ok || strings.Contains(server, "cloudflare") { switch {
return "Cloudflare" case h["cf-ray"] != "" || strings.Contains(server, "cloudflare"):
return prov("Cloudflare", "cloudflare.com")
case h["x-vercel-id"] != "" || strings.Contains(server, "vercel"):
return prov("Vercel", "vercel.com")
case h["x-nf-request-id"] != "" || strings.Contains(server, "netlify"):
return prov("Netlify", "netlify.com")
case h["x-amz-cf-id"] != "" || strings.Contains(server, "cloudfront"):
return prov("AWS CloudFront", "aws.amazon.com")
case h["x-github-request-id"] != "" || strings.Contains(server, "github"):
return prov("GitHub Pages", "github.com")
case strings.Contains(server, "fly.io") || h["fly-request-id"] != "":
return prov("Fly.io", "fly.io")
case strings.Contains(server, "render"):
return prov("Render", "render.com")
case strings.Contains(server, "shopify"):
return prov("Shopify", "shopify.com")
case strings.Contains(server, "squarespace"):
return prov("Squarespace", "squarespace.com")
case strings.Contains(server, "wix"):
return prov("Wix", "wix.com")
} }
if _, ok := h["x-vercel-id"]; ok || strings.Contains(server, "vercel") { webServers := []provMatch{
return "Vercel" {"awselb", "AWS ELB", "aws.amazon.com"},
{"gws", "Google", "google.com"},
{"kestrel", "Kestrel/.NET", "microsoft.com"},
{"cowboy", "Cowboy/Erlang", "heroku.com"},
} }
if _, ok := h["x-nf-request-id"]; ok || strings.Contains(server, "netlify") { if p := matchProvider(server, webServers); p != nil {
return "Netlify" return p
} }
if _, ok := h["x-amz-cf-id"]; ok || strings.Contains(server, "cloudfront") { if p := matchProvider(r.HostOrg+" "+r.HostAS+" "+r.HostISP, asnProviders); p != nil {
return "AWS CloudFront" return p
} }
if _, ok := h["x-github-request-id"]; ok || strings.Contains(server, "github") { if p := matchProvider(server, []provMatch{
return "GitHub Pages" {"nginx", "nginx", "nginx.org"},
{"apache", "Apache", "apache.org"},
{"caddy", "Caddy", "caddyserver.com"},
{"litespeed", "LiteSpeed", "litespeedtech.com"},
{"openresty", "OpenResty", "openresty.org"},
{"envoy", "Envoy", "envoyproxy.io"},
}); p != nil {
return p
} }
if strings.Contains(server, "fly.io") || h["fly-request-id"] != "" { return nil
return "Fly.io"
}
if strings.Contains(server, "render") {
return "Render"
}
table := []struct{ sub, name string }{
{"nginx", "nginx"}, {"apache", "Apache"}, {"caddy", "Caddy"},
{"litespeed", "LiteSpeed"}, {"openresty", "OpenResty"},
{"envoy", "Envoy"}, {"awselb", "AWS ELB"}, {"gws", "Google"},
{"kestrel", "Kestrel/.NET"}, {"cowboy", "Cowboy/Erlang"},
}
for _, t := range table {
if strings.Contains(server, t.sub) {
return t.name
}
}
return ""
} }
func detectCA(issuer string) string { // txtProviders: SPF includes and domain-verification strings → vendor.
i := strings.ToLower(issuer) var txtProviders = []provMatch{
table := []struct{ sub, name string }{ {"v=spf1 include:_spf.google.com", "Google Workspace", "google.com"},
{"let's encrypt", "Let's Encrypt"}, {"letsencrypt", "Let's Encrypt"}, {"include:spf.protonmail.ch", "Proton Mail", "proton.me"},
{"digicert", "DigiCert"}, {"sectigo", "Sectigo"}, {"comodoca", "Sectigo"}, {"include:mail.protonmail.ch", "Proton Mail", "proton.me"},
{"globalsign", "GlobalSign"}, {"godaddy", "GoDaddy"}, {"starfield", "GoDaddy"}, {"include:_spf.mx.cloudflare.net", "Cloudflare", "cloudflare.com"},
{"entrust", "Entrust"}, {"google trust", "Google Trust Services"}, {"include:servers.mcsv.net", "Mailchimp", "mailchimp.com"},
{"amazon", "Amazon"}, {"zerossl", "ZeroSSL"}, {"buypass", "Buypass"}, {"include:amazonses.com", "Amazon SES", "aws.amazon.com"},
{"ssl.com", "SSL.com"}, {"actalis", "Actalis"}, {"certum", "Certum"}, {"include:mailgun.org", "Mailgun", "mailgun.com"},
{"harica", "HARICA"}, {"swisssign", "SwissSign"}, {"cloudflare", "Cloudflare"}, {"include:sendgrid.net", "SendGrid", "sendgrid.com"},
{"pki.goog", "Google Trust Services"}, {"microsoft", "Microsoft"}, {"include:spf.protection.outlook.com", "Microsoft 365", "microsoft.com"},
} {"include:spf.purelymail.com", "Purelymail", "purelymail.com"},
for _, t := range table { {"_spf.purelymail.com", "Purelymail", "purelymail.com"},
if strings.Contains(i, t.sub) { {"purelymail_ownership_proof", "Purelymail", "purelymail.com"},
return t.name {"openai-domain-verification", "OpenAI", "openai.com"},
} {"include:_spf.migadu.com", "Migadu", "migadu.com"},
} {"include:mx.zohomail.com", "Zoho Mail", "zoho.com"},
return "" {"include:_spf.fastmail.com", "Fastmail", "fastmail.com"},
{"include:spf.mtasv.net", "Postmark", "postmarkapp.com"},
{"include:_spf.salesforce.com", "Salesforce", "salesforce.com"},
{"include:spf.improvmx.com", "ImprovMX", "improvmx.com"},
{"include:mail.zendesk.com", "Zendesk", "zendesk.com"},
{"include:spf.mandrillapp.com", "Mailchimp", "mailchimp.com"},
{"include:mailspike.net", "Mailspike", "mailspike.net"},
{"include:spf.messagelabs.com", "Broadcom", "broadcom.com"},
{"include:_spf.mx.yandex", "Yandex", "yandex.com"},
{"include:spf.tuta.io", "Tuta", "tuta.com"},
{"google-site-verification", "Google", "google.com"},
{"ms=", "Microsoft 365", "microsoft.com"},
{"facebook-domain-verification", "Meta", "meta.com"},
{"apple-domain-verification", "Apple", "apple.com"},
{"keybase-site-verification", "Keybase", "keybase.io"},
{"keybase", "Keybase", "keybase.io"},
{"protonmail-verification", "Proton", "proton.me"},
{"have-i-been-pwned-verification", "Have I Been Pwned", "haveibeenpwned.com"},
{"atlassian-domain-verification", "Atlassian", "atlassian.com"},
{"docker-verification", "Docker", "docker.com"},
{"docusign", "DocuSign", "docusign.com"},
{"yandex-verification", "Yandex", "yandex.com"},
{"stripe-verification", "Stripe", "stripe.com"},
{"dropbox-domain-verification", "Dropbox", "dropbox.com"},
{"brave-ledger-verification", "Brave", "brave.com"},
{"detectify-verification", "Detectify", "detectify.com"},
{"adobe-idp-site-verification", "Adobe", "adobe.com"},
{"webexdomainverification", "Cisco Webex", "webex.com"},
{"zoho-verification", "Zoho", "zoho.com"},
{"mailru-verification", "Mail.ru", "mail.ru"},
{"baidu-site-verification", "Baidu", "baidu.com"},
{"google-gws-recovery-domain-verification", "Google", "google.com"},
{"status-page-domain-verification", "Atlassian", "atlassian.com"},
{"citrix-verification", "Citrix", "citrix.com"},
{"tiktok-domain-verification", "TikTok", "tiktok.com"},
{"netlify-verification", "Netlify", "netlify.com"},
{"vercel-domain-verification", "Vercel", "vercel.com"},
{"workplace-domain-verification", "Meta", "meta.com"},
{"v=dmarc1", "DMARC", "dmarc.org"},
{"v=dkim1", "DKIM", "dkim.org"},
}
func matchTXTProvider(txt string) *Provider {
return matchProvider(txt, txtProviders)
} }
+82 -16
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Link } from "wouter"; import { Link } from "wouter";
import { ArrowLeft, Pause, Play, RefreshCw, Shield, Trash2 } from "lucide-react"; import { ArrowLeft, Globe, Pause, Play, RefreshCw, Shield, Trash2 } from "lucide-react";
import { api } from "@/api"; import { api } from "@/api";
import type { DomainCheck, DomainView, Subdomain } from "@/types"; import type { DNSRecord, DomainCheck, DomainView, Provider, Subdomain } from "@/types";
import { DomainDialog } from "@/components/DomainDialog"; import { DomainDialog } from "@/components/DomainDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -32,6 +32,61 @@ function fmtDate(iso: string | null | undefined) {
return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
} }
// Vendor chip shown next to records and inside the provider strip.
function ProviderBadge({ p }: { p?: Provider | null }) {
if (!p?.name) return null;
return (
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-border/60 bg-bg px-2 py-0.5">
{p.icon
? <img src={p.icon} alt="" className="size-3 rounded-[3px]" loading="lazy" />
: <Globe size={11} className="text-text-faint" />}
<span className="whitespace-nowrap text-[10.5px] text-text-dim">{p.name}</span>
</span>
);
}
function ProvTile({ label, p }: { label: string; p?: Provider }) {
return (
<div className="flex min-w-0 items-center gap-2.5 rounded-[9px] border border-border/60 bg-bg px-3 py-2.5">
{p?.icon
? <img src={p.icon} alt="" className="size-5 rounded-[5px]" loading="lazy" />
: <Globe size={17} className="shrink-0 text-text-faint" />}
<div className="min-w-0">
<div className="font-mono text-[9px] uppercase tracking-[0.1em] text-text-faint">{label}</div>
<div className="truncate text-[12px] font-medium text-text">{p?.name || "—"}</div>
</div>
</div>
);
}
const recTypeColor = {
A: "text-[var(--chart-1)]",
AAAA: "text-[var(--chart-1)]",
CNAME: "text-[var(--chart-4)]",
NS: "text-[var(--chart-2)]",
MX: "text-[var(--chart-3)]",
TXT: "text-[var(--chart-5)]",
} satisfies Record<NonNullable<DNSRecord["type"]>, string>;
const recOrder = ["A", "AAAA", "CNAME", "NS", "MX", "TXT"];
function RecordRow({ r }: { r: DNSRecord }) {
return (
<div className="flex items-center gap-3 border-b border-border/50 px-4 py-2 last:border-0">
<span className={cn("w-11 shrink-0 font-mono text-[9.5px] font-semibold uppercase tracking-[0.1em]",
(r.type ? recTypeColor[r.type] : undefined) ?? "text-text-faint")}>
{r.type}
</span>
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-text" title={r.value ?? ""}>
{r.value}
{r.type === "MX" && r.priority !== null && r.priority !== undefined
? <span className="text-text-faint"> (pri {r.priority})</span>
: null}
</span>
<ProviderBadge p={r.provider} />
</div>
);
}
export function DomainDetailPage({ id }: { id: string }) { export function DomainDetailPage({ id }: { id: string }) {
const [d, setD] = useState<DomainView | null>(null); const [d, setD] = useState<DomainView | null>(null);
const [checks, setChecks] = useState<DomainCheck[]>([]); const [checks, setChecks] = useState<DomainCheck[]>([]);
@@ -133,13 +188,26 @@ export function DomainDetailPage({ id }: { id: string }) {
<div className="font-mono text-[10px] text-text-faint">{d.caProvider || "—"}</div> <div className="font-mono text-[10px] text-text-faint">{d.caProvider || "—"}</div>
</div> </div>
<div className="rounded-[10px] border border-border bg-surface px-4 py-3"> <div className="rounded-[10px] border border-border bg-surface px-4 py-3">
<div className="font-mono text-[10px] uppercase tracking-[0.1em] text-text-faint">{t("domains.providers")}</div> <div className="font-mono text-[10px] uppercase tracking-[0.1em] text-text-faint">{t("domains.checked")}</div>
<div className="mt-1 truncate text-[13px] font-medium">{d.dnsProvider || "—"}</div> <div className="mt-1 truncate text-[13px] font-medium">
<div className="font-mono text-[10px] text-text-faint">{d.hostingProvider || d.emailProvider || "—"}</div> {d.lastChecked ? new Date(d.lastChecked).toLocaleDateString() : "—"}
</div>
<div className="font-mono text-[10px] text-text-faint">every {d.intervalH}h</div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3 max-[820px]:grid-cols-1"> {/* Vendor strip — registrar, DNS, hosting, email, CA */}
<Section title={t("domains.providers")}>
<div className="grid grid-cols-5 gap-2 p-2.5 max-[900px]:grid-cols-3 max-[560px]:grid-cols-2">
<ProvTile label={t("domains.registrar")} p={d.providers?.registrar} />
<ProvTile label={t("domains.dns")} p={d.providers?.dns} />
<ProvTile label={t("domains.hosting")} p={d.providers?.hosting} />
<ProvTile label={t("domains.email")} p={d.providers?.email} />
<ProvTile label={t("domains.tlsCert")} p={d.providers?.ca} />
</div>
</Section>
<div className="mt-3 grid grid-cols-2 gap-3 max-[820px]:grid-cols-1">
<Section title={t("domains.registration")}> <Section title={t("domains.registration")}>
<Row k={t("domains.created")} v={fmtDate(d.creationDate)} /> <Row k={t("domains.created")} v={fmtDate(d.creationDate)} />
<Row k={t("domains.updated")} v={fmtDate(d.updatedDate)} /> <Row k={t("domains.updated")} v={fmtDate(d.updatedDate)} />
@@ -153,19 +221,17 @@ export function DomainDetailPage({ id }: { id: string }) {
</Section> </Section>
<Section title={t("domains.dns")}> <Section title={t("domains.dns")}>
{d.nameServers?.map((n) => <Row key={n} k="NS" v={n} mono />)} {[...(d.records ?? [])]
{d.mxRecords?.map((m) => <Row key={m} k="MX" v={m} mono />)} .sort((a, b) => recOrder.indexOf(a.type ?? "") - recOrder.indexOf(b.type ?? ""))
{d.ipv4?.map((a) => <Row key={a} k="A" v={a} mono />)} .map((r, i) => <RecordRow key={i} r={r} />)}
{d.ipv6?.map((a) => <Row key={a} k="AAAA" v={a} mono />)} {(d.records ?? []).length === 0 ? (
{d.cname ? <Row k="CNAME" v={d.cname} mono /> : null} <p className="px-4 py-4 text-center text-[12px] text-text-faint">—</p>
{d.txtRecords?.slice(0, 4).map((t, i) => <Row key={i} k="TXT" v={t.length > 60 ? t.slice(0, 60) + "…" : t} mono />)} ) : null}
{d.dnsProvider ? <Row k={t("domains.dnsProvider")} v={d.dnsProvider} /> : null}
{d.emailProvider ? <Row k={t("domains.email")} v={d.emailProvider} /> : null}
</Section> </Section>
<Section title={t("domains.tlsCert")}> <Section title={t("domains.tlsCert")}>
<Row k={t("domains.issuer")} v={d.sslIssuer} /> <Row k={t("domains.issuer")} v={d.sslIssuer} />
<Row k="CA" v={d.caProvider} /> <Row k="CA" v={d.providers?.ca ? <ProviderBadge p={d.providers.ca} /> : d.caProvider} />
<Row k={t("domains.valid")} v={`${fmtDate(d.sslValidFrom)} → ${fmtDate(d.sslValidTo)}`} mono /> <Row k={t("domains.valid")} v={`${fmtDate(d.sslValidFrom)} → ${fmtDate(d.sslValidTo)}`} mono />
<Row k={t("domains.subject")} v={d.sslSubject} mono /> <Row k={t("domains.subject")} v={d.sslSubject} mono />
<Row k={t("domains.key")} v={d.sslKeySize ? `${d.sslKeySize} bit` : ""} mono /> <Row k={t("domains.key")} v={d.sslKeySize ? `${d.sslKeySize} bit` : ""} mono />
@@ -181,7 +247,7 @@ export function DomainDetailPage({ id }: { id: string }) {
<Row k={t("domains.isp")} v={d.hostIsp} /> <Row k={t("domains.isp")} v={d.hostIsp} />
<Row k={t("domains.org")} v={d.hostOrg} /> <Row k={t("domains.org")} v={d.hostOrg} />
<Row k="AS" v={d.hostAs} mono /> <Row k="AS" v={d.hostAs} mono />
<Row k={t("domains.hosting")} v={d.hostingProvider} /> <Row k={t("domains.hosting")} v={d.providers?.hosting ? <ProviderBadge p={d.providers.hosting} /> : d.hostingProvider} />
<Row k={t("domains.checked")} v={d.lastChecked ? new Date(d.lastChecked).toLocaleString() : "—"} mono /> <Row k={t("domains.checked")} v={d.lastChecked ? new Date(d.lastChecked).toLocaleString() : "—"} mono />
</Section> </Section>
</div> </div>
+3
View File
@@ -20,6 +20,9 @@ export type Heartbeat = components["schemas"]["Heartbeat"];
export type Domain = components["schemas"]["Domain"]; export type Domain = components["schemas"]["Domain"];
export type DomainInput = components["schemas"]["DomainInput"]; export type DomainInput = components["schemas"]["DomainInput"];
export type DomainView = components["schemas"]["DomainView"]; export type DomainView = components["schemas"]["DomainView"];
export type DNSRecord = components["schemas"]["DNSRecord"];
export type Provider = components["schemas"]["Provider"];
export type Providers = components["schemas"]["Providers"];
export type DomainCheck = components["schemas"]["DomainCheck"]; export type DomainCheck = components["schemas"]["DomainCheck"];
export type Subdomain = components["schemas"]["Subdomain"]; export type Subdomain = components["schemas"]["Subdomain"];
export type System = components["schemas"]["System"]; export type System = components["schemas"]["System"];
+51
View File
@@ -1765,6 +1765,12 @@ components:
type: string type: string
caProvider: caProvider:
type: string type: string
records:
type: array
items:
$ref: "#/components/schemas/DNSRecord"
providers:
$ref: "#/components/schemas/Providers"
headers: headers:
type: object type: object
additionalProperties: additionalProperties:
@@ -1807,6 +1813,51 @@ components:
type: string type: string
alerts: alerts:
$ref: "#/components/schemas/AlertRules" $ref: "#/components/schemas/AlertRules"
Provider:
type: object
description: Detected vendor behind a record or role
properties:
name:
type: string
domain:
type: string
description: brand domain the favicon icon was derived from
icon:
type: string
Providers:
type: object
description: Detected vendor strip — registrar, DNS, hosting, email, CA
properties:
registrar:
$ref: "#/components/schemas/Provider"
nullable: true
dns:
$ref: "#/components/schemas/Provider"
nullable: true
email:
$ref: "#/components/schemas/Provider"
nullable: true
hosting:
$ref: "#/components/schemas/Provider"
nullable: true
ca:
$ref: "#/components/schemas/Provider"
nullable: true
DNSRecord:
type: object
properties:
type:
type: string
enum: [A, AAAA, CNAME, NS, MX, TXT]
value:
type: string
priority:
type: integer
nullable: true
description: MX preference
provider:
$ref: "#/components/schemas/Provider"
nullable: true
DomainView: DomainView:
allOf: allOf:
- $ref: "#/components/schemas/Domain" - $ref: "#/components/schemas/Domain"