diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fea341..da5eb8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ All notable changes to Dash. Format follows
## [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
- Settings dialog regrouped into icon-labelled sections (renderer, appearance,
diff --git a/apps/backend/internal/api/domains.go b/apps/backend/internal/api/domains.go
index 02d43b5..8d3ad19 100644
--- a/apps/backend/internal/api/domains.go
+++ b/apps/backend/internal/api/domains.go
@@ -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,
host_region, host_city, host_isp, host_org, host_as, host_lat, host_lon,
dns_provider, email_provider, hosting_provider, ca_provider, headers,
+ records, providers,
favicon_url, lookup_error, interval_h, last_checked, position, alerts, created_at`
// domainRow mirrors the domains table; JSON text columns decode on scan.
@@ -82,6 +83,8 @@ type domainRow struct {
HostingProvider string `json:"hostingProvider"`
CAProvider string `json:"caProvider"`
Headers map[string]string `json:"headers"`
+ Records []domain.DNSRecord `json:"records"`
+ Providers domain.Providers `json:"providers"`
FaviconURL string `json:"faviconUrl"`
LookupError string `json:"lookupError"`
IntervalH int `json:"intervalH"`
@@ -93,7 +96,7 @@ type domainRow struct {
func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) {
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
err := row.Scan(&d.ID, &d.Name, &d.TLD, &active, &autoRenew, &d.AlertDaysBefore,
&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.HostRegion, &d.HostCity, &d.HostISP, &d.HostOrg, &d.HostAS, &d.HostLat,
&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)
if err != nil {
return nil, err
@@ -124,6 +127,9 @@ func scanDomain(row interface{ Scan(...any) error }) (*domainRow, error) {
d.SSLAltNames = decodeList(altNames)
d.Headers = map[string]string{}
_ = 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
}
@@ -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)
sslFrom, sslTo = fmtT(r.SSLValidFrom), fmtT(r.SSLValidTo)
hdr, _ := json.Marshal(r.Headers)
+ recs, _ := json.Marshal(r.Records)
+ provs, _ := json.Marshal(r.Providers)
_, err := s.db.Exec(`UPDATE domains SET
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=?,
host_country_code=?, host_region=?, host_city=?, host_isp=?, host_org=?,
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')
WHERE id=?`,
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,
r.HostCity, r.HostISP, r.HostOrg, r.HostAS, r.HostLat, r.HostLon,
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 {
s.log.Error("domain apply", zap.Error(err))
return
diff --git a/apps/backend/internal/db/migrations/0011_domain_intel.sql b/apps/backend/internal/db/migrations/0011_domain_intel.sql
new file mode 100644
index 0000000..ac3990d
--- /dev/null
+++ b/apps/backend/internal/db/migrations/0011_domain_intel.sql
@@ -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;
diff --git a/apps/backend/internal/domain/domain.go b/apps/backend/internal/domain/domain.go
index 419f046..bc0a36d 100644
--- a/apps/backend/internal/domain/domain.go
+++ b/apps/backend/internal/domain/domain.go
@@ -88,9 +88,46 @@ type Result struct {
HostingProvider string `json:"hostingProvider"`
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"`
FaviconURL string `json:"faviconUrl"`
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.
@@ -125,6 +162,7 @@ func (l *Lookup) Run(ctx context.Context, rawName string) *Result {
IPv4: []string{},
IPv6: []string{},
SSLAltNames: []string{},
+ Records: []DNSRecord{},
Headers: map[string]string{},
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.headers(ctx, name, r)
-
- r.DNSProvider = detectDNSProvider(r.NameServers)
- r.EmailProvider = detectEmailProvider(r.MXRecords)
- r.HostingProvider = detectHostingProvider(r.Headers)
- r.CAProvider = detectCA(r.SSLIssuer)
+ l.identify(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 --------------------------------------------------------------------
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 {
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 {
@@ -297,108 +391,639 @@ func (l *Lookup) headers(ctx context.Context, name string, r *Result) {
}
// --- 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 {
- table := []struct{ sub, name string }{
- {"cloudflare", "Cloudflare"}, {"awsdns", "Amazon Route 53"},
- {"googledomains", "Google Domains"}, {"google.com", "Google Domains"},
- {"namecheap", "Namecheap"}, {"godaddy", "GoDaddy"}, {"domaincontrol", "GoDaddy"},
- {"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 ""
+type provMatch struct{ sub, name, dom string }
+
+func prov(name, dom string) *Provider {
+ return &Provider{Name: name, Domain: dom,
+ Icon: fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=64", dom)}
}
-func detectEmailProvider(mx []string) string {
- table := []struct{ sub, name string }{
- {"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 {
- if strings.Contains(m, t.sub) {
- return t.name
- }
+func matchProvider(haystack string, table []provMatch) *Provider {
+ h := strings.ToLower(haystack)
+ for _, t := range table {
+ if strings.Contains(h, t.sub) {
+ return prov(t.name, t.dom)
}
}
- return ""
+ return nil
}
-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"])
- if _, ok := h["cf-ray"]; ok || strings.Contains(server, "cloudflare") {
- return "Cloudflare"
+ switch {
+ 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") {
- return "Vercel"
+ webServers := []provMatch{
+ {"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") {
- return "Netlify"
+ if p := matchProvider(server, webServers); p != nil {
+ return p
}
- if _, ok := h["x-amz-cf-id"]; ok || strings.Contains(server, "cloudfront") {
- return "AWS CloudFront"
+ if p := matchProvider(r.HostOrg+" "+r.HostAS+" "+r.HostISP, asnProviders); p != nil {
+ return p
}
- if _, ok := h["x-github-request-id"]; ok || strings.Contains(server, "github") {
- return "GitHub Pages"
+ if p := matchProvider(server, []provMatch{
+ {"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 "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 ""
+ return nil
}
-func detectCA(issuer string) string {
- i := strings.ToLower(issuer)
- table := []struct{ sub, name string }{
- {"let's encrypt", "Let's Encrypt"}, {"letsencrypt", "Let's Encrypt"},
- {"digicert", "DigiCert"}, {"sectigo", "Sectigo"}, {"comodoca", "Sectigo"},
- {"globalsign", "GlobalSign"}, {"godaddy", "GoDaddy"}, {"starfield", "GoDaddy"},
- {"entrust", "Entrust"}, {"google trust", "Google Trust Services"},
- {"amazon", "Amazon"}, {"zerossl", "ZeroSSL"}, {"buypass", "Buypass"},
- {"ssl.com", "SSL.com"}, {"actalis", "Actalis"}, {"certum", "Certum"},
- {"harica", "HARICA"}, {"swisssign", "SwissSign"}, {"cloudflare", "Cloudflare"},
- {"pki.goog", "Google Trust Services"}, {"microsoft", "Microsoft"},
- }
- for _, t := range table {
- if strings.Contains(i, t.sub) {
- return t.name
- }
- }
- return ""
+// txtProviders: SPF includes and domain-verification strings → vendor.
+var txtProviders = []provMatch{
+ {"v=spf1 include:_spf.google.com", "Google Workspace", "google.com"},
+ {"include:spf.protonmail.ch", "Proton Mail", "proton.me"},
+ {"include:mail.protonmail.ch", "Proton Mail", "proton.me"},
+ {"include:_spf.mx.cloudflare.net", "Cloudflare", "cloudflare.com"},
+ {"include:servers.mcsv.net", "Mailchimp", "mailchimp.com"},
+ {"include:amazonses.com", "Amazon SES", "aws.amazon.com"},
+ {"include:mailgun.org", "Mailgun", "mailgun.com"},
+ {"include:sendgrid.net", "SendGrid", "sendgrid.com"},
+ {"include:spf.protection.outlook.com", "Microsoft 365", "microsoft.com"},
+ {"include:spf.purelymail.com", "Purelymail", "purelymail.com"},
+ {"_spf.purelymail.com", "Purelymail", "purelymail.com"},
+ {"purelymail_ownership_proof", "Purelymail", "purelymail.com"},
+ {"openai-domain-verification", "OpenAI", "openai.com"},
+ {"include:_spf.migadu.com", "Migadu", "migadu.com"},
+ {"include:mx.zohomail.com", "Zoho Mail", "zoho.com"},
+ {"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)
}
diff --git a/apps/frontend/src/pages/DomainDetailPage.tsx b/apps/frontend/src/pages/DomainDetailPage.tsx
index 9f90755..905cde3 100644
--- a/apps/frontend/src/pages/DomainDetailPage.tsx
+++ b/apps/frontend/src/pages/DomainDetailPage.tsx
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useState } from "react";
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 type { DomainCheck, DomainView, Subdomain } from "@/types";
+import type { DNSRecord, DomainCheck, DomainView, Provider, Subdomain } from "@/types";
import { DomainDialog } from "@/components/DomainDialog";
import { Button } from "@/components/ui/button";
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" });
}
+// Vendor chip shown next to records and inside the provider strip.
+function ProviderBadge({ p }: { p?: Provider | null }) {
+ if (!p?.name) return null;
+ return (
+
+ {p.icon
+ ?
+ :
—
+ ) : null}