fix(public-repo-hygiene): anchor category-2 host patterns on DNS-label boundaries and allow ports (BE-8729) - #208
Conversation
…l boundaries and allow ports (BE-8729) `\b` is not a host boundary. A hyphen is a non-word character, so `\bposthog\.com/project/` matched inside `evil-posthog.com/project/1`, and half the category-2 patterns carried no left anchor at all -- `fooslack.com`, `my-linear.app`, `mydocs.google.com` and `foonotion.so` were all findings. `\bapp\.slack\.com\b` had the mirror-image hole on the right and accepted `app.slack.com.evil.com`. Every pattern now takes a `(?<![A-Za-z0-9-])` left anchor: a preceding dot is a real subdomain edge (so `www.notion.so` and `comfy.slack.com` keep firing) while a letter, digit or hyphen means a different registrable domain. The patterns also tolerate an explicit `(?::\d+)?` port, which used to walk straight past every rule that required `/` after the host -- `notion.so:443/` was a one-token bypass. Verified against a 37-case matrix (23 positives, 14 negatives) and an old-vs-new sweep of this repo's tree; the existing marker fixtures are untouched and still green.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 11 minutes Limit details: You’ve used the included review currently available. Your 97 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 5 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 2 |
| ⚪ Nit | 2 |
Panel: 8/8 reviewers contributed findings.
… case-folding holes in the category-2 anchors (BE-8729) Review follow-ups on the DNS-label anchoring, all four verified against the patterns before and after: - `app.slack.com`'s right anchor did not survive backtracking. `_PORT` is optional, so on `app.slack.com:443.evil.com` the greedy `:443` made the lookahead fail on `.evil`, the port group retried empty, and the lookahead then passed on `:` -- flagging the very suffix host the anchor exists to reject, and making the comment above it claim more than the code held. Adding `|:` to the rejected set closes the path; `\d*` (below) keeps the real prose case `app.slack.com: our workspace` matching. - `_PORT` required a digit, so the empty port walked past every `/`-requiring pattern. `port = *DIGIT` in RFC 3986, so `https://notion.so:/page` is a valid URL for that host and was the same one-token bypass `:443` was. - The patterns folded Unicode. `re.IGNORECASE` alone matches U+0131/U+0130 against `i` and U+017F/U+212A against `s`/`k`, so `lınear.app/x` and `ſlack.com/archives/x` -- different registrable domains -- read as the real hosts. `re.ASCII` restores the invariant `REPO_REF_RE` already scopes its flag to keep. The remaining direction, an IDN neighbour clearing the ASCII left anchor (`énotion.so/x` reports as `notion.so`), is pinned as a known limitation rather than fixed: rejecting any preceding non-ASCII character would also silence a real link written after a curly quote, an em dash or CJK prose, and a missed leak costs more than an extra finding in a leak guard. Docs/tests: the README's trailing-root-label limitation is scoped to the `/`-requiring patterns, because `app.slack.com` does not share it (`app.slack.com./x` IS matched); the `mydocs.google.com` fixture moves to its own same-namespace constant, since only Google can create `*.google.com` and it pins something narrower than the third-party-lookalike name claimed. 105 tests, plus two new smoke cases in test-public-repo-hygiene.yml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 2 — ledger: 5 prior finding(s) across 1 round(s) (0 never answered).
Found 7 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 5 |
| ⚪ Nit | 1 |
Panel: 7/8 reviewers contributed findings.
Reviewers that did not contribute: gemini-3.1-pro:adversarial (error)
…ssion, reject userinfo, and state the UTS-46 trade honestly (BE-8729) Second review round, on the previous commit's own fixes: - The bare `|:` was too wide and REGRESSED `app.slack.com:general` and `app.slack.com:443: our workspace` -- a colon in prose, not a port, both of which the pre-`\b` pattern matched. `:\d` closes the backtracking path just as completely (the empty-port retry always faces `:4`) without touching them. All three shapes are now in the right-boundary test. - Added `|@` to the same anchor. `https://app.slack.com@evil.com/` has host `evil.com`, so flagging it as Slack is the same lookalike false positive this pattern set exists to drop, in the canonical phishing shape. A leading `admin@app.slack.com` is the other direction and still matches. - Corrected the `re.ASCII` rationale, which was wrong about half its own evidence. U+0131/U+0130 are genuinely different hosts under UTS-46, so dropping `lınear.app` and `notİon.so` is a false-positive fix. U+017F and U+212A are NOT: UTS-46 maps them to `s` and `k`, so `ſlack.com` really does resolve to `slack.com` and `re.ASCII` turns it into a miss. That is a scope call -- an obfuscated spelling, like the punycode and percent-encoded ones already listed -- not a lookalike, and the comment, the tests and the README now say so instead of claiming a domain nobody else can reach. - Dropped `myapp.datadoghq.com` from the same-namespace fixture: Datadog hands customers their own `<name>.datadoghq.com`, so that namespace is not vendor-only the way `*.google.com` is, and a custom-sub-domain dashboard really is missed. Recorded as a limitation rather than pinned as correct. - Docs: the README quoted the pre-fix anchor, and claimed `re.ASCII` for all category-2 patterns when `incident-\d+` keeps plain `re.IGNORECASE`. Both scoped. The backslash separator (browsers resolve `notion.so\page` for special schemes) joins the obfuscated-spelling limitation. 107 tests; 33-case FP/FN matrix re-verified; clean-repo smoke fixture replayed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 3 — ledger: 12 prior finding(s) across 2 round(s) (0 never answered).
Found 5 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 1 |
| ⚪ Nit | 3 |
Panel: 8/8 reviewers contributed findings.
…t, closing the userinfo family and the prose-colon regressions (BE-8729) Third review round. Two rounds were lost to the same root cause, so this removes it rather than patching around it again: an optional, greedy `_PORT` in front of a negative lookahead BACKTRACKS, handing digits back one at a time until the lookahead is satisfied, so every alternative had to be reasoned about against every possible port split. Round 2 shipped a hole (`app.slack.com:443.evil.com` matching); the fix for that shipped a regression (`app.slack.com:general`); the fix for THAT relocated it (`app.slack.com:2FA`). A `search()` only needs a boolean, so nothing has to be consumed. The host-only pattern now reads its whole tail in one lookahead and never puts the port in the match: no optional group, no backtracking, each alternative independent. (?!\.?[A-Za-z0-9-]|:\d+\.[A-Za-z0-9-]|@|[.:][^\s/?#@]*@) - `:\d+\.[A-Za-z0-9-]` requires the DOT. `:\d` alone could not tell `app.slack.com:443.evil.com` (the host continues past a port) from `app.slack.com:2FA` (a colon in prose), and rejected both. - `[.:][^\s/?#@]*@` closes the rest of the userinfo family — `:@`, `:secret@` and `.@` were all still flagged as Slack when the real host is `evil.com`, because the previous `@` only fired adjacent to the host or an all-digit port. It is anchored on a leading `.`/`:` and stopped at a URL delimiter so it cannot reach across prose: `app.slack.com,bob@x` and `app.slack.com and email bob@x` still match. Also from this round: the test comment overstated the bare-`:` regression (`app.slack.com: the #general channel.` survived it — `\d*` swallowed the lone colon), the smoke fixture's "the last two" pointer named the wrong pair after the userinfo line was appended, and the README illustrated the left anchor with bare `comfy.slack.com`/`www.notion.so`, which those patterns never match without their paths. 107 tests; 52-case FP/FN matrix, 0 mismatches; clean-repo smoke replayed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 4 — ledger: 17 prior finding(s) across 3 round(s) (0 never answered).
Found 5 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 2 |
| 🟢 Low | 2 |
| ⚪ Nit | 1 |
Panel: 8/8 reviewers contributed findings.
… closing a quadratic scan and two prose misses (BE-8729)
Round 4 of the category-2 host anchor, all five findings from the panel.
The userinfo alternative was `[.:][^\s/?#@]*@` -- "anything but a URL
delimiter", unbounded. Both halves were wrong:
* The CLASS crossed commas, quotes and braces, so any unrelated `@` later
in the same non-whitespace run satisfied the lookahead and SILENCED a
real reference: `app.slack.com:443,ops@example.com` and
`{"slack":"app.slack.com:443","owner":"bob@x"}` both matched the pre-PR
`\b` pattern and had gone quiet. That is the miss direction this guard
cannot afford. It is now the characters a real credential uses
(unreserved + `%` + the `user:pass` colon). Narrowing a class inside a
NEGATIVE lookahead can only ever flag more, so it cannot add a miss of
its own; the cost is a false positive on a lookalike whose userinfo
holds a sub-delim, which is a maintainer glance rather than a leak.
This also drops the last `\s` from the host patterns, so `re.ASCII`
narrowing `\s` (NBSP) stops mattering here.
* The LENGTH was unbounded and quadratic. `MAX_FILE_BYTES` bounds a FILE;
nothing bounds a LINE, so on `('app.slack.com:' * N) + '@'` each of the
~L/14 host positions rescanned the whole tail -- measured 4x per
doubling, ~6s at 200 KiB, extrapolating past the 15-minute job timeout
well inside the 5 MiB cap. Author-controlled file content could turn a
required check into an unexplained timeout. `{0,64}` makes it linear.
Every shape the round-3 suite pinned still behaves identically.
Also, documentation the panel caught drifting from the code:
* The test comment quoting the anchor still read `:\d`, the alternative
round 3 REMOVED because it lost `app.slack.com:2FA` -- which the case
list directly below it pins as must-match.
* `test_a_port_does_not_let_a_suffix_host_backtrack_past_the_anchor`
described the consumed-`_PORT` backtracking mechanism, which round 3
deleted; renamed and rewritten, since that comment was a recipe for
re-adding the very port group whose removal was the point.
* `:\d+\.[A-Za-z0-9-]` also rejects a dotted number in prose, so
`app.slack.com:2.5` is a miss. Requiring a letter after the dot would
recover it and reopen `app.slack.com:443.1evil.com` (a digit-leading
DNS label is legal) -- a miss for a miss, in the direction that flags
less. Kept, documented in "Known limitations", and pinned so it cannot
drift unnoticed.
New tests fail against the previous pattern and pass against this one
(verified by reverting the pattern under the new suite); the cost guard
is an absolute ceiling rather than a ratio, because quadratic growth is
only ~4x per doubling and too close to linear's ~2x to gate on a noisy
runner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Round 5 — ledger: 17 prior finding(s) across 3 round(s) (0 never answered).
Found 9 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 3 |
| 🟢 Low | 4 |
| ⚪ Nit | 2 |
Panel: 8/8 reviewers contributed findings.
…pair the unreadable-file crash (BE-8729) Round 5 of review. Two behaviour changes and seven honesty fixes. `:\d+\.[A-Za-z0-9-]` leaves the host-only right anchor. It rejected `app.slack.com:443.evil.com` as "a port the host continues past", but `port = *DIGIT`: WHATWG's port state fails on the `.`, the URL does not parse, `urlsplit(...).hostname` is `app.slack.com`, and curl and Go's `net/url` both error. No parser reads `evil.com` there, so suppressing the line was a miss -- and it also cost the `:2.5` / `:1.0.1` prose misses that shared the shape. The genuine phishing form (`...evil.com@evil.com`) is rejected by the `[.:]...@` alternative and needed nothing here. The comment claiming this was "a miss for a miss, in the direction that flags less" had the direction backwards: removing an alternative from a negative lookahead can only flag MORE. `_read_text`'s `open()`-failure path returned a five-tuple where the docstring, every sibling branch and its only caller use four, so any EACCES/EMFILE/IO error on a tracked file raised `ValueError: too many values to unpack` and exited 1 -- rendered by the workflow as "internal-only references found" on a repo that is in fact clean. The existing coverage test reaches the `lstat` path only; the new one uses a mode-000 regular file to reach `open()`'s, and fails without this fix. The rest are documented and pinned rather than re-tuned, since every regex tweak in this PR has cost a round: the colon-chained userinfo run (`app.slack.com:443:ops@example.com`), the 64-character bound's over-flag past the boundary, ASCII `_` as a host character, the right-hand half of the IDN-neighbour limitation, and label characters adjacent to a three-label host (`my-app.slack.com`, `app.slack.com-hosted`) -- the same customer-sub-domain shape the README already documents for Datadog. The registrable-name rationale is scoped to the two-label patterns, where it is exact. Smoke fixture: the bare `app.slack.com:443.evil.com/x` line is now correctly flagged, so it moves out of the clean-repo fixture and is replaced by its userinfo form, which is still rejected. Its comment still explained the shape via the port-backtracking mechanism round 3 deleted. 115 tests (110 before), all 14 smoke steps green, 27-case FP/FN matrix clean, and an old-vs-new sweep over main's 175 tracked files moves zero lines in either direction.
ELI-5
The leak guard has a rule like "flag any link to
posthog.com/project/…". To decide where the host name starts and stops it used\b, the regex word boundary — and\bknows nothing about domain names. A hyphen is not a word character, soevil-posthog.com/project/1looked to it like a perfectly goodposthog.com, and half the rules had no left-hand check at all, sofooslack.com/archives/xandfoonotion.so/pagewere "internal links" too. Meanwhile a real internal link only had to add:443—notion.so:443/team/page— and every rule that expected a/right after the host went silent. This teaches the rules what a domain-name boundary actually is: a dot in front is a genuine subdomain edge and still matches, a letter/digit/hyphen in front means a different domain entirely, and an explicit port is no longer an escape hatch.What changed
INTERNAL_MARKER_RES(check_public_repo_hygiene.py) — two new shared fragments and every host pattern rebuilt on them:_HOST_L = (?<![A-Za-z0-9-])— a left anchor on the real DNS-label alphabet. It deliberately permits a preceding dot, because a dot is the one character that can precede a host and still leave it that host.(?<![A-Za-z0-9.-])is the tempting-but-wrong tightening: it would reject every subdomain-prefixed positive the existing fixtures pin (www.notion.so,comfy.notion.site,comfy.slack.com/archives,us.posthog.com/project). There is an in-file comment saying so._PORT = (?::\d*)?— an explicit port between host and path. The digits are*, not+:port = *DIGITin RFC 3986, so the empty porthttps://notion.so:/pageis a valid URL for the same host and was the same one-token bypass:443was._HOST_FLAGS = re.IGNORECASE | re.ASCIIon the eight host patterns (incident-\d+is not a host and keeps plainre.IGNORECASE). The ignore-case flag alone folds Unicode, and review was right that this is two boundaries, not one:i, solınear.app/xandnotİon.so/pageread as the real hosts — but UTS-46 leaves U+0131 alone and maps U+0130 toi+ a combining dot, so neither resolves anywhere near the real service. Flagging them was a false positive;re.ASCIIremoves it, restoring the invariantREPO_REF_REalready scopes its own flag to keep.sandk, soſlack.com/archives/C123really does resolve toslack.com.re.ASCIIturns those into misses. That is a deliberate scope call — an obfuscated spelling, in the same family as the punycode/percent-encoded/defanged spellings already listed under "Known limitations" — and the code comment, the test name and the README all now say so rather than claiming a domain nobody else can reach.app.slack.comis the only host-only pattern (nothing requires a/after it), so it loses\bfor a right anchor of its own — and, after four review rounds, it does not consume_PORTat all and carries only three alternatives:An optional, greedy port sitting in front of a negative lookahead backtracks — the engine hands digits back one at a time until the lookahead is satisfied — so every alternative had to be correct for every possible port split. That cost two rounds: round 2 shipped a hole (
app.slack.com:443.evil.commatched anyway), its fix shipped a regression (app.slack.com:generalwent silent), and that fix relocated it (app.slack.com:2FA). Asearch()only needs a boolean, so nothing has to be consumed; one lookahead reads the whole tail, the port never enters the match, and each alternative is independent:\.?[A-Za-z0-9-]— a following label (app.slack.com.evil.com), what\bused to accept. A dot is allowed when what follows is not a label, soapp.slack.com./xstill matches.@— the userinfo delimiter; inhttps://app.slack.com@evil.com/the real host isevil.com, the canonical phishing shape.[.:][A-Za-z0-9._~%:-]{0,64}@— the same with userinfo in between (:@,:secret@,.@, and:443.evil.com@), all of which an adjacent-only@left flagged. The class is the characters a real credential uses, not "anything but a URL delimiter": the first cut,[^\s/?#@]*, crossed commas, quotes and braces, so any later@on the line silenced the host. It is length-bounded because an unbounded run made a long single line quadratic to scan. Both bounds cost an over-flag, and both are now named in "Known limitations" and pinned: a colon-chained run still reaches a later@(:has to stay in the class, since:user:pass@is real userinfo), and userinfo past 64 characters is out of the run's reach. A leadingadmin@app.slack.comstill matches. Every other pattern's required/directly after host+port already blocks the suffix shape —notion.so.evil.com/pageandlinear.app.evil.com/xare non-matches before and after this change, and that is asserted.:\d+\.[A-Za-z0-9-]to rejectapp.slack.com:443.evil.comas "a port the host continues past". Round 5 removed it:port = *DIGIT, so WHATWG's port state fails on the., the URL does not parse at all,urlsplit(…).hostnameisapp.slack.com, and curl and Go'snet/urlboth error on the port. No mainstream parser resolves that line toevil.com, so the only readable host on it is the internal one — suppressing it was a miss, not a false-positive fix, and it also caused the:2.5/:1.0.1prose misses that shared the shape. The genuine phishing form,app.slack.com:443.evil.com@evil.com, is rejected by the userinfo alternative and needed nothing here (asserted both ways).InternalMarkerCategoryTest: 5 third-party lookalike hosts + 2 same-namespace neighbours, 3 ported URLs, 3 empty-port URLs, 8 userinfo shapes (5 rejected, 3 prose forms that must survive), 2 different-host homoglyphs, 2 UTS-46-mapped spellings pinned as out-of-scope misses, the IDN-neighbour limitation (both boundaries), the host-only trailing-root-label match, 7 host-only right-boundary shapes, the four non-numeric-port lines that must now be flagged plus their two userinfo forms that must not, the colon-chained userinfo miss, both sides of the 64-character bound, and the label-adjacency misses.test_each_marker_is_flaggedis untouched and green, which is what pins the subdomain-prefixed positives.CoverageReportingTestgainstest_an_unopenable_file_warns_instead_of_crashing(see below).test-public-repo-hygiene.ymlgains lookalike hosts (a homoglyph domain and two userinfo URLs) in the clean-repo fixture, and ported-URL + empty-port cases in the per-category dirty-repo fixture. Both were verified to fail against the pre-change checker (lookalikes: old exit 1 / new exit 0; ported URL: old exit 0 / new exit 1), so they are real pins rather than decoration. Round 5 moved the bareapp.slack.com:443.evil.com/xline out of the clean fixture — it is a real reference and is now correctly flagged — and replaced it with its userinfo form, which is still rejected._read_text'sopen()-failure path returned a five-tuple where the docstring, every sibling branch and its only caller use four, so any unreadable tracked file (EACCES on a mode-000 blob, EMFILE, an I/O error) raisedValueError: too many values to unpack (expected 4)and exited 1 — whichpublic-repo-hygiene.ymlrenders as internal-only references found, with no finding listed, on a repo that is in fact clean. It predates this branch (landed with the centralization). The existing coverage test reaches thelstatfailure path only, which is why nothing caught it; the new test uses a mode-000 regular file solstatsucceeds and only the read fails, and it reproduces theValueErrorwhen the fifth value is put back.Falsification, not just unit tests
Tests I author only assert my own premise, and the user-facing effect of this diff is that six line shapes stop being findings. So the premise — "these are unrelated third-party domains, not the internal services" — was checked read-only against DNS rather than assumed:
slack.com54.71.95.193ns-1493.awsdns-58.org.fooslack.composthog.com216.150.1.1julissa.ns.cloudflare.com.evil-posthog.comlinear.app172.64.147.211tim.ns.cloudflare.com.my-linear.appnotion.so208.103.161.1dana.ns.cloudflare.com.foonotion.soapp.slack.com35.81.85.251app.slack.com.evil.comNot one of the six lookalikes resolves at all, and none is a delegation of the real service's zone. They are separate registrable names, which is exactly what the DNS label alphabet says they are: labels are dot-separated and made of letters, digits and hyphens, so a letter/digit/hyphen in front of
posthog.comis part of a different label, while a dot in front is a delegation edge. The direction the diff moves detection is therefore: strictly fewer lookalike-third-party-host lines flagged (false positives removed), strictly more ported-URL lines flagged (false negatives closed).Corpus sweep — both halves
Old and new pattern sets run over the pre-change
maintree, all 175 tracked files (this repo is deliberately not self-enrolled, so this is a stress corpus rather than a clean one):posthog.com/docs).https://notion.so./page, the same host to any resolver — still walks past every category-2 pattern, because they require/(or a port then/) immediately after the host. Same for obfuscated spellings: percent-encoding (%2E), punycode (xn--), defanging (notion[.]so). All of these were misses before this change too and are unchanged by it; corpus occurrences on this tree: 0. They are now written down in the checker README's "Known limitations" rather than left implicit. Deliberately not fixed here: the ticket specifies an exact pattern set validated as a unit upstream, and quietly widening it would leave the shipped set unvalidated.Detection parity with the SDK scripts
The 18/18 findings-parity proof from BE-8654 no longer describes this branch, by design and for the second time (PR #207 shipped a detection change the same way and is the precedent). Direction of divergence, stated plainly: the checker now flags strictly fewer lookalike-third-party-host lines and strictly more ported-URL lines than the two per-repo scripts the centralization retired. Neither SDK copy is touched here — editing them is what would invalidate the parity proof that licenses their deletion.
Judgment calls / criteria not met as written
UTF-16/UTF-32toTICKET_ALLOWLIST) is not in this diff, because it is already satisfied onmainby a better mechanism. The ticket was written from a spike snapshot that predates the BE-8654 review, which addedTICKET_ALLOWED_PREFIXES(CVE,CWE,PEP,RFC,ISO,UTF).UTF-16andUTF-32therefore already clear by prefix, andtest_public_identifier_namespaces_clear_by_prefixalready pins both by name. Adding two exact entries next toUTF-8would be dead list content. The ticket's follow-on request — update the comment attest_an_excluded_path_may_carry_a_work_tree_encoding_attributeand restore its fixture to canonicalUTF-16casing — is likewise already done onmain; the comment there already citesTICKET_ALLOWED_PREFIXESand the fixture already readsUTF-16.UTF-99negative control was deliberately not added. It contradicts the shipped design:UTF-99splits to the prefixUTF, which is allowlisted, so it clears and is not a finding. Asserting otherwise would pin a behaviour this checker does not have and never agreed to have. The analogous control that is correct for a prefix allowlist already exists —test_an_allowlisted_prefix_does_not_clear_a_longer_acronym(CVEX-1234,ISOP-99).Ran 74 testsis stale —mainis at 97 (OK (skipped=1)), for the same snapshot reason. This branch is 100.https://énotion.so/page— a different registrable domain — reports asnotion.so. The blunt fix, a second lookbehind rejecting any non-ASCII character, would also silence a real link written straight after a curly quote, an em dash or CJK prose, and a missed leak costs more than an extra finding in a leak guard. Pinned bytest_an_idn_neighbour_still_reports_as_the_real_hostand written down in the README so changing it is deliberate.app.slack.com:443.evil.com, in both directions, and round 5 settles it. Round 2 let it match and this body defended that as a safe over-flag; rounds 3–4 rejected it with:\d+\.[A-Za-z0-9-]and this body defended that, calling the resulting:2.5miss "a miss for a miss, in the direction that flags less". Both readings were wrong, and the second was wrong twice: a non-numeric port makes the URL unparseable, so the line's only readable host is the internal one and suppressing it is a plain miss — and removing an alternative from a negative lookahead can only ever flag MORE, never less, which is the same rule stated twenty lines below it in the same file. The alternative is gone; both directions are now asserted (the bare line flagged, the userinfo form rejected)._as a host character, the right-hand half of the IDN-neighbour limitation, and label characters adjacent to a three-label host (my-app.slack.com,app.slack.com-hosted). Each is a real over-flag or miss in the safe direction whose only fix trades it for the opposite error; every regex tweak in this PR has cost a review round, so they are written into README "Known limitations" and pinned by tests instead of re-tuned blind. The last one also scopes the file's own rationale: "a letter, digit or hyphen in front means a different registrable name" is exact for the two-label patterns and over-claims for the three-label ones, where it is the same customer-sub-domain shape the README already documents for Datadog.\to/for special schemes, sohttps://notion.so\pageis hostnotion.so) and a Datadog custom organization sub-domain (comfyapp.datadoghq.com/dashboard/1— Datadog hands customers their own<name>.datadoghq.com, so unlike*.google.comthat namespace is not vendor-only) are both real misses. Neither is the accidental-paste shape this guard is scoped to, and both would widen what the shipped pattern set matches beyond what was validated. They are in README "Known limitations" so a deliberate widening has something to start from. The Datadog case also cost a fixture:myapp.datadoghq.comwas removed fromSAME_NAMESPACE_HOSTSrather than relabelled, because pinning it as a correct non-finding would assert that a custom-sub-domain dashboard should be missed.Unexercised artifacts
Verification
python3 -m unittest discover -s .github/public-repo-hygiene/tests -p 'test_*.py'— 114 passed, 0 failed, 1 skipped (110 before round 5). Five review rounds; every round is recorded in the in-file comments rather than only in this body.mainbaseline is 97 with the same single skip (pre-existing: APFS rejects the non-UTF-8 filename that test builds; it is Linux-only by design).run:smoke steps fromtest-public-repo-hygiene.ymlexecuted locally underbash -e— all pass, including the two new cases._read_textregression test re-run against a copy with the fifth tuple value restored — reproducesValueError: too many values to unpack (expected 4, got 5), so it pins the fix rather than merely passing beside it.maintree — 22 → 22 category-2 findings, 0 lines changed.python3 .github/workflow-pins/check_workflow_pins.py— OK, 11 workflows, no default, every ref checkout guarded.python3 .github/agents-md-integrity/check_agents_md.py --root .fails onAGENTS.mdbeing 305 lines against a 200-line ceiling. Pre-existing and unrelated — identical failure on unmodifiedmain, this diff does not touchAGENTS.md, and this repo carries no self-enrolled agents-md caller.Refs BE-8729 — deliberately not
Closes. The body above names an unfixed detection gap (the trailing-root-label host, plus percent-encoded / punycode / defanged spellings) onINTERNAL_MARKER_RES, the exact artifact this ticket names, and two of the ticket's own acceptance items were resolved as already-done-or-contradicted rather than implemented as written. The ticket should stay open to carry that residual and to confirm those two dispositions.Provenance
run:steps oftest-public-repo-hygiene.ymlreplayed locally underbash -e, all pass, including the moved clean-repo lookalike fixture and the ported/empty-port dirty fixtures; a 27-case FP/FN matrix run directly against the compiled round-5 host-only pattern, 0 mismatches (17 positives incl. every prose-colon shape rounds 2–3 lost,admin@app.slack.com, the four comma/quote/brace/NBSP prose forms, and the four non-numeric-port lines round 5 recovered; 10 negatives incl.app.slack.com.evil.com, the five userinfo shapes, both userinfo forms of:443.evil.com, and the two label-adjacency misses); a round-4-vs-round-5 corpus sweep over 175 tracked files of main (19 → 19 category-2 findings, zero lines newly silent in either direction); and the new_read_textregression test re-run against a copy with the five-tuple restored, which reproducesValueError: too many values to unpack (expected 4, got 5). Earlier revisions additionally ran 37-case and 52-case FP/FN matrices, an old-vs-new corpus sweep (22 → 22, 0 changed), read-only DNS resolution of 10 real-vs-lookalike hosts, and the workflow-pins lint (11 workflows).UTF-16/UTF-32toTICKET_ALLOWLIST) was not implemented because it is already satisfied on main byTICKET_ALLOWED_PREFIXES, and its requestedUTF-99negative control was not added because it contradicts that shipped design; both are argued above. Change 1 and all of the requested test coverage shipped as specified. Across five review rounds, twenty-six findings: eighteen fixed in code and eight deliberately documented as limitations rather than fixed — the IDN neighbour on both boundaries, ASCII_as a host character, the backslash separator, the Datadog custom organization sub-domain, label characters adjacent to a three-label host, the colon-chained userinfo run, and the 64-character userinfo bound's over-flag. Each trades a rare error for its opposite, or widens the shipped pattern set past what was validated; all are in README "Known limitations" with the reasoning, and every one is pinned by a test so changing it is deliberate. Round 5 also reversed a round-3/4 code decision (the:\d+\.[A-Za-z0-9-]alternative) and fixed one pre-existing crash in_read_textthat is outside this PR's stated scope — a one-token typo whose blast radius is a false "references found" failure on any repo with an unreadable tracked file.