From fb540b0f42be9d03f44c876d893ab7728a501ac0 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Tue, 19 Apr 2022 12:19:37 -0230 Subject: [PATCH] Support multiple phishing configurations (#7079) * Support multiple phishing configurations The phishing detector has been updated to support multiple phishing configurations. Both the configuration object and the result object have been updated to accommodate the need to identify the name of the config that the checked domain matched. Since the config and return value was already being changed, the nomenclature has been updated to replace `black/white` with `block/allow` as well, which is a change we have been meaning to make for some time. This change to both the configuration and result object applies only when the new configuration format is used. The old format preserves the old config and result value, making this a non-breaking change. The old configuration accepted three lists (`blacklist`, `whitelist`, and `fuzzylist`), and a `tolerance` value for the fuzzylist match. The new configuration is an array of objects rather than an object, to accommodate multiple configurations. Each configuration option accepts three lists (`blocklist`, `allowlist`, and `fuzzylist`), `tolerance` for the fuzzylist match, and two new properties: `name` and `version`. The `version` parameter was already used by the old configuration, but it was not required or used by the detector itself. It is now required with the new configuration, and it is returned with each match. The new `name` parameter describes which configuration matched the origin being checked (if any). This was critical for us because it allows us to direct the user to the appropriate place when they want to dispute a blocked site. The return value was updated to include the `name` and `version` parameters. The `type` was updated from `blacklist` to `blocklist` and from `whitelist` to `allowlist` as well. * v1.2.0 This release adds support for multiple phishing configurations, and includes changes to the configuration object and return value if an array of configuration values is passed to the phishing detector constructor. This is a non-breaking change because the old configuration format is still supported, and the return values remain the same if the old configuration format is used. Co-authored-by: Deven Blake Co-authored-by: H <409H@users.noreply.github.com> --- package.json | 2 +- src/detector.js | 165 ++++++-- test/index.js | 992 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1118 insertions(+), 41 deletions(-) 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}"`) }