From b67bcf233c98949e87f9594b1d03ec2de754fc8c Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 18:56:16 -0700 Subject: [PATCH] init commit --- .gitignore | 92 ++++++++++++++++++++++++ package.json | 17 +++++ src/config.json | 89 +++++++++++++++++++++++ src/detector.js | 73 +++++++++++++++++++ src/index.js | 11 +++ test/index.js | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/config.json create mode 100644 src/detector.js create mode 100644 src/index.js create mode 100644 test/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f377e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,92 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# End of https://www.gitignore.io/api/osx,node diff --git a/package.json b/package.json new file mode 100644 index 0000000..37a25b3 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "eth-phishing-detect", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "node test" + }, + "author": "", + "license": "ISC", + "dependencies": { + "fast-levenshtein": "^2.0.6" + }, + "devDependencies": { + "tape": "^4.8.0" + } +} diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..dc835fb --- /dev/null +++ b/src/config.json @@ -0,0 +1,89 @@ +{ + "whitelist": [ + "metamask.io", + "myetherwallet.com", + "myetheroll.com", + "myetherapi.com", + "ledgerwallet.com" + ], + "blacklist": [ + "wallet-ethereum.net", + "myelherwallel.com", + "etherswap.org", + "eos.ac", + "uasfwallet.com", + "ziber.io", + "mvetherwallet.com", + "etherswap.org", + "myethewallet.net", + "multiply-ethereum.info", + "bittrex.comze.com", + "karbon.vacau.com", + "xn--myetherwallt-7db.com", + "xn--myetherwallt-leb.com", + "etherdelta.gitlhub.io", + "etherdelta.glthub.io", + "myethewallet.net", + "myetherwillet.com", + "digitaldevelopersfund.vacau.com", + "myetherwallel.com", + "myeltherwallet.com", + "myelherwallet.com", + "wwwmyetherwallet.com", + "myethermwallet.com", + "district-0x.io", + "coin-dash.com", + "coindash.ru", + "myethervallet.com", + "myetherwallet.com.gl", + "myetherwallet.com.ua", + "myÄ—therwallet.com", + "myetherwallet.com.gl", + "xn--mytherwallet-fvb.com", + "district0x.net", + "aragonproject.io", + "coin-wallet.info", + "coinswallet.info", + "contribute-status.im", + "secure-myetherwallet.com", + "update-myetherwallet.com", + "ether-api.com", + "ether-wall.com", + "mycoinwallet.net", + "etherclassicwallet.com", + "ethereumchamber.com", + "ethereumchamber.net", + "ethereumchest.com", + "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", + "omg-omise.co", + "omise-go.com", + "tenx-tech.com", + "tokensale-tenx.tech", + "ubiqcoin.org", + "metamask.com" + ] +} \ No newline at end of file diff --git a/src/detector.js b/src/detector.js new file mode 100644 index 0000000..01d5eee --- /dev/null +++ b/src/detector.js @@ -0,0 +1,73 @@ +const levenshtein = require('fast-levenshtein') +const DEFAULT_TOLERANCE = 4 + +class PhishingDetector { + + constructor (opts) { + this.blacklist = processDomainList(opts.blacklist || []) + this.whitelist = processDomainList(opts.whitelist || []) + this.tolerance = ('tolerance' in opts) ? opts.tolerance : DEFAULT_TOLERANCE + } + + check (domain) { + const source = domainToParts(domain) + + // 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 } + + // check if near-match of whitelist domain, FAIL + const fuzzyForm = domainPartsToFuzzyForm(source) + const levenshteinMatched = this.whitelist.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 } + } + + // matched nothing, PASS + return { type: 'all', result: false } + } + +} + +module.exports = PhishingDetector + +// util + +function processDomainList (list) { + return list.map(domainToParts) +} + +function domainToParts (domain) { + return domain.split('.').reverse() +} + +function domainPartsToDomain(domainParts) { + return domainParts.slice().reverse().join('.') +} + +// for fuzzy search, drop TLD and re-stringify +function domainPartsToFuzzyForm(domainParts) { + return domainParts.slice(1).reverse().join('.') +} + +// match the target parts, ignoring extra subdomains on source +// source: [io, metamask, xyz] +// target: [io, metamask] +// result: PASS +function matchPartsAgainstList(source, list) { + return list.some((target) => { + // target domain has more parts than source, fail + if (target.length > source.length) return false + // source matches target or (is deeper subdomain) + return target.every((part, index) => source[index] === part) + }) +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1f61ed8 --- /dev/null +++ b/src/index.js @@ -0,0 +1,11 @@ +const PhishingDetector = require('./detector') +const config = require('./config.json') + +const detector = new PhishingDetector(config) + +module.exports = checkDomain + + +function checkDomain(domain) { + return detector.check(domain).result +} \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..ad52998 --- /dev/null +++ b/test/index.js @@ -0,0 +1,186 @@ +const test = require('tape') +const PhishingDetector = require('../src/detector') +const config = require('../src/config.json') + +const detector = new PhishingDetector(config) + + +test('basic test', (t) => { + + // blacklist + + testDomain(t, { + domain: 'metamask.com', + type: 'blacklist', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwaillet.com', + type: 'blacklist', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwaller.com', + type: 'blacklist', + expected: true, + }) + + testDomain(t, { + domain: 'myetherweb.com.de', + type: 'blacklist', + expected: true, + }) + + testDomain(t, { + domain: 'myeterwallet.com', + type: 'blacklist', + expected: true, + }) + + // whitelist + + testDomain(t, { + domain: 'ledgerwallet.com', + type: 'whitelist', + expected: false, + }) + + // whitelist subdomains + + testDomain(t, { + domain: 'www.metamask.io', + type: 'whitelist', + expected: false, + }) + + testDomain(t, { + domain: 'faucet.metamask.io', + type: 'whitelist', + expected: false, + }) + + testDomain(t, { + domain: 'zero.metamask.io', + type: 'whitelist', + expected: false, + }) + + testDomain(t, { + domain: 'zero-faucet.metamask.io', + type: 'whitelist', + expected: false, + }) + + // fuzzy + + testDomain(t, { + domain: 'metmask.io', + type: 'fuzzy', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwallet.cx', + type: 'fuzzy', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwallet.aaa', + type: 'fuzzy', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwallet.za', + type: 'fuzzy', + expected: true, + }) + + testDomain(t, { + domain: 'myetherwallet.z', + type: 'fuzzy', + expected: true, + }) + + // no match + + testDomain(t, { + domain: 'example.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'etherscan.io', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'ethereum.org', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'etherid.org', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'ether.cards', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'easyeth.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'etherdomain.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'ethnews.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'cryptocompare.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'kraken.com', + type: 'all', + expected: false, + }) + + testDomain(t, { + domain: 'myetherwallet.groovehq.com', + type: 'all', + expected: false, + }) + + t.end() +}) + +function testDomain(t, { domain, type, expected }) { + const value = detector.check(domain) + if (value.type === 'fuzzy') { + t.comment(`"${domain}" fuzzy matches against "${value.match}"`) + } + t.equal(value.type, type, `type: "${domain}" should be "${type}"`) + t.equal(value.result, expected, `result: "${domain}" should be match "${expected}"`) +} \ No newline at end of file