- Vendor community-maintained phishing domain blocklist into src/shared/phishingBlocklist.json (bundled at build time by esbuild) - Refactor phishingDomains.js: build vendored Sets at module load, fetch live list periodically, keep only delta (new entries not in vendored) in memory for small runtime footprint - Domain checker checks delta first (fresh scam sites), then vendored - Persist delta to localStorage if under 256 KiB - Load delta from localStorage on startup for instant coverage - Add startPeriodicRefresh() with 24h setInterval in background script - Remove dead code: popup's local isPhishingDomain() re-check was inert (popup never called updatePhishingList so its blacklistSet was always empty); now relies solely on background's authoritative flag - Remove all competitor name mentions from UI warning text and comments - Update README: document phishing domain protection architecture, update external services list - Update tests: cover vendored blocklist loading, delta computation, localStorage persistence, delta+vendored interaction Closes #114
260 lines
9.2 KiB
JavaScript
260 lines
9.2 KiB
JavaScript
// Provide a localStorage mock for Node.js test environment.
|
|
// Must be set before requiring the module since it calls loadDeltaFromStorage()
|
|
// at module load time.
|
|
const localStorageStore = {};
|
|
global.localStorage = {
|
|
getItem: (key) =>
|
|
Object.prototype.hasOwnProperty.call(localStorageStore, key)
|
|
? localStorageStore[key]
|
|
: null,
|
|
setItem: (key, value) => {
|
|
localStorageStore[key] = String(value);
|
|
},
|
|
removeItem: (key) => {
|
|
delete localStorageStore[key];
|
|
},
|
|
};
|
|
|
|
const {
|
|
isPhishingDomain,
|
|
loadConfig,
|
|
getBlocklistSize,
|
|
getDeltaSize,
|
|
hostnameVariants,
|
|
_reset,
|
|
_getVendoredBlacklistSize,
|
|
_getVendoredWhitelistSize,
|
|
_getDeltaBlacklist,
|
|
_getDeltaWhitelist,
|
|
} = require("../src/shared/phishingDomains");
|
|
|
|
// Reset delta state before each test to avoid cross-test contamination.
|
|
// Note: vendored sets are immutable and always present.
|
|
beforeEach(() => {
|
|
_reset();
|
|
// Clear localStorage mock between tests
|
|
for (const key of Object.keys(localStorageStore)) {
|
|
delete localStorageStore[key];
|
|
}
|
|
});
|
|
|
|
describe("phishingDomains", () => {
|
|
describe("vendored blocklist", () => {
|
|
test("vendored blacklist is loaded from bundled JSON", () => {
|
|
// The vendored blocklist should have a large number of entries
|
|
expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000);
|
|
});
|
|
|
|
test("vendored whitelist is loaded from bundled JSON", () => {
|
|
expect(_getVendoredWhitelistSize()).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("detects domains from vendored blacklist", () => {
|
|
// These are well-known phishing domains in the vendored list
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
|
|
});
|
|
|
|
test("vendored whitelist overrides vendored blacklist", () => {
|
|
// opensea.pro is whitelisted in the vendored config
|
|
expect(isPhishingDomain("opensea.pro")).toBe(false);
|
|
});
|
|
|
|
test("getBlocklistSize includes vendored entries", () => {
|
|
expect(getBlocklistSize()).toBeGreaterThan(100000);
|
|
});
|
|
});
|
|
|
|
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("delta computation via loadConfig", () => {
|
|
test("loadConfig computes delta of new entries not in vendored list", () => {
|
|
loadConfig({
|
|
blacklist: [
|
|
"brand-new-scam-site-xyz123.com",
|
|
"hopprotocol.pro", // already in vendored
|
|
],
|
|
whitelist: [],
|
|
});
|
|
// Only the new domain should be in the delta
|
|
expect(
|
|
_getDeltaBlacklist().has("brand-new-scam-site-xyz123.com"),
|
|
).toBe(true);
|
|
expect(_getDeltaBlacklist().has("hopprotocol.pro")).toBe(false);
|
|
expect(getDeltaSize()).toBe(1);
|
|
});
|
|
|
|
test("delta whitelist entries are computed correctly", () => {
|
|
loadConfig({
|
|
blacklist: [],
|
|
whitelist: [
|
|
"new-safe-site-xyz789.com",
|
|
"opensea.pro", // already in vendored whitelist
|
|
],
|
|
});
|
|
expect(_getDeltaWhitelist().has("new-safe-site-xyz789.com")).toBe(
|
|
true,
|
|
);
|
|
expect(_getDeltaWhitelist().has("opensea.pro")).toBe(false);
|
|
});
|
|
|
|
test("re-loading config replaces previous delta", () => {
|
|
loadConfig({
|
|
blacklist: ["first-scam-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("first-scam-xyz.com")).toBe(true);
|
|
|
|
loadConfig({
|
|
blacklist: ["second-scam-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("first-scam-xyz.com")).toBe(false);
|
|
expect(isPhishingDomain("second-scam-xyz.com")).toBe(true);
|
|
});
|
|
|
|
test("getBlocklistSize includes both vendored and delta", () => {
|
|
const baseSize = getBlocklistSize();
|
|
loadConfig({
|
|
blacklist: ["delta-only-scam-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(getBlocklistSize()).toBe(baseSize + 1);
|
|
});
|
|
});
|
|
|
|
describe("isPhishingDomain with delta + vendored", () => {
|
|
test("detects domain from delta blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["fresh-scam-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true);
|
|
});
|
|
|
|
test("detects domain from vendored blacklist", () => {
|
|
// No delta loaded — vendored still works
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
});
|
|
|
|
test("returns false for clean domains", () => {
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
expect(isPhishingDomain("example.com")).toBe(false);
|
|
});
|
|
|
|
test("detects subdomain of blacklisted domain (vendored)", () => {
|
|
expect(isPhishingDomain("app.hopprotocol.pro")).toBe(true);
|
|
});
|
|
|
|
test("detects subdomain of blacklisted domain (delta)", () => {
|
|
loadConfig({
|
|
blacklist: ["delta-phish-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true);
|
|
});
|
|
|
|
test("delta whitelist overrides vendored blacklist", () => {
|
|
// hopprotocol.pro is in the vendored blacklist
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
loadConfig({
|
|
blacklist: [],
|
|
whitelist: ["hopprotocol.pro"],
|
|
});
|
|
// Now whitelisted via delta — should not be flagged
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(false);
|
|
});
|
|
|
|
test("vendored whitelist overrides delta blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["opensea.pro"], // opensea.pro is vendored-whitelisted
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("opensea.pro")).toBe(false);
|
|
});
|
|
|
|
test("case-insensitive matching", () => {
|
|
loadConfig({
|
|
blacklist: ["Delta-Scam-XYZ.COM"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true);
|
|
expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true);
|
|
});
|
|
|
|
test("returns false for empty/null hostname", () => {
|
|
expect(isPhishingDomain("")).toBe(false);
|
|
expect(isPhishingDomain(null)).toBe(false);
|
|
});
|
|
|
|
test("handles config with no blacklist/whitelist keys", () => {
|
|
loadConfig({});
|
|
expect(getDeltaSize()).toBe(0);
|
|
// Vendored list still works
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("localStorage persistence", () => {
|
|
test("saveDeltaToStorage persists delta under 256KiB", () => {
|
|
loadConfig({
|
|
blacklist: ["persisted-scam-xyz.com"],
|
|
whitelist: ["persisted-safe-xyz.com"],
|
|
});
|
|
const stored = localStorage.getItem("phishing-delta");
|
|
expect(stored).not.toBeNull();
|
|
const data = JSON.parse(stored);
|
|
expect(data.blacklist).toContain("persisted-scam-xyz.com");
|
|
expect(data.whitelist).toContain("persisted-safe-xyz.com");
|
|
});
|
|
|
|
test("delta is cleared on _reset", () => {
|
|
loadConfig({
|
|
blacklist: ["temp-scam-xyz.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(getDeltaSize()).toBe(1);
|
|
_reset();
|
|
expect(getDeltaSize()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("real-world blocklist patterns", () => {
|
|
test("detects known phishing domains from vendored list", () => {
|
|
expect(isPhishingDomain("uniswap-trade.web.app")).toBe(true);
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
|
|
});
|
|
|
|
test("does not flag legitimate whitelisted domains", () => {
|
|
expect(isPhishingDomain("opensea.io")).toBe(false);
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
});
|
|
});
|
|
});
|