@@ -2,10 +2,12 @@ package output
22
33import (
44 "encoding/base64"
5+ stderrors "errors"
56 "fmt"
67 "io"
78 "log/slog"
89 "maps"
10+ "net"
911 "os"
1012 "path/filepath"
1113 "regexp"
@@ -27,6 +29,7 @@ import (
2729 "github.com/projectdiscovery/nuclei/v3/pkg/model"
2830 "github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
2931 "github.com/projectdiscovery/nuclei/v3/pkg/operators"
32+ "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/honeypotdetector"
3033 protocolUtils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
3134 "github.com/projectdiscovery/nuclei/v3/pkg/types"
3235 "github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
@@ -38,6 +41,10 @@ import (
3841 urlutil "github.com/projectdiscovery/utils/url"
3942)
4043
44+ // ErrHoneypotSuppressed is returned by the output writer when a match result is suppressed
45+ // due to honeypot detection.
46+ var ErrHoneypotSuppressed = stderrors .New ("honeypot suppressed result" )
47+
4148// Writer is an interface which writes output to somewhere for nuclei events.
4249type Writer interface {
4350 // Close closes the output writer interface
@@ -65,6 +72,9 @@ type StandardWriter struct {
6572 timestamp bool
6673 noMetadata bool
6774 matcherStatus bool
75+ honeypotDetector * honeypotdetector.Detector
76+ suppressHoneypot bool
77+ honeypotThreshold int
6878 mutex * sync.Mutex
6979 aurora aurora.Aurora
7080 outputFile io.WriteCloser
@@ -265,21 +275,23 @@ func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
265275 }
266276
267277 writer := & StandardWriter {
268- json : options .JSONL ,
269- jsonReqResp : ! options .OmitRawRequests ,
270- noMetadata : options .NoMeta ,
271- matcherStatus : options .MatcherStatus ,
272- timestamp : options .Timestamp ,
273- aurora : auroraColorizer ,
274- mutex : & sync.Mutex {},
275- outputFile : outputFile ,
276- traceFile : traceOutput ,
277- errorFile : errorOutput ,
278- severityColors : colorizer .New (auroraColorizer ),
279- storeResponse : options .StoreResponse ,
280- storeResponseDir : options .StoreResponseDir ,
281- omitTemplate : options .OmitTemplate ,
282- KeysToRedact : options .Redact ,
278+ json : options .JSONL ,
279+ jsonReqResp : ! options .OmitRawRequests ,
280+ noMetadata : options .NoMeta ,
281+ matcherStatus : options .MatcherStatus ,
282+ timestamp : options .Timestamp ,
283+ suppressHoneypot : options .SuppressHoneypotResults ,
284+ honeypotThreshold : options .HoneypotThreshold ,
285+ aurora : auroraColorizer ,
286+ mutex : & sync.Mutex {},
287+ outputFile : outputFile ,
288+ traceFile : traceOutput ,
289+ errorFile : errorOutput ,
290+ severityColors : colorizer .New (auroraColorizer ),
291+ storeResponse : options .StoreResponse ,
292+ storeResponseDir : options .StoreResponseDir ,
293+ omitTemplate : options .OmitTemplate ,
294+ KeysToRedact : options .Redact ,
283295 }
284296
285297 if v := os .Getenv ("DISABLE_STDOUT" ); v == "true" || v == "1" {
@@ -289,6 +301,14 @@ func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
289301 return writer , nil
290302}
291303
304+ // SetHoneypotDetector attaches an initialized honeypot detector to the writer.
305+ func (w * StandardWriter ) SetHoneypotDetector (detector * honeypotdetector.Detector ) {
306+ w .honeypotDetector = detector
307+ if detector != nil {
308+ w .honeypotThreshold = detector .Threshold ()
309+ }
310+ }
311+
292312func (w * StandardWriter ) ResultCount () int {
293313 return int (w .resultCount .Load ())
294314}
@@ -299,6 +319,29 @@ func (w *StandardWriter) Write(event *ResultEvent) error {
299319 return nil
300320 }
301321
322+ // Honeypot detection is performed only for successful matches.
323+ if event .MatcherStatus && w .honeypotDetector != nil {
324+ hostKey := event .URL
325+ if hostKey == "" && event .Host != "" {
326+ hostKey = event .Host
327+ if event .Port != "" {
328+ hostKey = net .JoinHostPort (event .Host , event .Port )
329+ }
330+ }
331+
332+ if hostKey != "" {
333+ justFlagged := w .honeypotDetector .RecordMatch (hostKey , event .TemplateID )
334+ if justFlagged {
335+ normalized := honeypotdetector .NormalizeHostKey (hostKey )
336+ gologger .Warning ().Msgf ("Potential honeypot detected: %s (matched %d distinct templates)" , normalized , w .honeypotThreshold )
337+ }
338+
339+ if w .suppressHoneypot && w .honeypotDetector .IsFlagged (hostKey ) {
340+ return ErrHoneypotSuppressed
341+ }
342+ }
343+ }
344+
302345 // Enrich the result event with extra metadata on the template-path and url.
303346 if event .TemplatePath != "" {
304347 event .Template , event .TemplateURL = utils .TemplatePathURL (types .ToString (event .TemplatePath ), types .ToString (event .TemplateID ), event .TemplateVerifier )
0 commit comments