diff --git a/package.json b/package.json index c728b51f8dd..c7d2ede117e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eth-phishing-detect", - "version": "1.1.16", + "version": "1.2.0", "description": "Utility for detecting phishing domains targeting Ethereum users", "main": "src/index.js", "scripts": { diff --git a/src/detector.js b/src/detector.js index 76c1e814f41..36a52e30f31 100644 --- a/src/detector.js +++ b/src/detector.js @@ -3,47 +3,112 @@ const DEFAULT_TOLERANCE = 3 class PhishingDetector { + /** + * Legacy phishing detector configuration. + * + * @typedef {object} LegacyPhishingDetectorConfiguration + * @property {string[]} [whitelist] - Origins that should not be blocked. + * @property {string[]} [blacklist] - Origins to block. + * @property {string[]} [fuzzylist] - Origins of common phishing targets. + * @property {number} [tolerance] - Tolerance to use for the fuzzylist levenshtein match. + */ + + /** + * A configuration object for phishing detection. + * + * @typedef {object} PhishingDetectorConfiguration + * @property {string[]} [allowlist] - Origins that should not be blocked. + * @property {string[]} [blocklist] - Origins to block. + * @property {string[]} [fuzzylist] - Origins of common phishing targets. + * @property {string} name - The name of this configuration. Used to explain to users why a site is being blocked. + * @property {number} [tolerance] - Tolerance to use for the fuzzylist levenshtein match. + * @property {number} version - The current version of the configuration. + */ + + /** + * Construct a phishing detector, which can check whether origins are known + * to be malicious or similar to common phishing targets. + * + * A list of configurations is accepted. Each origin checked is processed + * using each configuration in sequence, so the order defines which + * configurations take precedence. + * + * @param {LegacyPhishingDetectorConfiguration | PhishingDetectorConfiguration[]} opts - Phishing detection options + */ constructor (opts) { - this.whitelist = processDomainList(opts.whitelist || []) - this.blacklist = processDomainList(opts.blacklist || []) - this.fuzzylist = processDomainList(opts.fuzzylist || []) - this.tolerance = ('tolerance' in opts) ? opts.tolerance : DEFAULT_TOLERANCE + // recommended configuration + if (Array.isArray(opts)) { + this.configs = processConfigs(opts) + this.legacyConfig = false + // legacy configuration + } else { + this.configs = [{ + allowlist: processDomainList(opts.whitelist || []), + blocklist: processDomainList(opts.blacklist || []), + fuzzylist: processDomainList(opts.fuzzylist || []), + tolerance: ('tolerance' in opts) ? opts.tolerance : DEFAULT_TOLERANCE + }] + this.legacyConfig = true + } } - check (domain) { - let fqdn = domain.substring(domain.length - 1) === "." + check(domain) { + const result = this._check(domain) + + if (this.legacyConfig) { + let legacyType = result.type; + if (legacyType === 'allowlist') { + legacyType = 'whitelist' + } else if (legacyType === 'blocklist') { + legacyType = 'blacklist' + } + return { + match: result.match, + result: result.result, + type: legacyType, + } + } + return result + } + + _check (domain) { + let fqdn = domain.substring(domain.length - 1) === "." ? domain.slice(0, -1) : domain; const source = domainToParts(fqdn) - // if source matches whitelist domain (or subdomain thereof), PASS - const whitelistMatch = matchPartsAgainstList(source, this.whitelist) - if (whitelistMatch) return { type: 'whitelist', result: false } - - // if source matches blacklist domain (or subdomain thereof), FAIL - const blacklistMatch = matchPartsAgainstList(source, this.blacklist) - if (blacklistMatch) return { type: 'blacklist', result: true } - - if (this.tolerance > 0) { - // check if near-match of whitelist domain, FAIL - let fuzzyForm = domainPartsToFuzzyForm(source) - // strip www - fuzzyForm = fuzzyForm.replace('www.', '') - // check against fuzzylist - const levenshteinMatched = this.fuzzylist.find((targetParts) => { - const fuzzyTarget = domainPartsToFuzzyForm(targetParts) - const distance = levenshtein.get(fuzzyForm, fuzzyTarget) - return distance <= this.tolerance - }) - if (levenshteinMatched) { - const match = domainPartsToDomain(levenshteinMatched) - return { type: 'fuzzy', result: true, match } + for (const { allowlist, name, version } of this.configs) { + // if source matches whitelist domain (or subdomain thereof), PASS + const whitelistMatch = matchPartsAgainstList(source, allowlist) + if (whitelistMatch) return { name, result: false, type: 'allowlist', version } + } + + for (const { blocklist, fuzzylist, name, tolerance, version } of this.configs) { + // if source matches blacklist domain (or subdomain thereof), FAIL + const blacklistMatch = matchPartsAgainstList(source, blocklist) + if (blacklistMatch) return { name, result: true, type: 'blocklist', version } + + if (tolerance > 0) { + // check if near-match of whitelist domain, FAIL + let fuzzyForm = domainPartsToFuzzyForm(source) + // strip www + fuzzyForm = fuzzyForm.replace('www.', '') + // check against fuzzylist + const levenshteinMatched = fuzzylist.find((targetParts) => { + const fuzzyTarget = domainPartsToFuzzyForm(targetParts) + const distance = levenshtein.get(fuzzyForm, fuzzyTarget) + return distance <= tolerance + }) + if (levenshteinMatched) { + const match = domainPartsToDomain(levenshteinMatched) + return { name, match, result: true, type: 'fuzzy', version } + } } } // matched nothing, PASS - return { type: 'all', result: false } + return { result: false, type: 'all' } } } @@ -52,12 +117,52 @@ module.exports = PhishingDetector // util +function processConfigs(configs = []) { + return configs.map((config) => { + validateConfig(config) + return Object.assign({}, config, { + allowlist: processDomainList(config.allowlist || []), + blocklist: processDomainList(config.blocklist || []), + fuzzylist: processDomainList(config.fuzzylist || []), + tolerance: ('tolerance' in config) ? config.tolerance : DEFAULT_TOLERANCE + }) + }); +} + +function validateConfig(config) { + if (config === null || typeof config !== 'object') { + throw new Error('Invalid config') + } + + if (config.tolerance && !config.fuzzylist) { + throw new Error('Fuzzylist tolerance provided without fuzzylist') + } + + if ( + typeof config.name !== 'string' || + config.name === '' + ) { + throw new Error("Invalid config parameter: 'name'") + } + + if ( + !['number', 'string'].includes(typeof config.version) || + config.version === '' + ) { + throw new Error("Invalid config parameter: 'version'") + } +} + function processDomainList (list) { return list.map(domainToParts) } function domainToParts (domain) { + try { return domain.split('.').reverse() + } catch (e) { + throw new Error(JSON.stringify(domain)) + } } function domainPartsToDomain(domainParts) { @@ -80,4 +185,4 @@ function matchPartsAgainstList(source, list) { // source matches target or (is deeper subdomain) return target.every((part, index) => source[index] === part) }) -} \ No newline at end of file +} diff --git a/test/index.js b/test/index.js index ff97103aa3a..78913df2b46 100644 --- a/test/index.js +++ b/test/index.js @@ -9,7 +9,6 @@ const config = require("../src/config.json") const alexaTopSites = require("./alexa.json") const popularDapps = require("./dapps.json") -const detector = new PhishingDetector(config) const metamaskGaq = loadMetamaskGaq() let mewBlacklist, mewWhitelist const remoteBlacklistException = ['bittreat.com'] @@ -43,7 +42,7 @@ function loadMetamaskGaq () { function startTests () { - test("basic test", (t) => { + test("legacy config", (t) => { // blacklist @@ -184,6 +183,966 @@ function startTests () { t.end() }) + test("current config", (t) => { + + // allow missing allowlist + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + ]) + t.pass('Passed validation') + } catch (error) { + t.fail(error.message) + } + + // allow missing blocklist + try { + new PhishingDetector([ + { + allowlist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + ]) + t.pass('Passed validation') + } catch (error) { + t.fail(error.message) + } + + // allow missing fuzzylist and tolerance + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: [], + name: 'first', + version: 1 + }, + ]) + t.pass('Passed validation') + } catch (error) { + t.fail(error.message) + } + + // allow missing tolerance + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + version: 1 + }, + ]) + t.pass('Passed validation') + } catch (error) { + t.fail(error.message) + } + + // throw when config is invalid + const invalidConfigValues = [ + undefined, + null, + true, + false, + 0, + 1, + 1.1, + '', + 'test', + () => { + return {name: 'test', version: 1 } + }, + ] + for (const invalidValue of invalidConfigValues) { + try { + new PhishingDetector([invalidValue]) + t.fail('Did not fail validation') + } catch (error) { + t.equal(error.message, 'Invalid config') + } + } + + // throw when tolerance is provided without fuzzylist + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + name: 'first', + tolerance: 2, + version: 1, + }, + ]) + t.fail('Did not fail validation') + } catch (error) { + t.equal(error.message, 'Fuzzylist tolerance provided without fuzzylist') + } + + // throw when config name is invalid + const invalidNameValues = [ + undefined, + null, + true, + false, + 0, + 1, + 1.1, + '', + () => { + return {name: 'test', version: 1 } + }, + {} + ] + for (const invalidValue of invalidNameValues) { + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + fuzzylist: [], + name: invalidValue, + tolerance: 2, + version: 1 + }, + ]) + t.fail('Did not fail validation') + } catch (error) { + t.equal(error.message, "Invalid config parameter: 'name'") + } + } + + // throw when config version is invalid + const invalidVersionValues = [ + undefined, + null, + true, + false, + '', + () => { + return {name: 'test', version: 1 } + }, + {} + ] + for (const invalidValue of invalidVersionValues) { + try { + new PhishingDetector([ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: invalidValue + }, + ]) + t.fail('Did not fail validation') + } catch (error) { + t.equal(error.message, "Invalid config parameter: 'version'") + } + } + + const currentConfig = [{ + allowlist: config.whitelist, + blocklist: config.blacklist, + disputeUrl: 'https://github.com/MetaMask/eth-phishing-detect', + fuzzylist: config.fuzzylist, + name: 'MetaMask', + tolerance: config.tolerance, + version: config.version + }] + + // return version with match + testDomain(t, { + domain: 'blocked-by-first.com', + expected: true, + version: 1, + options: [ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + ], + }) + + // return name with match + testDomain(t, { + domain: 'blocked-by-first.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + ], + }) + + // blacklist + + testBlacklist(t, [ + "metamask.com", + "wallet-ethereum.net", + "etherclassicwallet.com", + "wallet-ethereum.net." //Test for absolute fully-qualified domain name + ], currentConfig) + + // whitelist + + testWhitelist(t, [ + "ledgerwallet.com", + "metamask.io", + "etherscan.io", + "ethereum.org", + // whitelist subdomains + "www.metamask.io", + "faucet.metamask.io", + "zero.metamask.io", + "zero-faucet.metamask.io", + "www.myetherwallet.com", + ], currentConfig) + + // fuzzy + + testFuzzylist(t, [ + "metmask.io", + "myetherwallet.cx", + "myetherwallet.aaa", + "myetherwallet.za", + "myetherwallet.z", + ], currentConfig) + + // DO NOT detected as phishing + + testAnyType(t, false, [ + "example.com", + "etherid.org", + "ether.cards", + "easyeth.com", + "etherdomain.com", + "ethnews.com", + "cryptocompare.com", + "kraken.com", + "myetherwallet.groovehq.com", + "dether.io", + "ethermine.org", + "slaask.com", + "ethereumdev.io", + "ethereumdev.kr", + "etherplan.com", + "etherplay.io", + "ethtrade.org", + "ethereumpool.co", + "estream.to", + "ethereum.os.tc", + "theethereum.wiki", + "taas.fund", + "tether.to", + "ether.direct", + "themem.io", + "metajack.im", + "mestatalsl.biz", + "thregg.com", + "steem.io", + "ethereum1.cz", + "metalab.co", + "originprotocol.com" + ], currentConfig) + + // DO INDEED detect as phishing + testAnyType(t, true, [ + "etherdelta-glthub.com", + "omise-go.com", + "omise-go.net", + "numerai.tech", + "decentraiand.org", + "myetherwallet.com.ethpromonodes.com", + "blockcrein.info", + "blockchealn.info", + "bllookchain.info", + "blockcbhain.info", + "tokenswap.org", + "ethtrade.io", + "myetherwallèt.com", + "myetherwallet.cm", + "myethervvallet.com", + "metherwallet.com", + "mtetherwallet.com", + "my-etherwallet.com", + "my-etherwallet.in", + "myeherwallet.com", + "myetcwallet.com", + "myetehrwallet.com", + "myeterwallet.com", + "myethe.rwallet.com", + "myethereallet.com", + "myetherieumwallet.com", + "myetherswallet.com", + "myetherw.allet.com", + "myetherwal.let.com", + "myetherwalet.com", + "myetherwaliet.com", + "myetherwall.et.com", + "myetherwaller.com", + "myetherwallett.com", + "myetherwaillet.com", + "myetherwalllet.com", + "myetherweb.com.de", + "myethetwallet.com", + "myethewallet.com", + "myÄ—therwallet.com", + "myelherwallel.com", + "mvetherwallet.com", + "myethewallet.net", + "myetherwillet.com", + "myetherwallel.com", + "myeltherwallet.com", + "myelherwallet.com", + "wwwmyetherwallet.com", + "myethermwallet.com", + "myeth4rwallet.com", + "myethterwallet.com", + "origirprotocol.com" + ], currentConfig) + + // etc... + + testNoMatch(t, [ + "MetaMask", + "localhost", + "bancor", + "127.0.0.1", + ], currentConfig) + + t.end() + }) + + test("multiple configs", (t) => { + + // allow no config + testDomain(t, { + domain: 'default.com', + expected: false, + options: [], + type: 'all' + }) + + // allow by default + testDomain(t, { + domain: 'default.com', + expected: false, + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'all' + }) + + // block origin in first config + testDomain(t, { + domain: 'blocked-by-first.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: ['blocked-by-first.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'blocklist' + }) + + // block origin in second config + testDomain(t, { + domain: 'blocked-by-second.com', + expected: true, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: ['blocked-by-second.com'], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'blocklist' + }) + + // prefer first config when origin blocked by both + testDomain(t, { + domain: 'blocked-by-both.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: ['blocked-by-both.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: ['blocked-by-both.com'], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'blocklist' + }) + + // test first fuzzylist + testDomain(t, { + domain: 'fuzzy-first.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-first.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // test first fuzzylist at tolerance + testDomain(t, { + domain: 'fuzzy-firstab.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-first.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // allow first fuzzylist beyond tolerance + testDomain(t, { + domain: 'fuzzy-firstabc.com', + expected: false, + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-first.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'all' + }) + + // test second fuzzylist + testDomain(t, { + domain: 'fuzzy-second.com', + expected: true, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // test second fuzzylist at tolerance + testDomain(t, { + domain: 'fuzzy-secondab.com', + expected: true, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // allow second fuzzylist past tolerance + testDomain(t, { + domain: 'fuzzy-secondabc.com', + expected: false, + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'all' + }) + + // prefer first config when blocked by both fuzzylists + testDomain(t, { + domain: 'fuzzy-both.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-both.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-both.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // prefer first config when blocked by first and fuzzy blocked by second + testDomain(t, { + domain: 'blocked-first-fuzzy-second.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: ['blocked-first-fuzzy-second.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['blocked-first-fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'blocklist' + }) + + // prefer first config when fuzzy blocked by first and blocked by second + testDomain(t, { + domain: 'fuzzy-first-blocked-second.com', + expected: true, + name: 'first', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-first-blocked-second.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: ['fuzzy-first-blocked-second.com'], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'fuzzy' + }) + + // allow origin that is allowed and not blocked on first config + testDomain(t, { + domain: 'allowed-first.com', + expected: false, + name: 'first', + options: [ + { + allowlist: ['allowed-first.com'], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin that is allowed and not blocked on second config + testDomain(t, { + domain: 'allowed-second.com', + expected: false, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: ['allowed-second.com'], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin that is blocklisted and allowlisted, both on first config + testDomain(t, { + domain: 'allowed-and-blocked-first.com', + expected: false, + name: 'first', + options: [ + { + allowlist: ['allowed-and-blocked-first.com'], + blocklist: ['allowed-and-blocked-first.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin blocked by fuzzylist and allowlisted, both on first config + testDomain(t, { + domain: 'allowed-and-fuzzy-first.com', + expected: false, + name: 'first', + options: [ + { + allowlist: ['allowed-and-fuzzy-first.com'], + blocklist: [], + fuzzylist: ['allowed-and-fuzzy-first.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin that is blocklisted and allowlisted, both on second config + testDomain(t, { + domain: 'allowed-and-blocked-second.com', + expected: false, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: ['allowed-and-blocked-second.com'], + blocklist: ['allowed-and-blocked-second.com'], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin blocked by fuzzylist and allowlisted, both on second config + testDomain(t, { + domain: 'allowed-and-fuzzy-second.com', + expected: false, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: ['allowed-and-fuzzy-second.com'], + blocklist: [], + fuzzylist: ['allowed-and-fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin blocked by first config but allowedlisted by second + testDomain(t, { + domain: 'blocked-first-allowed-second.com', + expected: false, + name: 'second', + options: [ + { + allowlist: [], + blocklist: ['blocked-first-allowed-second.com'], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: ['blocked-first-allowed-second.com'], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin allowed by first config but blocked by second + testDomain(t, { + domain: 'allowed-first-blocked-second.com', + expected: false, + name: 'first', + options: [ + { + allowlist: ['allowed-first-blocked-second.com'], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: ['allowed-first-blocked-second.com'], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin fuzzylist blocked by first config but allowed by second + testDomain(t, { + domain: 'fuzzy-first-allowed-second.com', + expected: false, + name: 'second', + options: [ + { + allowlist: [], + blocklist: [], + fuzzylist: ['fuzzy-first-allowed-second.com'], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: ['fuzzy-first-allowed-second.com'], + blocklist: [], + fuzzylist: [], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + // allow origin allowed by first config but fuzzylist blocked by second + testDomain(t, { + domain: 'allowed-first-fuzzy-second.com', + expected: false, + name: 'first', + options: [ + { + allowlist: ['allowed-first-fuzzy-second.com'], + blocklist: [], + fuzzylist: [], + name: 'first', + tolerance: 2, + version: 1 + }, + { + allowlist: [], + blocklist: [], + fuzzylist: ['allowed-first-fuzzy-second.com'], + name: 'second', + tolerance: 2, + version: 1 + }, + ], + type: 'allowlist' + }) + + t.end() + }) + test("alexa top sites", (t) => { testAnyType(t, false, alexaTopSites) t.end() @@ -206,6 +1165,7 @@ function startTests () { test("metamask gaq", (t) => { testListIsPunycode(t, metamaskGaq) metamaskGaq.forEach((domain) => { + const detector = new PhishingDetector(config) const value = detector.check(domain) // enforcing type is optional // if (value.type === 'all') { @@ -239,32 +1199,35 @@ function startTests () { } -function testBlacklist (t, domains) { +function testBlacklist (t, domains, options) { domains.forEach((domain) => { testDomain(t, { domain: domain, - type: "blacklist", + type: options && Array.isArray(options) ? "blocklist" : "blacklist", expected: true, + options, }) }) } -function testWhitelist (t, domains) { +function testWhitelist (t, domains, options) { domains.forEach((domain) => { testDomain(t, { domain: domain, - type: "whitelist", + type: options && Array.isArray(options) ? "allowlist" : "whitelist", expected: false, + options, }) }) } -function testFuzzylist (t, domains) { +function testFuzzylist (t, domains, options) { domains.forEach((domain) => { testDomain(t, { domain: domain, type: "fuzzy", expected: true, + options, }) }) } @@ -296,26 +1259,29 @@ function testListDoesntContainRepeats (t, list) { }) } -function testNoMatch (t, domains) { +function testNoMatch (t, domains, options) { domains.forEach((domain) => { testDomain(t, { domain: domain, type: "all", expected: false, + options }) }) } -function testAnyType (t, expected, domains) { +function testAnyType (t, expected, domains, options) { domains.forEach((domain) => { testDomain(t, { domain: domain, expected, + options, }) }) } -function testDomain (t, { domain, type, expected }) { +function testDomain (t, { domain, name, type, expected, options = config, version }) { + const detector = new PhishingDetector(options) const value = detector.check(domain) // log fuzzy match for debugging // if (value.type === "fuzzy") { @@ -325,6 +1291,12 @@ function testDomain (t, { domain, type, expected }) { if (type) { t.equal(value.type, type, `type: "${domain}" should be "${type}"`) } + if (name) { + t.equal(value.name, name, `name: "${domain}" should return result from config "${name}"`) + } + if (version) { + t.equal(value.version, version, `version: "${domain}" should return result from config version '${version}'`) + } // enforcing result is required t.equal(value.result, expected, `result: "${domain}" should be match "${expected}"`) }