checker: add honeypot-path collector and rules
All checks were successful
continuous-integration/drone/push Build is passing

Probes 20 known-bad paths (/.env, /.git/config, /actuator/env, etc.)
that CT-log scanners hit immediately after a new certificate is issued.
Critical credential/source-leak paths raise StatusCrit; other exposed
paths raise StatusWarn; 401/403 responses raise StatusInfo.

Fixes: #1
This commit is contained in:
nemunaire 2026-06-13 16:01:22 +09:00
commit 086d3e151d
5 changed files with 397 additions and 46 deletions

View file

@ -0,0 +1,109 @@
// 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{}) }