Skip to content

Add WHOIS referral loop cycle detection (ENG-5457) - #137

Open
ryderdaniel wants to merge 2 commits into
mainfrom
ryderdaniel/eng-5457-whois-referral-loop-cycle-detection
Open

Add WHOIS referral loop cycle detection (ENG-5457)#137
ryderdaniel wants to merge 2 commits into
mainfrom
ryderdaniel/eng-5457-whois-referral-loop-cycle-detection

Conversation

@ryderdaniel

Copy link
Copy Markdown

Summary

  • Add visited-set cycle detection to whoisQuery so a referral chain that revisits a server is detected in O(1) and classified as whoisIncompleteCycle (referral_cycle) instead of silently burning through the hop budget and reporting referral_budget
  • Factor ctx.Err()-first classification into classifyIncomplete() so every exit from the referral loop applies the same precedence without duplication
  • Count referral_cycle in summarizeVerifyPass degradation predicate and slog output

What this deliberately does NOT change

  • Scoring: cycle detection is purely observational — an unresolved registrant still scores confReverseWhoisUnverified (0.50) whether the lookup was truncated by a cycle or the domain genuinely has no registrant org. No AddConfidence calls are touched.
  • De-rank, never drop: every candidate is still emitted. The salvaged record from the last successful hop is returned with a nil error — recall is unchanged.
  • Salvage arms: both salvage return statements remain return — no continue/break introduced in the loop body.
  • SSRF controls and byte-cap: ssrfSafeControl and maxWhoisResponseBytes are untouched.
  • Host normalization: trailing-dot stripping + lowercasing prevents Whois.Example.COM. from evading the visited set.

Test plan

  • TestWhoisQuery_ReferralCycleDetected — A→B→A cycle is detected and short-circuits before hop budget
  • TestWhoisQuery_ReferralCycleNormalizedComparison — trailing-dot evasion blocked by normalization
  • TestWhoisQuery_ReferralCycleBackToSeedSalvages — cycle back to bootstrap seed still salvages the post-seed record
  • TestWhoisQuery_HopBudgetExhaustedWithExpiredCtxReportsDeadline — expired ctx reports deadline_expired, live ctx reports referral_budget (AC10)
  • TestClassifyIncomplete_BothExitsAgreeUnderExpiredCtx — all non-ctx reasons map to deadline_expired under expired ctx, pass through under clean ctx (AC11)
  • TestNormalizeWhoisServer — trailing dot, case, and combination normalization
  • Existing TestWhoisQuery_MidHopFailureClassifiesOnCtxErrNotTheHopError (6 subtests) passes unchanged
  • go test ./... — all packages green

🤖 Generated with Claude Code

A referral chain that revisits a server it already queried would silently
burn through the hop budget and report the symptom (referral_budget) instead
of the cause. Add a visited-set to whoisQuery so cycles are detected in O(1)
and reported as whoisIncompleteCycle (referral_cycle). The salvaged record
from the last successful hop is still returned — de-rank, never drop.

Also factor ctx.Err()-first classification into classifyIncomplete() so every
exit from the referral loop applies the same precedence without duplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Instruction-File Drift Detection

Code changes in this PR may have made documentation stale:

CLAUDE.md

Section Issue Evidence
"Incomplete lookups are observable, not silent:" (line 119) New whoisIncompleteCycle reason not documented PR adds referral loop detection (whoisIncompleteCycle) as a 4th incomplete reason, but line 119 lists only 3 ways a chain can end prematurely: deadline expiry, transport failure, and hop budget exhaustion. Missing: referral cycle detection.
"Degraded predicate" (line 125) Says "three incompleteness reasons" but code now has four Documentation states "A pass counts as degraded when any of the five per-candidate counters is non-zero — the three incompleteness reasons plus `lookup_failed` and `panicked`" but code diff shows cycle detection added as a 4th incompleteness reason tracked alongside the three documented ones.

Automated drift check — please review and update if needed.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ryderdaniel, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c5c5c571-2a03-440c-bec9-8254c9c77e68

📥 Commits

Reviewing files that changed from the base of the PR and between 770ef9c and f5f2c8c.

📒 Files selected for processing (2)
  • pkg/plugins/domains/whoisclient.go
  • pkg/plugins/domains/whoisclient_test.go

Walkthrough

The WHOIS client now detects referral cycles with normalized server names and a bounded visited set. It classifies incomplete results with context cancellation and deadline precedence over referral, cycle, and hop-budget causes. Tests cover cycle detection, hostname normalization, bootstrap-seed cycles, and deadline classification. Reverse-WHOIS verification counts cycle outcomes and reports them in degraded-pass logs.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ryderdaniel/eng-5457-whois-referral-loop-cycle-detection

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/plugins/domains/whoisclient_test.go`:
- Around line 1119-1120: Update the cycle-detection assertion in the relevant
WHOIS referral test to require exactly 3 hops, covering the bootstrap, serverA,
and serverB requests, instead of only asserting hops is below the referral
budget.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec2a3ee2-4eaa-438c-b86b-c9ec17a99cf1

📥 Commits

Reviewing files that changed from the base of the PR and between 1df8d6d and 770ef9c.

📒 Files selected for processing (3)
  • pkg/plugins/domains/reverse_whois_verify.go
  • pkg/plugins/domains/whoisclient.go
  • pkg/plugins/domains/whoisclient_test.go

Comment thread pkg/plugins/domains/whoisclient_test.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex Review

Critical Issues

None.

Security

No security concerns flagged.

Suggestions

  • pkg/plugins/domains/whoisclient.go:61: normalizeWhoisServer does not canonicalize the same server identity that whoisDialAddr uses. A referral loop can alternate whois.example and whois.example:43; both dial the same WHOIS endpoint because explicit ports are dropped, but the visited set treats them as different and reports referral_budget instead of referral_cycle. Consider normalizing by stripping scheme/slash/port/brackets consistently with the dial path.

Tests not run: the sandbox is read-only, and Go could not create a module/cache directory.


Reviewed by Codex (gpt-5.5)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gemini Review

Critical Issues

None.

Security

No security concerns flagged.

Suggestions

No suggestions.

No critical issues — LGTM pending human review.


Reviewed by Gemini (gemini-3.1-pro-preview)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 770ef9c77e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pkg/plugins/domains/whoisclient.go Outdated
// is used ONLY for membership testing, never logged — the raw referral string
// is unbounded attacker-chosen text and stays on the ENG-5405 denylist.
func normalizeWhoisServer(server string) string {
return strings.TrimSuffix(strings.ToLower(server), ".")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Strip referral ports before cycle-key comparison

When a WHOIS referral carries an explicit port such as whois.iana.org:43, the visited key remains whois.iana.org:43, while whoisDialAddr strips that port and actually dials whois.iana.org on tcp/43. In a back-to-seed cycle the new visited-set check therefore misses the seed, so the lookup can re-query IANA and classify/report the chain via the hop budget (or even overwrite lastRaw with the bootstrap response) instead of taking the new referral_cycle path. Please canonicalize the visited key with the same host/port normalization used for dialing.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Review

Critical issues

None. Traced the cycle-detection loop against all three new cycle tests and the AC10 hop-budget cases: visited is seeded with the normalized defaultServer and each referral target is recorded before advancing, so A->B->A and cycle-back-to-seed are caught in O(1) before the hop budget is spent, and the no-lastRaw branch (cycle before any post-seed record) correctly errors rather than salvaging. The factored classifyIncomplete preserves the ctx.Err()-first precedence at every exit, including the reworked pendingRefer block (AC10). Recall is unchanged (salvage still returns a nil error). summarizeVerifyPass adds cycle to both the clean-pass zero-check and the degradation predicate consistently.

Security

No security concerns flagged. The untrusted, attacker-chosen referral string is used only for normalized membership testing and is never logged; only the bounded whoisIncompleteCycle enum value escapes. SSRF control and byte-cap are untouched.

Test coverage

Tests are present and comprehensive (cycle detection, trailing-dot normalization evasion, back-to-seed salvage, AC10 deadline-vs-budget discrimination, and classifyIncomplete both-exit agreement).

No critical issues -- LGTM pending human review.

…ertion

- normalizeWhoisServer now strips scheme, trailing slash, and explicit port
  consistent with whoisDialAddr, so "whois.example:43" and "whois.example"
  compare equal in the visited set (Codex review)
- Tighten cycle-detection test assertion to require exactly 3 hops instead
  of < maxWhoisReferrals (CodeRabbit review)
- Add port, scheme, and combined normalization test cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Instruction-File Drift Detection

Code changes in this PR may have made documentation stale:

CLAUDE.md

Section Issue Evidence
"Reverse-WHOIS Verify-After-Retrieve" (line 119) New incompleteness reason not mentioned Code adds whoisIncompleteCycle for cycle detection, but documentation only lists three causes: "deadline expiry mid-chain, a referral hop whose transport failed, or the hop budget exhausted"
"Reverse-WHOIS Verify-After-Retrieve" (line 125) Documentation refers to "the three incompleteness reasons" PR adds whoisIncompleteCycle as a fourth incompleteness type; code now has 4 incompleteness reasons but docs still say 3

Automated drift check — please review and update if needed.

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