Skip to content

feat: implement honeypot detection mechanism#7056

Open
FuZoe wants to merge 2 commits intoprojectdiscovery:devfrom
FuZoe:feature/honeypot-detection
Open

feat: implement honeypot detection mechanism#7056
FuZoe wants to merge 2 commits intoprojectdiscovery:devfrom
FuZoe:feature/honeypot-detection

Conversation

@FuZoe
Copy link

@FuZoe FuZoe commented Feb 27, 2026

Add honeypot detection to identify and mitigate hosts that return positive matches for an abnormally high number of templates.

Features:

  • Add --honeypot-threshold (-hpt) flag to set detection threshold (percentage of templates matched by host to flag as honeypot)
  • Add --honeypot-suppress (-hps) flag to automatically suppress results from flagged honeypot hosts
  • Implement match density tracking per host with unique template counting
  • Add terminal warning: [HONEYPOT?] host matched X% of templates
  • Implement honeypot signature detection for common honeypots: Cowrie, Dionaea, Glastopf, Conpot, Elastichoney

Usage:
nuclei -hpt 30 -hps -t templates/ -l targets.txt

Changes:

  • pkg/types/types.go: Add HoneypotThreshold and HoneypotSuppress fields to Options struct
  • cmd/nuclei/main.go: Add CLI flags in optimization group
  • pkg/protocols/common/honeypotcache/: New package for honeypot detection cache with tests
  • pkg/protocols/protocols.go: Add HoneypotCache to ExecutorOptions
  • internal/runner/runner.go: Initialize and close honeypot cache
  • pkg/tmplexec/exec.go: Integrate honeypot check in result writing

Refs #6403

Proposed changes

This PR implements honeypot detection as requested in #6403.

The implementation includes:

  1. Match density tracking per host
  2. Configurable threshold via CLI flags
  3. Optional result suppression for flagged hosts
  4. Known honeypot signature detection

Proof

  • Unit tests added for honeypotcache package (12 tests, all passing)
  • All existing tests pass (go test ./pkg/protocols/... ./pkg/tmplexec/... ./internal/runner/...)
  • Build successful (go build ./...)

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • New Features

    • Added honeypot detection and suppression to identify and filter results from likely honeypot hosts using per-host template-match percentages.
    • New CLI flags to configure detection threshold and enable result suppression.
    • Signature-based detection against known honeypot patterns, host normalization, and runtime statistics/verbose tracing.
    • Result emission now respects honeypot suppression when enabled.
  • Tests

    • Added comprehensive tests covering detection, suppression, normalization, uniqueness, and signature matching.

Add honeypot detection to identify and mitigate hosts that return
positive matches for an abnormally high number of templates.

Features:
- Add --honeypot-threshold (-hpt) flag to set detection threshold
  (percentage of templates matched by host to flag as honeypot)
- Add --honeypot-suppress (-hps) flag to automatically suppress
  results from flagged honeypot hosts
- Implement match density tracking per host with unique template
  counting
- Add terminal warning: [HONEYPOT?] host matched X% of templates
- Implement honeypot signature detection for common honeypots:
  Cowrie, Dionaea, Glastopf, Conpot, Elastichoney

Usage:
  nuclei -hpt 30 -hps -t templates/ -l targets.txt

Changes:
- pkg/types/types.go: Add HoneypotThreshold and HoneypotSuppress
  fields to Options struct
- cmd/nuclei/main.go: Add CLI flags in optimization group
- pkg/protocols/common/honeypotcache/: New package for honeypot
  detection cache with tests
- pkg/protocols/protocols.go: Add HoneypotCache to ExecutorOptions
- internal/runner/runner.go: Initialize and close honeypot cache
- pkg/tmplexec/exec.go: Integrate honeypot check in result writing

Refs projectdiscovery#6403
@auto-assign auto-assign bot requested a review from dwisiswant0 February 27, 2026 03:38
@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 27, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Changed honeypot-suppress flag short name from -hps to -hpsu for improved clarity
  • Added documentation comment for HoneypotCache field in ExecutorOptions
  • Internal refactoring in honeypotcache.go with no security impact

Comment @neo help for available commands. · Open in Neo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Walkthrough

Adds honeypot detection and suppression: new CLI flags, a concurrent per-host honeypot cache with signature and threshold-based detection, integration into runner/executor to propagate the cache and optionally suppress results, plus tests for detection and normalization.

Changes

Cohort / File(s) Summary
CLI Configuration
cmd/nuclei/main.go
Adds CLI flags honeypot-threshold (int, alias -hpt) and honeypot-suppress (bool, alias -hpsu) binding to options.HoneypotThreshold and options.HoneypotSuppress.
Honeypot Cache Implementation
pkg/protocols/common/honeypotcache/honeypotcache.go, pkg/protocols/common/honeypotcache/honeypotcache_test.go
New package implementing concurrent per-host template-match tracking, threshold % calculation, signature-based detection, suppression support, normalization utilities, and comprehensive tests.
Runner Integration
internal/runner/runner.go
Adds honeypotCache field, initializes cache when threshold > 0, sets total templates count, propagates cache to executor options, and closes cache on runner cleanup.
Executor & Template Execution
pkg/protocols/protocols.go, pkg/tmplexec/exec.go
Adds HoneypotCache to ExecutorOptions and propagates it; Execute checks cache to suppress writes for detected honeypots and marks template matches when results are emitted.
Options Types
pkg/types/types.go
Adds HoneypotThreshold (int) and HoneypotSuppress (bool) to Options and copies them in Options.Copy().

Sequence Diagram

sequenceDiagram
    participant CLI as CLI/Main
    participant Runner as Runner
    participant Cache as HoneypotCache
    participant Executor as Executor
    participant Template as TemplateExec

    CLI->>Runner: RunEnumeration(honeypotThreshold, honeypotSuppress)
    alt threshold > 0
        Runner->>Cache: New(threshold, suppress, maxHosts)
        Runner->>Runner: store honeypotCache
    end
    Runner->>Runner: Load templates
    alt Cache exists
        Runner->>Cache: SetTotalTemplates(count)
    end
    Runner->>Executor: set HoneypotCache(cache)
    loop for each template execution
        Template->>Cache: Check(ctx)
        alt Cache indicates honeypot
            Cache-->>Template: Suppress result
            Template->>Template: Skip WriteResult
        else not honeypot
            Template->>Template: Execute matchers
            alt Match found
                Template->>Cache: MarkMatch(ctx, templateID)
                Cache->>Cache: update match count, evaluate %
                alt threshold exceeded
                    Cache-->>Template: mark host honeypot
                end
            end
            Template->>Executor: WriteResult
        end
    end
    Runner->>Cache: Close()
    Cache->>Cache: Log stats & purge
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hop and count each quirky trace,
Templates matched across the place.
When percentages climb and signals chime,
I hush the noise — one silent rhyme.
Cheers to fewer false alarms, carrot-time! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement honeypot detection mechanism' clearly and concisely summarizes the main feature being added across all changed files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
pkg/protocols/common/honeypotcache/honeypotcache.go (1)

103-115: Consider handling potential panic from cache iteration during concurrent access.

The GetALL call in Close() may interact with concurrent Set() operations. While gcache is generally thread-safe, ensure this is intentional shutdown behavior where no more matches are being recorded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/common/honeypotcache/honeypotcache.go` around lines 103 - 115,
Close may panic if matchedHosts.GetALL iterates while Set is called
concurrently; before iterating, set a shutdown flag (e.g., add an atomic/boolean
field on Cache like closing and set c.closing.Store(true) in Cache.Close) so
Cache.Set() can return early when closing, then perform the GetALL iteration;
additionally wrap the iteration in a defer-recover to catch/log any unexpected
panic during GetALL to avoid crashing; update Cache.Set (or equivalent mutators)
to check the closing flag and skip updates once closing is true, and keep
existing calls to matchedHosts.Purge() and calculatePercentage/Threshold logic
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/nuclei/main.go`:
- Around line 438-439: The short alias "-hps" for HoneypotSuppress conflicts
with the existing short alias used by --http-stats; update the flag registration
for options.HoneypotSuppress (the flagSet.BoolVarP call) to use a unique
single-character short name (e.g., change "hps" to an unused single-letter like
"y" or "z"), and ensure no other flag in the same flagSet uses that letter;
adjust only the BoolVarP invocation for HoneypotSuppress and verify there are no
remaining duplicate short aliases (leave --http-stats unchanged).

In `@pkg/protocols/common/honeypotcache/honeypotcache.go`:
- Around line 180-211: MarkMatch has a race on initializing per-host cache via
the GetIFPresent/Set pattern on matchedHosts: concurrent goroutines can each
create separate hostMatches and overwrite each other. Replace the manual
GetIFPresent/Set logic in MarkMatch by using the gcache LoaderFunc-based atomic
initialization (configure matchedHosts in New() with LoaderFunc returning
&hostMatches{matches: make(map[string]bool)}), then use
matchedHosts.Get(cacheKey) to retrieve the *hostMatches so creation is
singleflight/atomic; keep subsequent cache.mu locking and updates (matchCount,
matches map, flaggedWarning) unchanged.

In `@pkg/protocols/protocols.go`:
- Around line 102-103: The new HoneypotCache field of ExecutorOptions isn't
being propagated when creating copies or when reapplying engine options; update
the Copy() method and ApplyNewEngineOptions() to copy/assign the HoneypotCache
the same way HostErrorsCache is handled so the cache is preserved across copies
and option reapplication (ensure you mirror the nil-safety/assignment logic used
for HostErrorsCache when setting HoneypotCache on the returned ExecutorOptions
and when applying new engine options).

---

Nitpick comments:
In `@pkg/protocols/common/honeypotcache/honeypotcache.go`:
- Around line 103-115: Close may panic if matchedHosts.GetALL iterates while Set
is called concurrently; before iterating, set a shutdown flag (e.g., add an
atomic/boolean field on Cache like closing and set c.closing.Store(true) in
Cache.Close) so Cache.Set() can return early when closing, then perform the
GetALL iteration; additionally wrap the iteration in a defer-recover to
catch/log any unexpected panic during GetALL to avoid crashing; update Cache.Set
(or equivalent mutators) to check the closing flag and skip updates once closing
is true, and keep existing calls to matchedHosts.Purge() and
calculatePercentage/Threshold logic unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9142eae and 935b491.

📒 Files selected for processing (7)
  • cmd/nuclei/main.go
  • internal/runner/runner.go
  • pkg/protocols/common/honeypotcache/honeypotcache.go
  • pkg/protocols/common/honeypotcache/honeypotcache_test.go
  • pkg/protocols/protocols.go
  • pkg/tmplexec/exec.go
  • pkg/types/types.go

Comment on lines 438 to 439
flagSet.IntVarP(&options.HoneypotThreshold, "honeypot-threshold", "hpt", 0, "honeypot detection threshold (percentage of templates matched by host to flag as honeypot)"),
flagSet.BoolVarP(&options.HoneypotSuppress, "honeypot-suppress", "hps", false, "suppress results from hosts flagged as honeypots"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n -B2 -A2 'hps|http-stats' cmd/nuclei/main.go | head -50

Repository: projectdiscovery/nuclei

Length of output: 868


🏁 Script executed:

rg -c -n 'hps' cmd/nuclei/main.go

Repository: projectdiscovery/nuclei

Length of output: 68


🏁 Script executed:

rg -n '"hps"' cmd/nuclei/main.go

Repository: projectdiscovery/nuclei

Length of output: 323


Alias conflict: -hps is already used by --http-stats on line 485.

The -hps short flag alias on line 439 duplicates the one used by --http-stats on line 485. Go's pflag library does not permit duplicate short flag aliases within the same flagSet, causing the CLI to fail at startup.

🐛 Proposed fix: Use a different alias
-		flagSet.BoolVarP(&options.HoneypotSuppress, "honeypot-suppress", "hps", false, "suppress results from hosts flagged as honeypots"),
+		flagSet.BoolVarP(&options.HoneypotSuppress, "honeypot-suppress", "hpsu", false, "suppress results from hosts flagged as honeypots"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
flagSet.IntVarP(&options.HoneypotThreshold, "honeypot-threshold", "hpt", 0, "honeypot detection threshold (percentage of templates matched by host to flag as honeypot)"),
flagSet.BoolVarP(&options.HoneypotSuppress, "honeypot-suppress", "hps", false, "suppress results from hosts flagged as honeypots"),
flagSet.IntVarP(&options.HoneypotThreshold, "honeypot-threshold", "hpt", 0, "honeypot detection threshold (percentage of templates matched by host to flag as honeypot)"),
flagSet.BoolVarP(&options.HoneypotSuppress, "honeypot-suppress", "hpsu", false, "suppress results from hosts flagged as honeypots"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/nuclei/main.go` around lines 438 - 439, The short alias "-hps" for
HoneypotSuppress conflicts with the existing short alias used by --http-stats;
update the flag registration for options.HoneypotSuppress (the flagSet.BoolVarP
call) to use a unique single-character short name (e.g., change "hps" to an
unused single-letter like "y" or "z"), and ensure no other flag in the same
flagSet uses that letter; adjust only the BoolVarP invocation for
HoneypotSuppress and verify there are no remaining duplicate short aliases
(leave --http-stats unchanged).

@FuZoe
Copy link
Author

FuZoe commented Feb 27, 2026

Test Results

All honeypotcache tests pass:

testresult
  • 12 tests passed (TestHoneypotCache + TestHoneypotSignatureDetection)
  • Build successful

- Change -hps to -hpsu to avoid alias conflict with --http-stats
- Use LoaderFunc for atomic cache initialization (fix race condition)
- Propagate HoneypotCache in Copy() and ApplyNewEngineOptions()
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
pkg/protocols/common/honeypotcache/honeypotcache.go (1)

125-152: ⚠️ Potential issue | 🟡 Minor

Normalize cache keys consistently to avoid host key fragmentation.

NormalizeCacheValue preserves host casing, so the same host can be split across keys (for example, mixed-case inputs), which undercounts matches and can delay honeypot flagging.

💡 Suggested fix
 func (c *Cache) NormalizeCacheValue(value string) string {
-	var normalizedValue = value
+	var normalizedValue = strings.TrimSpace(value)
 
 	u, err := url.ParseRequestURI(value)
 	if err != nil || u.Host == "" {
 		if strings.Contains(value, ":") {
 			return normalizedValue
 		}
 		u, err2 := url.ParseRequestURI("https://" + value)
 		if err2 != nil {
 			return normalizedValue
 		}
-		normalizedValue = u.Host
+		host := strings.ToLower(u.Hostname())
+		if port := u.Port(); port != "" {
+			normalizedValue = net.JoinHostPort(host, port)
+		} else {
+			normalizedValue = host
+		}
 	} else {
-		port := u.Port()
+		host := strings.ToLower(u.Hostname())
+		port := u.Port()
 		if port == "" {
 			switch u.Scheme {
 			case "https":
-				normalizedValue = net.JoinHostPort(u.Host, "443")
+				normalizedValue = net.JoinHostPort(host, "443")
 			case "http":
-				normalizedValue = net.JoinHostPort(u.Host, "80")
+				normalizedValue = net.JoinHostPort(host, "80")
+			default:
+				normalizedValue = host
 			}
 		} else {
-			normalizedValue = u.Host
+			normalizedValue = net.JoinHostPort(host, port)
 		}
 	}
 
 	return normalizedValue
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/common/honeypotcache/honeypotcache.go` around lines 125 - 152,
NormalizeCacheValue currently preserves host casing and can fragment cache keys;
fix by lowercasing any host-derived parts before assigning normalizedValue.
Specifically, when you parse a URL (using url.ParseRequestURI) and read u.Host
or u.Port(), use u.Hostname() and do strings.ToLower(u.Hostname()) and then
rebuild the host+port with net.JoinHostPort(lowercaseHostname, port) (or set
normalizedValue to strings.ToLower(u.Host) when no port logic applies) so all
host-based normalizedValue assignments are canonicalized to lowercase.
🧹 Nitpick comments (1)
pkg/protocols/common/honeypotcache/honeypotcache.go (1)

98-101: SetVerbose is currently a no-op.

SetVerbose sets c.verbose, but log emission ignores it. Either gate honeypot logs with c.verbose or remove this API to avoid misleading behavior.

Also applies to: 117-117, 179-180, 213-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/common/honeypotcache/honeypotcache.go` around lines 98 - 101,
SetVerbose currently stores the flag but logging code ignores it; either honor
the flag or remove the API. Fix by locating the Cache struct's log emission
sites (any calls that use the cache logger inside Cache methods) and wrap those
log calls with if c.verbose { ... } (or check c.verbose before calling
c.logger.*), or alternatively remove the SetVerbose method and any callers so
there’s no misleading API. Ensure you update all related log sites referenced in
the review (the other Cache methods that emit honeypot logs) and keep the
Cache.verbose field and SetVerbose in sync with that change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/protocols/common/honeypotcache/honeypotcache.go`:
- Around line 125-152: NormalizeCacheValue currently preserves host casing and
can fragment cache keys; fix by lowercasing any host-derived parts before
assigning normalizedValue. Specifically, when you parse a URL (using
url.ParseRequestURI) and read u.Host or u.Port(), use u.Hostname() and do
strings.ToLower(u.Hostname()) and then rebuild the host+port with
net.JoinHostPort(lowercaseHostname, port) (or set normalizedValue to
strings.ToLower(u.Host) when no port logic applies) so all host-based
normalizedValue assignments are canonicalized to lowercase.

---

Nitpick comments:
In `@pkg/protocols/common/honeypotcache/honeypotcache.go`:
- Around line 98-101: SetVerbose currently stores the flag but logging code
ignores it; either honor the flag or remove the API. Fix by locating the Cache
struct's log emission sites (any calls that use the cache logger inside Cache
methods) and wrap those log calls with if c.verbose { ... } (or check c.verbose
before calling c.logger.*), or alternatively remove the SetVerbose method and
any callers so there’s no misleading API. Ensure you update all related log
sites referenced in the review (the other Cache methods that emit honeypot logs)
and keep the Cache.verbose field and SetVerbose in sync with that change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 935b491 and ba860ea.

📒 Files selected for processing (3)
  • cmd/nuclei/main.go
  • pkg/protocols/common/honeypotcache/honeypotcache.go
  • pkg/protocols/protocols.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/protocols/protocols.go

@FuZoe
Copy link
Author

FuZoe commented Feb 27, 2026

Hi maintainers, I have only one active PR and have addressed all CodeRabbit feedback.
Test results screenshot attached above. Ready for review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant