checker-http/checker/rules_honeypot.go
Pierre-Olivier Mercier 086d3e151d
All checks were successful
continuous-integration/drone/push Build is passing
checker: add honeypot-path collector and rules
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
2026-06-13 16:25:21 +09:00

81 lines
2.5 KiB
Go

// 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"
sdk "git.happydns.org/checker-sdk-go/checker"
)
func init() { RegisterRule(&honeypotRule{}) }
// honeypotRule reports any honeypot path that returned a non-404/410
// response, signalling a potentially exposed sensitive endpoint.
type honeypotRule struct{}
func (r *honeypotRule) Name() string { return "http.honeypot" }
func (r *honeypotRule) Description() string {
return "Reports sensitive paths (/.env, /.git/config, /actuator/env, …) that are reachable from the internet."
}
func (r *honeypotRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
data, errSt := loadHTTPData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
hp, ok, err := LoadExtension[HoneypotData](data, ObservationKeyHoneypot)
if err != nil {
return []sdk.CheckState{{Status: sdk.StatusError, Code: "http.honeypot.decode_error", Message: err.Error()}}
}
if !ok {
return []sdk.CheckState{unknownState("http.honeypot.no_data", "Honeypot collector did not run.")}
}
var states []sdk.CheckState
successfulProbes := 0
for path, probe := range hp.Probes {
if probe.Error != "" || probe.StatusCode == 0 {
continue
}
successfulProbes++
st := sdk.CheckState{
Subject: path,
Meta: map[string]any{"url": probe.URL, "status_code": probe.StatusCode},
}
switch {
case probe.StatusCode == 200 || (probe.StatusCode >= 301 && probe.StatusCode <= 308):
st.Status = sdk.StatusWarn
st.Code = "http.honeypot.exposed"
st.Message = fmt.Sprintf("%s is accessible (HTTP %d, %d bytes).", path, probe.StatusCode, probe.Bytes)
if probe.Critical {
st.Status = sdk.StatusCrit
st.Code = "http.honeypot.critical_exposed"
}
case probe.StatusCode == 401 || probe.StatusCode == 403:
st.Status = sdk.StatusInfo
st.Code = "http.honeypot.protected"
st.Message = fmt.Sprintf("%s exists but is access-controlled (HTTP %d).", path, probe.StatusCode)
default:
continue
}
states = append(states, st)
}
if successfulProbes == 0 {
return []sdk.CheckState{unknownState("http.honeypot.no_response", "All honeypot probes failed or timed out.")}
}
if len(states) == 0 {
return []sdk.CheckState{{
Status: sdk.StatusOK,
Code: "http.honeypot.clean",
Subject: data.Domain,
Message: "No sensitive honeypot paths are reachable.",
}}
}
return states
}