From 041c54c18caf47addbf3459f22a19aa18e48c6a3 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Tue, 22 Sep 2026 18:26:24 +0200 Subject: [PATCH] feat(domains): per-record provider intel + vendor strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- CHANGELOG.md | 9 + apps/backend/internal/api/domains.go | 17 +- .../db/migrations/0011_domain_intel.sql | 9 + apps/backend/internal/domain/domain.go | 815 ++++++++++++++++-- apps/frontend/src/pages/DomainDetailPage.tsx | 98 ++- apps/frontend/src/types.ts | 3 + openapi.yaml | 51 ++ 7 files changed, 887 insertions(+), 115 deletions(-) create mode 100644 apps/backend/internal/db/migrations/0011_domain_intel.sql 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 + ? + : } + {p.name} + + ); +} + +function ProvTile({ label, p }: { label: string; p?: Provider }) { + return ( +
+ {p?.icon + ? + : } +
+
{label}
+
{p?.name || "—"}
+
+
+ ); +} + +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, string>; +const recOrder = ["A", "AAAA", "CNAME", "NS", "MX", "TXT"]; + +function RecordRow({ r }: { r: DNSRecord }) { + return ( +
+ + {r.type} + + + {r.value} + {r.type === "MX" && r.priority !== null && r.priority !== undefined + ? (pri {r.priority}) + : null} + + +
+ ); +} + export function DomainDetailPage({ id }: { id: string }) { const [d, setD] = useState(null); const [checks, setChecks] = useState([]); @@ -133,13 +188,26 @@ export function DomainDetailPage({ id }: { id: string }) {
{d.caProvider || "—"}
-
{t("domains.providers")}
-
{d.dnsProvider || "—"}
-
{d.hostingProvider || d.emailProvider || "—"}
+
{t("domains.checked")}
+
+ {d.lastChecked ? new Date(d.lastChecked).toLocaleDateString() : "—"} +
+
every {d.intervalH}h
-
+ {/* Vendor strip — registrar, DNS, hosting, email, CA */} +
+
+ + + + + +
+
+ +
@@ -153,19 +221,17 @@ export function DomainDetailPage({ id }: { id: string }) {
- {d.nameServers?.map((n) => )} - {d.mxRecords?.map((m) => )} - {d.ipv4?.map((a) => )} - {d.ipv6?.map((a) => )} - {d.cname ? : null} - {d.txtRecords?.slice(0, 4).map((t, i) => 60 ? t.slice(0, 60) + "…" : t} mono />)} - {d.dnsProvider ? : null} - {d.emailProvider ? : null} + {[...(d.records ?? [])] + .sort((a, b) => recOrder.indexOf(a.type ?? "") - recOrder.indexOf(b.type ?? "")) + .map((r, i) => )} + {(d.records ?? []).length === 0 ? ( +

—

+ ) : null}
- + : d.caProvider} /> @@ -181,7 +247,7 @@ export function DomainDetailPage({ id }: { id: string }) { - + : d.hostingProvider} />
diff --git a/apps/frontend/src/types.ts b/apps/frontend/src/types.ts index ded4c9e..583b8a0 100644 --- a/apps/frontend/src/types.ts +++ b/apps/frontend/src/types.ts @@ -20,6 +20,9 @@ export type Heartbeat = components["schemas"]["Heartbeat"]; export type Domain = components["schemas"]["Domain"]; export type DomainInput = components["schemas"]["DomainInput"]; 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 Subdomain = components["schemas"]["Subdomain"]; export type System = components["schemas"]["System"]; diff --git a/openapi.yaml b/openapi.yaml index 4bb7d9f..20316a5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1765,6 +1765,12 @@ components: type: string caProvider: type: string + records: + type: array + items: + $ref: "#/components/schemas/DNSRecord" + providers: + $ref: "#/components/schemas/Providers" headers: type: object additionalProperties: @@ -1807,6 +1813,51 @@ components: type: string alerts: $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: allOf: - $ref: "#/components/schemas/Domain"