Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/common/honeypotdetector/honeypotdetector.go
4538 views
1
package honeypotdetector
2
3
import (
4
"fmt"
5
"net"
6
"net/url"
7
"strings"
8
"sync"
9
)
10
11
// Detector tracks honeypot likelihood by counting distinct template matches per normalized host.
12
// Once a host reaches the configured threshold, it becomes flagged.
13
type Detector struct {
14
threshold int
15
hosts sync.Map // map[normalizedHost]*hostState
16
}
17
18
type hostState struct {
19
mu sync.Mutex
20
templateIDs map[string]struct{}
21
flagged bool
22
}
23
24
// New creates a new honeypot detector.
25
func New(threshold int) *Detector {
26
if threshold <= 0 {
27
threshold = 1
28
}
29
return &Detector{
30
threshold: threshold,
31
}
32
}
33
34
// Threshold returns the distinct template count required to flag a host.
35
func (d *Detector) Threshold() int {
36
if d == nil {
37
return 0
38
}
39
return d.threshold
40
}
41
42
// RecordMatch records a match for the given host and templateID.
43
//
44
// It returns true only when the host has just become flagged (i.e. crossed the threshold).
45
func (d *Detector) RecordMatch(host, templateID string) bool {
46
if d == nil {
47
return false
48
}
49
50
normalizedHost := normalizeHostKey(host)
51
if normalizedHost == "" || templateID == "" {
52
return false
53
}
54
55
stateAny, _ := d.hosts.LoadOrStore(normalizedHost, &hostState{
56
templateIDs: make(map[string]struct{}),
57
})
58
state := stateAny.(*hostState)
59
60
state.mu.Lock()
61
defer state.mu.Unlock()
62
63
if state.flagged {
64
return false
65
}
66
if _, ok := state.templateIDs[templateID]; ok {
67
return false
68
}
69
70
state.templateIDs[templateID] = struct{}{}
71
if len(state.templateIDs) >= d.threshold {
72
state.flagged = true
73
state.templateIDs = nil
74
return true
75
}
76
return false
77
}
78
79
// IsFlagged returns whether the given host is flagged.
80
func (d *Detector) IsFlagged(host string) bool {
81
if d == nil {
82
return false
83
}
84
85
normalizedHost := normalizeHostKey(host)
86
if normalizedHost == "" {
87
return false
88
}
89
90
stateAny, ok := d.hosts.Load(normalizedHost)
91
if !ok {
92
return false
93
}
94
state := stateAny.(*hostState)
95
96
state.mu.Lock()
97
defer state.mu.Unlock()
98
return state.flagged
99
}
100
101
// Summary returns a short string with the total number of flagged hosts.
102
func (d *Detector) Summary() string {
103
if d == nil {
104
return "honeypot-detected hosts: 0"
105
}
106
107
var flagged int
108
d.hosts.Range(func(_, v any) bool {
109
state := v.(*hostState)
110
state.mu.Lock()
111
if state.flagged {
112
flagged++
113
}
114
state.mu.Unlock()
115
return true
116
})
117
118
return fmt.Sprintf("honeypot-detected hosts: %d", flagged)
119
}
120
121
// NormalizeHostKey normalizes host strings so different input formats map to the same key.
122
func NormalizeHostKey(input string) string {
123
return normalizeHostKey(input)
124
}
125
126
func normalizeHostKey(input string) string {
127
s := strings.TrimSpace(input)
128
if s == "" {
129
return ""
130
}
131
132
// Strip trailing slashes early.
133
s = strings.TrimRight(s, "/")
134
135
// If an absolute URL is present, parse it to reliably extract host and optional port.
136
if strings.Contains(s, "://") {
137
u, err := url.Parse(s)
138
if err == nil && u != nil {
139
host := u.Hostname()
140
port := u.Port()
141
if host == "" {
142
return ""
143
}
144
host = normalizeHostWithoutPort(host)
145
if port != "" {
146
return net.JoinHostPort(host, port)
147
}
148
return host
149
}
150
// fall through if parsing fails
151
}
152
153
// Remove any path suffix (we only care about the authority).
154
if idx := strings.IndexByte(s, '/'); idx >= 0 {
155
s = s[:idx]
156
}
157
158
// If it looks like host:port (including bracketed IPv6), try SplitHostPort first.
159
if host, port, err := net.SplitHostPort(s); err == nil {
160
host = normalizeHostWithoutPort(host)
161
if port == "" {
162
return host
163
}
164
return net.JoinHostPort(host, port)
165
}
166
167
// Handle bracketed IPv6 without port: [2001:db8::1]
168
if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
169
host := strings.TrimSuffix(strings.TrimPrefix(s, "["), "]")
170
return normalizeHostWithoutPort(host)
171
}
172
173
// Handle bare IPv6 or host without port.
174
return normalizeHostWithoutPort(s)
175
}
176
177
func normalizeHostWithoutPort(host string) string {
178
h := strings.TrimSpace(host)
179
if h == "" {
180
return ""
181
}
182
h = strings.TrimPrefix(h, "[")
183
h = strings.TrimSuffix(h, "]")
184
h = strings.ToLower(h)
185
186
if ip := net.ParseIP(h); ip != nil {
187
return ip.String()
188
}
189
return h
190
}
191
192