From 96b75e19c3e84522db7da005a0166e04be01d449 Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:11:12 +0100 Subject: [PATCH 01/12] Add description about bypassing trusted lists --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d21831cef56f..1b1205384dcc 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,24 @@ removeDomains(config, "allowlist", ["crypto-phishing-site.tld"]); ## Safeguards -We maintain a list of domains pulled from various sources in `test/resources`. Each file is plaintext with one host per domain. These domains are used to reduce the risk of false positives. If you need to block a domain that is featured on one of these lists, you'll need to add a bypass to `test/test-lists.ts`. +We maintain trusted comparison lists in `test/resources`. Each file is plaintext with one host per line and is used by CI to reduce the risk of false positives. These lists include sources such as Tranco, CoinMarketCap, CoinGecko, the Snaps registry, and known dapps. + +During `yarn test` / `yarn ci`, the list tests compare `src/config.json`'s `blacklist` entries against these trusted lists. If a blocklisted domain appears on one of the trusted lists and is not already bypassed, CI fails with the domain in the failure message. This is intentional: domains on these lists are often legitimate, so blocking them should require extra review. + +Sometimes a trusted domain still needs to be blocked, for example during a DNS compromise, frontend compromise, or active malicious takeover. In those cases, add the exact blocklist entry to `test/resources/trusted-list-bypass.txt` with evidence or a short reason when possible: + +```text +example.com # DNS compromise confirmed: https://example.com/evidence +``` + +Reviewers listed in `.github/trusted-list-bypass-reviewers.json` can also comment `/skip-trusted-lists` on a pull request. The automation will: + +1. Confirm that the commenter is approved to request trusted-list bypasses. +2. Find trusted-list failures caused by the PR's current blocklist changes. +3. Append the required entries to `test/resources/trusted-list-bypass.txt`. +4. Commit the bypass file update back to the pull request branch so CI can rerun. + +The automation can only push updates to pull request branches in this repository. For forked pull requests, a maintainer must apply the bypass file update manually. To update the lists, run `yarn update:lists`. Note that you'll need a CoinMarketCap Pro API key. From 0e53b7eab656fc88eb686fee48f3c759e2fa68b1 Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:13:24 +0100 Subject: [PATCH 02/12] Moved domains to skip check on trusted lists to another list file --- test/resources/trusted-list-bypass.txt | 72 ++++++++++++++++++++ test/test-lists.ts | 93 +++----------------------- 2 files changed, 83 insertions(+), 82 deletions(-) create mode 100644 test/resources/trusted-list-bypass.txt diff --git a/test/resources/trusted-list-bypass.txt b/test/resources/trusted-list-bypass.txt new file mode 100644 index 000000000000..818904c646ab --- /dev/null +++ b/test/resources/trusted-list-bypass.txt @@ -0,0 +1,72 @@ +# These domains are intentionally blocklisted even though they appear on a trusted comparison list. +# Add evidence or a short reason after the domain when possible. +mystrikingly.com +simdif.com +gb.net +btcs.love +ferozo.com +im-creator.com +free-ethereum.io +890m.com +b5z.net +test.com +multichain.org # https://twitter.com/MultichainOrg/status/1677180114227056641 +dydx.exchange # https://x.com/dydx/status/1815780835473129702 +ambient.finance # https://x.com/pcaversaccio/status/1846851269207392722 +xyz.cutestat.com + +# Below are unknown websites that should stay on the blocklist for brevity but make tests fail. +# This is likely because they exist on the Tranco list and for one reason or another have a high reputation score. +# NOTE: If it is on the Tranco list, please CONFIRM that you are NOT adding a false positive. +# This will trigger a manual review within the CI pipeline. +# Only once it is confirmed not to be a false positive can it be added to this list. +azureserv.com +dnset.com +dnsrd.com +prohoster.biz +kucoin.plus +ewp.live +sdstarfx.com +1mobile.com +v6.rocks +linkpc.net +bookmanga.com +lihi.cc +mytradift.com +anondns.net +bitkeep.vip +temporary.site +misecure.com +myz.info +ton-claim.org +servehalflife.com +earnstations.com +web3quests.com +qubitscube.com +teknik.io +nflfan.org +purworejokab.go.id +ditchain.org +kuex.com +cloud.dbank.com +bybi75-alternate.app.link +mz4t6.rdtk.io +tornadoeth.cash +ether.fi # https://x.com/ether_fi/status/1838643492102283571 +curve.fi # https://x.com/CurveFinance/status/1922040492121829678 +coinmarketcap.com # https://x.com/Auri_0x/status/1936173321244066273 +cointelegraph.com # https://x.com/Cointelegraph/status/1936959898094583916 +card.inertix.pro +pepe.vip # FE Compromise +kuroro.com # FE Compromise? +maxidogetoken.com +bondex.app # XSS +irys.xyz # Drainer frontends being hosted on IPFS +25u.com # DNS may be hijacked. +decentreland.live # somehow on tranco +mssg.me # hosting drainers +defi2026.z13.web.core.windows.net +samouraiwallet.com # https://x.com/econoalchemist/status/2035735206691315848 +chainge.finance +cow.fi # https://x.com/CoWSwap/status/2044078590514327888 +eth.limo # https://x.com/eth_limo/status/2045413512986411467 diff --git a/test/test-lists.ts b/test/test-lists.ts index 18a1eba50858..0881b349629e 100644 --- a/test/test-lists.ts +++ b/test/test-lists.ts @@ -1,91 +1,21 @@ -import { readFile } from 'node:fs/promises'; -import path from 'node:path'; +import { readFile } from "node:fs/promises"; +import path from "node:path"; import test from "tape"; -import { Config } from '../src/types'; -import { detectFalsePositives, parseDomainWithCustomPSL } from './utils'; +import { Config } from "../src/types"; +import { detectFalsePositives, parseTrustedListBypass } from "./trusted-list-utils.js"; -// This is a list of "bad domains" (false positive) that we don't want to include in the Tranco test -const bypass = new Set([ - "mystrikingly.com", - "simdif.com", - "gb.net", - "btcs.love", - "ferozo.com", - "im-creator.com", - "free-ethereum.io", - "890m.com", - "b5z.net", - "test.com", - "multichain.org", // https://twitter.com/MultichainOrg/status/1677180114227056641 - "dydx.exchange", // https://x.com/dydx/status/1815780835473129702 - "ambient.finance", // https://x.com/pcaversaccio/status/1846851269207392722 - "xyz.cutestat.com", - - /* - // Below are unknown websites that should stay on the blocklist for brevity but make tests fail. This is likely because they exist on the - // Tranco list and for one reason or another have a high repuatation score. - - // NOTE: If it is on the Tranco list, please CONFIRM that you are NOT adding a false positive. This will trigger a manual review within the CICD pipeline. - Only once it is confirmed not to be a false positive can it be added to this list. - */ - "azureserv.com", - "dnset.com", - "dnsrd.com", - "prohoster.biz", - "kucoin.plus", - "ewp.live", - "sdstarfx.com", - "1mobile.com", - "v6.rocks", - "linkpc.net", - "bookmanga.com", - "lihi.cc", - "mytradift.com", - "anondns.net", - "bitkeep.vip", - "temporary.site", - "misecure.com", - "myz.info", - "ton-claim.org", - "servehalflife.com", - "earnstations.com", - "web3quests.com", - "qubitscube.com", - "teknik.io", - "nflfan.org", - "purworejokab.go.id", - "ditchain.org", - "kuex.com", - "cloud.dbank.com", - "bybi75-alternate.app.link", - "mz4t6.rdtk.io", - "tornadoeth.cash", - "ether.fi", // https://x.com/ether_fi/status/1838643492102283571 - "curve.fi", // https://x.com/CurveFinance/status/1922040492121829678 - "coinmarketcap.com", // https://x.com/Auri_0x/status/1936173321244066273 - "cointelegraph.com", // https://x.com/Cointelegraph/status/1936959898094583916 - "card.inertix.pro", - "pepe.vip", // FE Compromise - "kuroro.com", // FE Compromise? - "maxidogetoken.com", - "bondex.app", // XSS - "irys.xyz", // Drainer frontends being hosted on IPFS - "25u.com", // DNS may be hijacked. - "decentreland.live", // somehow on tranco - "mssg.me", // hosting drainers - "defi2026.z13.web.core.windows.net", - "samouraiwallet.com", // https://x.com/econoalchemist/status/2035735206691315848 - "chainge.finance", - "cow.fi", // https://x.com/CoWSwap/status/2044078590514327888 - "eth.limo", // https://x.com/eth_limo/status/2045413512986411467 -]); +const resourcesDir = path.join(__dirname, "resources"); export const runTests = (config: Config) => { const testList = (listId: string) => { - test(`ensure no domains on allowlist are blocked: ${listId}`, async (t) => { - const contents = await readFile(path.join(__dirname, "resources", `${listId}.txt`), { encoding: 'utf-8' }); + test(`ensure no trusted-list domains are blocked: ${listId}`, async (t) => { + const [contents, bypassContents] = await Promise.all([ + readFile(path.join(resourcesDir, `${listId}.txt`), { encoding: "utf-8" }), + readFile(path.join(resourcesDir, "trusted-list-bypass.txt"), { encoding: "utf-8" }), + ]); const domains = new Set(contents.split("\n")); + const bypass = parseTrustedListBypass(bypassContents); const falsePositives = detectFalsePositives(config.blacklist!, domains, bypass); @@ -101,4 +31,3 @@ export const runTests = (config: Config) => { testList("coingecko"); testList("dapps"); }; - From 353575f82272855733fd01a47c327b5fc60c3f5a Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:13:50 +0100 Subject: [PATCH 03/12] Add placeholder trusted list bypass reviewer file --- .github/trusted-list-bypass-reviewers.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/trusted-list-bypass-reviewers.json diff --git a/.github/trusted-list-bypass-reviewers.json b/.github/trusted-list-bypass-reviewers.json new file mode 100644 index 000000000000..f12fb4bbb065 --- /dev/null +++ b/.github/trusted-list-bypass-reviewers.json @@ -0,0 +1,3 @@ +[ + "samczsun" +] From 877dbfb71eeaa075d2562d0e344e67231b8f856b Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:20:14 +0100 Subject: [PATCH 04/12] Add bypass trusted list workflow --- .github/workflows/trusted-list-bypass.yml | 105 ++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 .github/workflows/trusted-list-bypass.yml diff --git a/.github/workflows/trusted-list-bypass.yml b/.github/workflows/trusted-list-bypass.yml new file mode 100644 index 000000000000..f0e5b85a18f5 --- /dev/null +++ b/.github/workflows/trusted-list-bypass.yml @@ -0,0 +1,105 @@ +name: Trusted List Bypass + +on: + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + pull-requests: read + +jobs: + trusted-list-bypass: + name: Trusted List Bypass + if: github.event.issue.pull_request && (startsWith(github.event.comment.body, '/skip-trusted-lists') || github.event.comment.body == 'Skip trusted lists') + runs-on: ubuntu-latest + steps: + - name: Get pull request + id: pr + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const { data: pull } = await github.rest.pulls.get({ + owner, + repo, + pull_number: context.issue.number, + }); + + core.setOutput('head_repo', pull.head.repo.full_name); + core.setOutput('head_ref', pull.head.ref); + core.setOutput('is_fork', String(pull.head.repo.full_name !== `${owner}/${repo}`)); + + - name: Report unsupported fork + if: steps.pr.outputs.is_fork == 'true' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: 'Trusted-list bypass automation can only push updates to branches in this repository. Please have a maintainer apply the bypass file update manually.', + }); + + - name: Checkout automation + if: steps.pr.outputs.is_fork == 'false' + uses: actions/checkout@v4 + with: + path: automation + + - name: Use Node.js + if: steps.pr.outputs.is_fork == 'false' + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: yarn + cache-dependency-path: automation/yarn.lock + + - name: Install automation dependencies + if: steps.pr.outputs.is_fork == 'false' + working-directory: automation + run: yarn --frozen-lockfile + + - name: Checkout pull request branch + if: steps.pr.outputs.is_fork == 'false' + uses: actions/checkout@v4 + with: + repository: ${{ steps.pr.outputs.head_repo }} + ref: ${{ steps.pr.outputs.head_ref }} + path: pull-request + + - name: Apply trusted-list bypass + if: steps.pr.outputs.is_fork == 'false' + working-directory: automation + env: + TRUSTED_LIST_BYPASS_APPROVED_BY: ${{ github.event.comment.user.login }} + TRUSTED_LIST_BYPASS_COMMENT: ${{ github.event.comment.body }} + TRUSTED_LIST_BYPASS_PR_NUMBER: ${{ github.event.issue.number }} + TRUSTED_LIST_BYPASS_TARGET: ${{ github.workspace }}/pull-request + run: node --import tsx bin/apply-trusted-list-bypass.ts + + - name: Commit trusted-list bypass + if: steps.pr.outputs.is_fork == 'false' + working-directory: pull-request + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add test/resources/trusted-list-bypass.txt + if git diff --cached --quiet; then + echo "No trusted-list bypass changes to commit." + exit 0 + fi + git commit -m "Add trusted list bypass for PR #${{ github.event.issue.number }}" + git push origin HEAD:${{ steps.pr.outputs.head_ref }} + + - name: Report completion + if: steps.pr.outputs.is_fork == 'false' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: 'Trusted-list bypass check completed. If a bypass was needed, I pushed it to this PR branch and CI will rerun.', + }); From 564c4ed7d8c29c71895f6f25fe34b551674cfe38 Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:20:54 +0100 Subject: [PATCH 05/12] Add script to write bypassed domain to list to skip logic --- bin/apply-trusted-list-bypass.ts | 148 +++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 bin/apply-trusted-list-bypass.ts diff --git a/bin/apply-trusted-list-bypass.ts b/bin/apply-trusted-list-bypass.ts new file mode 100644 index 000000000000..089dde3c8a52 --- /dev/null +++ b/bin/apply-trusted-list-bypass.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env node + +import { readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { detectFalsePositives, parseTrustedListBypass } from "../test/trusted-list-utils.js"; +import { Config } from "../src/types.js"; + +const TRUSTED_LIST_IDS = ["tranco", "coinmarketcap", "snapsregistry", "coingecko", "dapps"]; +const COMMANDS = new Set(["/skip-trusted-lists", "Skip trusted lists"]); + +const policyRoot = path.dirname(__dirname); +const targetRoot = process.env.TRUSTED_LIST_BYPASS_TARGET || policyRoot; +const resourcesDir = path.join(targetRoot, "test", "resources"); +const bypassPath = path.join(resourcesDir, "trusted-list-bypass.txt"); + +const readJson = async (filePath: string): Promise => { + return JSON.parse(await readFile(filePath, { encoding: "utf-8" })) as T; +}; + +const normalizeHandle = (handle: string): string => { + return handle.replace(/^@/u, "").toLowerCase(); +}; + +const readExistingBypass = async (): Promise => { + try { + return await readFile(bypassPath, { encoding: "utf-8" }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + + return [ + "# These domains are intentionally blocklisted even though they appear on a trusted comparison list.", + "# Add evidence or a short reason after the domain when possible.", + "", + ].join("\n"); + } +}; + +const getCommand = (): string => { + const firstLine = (process.env.TRUSTED_LIST_BYPASS_COMMENT || "").trim().split(/\r?\n/u)[0]?.trim() || ""; + + if (firstLine.startsWith("/skip-trusted-lists")) { + return "/skip-trusted-lists"; + } + + return firstLine; +}; + +const getApprovedBy = (): string => { + const approvedBy = process.env.TRUSTED_LIST_BYPASS_APPROVED_BY; + if (!approvedBy) { + throw new Error("TRUSTED_LIST_BYPASS_APPROVED_BY must be set"); + } + + return normalizeHandle(approvedBy); +}; + +const getPrNumber = (): string => { + const prNumber = process.env.TRUSTED_LIST_BYPASS_PR_NUMBER; + if (!prNumber) { + throw new Error("TRUSTED_LIST_BYPASS_PR_NUMBER must be set"); + } + + return prNumber; +}; + +const assertCommand = () => { + const command = getCommand(); + if (!COMMANDS.has(command)) { + throw new Error(`Unsupported trusted-list bypass command: ${command}`); + } +}; + +const assertApprovedReviewer = async (approvedBy: string) => { + const reviewers = await readJson(path.join(policyRoot, ".github", "trusted-list-bypass-reviewers.json")); + const normalizedReviewers = new Set(reviewers.map(normalizeHandle)); + + if (!normalizedReviewers.has(approvedBy)) { + throw new Error(`@${approvedBy} is not approved to apply trusted-list bypasses`); + } +}; + +const findBypassableEntries = async (config: Config, bypass: Set): Promise => { + const entries = new Set(); + + for (const listId of TRUSTED_LIST_IDS) { + const contents = await readFile(path.join(resourcesDir, `${listId}.txt`), { encoding: "utf-8" }); + const comparisonList = new Set(contents.split("\n")); + const falsePositives = detectFalsePositives(config.blacklist!, comparisonList, bypass); + + for (const entry of falsePositives) { + const bypassWithEntry = new Set([...bypass, entry]); + const remaining = detectFalsePositives(config.blacklist!, comparisonList, bypassWithEntry); + + if (!remaining.includes(entry)) { + entries.add(entry); + } + } + } + + return Array.from(entries).sort(); +}; + +const appendBypasses = async (contents: string, entries: string[], approvedBy: string, prNumber: string) => { + if (entries.length === 0) { + console.log("No trusted-list bypass entries need to be added."); + return; + } + + const existing = parseTrustedListBypass(contents); + const newEntries = entries.filter((entry) => !existing.has(entry)); + + if (newEntries.length === 0) { + console.log("All trusted-list bypass entries are already present."); + return; + } + + const lines = newEntries.map((entry) => `${entry} # trusted-list bypass approved by @${approvedBy} in PR #${prNumber}`); + const nextContents = `${contents.trimEnd()}\n${lines.join("\n")}\n`; + + await writeFile(bypassPath, nextContents); + console.log(`Added trusted-list bypass entries: ${newEntries.join(", ")}`); +}; + +const main = async () => { + assertCommand(); + + const approvedBy = getApprovedBy(); + const prNumber = getPrNumber(); + + await assertApprovedReviewer(approvedBy); + + const [config, bypassContents] = await Promise.all([ + readJson(path.join(targetRoot, "src", "config.json")), + readExistingBypass(), + ]); + + const bypass = parseTrustedListBypass(bypassContents); + const entries = await findBypassableEntries(config, bypass); + + await appendBypasses(bypassContents, entries, approvedBy, prNumber); +}; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); From c838b4a1618adf16da89ff3a12ffe2853c80d673 Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 16:21:10 +0100 Subject: [PATCH 06/12] Add utils to help with bypassing domain logic against trusted lists --- test/trusted-list-utils.ts | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/trusted-list-utils.ts diff --git a/test/trusted-list-utils.ts b/test/trusted-list-utils.ts new file mode 100644 index 000000000000..5d344e5140f5 --- /dev/null +++ b/test/trusted-list-utils.ts @@ -0,0 +1,94 @@ +import { parse } from "tldts"; +import { customTlds } from "./custom-tlds.js"; +import { PATH_REQUIRED_DOMAINS } from "./path-enabled-domains.js"; + +type CustomParseResult = { + domain: string | null; + subdomain: string | null; + publicSuffix: string | null; +}; + +export function parseTrustedListBypass(contents: string): Set { + return new Set( + contents + .split(/\r?\n/u) + .map((line) => line.replace(/\s+#.*$/u, "").trim()) + .filter(Boolean), + ); +} + +export function parseDomainWithCustomPSL(domain: string): CustomParseResult { + const customSuffix = customTlds.find((suffix) => domain === suffix || domain.endsWith("." + suffix)); + + if (customSuffix) { + const parts = domain.split("."); + const suffixParts = customSuffix.split("."); + const domainParts = parts.slice(0, parts.length - suffixParts.length); + const mainDomain = domainParts.length > 0 ? domainParts.join(".") : ""; + + return { + domain: mainDomain ? `${mainDomain}.${customSuffix}` : customSuffix, + subdomain: mainDomain, + publicSuffix: customSuffix, + }; + } + + const parsedDomain = parse(domain, { + allowPrivateDomains: true, + }); + + return { + domain: parsedDomain.domain, + subdomain: parsedDomain.subdomain, + publicSuffix: parsedDomain.publicSuffix, + }; +} + +function extractHostname(domainWithPath: string): string { + try { + const url = new URL(`https://${domainWithPath}`); + return url.hostname; + } catch { + return domainWithPath.substring(0, domainWithPath.indexOf("/")); + } +} + +function isInvalidPathDomain(hostname: string): boolean { + const hasPath = hostname.includes("/"); + if (!hasPath) return false; + + const domainPart = extractHostname(hostname); + return !PATH_REQUIRED_DOMAINS.includes(domainPart); +} + +function isOnComparisonListNotBypassed( + hostname: string, + comparisonList: Set, + bypassList: Set, +): boolean { + if (bypassList.has(hostname)) return false; + + // Only check domains without paths - domains with paths are allowed to be blocked specifically. + if (hostname.includes("/")) return false; + + const parsedDomain = parseDomainWithCustomPSL(hostname); + return comparisonList.has(parsedDomain.domain || ""); +} + +export function detectFalsePositives( + blocklist: string[], + comparisonList: Set, + bypassList: Set, +): string[] { + return blocklist.filter((hostname) => { + if (isInvalidPathDomain(hostname)) { + return true; + } + + if (isOnComparisonListNotBypassed(hostname, comparisonList, bypassList)) { + return true; + } + + return false; + }); +} From 71983c4e43cbde639c076bf4f220224dc269dce4 Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 17:18:54 +0100 Subject: [PATCH 07/12] Updated list of people who can skip trusted lists --- .github/trusted-list-bypass-reviewers.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/trusted-list-bypass-reviewers.json b/.github/trusted-list-bypass-reviewers.json index f12fb4bbb065..32a4db056c21 100644 --- a/.github/trusted-list-bypass-reviewers.json +++ b/.github/trusted-list-bypass-reviewers.json @@ -1,3 +1,7 @@ [ - "samczsun" + "samczsun", + "409h", + "tayvano", + "mindofmar", + "0xOhm" ] From 4e468215915d0d99c10dfa44ef06daaf674915ce Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 17:53:43 +0100 Subject: [PATCH 08/12] Add SEAL bot --- .github/trusted-list-bypass-reviewers.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/trusted-list-bypass-reviewers.json b/.github/trusted-list-bypass-reviewers.json index 32a4db056c21..65112c2ecf63 100644 --- a/.github/trusted-list-bypass-reviewers.json +++ b/.github/trusted-list-bypass-reviewers.json @@ -3,5 +3,6 @@ "409h", "tayvano", "mindofmar", - "0xOhm" + "0xOhm", + "security-alliance-bot" ] From 5f3cfdf576aafd0c2f572d557db6cbf97e3620ae Mon Sep 17 00:00:00 2001 From: 409H Date: Tue, 21 Apr 2026 17:55:56 +0100 Subject: [PATCH 09/12] Add pcaversaccio to trusted list --- .github/trusted-list-bypass-reviewers.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/trusted-list-bypass-reviewers.json b/.github/trusted-list-bypass-reviewers.json index 65112c2ecf63..d05c5be949d6 100644 --- a/.github/trusted-list-bypass-reviewers.json +++ b/.github/trusted-list-bypass-reviewers.json @@ -4,5 +4,6 @@ "tayvano", "mindofmar", "0xOhm", - "security-alliance-bot" + "security-alliance-bot", + "pcaversaccio" ] From c4846e98edd52b7966f714f76bc7853d1f3903c8 Mon Sep 17 00:00:00 2001 From: 409H Date: Wed, 22 Apr 2026 13:31:10 +0100 Subject: [PATCH 10/12] Fix cursor issues --- .github/workflows/trusted-list-bypass.yml | 7 +- test/trusted-list-detection.ts | 85 ++++++++++++++++++++++ test/trusted-list-utils.ts | 86 +---------------------- test/utils.ts | 85 +--------------------- 4 files changed, 93 insertions(+), 170 deletions(-) create mode 100644 test/trusted-list-detection.ts diff --git a/.github/workflows/trusted-list-bypass.yml b/.github/workflows/trusted-list-bypass.yml index f0e5b85a18f5..fe59ec60a8c3 100644 --- a/.github/workflows/trusted-list-bypass.yml +++ b/.github/workflows/trusted-list-bypass.yml @@ -82,6 +82,9 @@ jobs: - name: Commit trusted-list bypass if: steps.pr.outputs.is_fork == 'false' working-directory: pull-request + env: + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + PR_NUMBER: ${{ github.event.issue.number }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" @@ -90,8 +93,8 @@ jobs: echo "No trusted-list bypass changes to commit." exit 0 fi - git commit -m "Add trusted list bypass for PR #${{ github.event.issue.number }}" - git push origin HEAD:${{ steps.pr.outputs.head_ref }} + git commit -m "Add trusted list bypass for PR #${PR_NUMBER}" + git push origin "HEAD:${HEAD_REF}" - name: Report completion if: steps.pr.outputs.is_fork == 'false' diff --git a/test/trusted-list-detection.ts b/test/trusted-list-detection.ts new file mode 100644 index 000000000000..c18afa8c7360 --- /dev/null +++ b/test/trusted-list-detection.ts @@ -0,0 +1,85 @@ +import { parse } from "tldts"; +import { customTlds } from "./custom-tlds.js"; +import { PATH_REQUIRED_DOMAINS } from "./path-enabled-domains.js"; + +type CustomParseResult = { + domain: string | null; + subdomain: string | null; + publicSuffix: string | null; +}; + +export function parseDomainWithCustomPSL(domain: string): CustomParseResult { + const customSuffix = customTlds.find((suffix) => domain === suffix || domain.endsWith("." + suffix)); + + if (customSuffix) { + const parts = domain.split("."); + const suffixParts = customSuffix.split("."); + const domainParts = parts.slice(0, parts.length - suffixParts.length); + const mainDomain = domainParts.length > 0 ? domainParts.join(".") : ""; + + return { + domain: mainDomain ? `${mainDomain}.${customSuffix}` : customSuffix, + subdomain: mainDomain, + publicSuffix: customSuffix, + }; + } + + const parsedDomain = parse(domain, { + allowPrivateDomains: true, + }); + + return { + domain: parsedDomain.domain, + subdomain: parsedDomain.subdomain, + publicSuffix: parsedDomain.publicSuffix, + }; +} + +export function extractHostname(domainWithPath: string): string { + try { + const url = new URL(`https://${domainWithPath}`); + return url.hostname; + } catch { + return domainWithPath.substring(0, domainWithPath.indexOf("/")); + } +} + +function isInvalidPathDomain(hostname: string): boolean { + const hasPath = hostname.includes("/"); + if (!hasPath) return false; + + const domainPart = extractHostname(hostname); + return !PATH_REQUIRED_DOMAINS.includes(domainPart); +} + +function isOnComparisonListNotBypassed( + hostname: string, + comparisonList: Set, + bypassList: Set, +): boolean { + if (bypassList.has(hostname)) return false; + + // Only check domains without paths - domains with paths are allowed to be blocked specifically. + if (hostname.includes("/")) return false; + + const parsedDomain = parseDomainWithCustomPSL(hostname); + return comparisonList.has(parsedDomain.domain || ""); +} + +export function detectFalsePositives( + blocklist: string[], + comparisonList: Set, + bypassList: Set, +): string[] { + return blocklist.filter((hostname) => { + if (isInvalidPathDomain(hostname)) { + return true; + } + + if (isOnComparisonListNotBypassed(hostname, comparisonList, bypassList)) { + return true; + } + + return false; + }); +} diff --git a/test/trusted-list-utils.ts b/test/trusted-list-utils.ts index 5d344e5140f5..703ef4775cf4 100644 --- a/test/trusted-list-utils.ts +++ b/test/trusted-list-utils.ts @@ -1,12 +1,4 @@ -import { parse } from "tldts"; -import { customTlds } from "./custom-tlds.js"; -import { PATH_REQUIRED_DOMAINS } from "./path-enabled-domains.js"; - -type CustomParseResult = { - domain: string | null; - subdomain: string | null; - publicSuffix: string | null; -}; +export { detectFalsePositives, parseDomainWithCustomPSL } from "./trusted-list-detection.js"; export function parseTrustedListBypass(contents: string): Set { return new Set( @@ -16,79 +8,3 @@ export function parseTrustedListBypass(contents: string): Set { .filter(Boolean), ); } - -export function parseDomainWithCustomPSL(domain: string): CustomParseResult { - const customSuffix = customTlds.find((suffix) => domain === suffix || domain.endsWith("." + suffix)); - - if (customSuffix) { - const parts = domain.split("."); - const suffixParts = customSuffix.split("."); - const domainParts = parts.slice(0, parts.length - suffixParts.length); - const mainDomain = domainParts.length > 0 ? domainParts.join(".") : ""; - - return { - domain: mainDomain ? `${mainDomain}.${customSuffix}` : customSuffix, - subdomain: mainDomain, - publicSuffix: customSuffix, - }; - } - - const parsedDomain = parse(domain, { - allowPrivateDomains: true, - }); - - return { - domain: parsedDomain.domain, - subdomain: parsedDomain.subdomain, - publicSuffix: parsedDomain.publicSuffix, - }; -} - -function extractHostname(domainWithPath: string): string { - try { - const url = new URL(`https://${domainWithPath}`); - return url.hostname; - } catch { - return domainWithPath.substring(0, domainWithPath.indexOf("/")); - } -} - -function isInvalidPathDomain(hostname: string): boolean { - const hasPath = hostname.includes("/"); - if (!hasPath) return false; - - const domainPart = extractHostname(hostname); - return !PATH_REQUIRED_DOMAINS.includes(domainPart); -} - -function isOnComparisonListNotBypassed( - hostname: string, - comparisonList: Set, - bypassList: Set, -): boolean { - if (bypassList.has(hostname)) return false; - - // Only check domains without paths - domains with paths are allowed to be blocked specifically. - if (hostname.includes("/")) return false; - - const parsedDomain = parseDomainWithCustomPSL(hostname); - return comparisonList.has(parsedDomain.domain || ""); -} - -export function detectFalsePositives( - blocklist: string[], - comparisonList: Set, - bypassList: Set, -): string[] { - return blocklist.filter((hostname) => { - if (isInvalidPathDomain(hostname)) { - return true; - } - - if (isOnComparisonListNotBypassed(hostname, comparisonList, bypassList)) { - return true; - } - - return false; - }); -} diff --git a/test/utils.ts b/test/utils.ts index 6b5e5cee171e..21e8fb085550 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -3,9 +3,8 @@ import punycode from "punycode/"; import { cleanAllowlist, cleanBlocklist } from "../src/clean-config.js"; import PhishingDetector from "../src/detector.js"; import { Config } from "../src/types.js"; -import { parse } from 'tldts'; -import { customTlds } from './custom-tlds.js'; -import { PATH_REQUIRED_DOMAINS } from './path-enabled-domains.js'; +import { PATH_REQUIRED_DOMAINS } from "./path-enabled-domains.js"; +import { detectFalsePositives, extractHostname, parseDomainWithCustomPSL } from "./trusted-list-detection.js"; export const testBlocklist = (t: Test, domains: string[], options: Config) => { const detector = new PhishingDetector(options); @@ -230,40 +229,6 @@ export const testDomainWithDetector = (t: Test, { domain, name, type, expected, t.equal(value.result, expected, `result: "${domain}" should be match "${expected}"`); }; -type CustomParseResult = { - domain: string | null; - subdomain: string | null; - publicSuffix: string | null; -}; - -// Create a wrapper function for PSL parsing -export function parseDomainWithCustomPSL(domain: string): CustomParseResult { - // Check if the domain ends with any custom suffix - const customSuffix = customTlds.find(suffix => domain === suffix || domain.endsWith('.' + suffix)); - - if (customSuffix) { - const parts = domain.split('.'); - const suffixParts = customSuffix.split('.'); - const domainParts = parts.slice(0, parts.length - suffixParts.length); - const mainDomain = domainParts.length > 0 ? domainParts.join('.') : ''; - - return { - domain: mainDomain ? `${mainDomain}.${customSuffix}` : customSuffix, - subdomain: mainDomain, - publicSuffix: customSuffix, - }; - } - // Fallback to tldts parse - const parsedDomain = parse(domain, { - allowPrivateDomains: true, - }); - return { - domain: parsedDomain.domain, - subdomain: parsedDomain.subdomain, - publicSuffix: parsedDomain.publicSuffix, - } -}; - test("parseDomainWithCustomPSL", (t) => { const testCases = [ { @@ -364,52 +329,6 @@ test("parseDomainWithCustomPSL", (t) => { t.end(); }); -// Helper function to extract hostname from URL with path -function extractHostname(domainWithPath: string): string { - try { - const url = new URL(`https://${domainWithPath}`); - return url.hostname; - } catch { - return domainWithPath.substring(0, domainWithPath.indexOf("/")); - } -} - -// Check if a domain with path is invalid (not in allowed path domains) -function isInvalidPathDomain(hostname: string): boolean { - const hasPath = hostname.includes("/"); - if (!hasPath) return false; - - const domainPart = extractHostname(hostname); - return !PATH_REQUIRED_DOMAINS.includes(domainPart); -} - -// Check if a domain WITHOUT path is on the comparison list but not bypassed -function isOnComparisonListNotBypassed(hostname: string, comparisonList: Set, bypassList: Set): boolean { - if (bypassList.has(hostname)) return false; - - // Only check domains without paths - domains with paths are allowed to be blocked specifically - if (hostname.includes("/")) return false; - - const parsedDomain = parseDomainWithCustomPSL(hostname); - return comparisonList.has(parsedDomain.domain || ""); -} - -export function detectFalsePositives(blocklist: string[], comparisonList: Set, bypassList: Set): string[] { - return blocklist.filter(hostname => { - // Case 1: Domain has path but isn't in PATH_REQUIRED_DOMAINS = false positive - if (isInvalidPathDomain(hostname)) { - return true; - } - - // Case 2: Domain is on comparison list but not bypassed = false positive - if (isOnComparisonListNotBypassed(hostname, comparisonList, bypassList)) { - return true; - } - - return false; - }); -} - test("detectFalsePositives", (t) => { const testCases = [ { From 135ab3acb52b165670c14ccd352d845563f6771c Mon Sep 17 00:00:00 2001 From: 409H Date: Thu, 30 Apr 2026 17:48:47 +0100 Subject: [PATCH 11/12] Add code changes per @0xOhm review --- .github/workflows/trusted-list-bypass.yml | 4 ++-- test/trusted-list-utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trusted-list-bypass.yml b/.github/workflows/trusted-list-bypass.yml index fe59ec60a8c3..0289168eb32a 100644 --- a/.github/workflows/trusted-list-bypass.yml +++ b/.github/workflows/trusted-list-bypass.yml @@ -80,7 +80,7 @@ jobs: run: node --import tsx bin/apply-trusted-list-bypass.ts - name: Commit trusted-list bypass - if: steps.pr.outputs.is_fork == 'false' + if: success() && steps.pr.outputs.is_fork == 'false' working-directory: pull-request env: HEAD_REF: ${{ steps.pr.outputs.head_ref }} @@ -97,7 +97,7 @@ jobs: git push origin "HEAD:${HEAD_REF}" - name: Report completion - if: steps.pr.outputs.is_fork == 'false' + if: success() && steps.pr.outputs.is_fork == 'false' uses: actions/github-script@v7 with: script: | diff --git a/test/trusted-list-utils.ts b/test/trusted-list-utils.ts index 703ef4775cf4..72d329e97d9e 100644 --- a/test/trusted-list-utils.ts +++ b/test/trusted-list-utils.ts @@ -4,7 +4,7 @@ export function parseTrustedListBypass(contents: string): Set { return new Set( contents .split(/\r?\n/u) - .map((line) => line.replace(/\s+#.*$/u, "").trim()) + .map((line) => line.replace(/^\s*#.*$/u, "").replace(/\s+#.*$/u, "").trim()) .filter(Boolean), ); } From 76fc20ea66376848744ce68cec08ee6356dd8526 Mon Sep 17 00:00:00 2001 From: 409H Date: Thu, 30 Apr 2026 17:51:57 +0100 Subject: [PATCH 12/12] Fix cursor comment on case sensitivity --- bin/apply-trusted-list-bypass.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/apply-trusted-list-bypass.ts b/bin/apply-trusted-list-bypass.ts index 089dde3c8a52..99d7725612b5 100644 --- a/bin/apply-trusted-list-bypass.ts +++ b/bin/apply-trusted-list-bypass.ts @@ -6,7 +6,7 @@ import { detectFalsePositives, parseTrustedListBypass } from "../test/trusted-li import { Config } from "../src/types.js"; const TRUSTED_LIST_IDS = ["tranco", "coinmarketcap", "snapsregistry", "coingecko", "dapps"]; -const COMMANDS = new Set(["/skip-trusted-lists", "Skip trusted lists"]); +const COMMANDS = new Set(["/skip-trusted-lists", "skip trusted lists"]); const policyRoot = path.dirname(__dirname); const targetRoot = process.env.TRUSTED_LIST_BYPASS_TARGET || policyRoot; @@ -39,12 +39,13 @@ const readExistingBypass = async (): Promise => { const getCommand = (): string => { const firstLine = (process.env.TRUSTED_LIST_BYPASS_COMMENT || "").trim().split(/\r?\n/u)[0]?.trim() || ""; + const normalizedFirstLine = firstLine.toLowerCase(); - if (firstLine.startsWith("/skip-trusted-lists")) { + if (normalizedFirstLine.startsWith("/skip-trusted-lists")) { return "/skip-trusted-lists"; } - return firstLine; + return normalizedFirstLine; }; const getApprovedBy = (): string => {