All checks were successful
check / check (push) Successful in 25s
Vendor the MetaMask eth-phishing-detect config.json (231k domains) into src/data/phishing-domains.json as the baseline blocklist shipped with the extension. On 24h refresh, only the delta (new domains not in the vendored snapshot) is kept in memory. Domain checks hit the in-memory delta first (fresh scam sites), then binary-search the vendored sorted array. If the delta is under 256 KiB it is persisted to chrome.storage.local so it survives service-worker restarts without re-fetching. Removes the previous approach of downloading and holding the full blocklist in memory as a Set.
218 lines
7.7 KiB
JavaScript
218 lines
7.7 KiB
JavaScript
const {
|
|
isPhishingDomain,
|
|
loadConfig,
|
|
getBlocklistSize,
|
|
getDeltaSize,
|
|
hostnameVariants,
|
|
binarySearch,
|
|
_reset,
|
|
} = require("../src/shared/phishingDomains");
|
|
|
|
// The vendored baseline is loaded automatically via require().
|
|
// _reset() clears only the delta state, not the vendored baseline.
|
|
beforeEach(() => {
|
|
_reset();
|
|
});
|
|
|
|
describe("phishingDomains", () => {
|
|
describe("hostnameVariants", () => {
|
|
test("returns exact hostname plus parent domains", () => {
|
|
const variants = hostnameVariants("sub.evil.com");
|
|
expect(variants).toEqual(["sub.evil.com", "evil.com"]);
|
|
});
|
|
|
|
test("returns just the hostname for a bare domain", () => {
|
|
const variants = hostnameVariants("example.com");
|
|
expect(variants).toEqual(["example.com"]);
|
|
});
|
|
|
|
test("handles deep subdomain chains", () => {
|
|
const variants = hostnameVariants("a.b.c.d.com");
|
|
expect(variants).toEqual([
|
|
"a.b.c.d.com",
|
|
"b.c.d.com",
|
|
"c.d.com",
|
|
"d.com",
|
|
]);
|
|
});
|
|
|
|
test("lowercases hostnames", () => {
|
|
const variants = hostnameVariants("Evil.COM");
|
|
expect(variants).toEqual(["evil.com"]);
|
|
});
|
|
});
|
|
|
|
describe("binarySearch", () => {
|
|
const sorted = ["alpha.com", "beta.com", "gamma.com", "zeta.com"];
|
|
|
|
test("finds existing elements", () => {
|
|
expect(binarySearch(sorted, "alpha.com")).toBe(true);
|
|
expect(binarySearch(sorted, "gamma.com")).toBe(true);
|
|
expect(binarySearch(sorted, "zeta.com")).toBe(true);
|
|
});
|
|
|
|
test("returns false for missing elements", () => {
|
|
expect(binarySearch(sorted, "aaa.com")).toBe(false);
|
|
expect(binarySearch(sorted, "delta.com")).toBe(false);
|
|
expect(binarySearch(sorted, "zzz.com")).toBe(false);
|
|
});
|
|
|
|
test("handles empty array", () => {
|
|
expect(binarySearch([], "anything")).toBe(false);
|
|
});
|
|
|
|
test("handles single-element array", () => {
|
|
expect(binarySearch(["only.com"], "only.com")).toBe(true);
|
|
expect(binarySearch(["only.com"], "other.com")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("vendored baseline detection", () => {
|
|
// These tests verify that the vendored phishing-domains.json
|
|
// is loaded and searchable without any delta loaded.
|
|
|
|
test("getBlocklistSize reflects vendored list (no delta)", () => {
|
|
// The vendored list has 231k+ domains; delta is empty after reset.
|
|
expect(getBlocklistSize()).toBeGreaterThan(200000);
|
|
expect(getDeltaSize()).toBe(0);
|
|
});
|
|
|
|
test("returns false for clean domains against vendored list", () => {
|
|
expect(isPhishingDomain("google.com")).toBe(false);
|
|
expect(isPhishingDomain("github.com")).toBe(false);
|
|
});
|
|
|
|
test("returns false for empty/null hostname", () => {
|
|
expect(isPhishingDomain("")).toBe(false);
|
|
expect(isPhishingDomain(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("delta (loadConfig) + isPhishingDomain", () => {
|
|
test("detects domains loaded into delta via loadConfig", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com", "scam-swap.xyz"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("scam-swap.xyz")).toBe(true);
|
|
});
|
|
|
|
test("detects subdomain of delta-blacklisted domain", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("app.evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("sub.app.evil-phishing.com")).toBe(true);
|
|
});
|
|
|
|
test("delta whitelist overrides delta blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["metamask.io"],
|
|
whitelist: ["metamask.io"],
|
|
});
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
});
|
|
|
|
test("delta whitelist on parent domain overrides blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["sub.legit.com"],
|
|
whitelist: ["legit.com"],
|
|
});
|
|
expect(isPhishingDomain("sub.legit.com")).toBe(false);
|
|
});
|
|
|
|
test("case-insensitive matching in delta", () => {
|
|
loadConfig({
|
|
blacklist: ["Evil-Phishing.COM"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("EVIL-PHISHING.COM")).toBe(true);
|
|
});
|
|
|
|
test("getDeltaSize reflects loaded delta", () => {
|
|
loadConfig({
|
|
blacklist: ["a.com", "b.com", "c.com"],
|
|
whitelist: ["d.com"],
|
|
});
|
|
expect(getDeltaSize()).toBe(3);
|
|
});
|
|
|
|
test("re-loading config replaces previous delta", () => {
|
|
loadConfig({
|
|
blacklist: ["old-scam.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("old-scam.com")).toBe(true);
|
|
|
|
loadConfig({
|
|
blacklist: ["new-scam.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("old-scam.com")).toBe(false);
|
|
expect(isPhishingDomain("new-scam.com")).toBe(true);
|
|
});
|
|
|
|
test("handles config with no blacklist/whitelist keys", () => {
|
|
loadConfig({});
|
|
expect(getDeltaSize()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("real-world MetaMask blocklist patterns (via delta)", () => {
|
|
test("detects known phishing domains loaded as delta", () => {
|
|
loadConfig({
|
|
blacklist: [
|
|
"uniswap-trade.web.app",
|
|
"hopprotocol.pro",
|
|
"blast-pools.pages.dev",
|
|
],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("uniswap-trade.web.app")).toBe(true);
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
|
|
});
|
|
|
|
test("delta whitelist overrides vendored blacklist entries", () => {
|
|
// If a domain is in the vendored blacklist but a fresh whitelist
|
|
// update adds it, the whitelist should win.
|
|
loadConfig({
|
|
blacklist: [],
|
|
whitelist: ["opensea.io", "metamask.io", "etherscan.io"],
|
|
});
|
|
expect(isPhishingDomain("opensea.io")).toBe(false);
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("delta + vendored interaction", () => {
|
|
test("delta blacklist entries are found even with empty vendored match", () => {
|
|
// This domain is (almost certainly) not in the vendored list
|
|
const uniqueDomain =
|
|
"test-unique-domain-not-in-vendored-" +
|
|
Date.now() +
|
|
".example.com";
|
|
expect(isPhishingDomain(uniqueDomain)).toBe(false);
|
|
|
|
loadConfig({
|
|
blacklist: [uniqueDomain],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain(uniqueDomain)).toBe(true);
|
|
});
|
|
|
|
test("getBlocklistSize includes both vendored and delta", () => {
|
|
const baseSize = getBlocklistSize();
|
|
loadConfig({
|
|
blacklist: ["new-a.com", "new-b.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(getBlocklistSize()).toBe(baseSize + 2);
|
|
});
|
|
});
|
|
});
|