// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. package checker import ( "context" "fmt" "net/http" "sync" ) const ObservationKeyHoneypot = "honeypot" // honeypotPaths is the curated list of paths that internet scanners probe // immediately after a new TLS certificate appears in CT logs. Any path // returning a non-404/410 response is worth reporting to the site owner. // // /.well-known/security.txt is intentionally absent — it is already // evaluated by securityTxtRule via the wellknown collector. var honeypotPaths = []struct { path string critical bool // true → leak of credentials or source code }{ // Credentials / source leaks — critical {"/.env", true}, {"/.git/config", true}, {"/.vscode/sftp.json", true}, {"/.DS_Store", true}, // Debug / metrics endpoints {"/actuator/env", true}, {"/server-status", false}, {"/debug/default/view", false}, {"/trace.axd", false}, {"/@vite/env", false}, {"/info.php", false}, // Admin panels {"/console/", false}, {"/server", false}, {"/login.action", false}, {"/telescope/requests", false}, // APIs / registries {"/v2/_catalog", false}, // CMS entry points {"/xmlrpc.php", false}, {"/wp-json/", false}, {"/?rest_route=/wp/v2/users/", false}, // Config dumps {"/config.json", false}, {"/version", false}, } // HoneypotData is the observation written under ObservationKeyHoneypot. type HoneypotData struct { Probes map[string]HoneypotProbe `json:"probes"` } // HoneypotProbe is the outcome of a single path probe. type HoneypotProbe struct { PathProbe Critical bool `json:"critical,omitempty"` } // honeypotCollector probes known-bad paths on the first HTTPS IP without // following redirects. A redirect is itself a finding. type honeypotCollector struct{} func (honeypotCollector) Key() string { return ObservationKeyHoneypot } func (honeypotCollector) Collect(ctx context.Context, t Target) (any, error) { if len(t.IPs) == 0 { return nil, fmt.Errorf("no IPs to probe") } transport, cleanup := newPinnedHTTPSTransport(t.IPs[0], t.Host, t.Timeout) defer cleanup() // No redirect following: a redirect to /login is itself a finding. client := &http.Client{ Transport: transport, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, } out := HoneypotData{Probes: make(map[string]HoneypotProbe, len(honeypotPaths))} var mu sync.Mutex var wg sync.WaitGroup sem := make(chan struct{}, MaxConcurrentProbes) for _, hp := range honeypotPaths { wg.Add(1) sem <- struct{}{} go func(hp struct { path string critical bool }) { defer wg.Done() defer func() { <-sem }() probe := HoneypotProbe{ PathProbe: fetchHTTPSPath(ctx, client, t.Host, hp.path, t.UserAgent, 4<<10), Critical: hp.critical, } mu.Lock() out.Probes[hp.path] = probe mu.Unlock() }(hp) } wg.Wait() return &out, nil } func init() { RegisterCollector(honeypotCollector{}) }