checker: add honeypot-path collector and rules
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
4cae94153f
commit
086d3e151d
5 changed files with 397 additions and 46 deletions
81
checker/rules_honeypot.go
Normal file
81
checker/rules_honeypot.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue