@metamask-previews/phishing-controller 12.0.2-preview-367cb1da → 12.0.2-preview-cf09c0a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PhishingController.cjs +453 -0
- package/dist/PhishingController.cjs.map +1 -0
- package/dist/{types/PhishingController.d.ts → PhishingController.d.cts} +4 -4
- package/dist/PhishingController.d.cts.map +1 -0
- package/dist/PhishingController.d.mts +304 -0
- package/dist/PhishingController.d.mts.map +1 -0
- package/dist/PhishingController.mjs +448 -37
- package/dist/PhishingController.mjs.map +1 -1
- package/dist/PhishingDetector.cjs +233 -0
- package/dist/PhishingDetector.cjs.map +1 -0
- package/dist/{types/PhishingDetector.d.ts → PhishingDetector.d.cts} +2 -2
- package/dist/PhishingDetector.d.cts.map +1 -0
- package/dist/PhishingDetector.d.mts +64 -0
- package/dist/PhishingDetector.d.mts.map +1 -0
- package/dist/PhishingDetector.mjs +227 -7
- package/dist/PhishingDetector.mjs.map +1 -1
- package/dist/index.cjs +23 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -41
- package/dist/index.mjs.map +1 -1
- package/dist/tests/utils.cjs +24 -0
- package/dist/tests/utils.cjs.map +1 -0
- package/dist/{types/tests/utils.d.ts → tests/utils.d.cts} +1 -1
- package/dist/tests/utils.d.cts.map +1 -0
- package/dist/tests/utils.d.mts +11 -0
- package/dist/tests/utils.d.mts.map +1 -0
- package/dist/tests/utils.mjs +18 -14
- package/dist/tests/utils.mjs.map +1 -1
- package/dist/types.cjs +40 -0
- package/dist/types.cjs.map +1 -0
- package/dist/{types/types.d.ts → types.d.cts} +1 -1
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +50 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +36 -7
- package/dist/types.mjs.map +1 -1
- package/dist/utils.cjs +242 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/{types/utils.d.ts → utils.d.cts} +4 -4
- package/dist/utils.d.cts.map +1 -0
- package/dist/utils.d.mts +119 -0
- package/dist/utils.d.mts.map +1 -0
- package/dist/utils.mjs +224 -31
- package/dist/utils.mjs.map +1 -1
- package/package.json +12 -7
- package/dist/PhishingController.js +0 -39
- package/dist/PhishingController.js.map +0 -1
- package/dist/PhishingDetector.js +0 -9
- package/dist/PhishingDetector.js.map +0 -1
- package/dist/chunk-5NDIXQG5.js +0 -16
- package/dist/chunk-5NDIXQG5.js.map +0 -1
- package/dist/chunk-G2RL74NS.mjs +0 -16
- package/dist/chunk-G2RL74NS.mjs.map +0 -1
- package/dist/chunk-I3U4U3MW.mjs +0 -812
- package/dist/chunk-I3U4U3MW.mjs.map +0 -1
- package/dist/chunk-XUI43LEZ.mjs +0 -30
- package/dist/chunk-XUI43LEZ.mjs.map +0 -1
- package/dist/chunk-Z4BLTVTB.js +0 -30
- package/dist/chunk-Z4BLTVTB.js.map +0 -1
- package/dist/chunk-ZAOBCAQT.js +0 -812
- package/dist/chunk-ZAOBCAQT.js.map +0 -1
- package/dist/index.js +0 -42
- package/dist/index.js.map +0 -1
- package/dist/tests/utils.js +0 -16
- package/dist/tests/utils.js.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/dist/types/PhishingController.d.ts.map +0 -1
- package/dist/types/PhishingDetector.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -5
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/tests/utils.d.ts.map +0 -1
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/utils.d.ts.map +0 -1
- package/dist/types.js +0 -8
- package/dist/types.js.map +0 -1
- package/dist/utils.js +0 -33
- package/dist/utils.js.map +0 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
3
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
4
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
5
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
6
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
7
|
+
};
|
|
8
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
|
+
};
|
|
13
|
+
var _PhishingDetector_instances, _PhishingDetector_configs, _PhishingDetector_legacyConfig, _PhishingDetector_check;
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.PhishingDetector = void 0;
|
|
16
|
+
const fastest_levenshtein_1 = require("fastest-levenshtein");
|
|
17
|
+
const types_1 = require("./types.cjs");
|
|
18
|
+
const utils_1 = require("./utils.cjs");
|
|
19
|
+
class PhishingDetector {
|
|
20
|
+
/**
|
|
21
|
+
* Construct a phishing detector, which can check whether origins are known
|
|
22
|
+
* to be malicious or similar to common phishing targets.
|
|
23
|
+
*
|
|
24
|
+
* A list of configurations is accepted. Each origin checked is processed
|
|
25
|
+
* using each configuration in sequence, so the order defines which
|
|
26
|
+
* configurations take precedence.
|
|
27
|
+
*
|
|
28
|
+
* @param opts - Phishing detection options
|
|
29
|
+
*/
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
_PhishingDetector_instances.add(this);
|
|
32
|
+
_PhishingDetector_configs.set(this, void 0);
|
|
33
|
+
_PhishingDetector_legacyConfig.set(this, void 0);
|
|
34
|
+
// recommended configuration
|
|
35
|
+
if (Array.isArray(opts)) {
|
|
36
|
+
__classPrivateFieldSet(this, _PhishingDetector_configs, (0, utils_1.processConfigs)(opts), "f");
|
|
37
|
+
__classPrivateFieldSet(this, _PhishingDetector_legacyConfig, false, "f");
|
|
38
|
+
// legacy configuration
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
__classPrivateFieldSet(this, _PhishingDetector_configs, [
|
|
42
|
+
(0, utils_1.getDefaultPhishingDetectorConfig)({
|
|
43
|
+
allowlist: opts.whitelist,
|
|
44
|
+
blocklist: opts.blacklist,
|
|
45
|
+
c2DomainBlocklist: opts.c2DomainBlocklist,
|
|
46
|
+
fuzzylist: opts.fuzzylist,
|
|
47
|
+
tolerance: opts.tolerance,
|
|
48
|
+
}),
|
|
49
|
+
], "f");
|
|
50
|
+
__classPrivateFieldSet(this, _PhishingDetector_legacyConfig, true, "f");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a url is known to be malicious or similar to a common phishing
|
|
55
|
+
* target. This will check the hostname and IPFS CID that is sometimes
|
|
56
|
+
* located in the path.
|
|
57
|
+
*
|
|
58
|
+
* @param url - The url to check.
|
|
59
|
+
* @returns The result of the check.
|
|
60
|
+
*/
|
|
61
|
+
check(url) {
|
|
62
|
+
const result = __classPrivateFieldGet(this, _PhishingDetector_instances, "m", _PhishingDetector_check).call(this, url);
|
|
63
|
+
if (__classPrivateFieldGet(this, _PhishingDetector_legacyConfig, "f")) {
|
|
64
|
+
let legacyType = result.type;
|
|
65
|
+
if (legacyType === types_1.PhishingDetectorResultType.Allowlist) {
|
|
66
|
+
legacyType = types_1.PhishingDetectorResultType.Whitelist;
|
|
67
|
+
}
|
|
68
|
+
else if (legacyType === types_1.PhishingDetectorResultType.Blocklist) {
|
|
69
|
+
legacyType = types_1.PhishingDetectorResultType.Blacklist;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
match: result.match,
|
|
73
|
+
result: result.result,
|
|
74
|
+
type: legacyType,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Checks if a URL is blocked against the hashed request blocklist.
|
|
81
|
+
* This is done by hashing the URL's hostname and checking it against the hashed request blocklist.
|
|
82
|
+
*
|
|
83
|
+
*
|
|
84
|
+
* @param urlString - The URL to check.
|
|
85
|
+
* @returns An object indicating if the URL is blocked and relevant metadata.
|
|
86
|
+
*/
|
|
87
|
+
isMaliciousC2Domain(urlString) {
|
|
88
|
+
let hostname;
|
|
89
|
+
try {
|
|
90
|
+
hostname = new URL(urlString).hostname;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
result: false,
|
|
95
|
+
type: types_1.PhishingDetectorResultType.C2DomainBlocklist,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const fqdn = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;
|
|
99
|
+
const source = (0, utils_1.domainToParts)(fqdn);
|
|
100
|
+
for (const { allowlist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
101
|
+
// if source matches allowlist hostname (or subdomain thereof), PASS
|
|
102
|
+
const allowlistMatch = (0, utils_1.matchPartsAgainstList)(source, allowlist);
|
|
103
|
+
if (allowlistMatch) {
|
|
104
|
+
const match = (0, utils_1.domainPartsToDomain)(allowlistMatch);
|
|
105
|
+
return {
|
|
106
|
+
match,
|
|
107
|
+
name,
|
|
108
|
+
result: false,
|
|
109
|
+
type: types_1.PhishingDetectorResultType.Allowlist,
|
|
110
|
+
version: version === undefined ? version : String(version),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const { c2DomainBlocklist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
115
|
+
const hash = (0, utils_1.sha256Hash)(hostname.toLowerCase());
|
|
116
|
+
const blocked = c2DomainBlocklist?.includes(hash) ?? false;
|
|
117
|
+
if (blocked) {
|
|
118
|
+
return {
|
|
119
|
+
name,
|
|
120
|
+
result: true,
|
|
121
|
+
type: types_1.PhishingDetectorResultType.C2DomainBlocklist,
|
|
122
|
+
version: version === undefined ? version : String(version),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// did not match, PASS
|
|
127
|
+
return {
|
|
128
|
+
result: false,
|
|
129
|
+
type: types_1.PhishingDetectorResultType.C2DomainBlocklist,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.PhishingDetector = PhishingDetector;
|
|
134
|
+
_PhishingDetector_configs = new WeakMap(), _PhishingDetector_legacyConfig = new WeakMap(), _PhishingDetector_instances = new WeakSet(), _PhishingDetector_check = function _PhishingDetector_check(url) {
|
|
135
|
+
const ipfsCidMatch = url.match(ipfsCidRegex());
|
|
136
|
+
// Check for IPFS CID related blocklist entries
|
|
137
|
+
if (ipfsCidMatch !== null) {
|
|
138
|
+
// there is a cID string somewhere
|
|
139
|
+
// Determine if any of the entries are ipfs cids
|
|
140
|
+
// Depending on the gateway, the CID is in the path OR a subdomain, so we do a regex match on it all
|
|
141
|
+
const cID = ipfsCidMatch[0];
|
|
142
|
+
for (const { blocklist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
143
|
+
const blocklistMatch = blocklist
|
|
144
|
+
.filter((entries) => entries.length === 1)
|
|
145
|
+
.find((entries) => {
|
|
146
|
+
return entries[0] === cID;
|
|
147
|
+
});
|
|
148
|
+
if (blocklistMatch) {
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
match: cID,
|
|
152
|
+
result: true,
|
|
153
|
+
type: types_1.PhishingDetectorResultType.Blocklist,
|
|
154
|
+
version: version === undefined ? version : String(version),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let domain;
|
|
160
|
+
try {
|
|
161
|
+
domain = new URL(url).hostname;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
result: false,
|
|
166
|
+
type: types_1.PhishingDetectorResultType.All,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const fqdn = domain.endsWith('.') ? domain.slice(0, -1) : domain;
|
|
170
|
+
const source = (0, utils_1.domainToParts)(fqdn);
|
|
171
|
+
for (const { allowlist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
172
|
+
// if source matches allowlist hostname (or subdomain thereof), PASS
|
|
173
|
+
const allowlistMatch = (0, utils_1.matchPartsAgainstList)(source, allowlist);
|
|
174
|
+
if (allowlistMatch) {
|
|
175
|
+
const match = (0, utils_1.domainPartsToDomain)(allowlistMatch);
|
|
176
|
+
return {
|
|
177
|
+
match,
|
|
178
|
+
name,
|
|
179
|
+
result: false,
|
|
180
|
+
type: types_1.PhishingDetectorResultType.Allowlist,
|
|
181
|
+
version: version === undefined ? version : String(version),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const { blocklist, fuzzylist, name, tolerance, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
186
|
+
// if source matches blocklist hostname (or subdomain thereof), FAIL
|
|
187
|
+
const blocklistMatch = (0, utils_1.matchPartsAgainstList)(source, blocklist);
|
|
188
|
+
if (blocklistMatch) {
|
|
189
|
+
const match = (0, utils_1.domainPartsToDomain)(blocklistMatch);
|
|
190
|
+
return {
|
|
191
|
+
match,
|
|
192
|
+
name,
|
|
193
|
+
result: true,
|
|
194
|
+
type: types_1.PhishingDetectorResultType.Blocklist,
|
|
195
|
+
version: version === undefined ? version : String(version),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (tolerance > 0) {
|
|
199
|
+
// check if near-match of whitelist domain, FAIL
|
|
200
|
+
let fuzzyForm = (0, utils_1.domainPartsToFuzzyForm)(source);
|
|
201
|
+
// strip www
|
|
202
|
+
fuzzyForm = fuzzyForm.replace(/^www\./u, '');
|
|
203
|
+
// check against fuzzylist
|
|
204
|
+
const levenshteinMatched = fuzzylist.find((targetParts) => {
|
|
205
|
+
const fuzzyTarget = (0, utils_1.domainPartsToFuzzyForm)(targetParts);
|
|
206
|
+
const dist = (0, fastest_levenshtein_1.distance)(fuzzyForm, fuzzyTarget);
|
|
207
|
+
return dist <= tolerance;
|
|
208
|
+
});
|
|
209
|
+
if (levenshteinMatched) {
|
|
210
|
+
const match = (0, utils_1.domainPartsToDomain)(levenshteinMatched);
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
match,
|
|
214
|
+
result: true,
|
|
215
|
+
type: types_1.PhishingDetectorResultType.Fuzzy,
|
|
216
|
+
version: version === undefined ? version : String(version),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// matched nothing, PASS
|
|
222
|
+
return { result: false, type: types_1.PhishingDetectorResultType.All };
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Runs a regex match to determine if a string is a IPFS CID
|
|
226
|
+
* @returns Regex string for IPFS CID
|
|
227
|
+
*/
|
|
228
|
+
function ipfsCidRegex() {
|
|
229
|
+
// regex from https://stackoverflow.com/a/67176726
|
|
230
|
+
const reg = 'Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}';
|
|
231
|
+
return new RegExp(reg, 'u');
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=PhishingDetector.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhishingDetector.cjs","sourceRoot":"","sources":["../src/PhishingDetector.ts"],"names":[],"mappings":"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","sourcesContent":["import { distance } from 'fastest-levenshtein';\n\nimport {\n PhishingDetectorResultType,\n type PhishingDetectorResult,\n} from './types';\nimport {\n domainPartsToDomain,\n domainPartsToFuzzyForm,\n domainToParts,\n getDefaultPhishingDetectorConfig,\n matchPartsAgainstList,\n processConfigs,\n sha256Hash,\n} from './utils';\n\nexport type LegacyPhishingDetectorList = {\n whitelist?: string[];\n blacklist?: string[];\n c2DomainBlocklist?: string[];\n} & FuzzyTolerance;\n\nexport type PhishingDetectorList = {\n allowlist?: string[];\n blocklist?: string[];\n c2DomainBlocklist?: string[];\n name?: string;\n version?: string | number;\n tolerance?: number;\n} & FuzzyTolerance;\n\nexport type FuzzyTolerance =\n | {\n tolerance?: number;\n fuzzylist: string[];\n }\n | {\n tolerance?: never;\n fuzzylist?: never;\n };\n\nexport type PhishingDetectorOptions =\n | LegacyPhishingDetectorList\n | PhishingDetectorList[];\n\nexport type PhishingDetectorConfiguration = {\n name?: string;\n version?: number | string;\n allowlist: string[][];\n blocklist: string[][];\n c2DomainBlocklist?: string[];\n fuzzylist: string[][];\n tolerance: number;\n};\n\nexport class PhishingDetector {\n #configs: PhishingDetectorConfiguration[];\n\n #legacyConfig: boolean;\n\n /**\n * Construct a phishing detector, which can check whether origins are known\n * to be malicious or similar to common phishing targets.\n *\n * A list of configurations is accepted. Each origin checked is processed\n * using each configuration in sequence, so the order defines which\n * configurations take precedence.\n *\n * @param opts - Phishing detection options\n */\n constructor(opts: PhishingDetectorOptions) {\n // recommended configuration\n if (Array.isArray(opts)) {\n this.#configs = processConfigs(opts);\n this.#legacyConfig = false;\n // legacy configuration\n } else {\n this.#configs = [\n getDefaultPhishingDetectorConfig({\n allowlist: opts.whitelist,\n blocklist: opts.blacklist,\n c2DomainBlocklist: opts.c2DomainBlocklist,\n fuzzylist: opts.fuzzylist,\n tolerance: opts.tolerance,\n }),\n ];\n this.#legacyConfig = true;\n }\n }\n\n /**\n * Check if a url is known to be malicious or similar to a common phishing\n * target. This will check the hostname and IPFS CID that is sometimes\n * located in the path.\n *\n * @param url - The url to check.\n * @returns The result of the check.\n */\n check(url: string): PhishingDetectorResult {\n const result = this.#check(url);\n\n if (this.#legacyConfig) {\n let legacyType = result.type;\n if (legacyType === PhishingDetectorResultType.Allowlist) {\n legacyType = PhishingDetectorResultType.Whitelist;\n } else if (legacyType === PhishingDetectorResultType.Blocklist) {\n legacyType = PhishingDetectorResultType.Blacklist;\n }\n return {\n match: result.match,\n result: result.result,\n type: legacyType,\n };\n }\n return result;\n }\n\n #check(url: string): PhishingDetectorResult {\n const ipfsCidMatch = url.match(ipfsCidRegex());\n\n // Check for IPFS CID related blocklist entries\n if (ipfsCidMatch !== null) {\n // there is a cID string somewhere\n // Determine if any of the entries are ipfs cids\n // Depending on the gateway, the CID is in the path OR a subdomain, so we do a regex match on it all\n const cID = ipfsCidMatch[0];\n for (const { blocklist, name, version } of this.#configs) {\n const blocklistMatch = blocklist\n .filter((entries) => entries.length === 1)\n .find((entries) => {\n return entries[0] === cID;\n });\n if (blocklistMatch) {\n return {\n name,\n match: cID,\n result: true,\n type: PhishingDetectorResultType.Blocklist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n }\n\n let domain;\n try {\n domain = new URL(url).hostname;\n } catch (error) {\n return {\n result: false,\n type: PhishingDetectorResultType.All,\n };\n }\n\n const fqdn = domain.endsWith('.') ? domain.slice(0, -1) : domain;\n\n const source = domainToParts(fqdn);\n\n for (const { allowlist, name, version } of this.#configs) {\n // if source matches allowlist hostname (or subdomain thereof), PASS\n const allowlistMatch = matchPartsAgainstList(source, allowlist);\n if (allowlistMatch) {\n const match = domainPartsToDomain(allowlistMatch);\n return {\n match,\n name,\n result: false,\n type: PhishingDetectorResultType.Allowlist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n\n for (const { blocklist, fuzzylist, name, tolerance, version } of this\n .#configs) {\n // if source matches blocklist hostname (or subdomain thereof), FAIL\n const blocklistMatch = matchPartsAgainstList(source, blocklist);\n if (blocklistMatch) {\n const match = domainPartsToDomain(blocklistMatch);\n return {\n match,\n name,\n result: true,\n type: PhishingDetectorResultType.Blocklist,\n version: version === undefined ? version : String(version),\n };\n }\n\n if (tolerance > 0) {\n // check if near-match of whitelist domain, FAIL\n let fuzzyForm = domainPartsToFuzzyForm(source);\n // strip www\n fuzzyForm = fuzzyForm.replace(/^www\\./u, '');\n // check against fuzzylist\n const levenshteinMatched = fuzzylist.find((targetParts) => {\n const fuzzyTarget = domainPartsToFuzzyForm(targetParts);\n const dist = distance(fuzzyForm, fuzzyTarget);\n return dist <= tolerance;\n });\n if (levenshteinMatched) {\n const match = domainPartsToDomain(levenshteinMatched);\n return {\n name,\n match,\n result: true,\n type: PhishingDetectorResultType.Fuzzy,\n version: version === undefined ? version : String(version),\n };\n }\n }\n }\n\n // matched nothing, PASS\n return { result: false, type: PhishingDetectorResultType.All };\n }\n\n /**\n * Checks if a URL is blocked against the hashed request blocklist.\n * This is done by hashing the URL's hostname and checking it against the hashed request blocklist.\n *\n *\n * @param urlString - The URL to check.\n * @returns An object indicating if the URL is blocked and relevant metadata.\n */\n isMaliciousC2Domain(urlString: string): PhishingDetectorResult {\n let hostname;\n try {\n hostname = new URL(urlString).hostname;\n } catch (error) {\n return {\n result: false,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n };\n }\n\n const fqdn = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;\n\n const source = domainToParts(fqdn);\n\n for (const { allowlist, name, version } of this.#configs) {\n // if source matches allowlist hostname (or subdomain thereof), PASS\n const allowlistMatch = matchPartsAgainstList(source, allowlist);\n if (allowlistMatch) {\n const match = domainPartsToDomain(allowlistMatch);\n return {\n match,\n name,\n result: false,\n type: PhishingDetectorResultType.Allowlist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n\n for (const { c2DomainBlocklist, name, version } of this.#configs) {\n const hash = sha256Hash(hostname.toLowerCase());\n const blocked = c2DomainBlocklist?.includes(hash) ?? false;\n\n if (blocked) {\n return {\n name,\n result: true,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n // did not match, PASS\n return {\n result: false,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n };\n }\n}\n\n/**\n * Runs a regex match to determine if a string is a IPFS CID\n * @returns Regex string for IPFS CID\n */\nfunction ipfsCidRegex() {\n // regex from https://stackoverflow.com/a/67176726\n const reg =\n 'Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}';\n return new RegExp(reg, 'u');\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PhishingDetectorResult } from
|
|
1
|
+
import { type PhishingDetectorResult } from "./types.cjs";
|
|
2
2
|
export type LegacyPhishingDetectorList = {
|
|
3
3
|
whitelist?: string[];
|
|
4
4
|
blacklist?: string[];
|
|
@@ -61,4 +61,4 @@ export declare class PhishingDetector {
|
|
|
61
61
|
*/
|
|
62
62
|
isMaliciousC2Domain(urlString: string): PhishingDetectorResult;
|
|
63
63
|
}
|
|
64
|
-
//# sourceMappingURL=PhishingDetector.d.
|
|
64
|
+
//# sourceMappingURL=PhishingDetector.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhishingDetector.d.cts","sourceRoot":"","sources":["../src/PhishingDetector.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,sBAAsB,EAC5B,oBAAgB;AAWjB,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,GAAG,cAAc,CAAC;AAEnB,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,cAAc,CAAC;AAEnB,MAAM,MAAM,cAAc,GACtB;IACE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,GACD;IACE,SAAS,CAAC,EAAE,KAAK,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,CAAC;AAEN,MAAM,MAAM,uBAAuB,GAC/B,0BAA0B,GAC1B,oBAAoB,EAAE,CAAC;AAE3B,MAAM,MAAM,6BAA6B,GAAG;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,gBAAgB;;IAK3B;;;;;;;;;OASG;gBACS,IAAI,EAAE,uBAAuB;IAoBzC;;;;;;;OAOG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB;IAsH1C;;;;;;;OAOG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,sBAAsB;CAiD/D"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type PhishingDetectorResult } from "./types.mjs";
|
|
2
|
+
export type LegacyPhishingDetectorList = {
|
|
3
|
+
whitelist?: string[];
|
|
4
|
+
blacklist?: string[];
|
|
5
|
+
c2DomainBlocklist?: string[];
|
|
6
|
+
} & FuzzyTolerance;
|
|
7
|
+
export type PhishingDetectorList = {
|
|
8
|
+
allowlist?: string[];
|
|
9
|
+
blocklist?: string[];
|
|
10
|
+
c2DomainBlocklist?: string[];
|
|
11
|
+
name?: string;
|
|
12
|
+
version?: string | number;
|
|
13
|
+
tolerance?: number;
|
|
14
|
+
} & FuzzyTolerance;
|
|
15
|
+
export type FuzzyTolerance = {
|
|
16
|
+
tolerance?: number;
|
|
17
|
+
fuzzylist: string[];
|
|
18
|
+
} | {
|
|
19
|
+
tolerance?: never;
|
|
20
|
+
fuzzylist?: never;
|
|
21
|
+
};
|
|
22
|
+
export type PhishingDetectorOptions = LegacyPhishingDetectorList | PhishingDetectorList[];
|
|
23
|
+
export type PhishingDetectorConfiguration = {
|
|
24
|
+
name?: string;
|
|
25
|
+
version?: number | string;
|
|
26
|
+
allowlist: string[][];
|
|
27
|
+
blocklist: string[][];
|
|
28
|
+
c2DomainBlocklist?: string[];
|
|
29
|
+
fuzzylist: string[][];
|
|
30
|
+
tolerance: number;
|
|
31
|
+
};
|
|
32
|
+
export declare class PhishingDetector {
|
|
33
|
+
#private;
|
|
34
|
+
/**
|
|
35
|
+
* Construct a phishing detector, which can check whether origins are known
|
|
36
|
+
* to be malicious or similar to common phishing targets.
|
|
37
|
+
*
|
|
38
|
+
* A list of configurations is accepted. Each origin checked is processed
|
|
39
|
+
* using each configuration in sequence, so the order defines which
|
|
40
|
+
* configurations take precedence.
|
|
41
|
+
*
|
|
42
|
+
* @param opts - Phishing detection options
|
|
43
|
+
*/
|
|
44
|
+
constructor(opts: PhishingDetectorOptions);
|
|
45
|
+
/**
|
|
46
|
+
* Check if a url is known to be malicious or similar to a common phishing
|
|
47
|
+
* target. This will check the hostname and IPFS CID that is sometimes
|
|
48
|
+
* located in the path.
|
|
49
|
+
*
|
|
50
|
+
* @param url - The url to check.
|
|
51
|
+
* @returns The result of the check.
|
|
52
|
+
*/
|
|
53
|
+
check(url: string): PhishingDetectorResult;
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a URL is blocked against the hashed request blocklist.
|
|
56
|
+
* This is done by hashing the URL's hostname and checking it against the hashed request blocklist.
|
|
57
|
+
*
|
|
58
|
+
*
|
|
59
|
+
* @param urlString - The URL to check.
|
|
60
|
+
* @returns An object indicating if the URL is blocked and relevant metadata.
|
|
61
|
+
*/
|
|
62
|
+
isMaliciousC2Domain(urlString: string): PhishingDetectorResult;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=PhishingDetector.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhishingDetector.d.mts","sourceRoot":"","sources":["../src/PhishingDetector.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,sBAAsB,EAC5B,oBAAgB;AAWjB,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,GAAG,cAAc,CAAC;AAEnB,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,cAAc,CAAC;AAEnB,MAAM,MAAM,cAAc,GACtB;IACE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,GACD;IACE,SAAS,CAAC,EAAE,KAAK,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,CAAC;AAEN,MAAM,MAAM,uBAAuB,GAC/B,0BAA0B,GAC1B,oBAAoB,EAAE,CAAC;AAE3B,MAAM,MAAM,6BAA6B,GAAG;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,gBAAgB;;IAK3B;;;;;;;;;OASG;gBACS,IAAI,EAAE,uBAAuB;IAoBzC;;;;;;;OAOG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,sBAAsB;IAsH1C;;;;;;;OAOG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,sBAAsB;CAiD/D"}
|
|
@@ -1,9 +1,229 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
PhishingDetector
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
8
6
|
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _PhishingDetector_instances, _PhishingDetector_configs, _PhishingDetector_legacyConfig, _PhishingDetector_check;
|
|
13
|
+
import { distance } from "fastest-levenshtein";
|
|
14
|
+
import { PhishingDetectorResultType } from "./types.mjs";
|
|
15
|
+
import { domainPartsToDomain, domainPartsToFuzzyForm, domainToParts, getDefaultPhishingDetectorConfig, matchPartsAgainstList, processConfigs, sha256Hash } from "./utils.mjs";
|
|
16
|
+
export class PhishingDetector {
|
|
17
|
+
/**
|
|
18
|
+
* Construct a phishing detector, which can check whether origins are known
|
|
19
|
+
* to be malicious or similar to common phishing targets.
|
|
20
|
+
*
|
|
21
|
+
* A list of configurations is accepted. Each origin checked is processed
|
|
22
|
+
* using each configuration in sequence, so the order defines which
|
|
23
|
+
* configurations take precedence.
|
|
24
|
+
*
|
|
25
|
+
* @param opts - Phishing detection options
|
|
26
|
+
*/
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
_PhishingDetector_instances.add(this);
|
|
29
|
+
_PhishingDetector_configs.set(this, void 0);
|
|
30
|
+
_PhishingDetector_legacyConfig.set(this, void 0);
|
|
31
|
+
// recommended configuration
|
|
32
|
+
if (Array.isArray(opts)) {
|
|
33
|
+
__classPrivateFieldSet(this, _PhishingDetector_configs, processConfigs(opts), "f");
|
|
34
|
+
__classPrivateFieldSet(this, _PhishingDetector_legacyConfig, false, "f");
|
|
35
|
+
// legacy configuration
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
__classPrivateFieldSet(this, _PhishingDetector_configs, [
|
|
39
|
+
getDefaultPhishingDetectorConfig({
|
|
40
|
+
allowlist: opts.whitelist,
|
|
41
|
+
blocklist: opts.blacklist,
|
|
42
|
+
c2DomainBlocklist: opts.c2DomainBlocklist,
|
|
43
|
+
fuzzylist: opts.fuzzylist,
|
|
44
|
+
tolerance: opts.tolerance,
|
|
45
|
+
}),
|
|
46
|
+
], "f");
|
|
47
|
+
__classPrivateFieldSet(this, _PhishingDetector_legacyConfig, true, "f");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a url is known to be malicious or similar to a common phishing
|
|
52
|
+
* target. This will check the hostname and IPFS CID that is sometimes
|
|
53
|
+
* located in the path.
|
|
54
|
+
*
|
|
55
|
+
* @param url - The url to check.
|
|
56
|
+
* @returns The result of the check.
|
|
57
|
+
*/
|
|
58
|
+
check(url) {
|
|
59
|
+
const result = __classPrivateFieldGet(this, _PhishingDetector_instances, "m", _PhishingDetector_check).call(this, url);
|
|
60
|
+
if (__classPrivateFieldGet(this, _PhishingDetector_legacyConfig, "f")) {
|
|
61
|
+
let legacyType = result.type;
|
|
62
|
+
if (legacyType === PhishingDetectorResultType.Allowlist) {
|
|
63
|
+
legacyType = PhishingDetectorResultType.Whitelist;
|
|
64
|
+
}
|
|
65
|
+
else if (legacyType === PhishingDetectorResultType.Blocklist) {
|
|
66
|
+
legacyType = PhishingDetectorResultType.Blacklist;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
match: result.match,
|
|
70
|
+
result: result.result,
|
|
71
|
+
type: legacyType,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a URL is blocked against the hashed request blocklist.
|
|
78
|
+
* This is done by hashing the URL's hostname and checking it against the hashed request blocklist.
|
|
79
|
+
*
|
|
80
|
+
*
|
|
81
|
+
* @param urlString - The URL to check.
|
|
82
|
+
* @returns An object indicating if the URL is blocked and relevant metadata.
|
|
83
|
+
*/
|
|
84
|
+
isMaliciousC2Domain(urlString) {
|
|
85
|
+
let hostname;
|
|
86
|
+
try {
|
|
87
|
+
hostname = new URL(urlString).hostname;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
result: false,
|
|
92
|
+
type: PhishingDetectorResultType.C2DomainBlocklist,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const fqdn = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;
|
|
96
|
+
const source = domainToParts(fqdn);
|
|
97
|
+
for (const { allowlist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
98
|
+
// if source matches allowlist hostname (or subdomain thereof), PASS
|
|
99
|
+
const allowlistMatch = matchPartsAgainstList(source, allowlist);
|
|
100
|
+
if (allowlistMatch) {
|
|
101
|
+
const match = domainPartsToDomain(allowlistMatch);
|
|
102
|
+
return {
|
|
103
|
+
match,
|
|
104
|
+
name,
|
|
105
|
+
result: false,
|
|
106
|
+
type: PhishingDetectorResultType.Allowlist,
|
|
107
|
+
version: version === undefined ? version : String(version),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const { c2DomainBlocklist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
112
|
+
const hash = sha256Hash(hostname.toLowerCase());
|
|
113
|
+
const blocked = c2DomainBlocklist?.includes(hash) ?? false;
|
|
114
|
+
if (blocked) {
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
result: true,
|
|
118
|
+
type: PhishingDetectorResultType.C2DomainBlocklist,
|
|
119
|
+
version: version === undefined ? version : String(version),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// did not match, PASS
|
|
124
|
+
return {
|
|
125
|
+
result: false,
|
|
126
|
+
type: PhishingDetectorResultType.C2DomainBlocklist,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
_PhishingDetector_configs = new WeakMap(), _PhishingDetector_legacyConfig = new WeakMap(), _PhishingDetector_instances = new WeakSet(), _PhishingDetector_check = function _PhishingDetector_check(url) {
|
|
131
|
+
const ipfsCidMatch = url.match(ipfsCidRegex());
|
|
132
|
+
// Check for IPFS CID related blocklist entries
|
|
133
|
+
if (ipfsCidMatch !== null) {
|
|
134
|
+
// there is a cID string somewhere
|
|
135
|
+
// Determine if any of the entries are ipfs cids
|
|
136
|
+
// Depending on the gateway, the CID is in the path OR a subdomain, so we do a regex match on it all
|
|
137
|
+
const cID = ipfsCidMatch[0];
|
|
138
|
+
for (const { blocklist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
139
|
+
const blocklistMatch = blocklist
|
|
140
|
+
.filter((entries) => entries.length === 1)
|
|
141
|
+
.find((entries) => {
|
|
142
|
+
return entries[0] === cID;
|
|
143
|
+
});
|
|
144
|
+
if (blocklistMatch) {
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
match: cID,
|
|
148
|
+
result: true,
|
|
149
|
+
type: PhishingDetectorResultType.Blocklist,
|
|
150
|
+
version: version === undefined ? version : String(version),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let domain;
|
|
156
|
+
try {
|
|
157
|
+
domain = new URL(url).hostname;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
result: false,
|
|
162
|
+
type: PhishingDetectorResultType.All,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const fqdn = domain.endsWith('.') ? domain.slice(0, -1) : domain;
|
|
166
|
+
const source = domainToParts(fqdn);
|
|
167
|
+
for (const { allowlist, name, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
168
|
+
// if source matches allowlist hostname (or subdomain thereof), PASS
|
|
169
|
+
const allowlistMatch = matchPartsAgainstList(source, allowlist);
|
|
170
|
+
if (allowlistMatch) {
|
|
171
|
+
const match = domainPartsToDomain(allowlistMatch);
|
|
172
|
+
return {
|
|
173
|
+
match,
|
|
174
|
+
name,
|
|
175
|
+
result: false,
|
|
176
|
+
type: PhishingDetectorResultType.Allowlist,
|
|
177
|
+
version: version === undefined ? version : String(version),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const { blocklist, fuzzylist, name, tolerance, version } of __classPrivateFieldGet(this, _PhishingDetector_configs, "f")) {
|
|
182
|
+
// if source matches blocklist hostname (or subdomain thereof), FAIL
|
|
183
|
+
const blocklistMatch = matchPartsAgainstList(source, blocklist);
|
|
184
|
+
if (blocklistMatch) {
|
|
185
|
+
const match = domainPartsToDomain(blocklistMatch);
|
|
186
|
+
return {
|
|
187
|
+
match,
|
|
188
|
+
name,
|
|
189
|
+
result: true,
|
|
190
|
+
type: PhishingDetectorResultType.Blocklist,
|
|
191
|
+
version: version === undefined ? version : String(version),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (tolerance > 0) {
|
|
195
|
+
// check if near-match of whitelist domain, FAIL
|
|
196
|
+
let fuzzyForm = domainPartsToFuzzyForm(source);
|
|
197
|
+
// strip www
|
|
198
|
+
fuzzyForm = fuzzyForm.replace(/^www\./u, '');
|
|
199
|
+
// check against fuzzylist
|
|
200
|
+
const levenshteinMatched = fuzzylist.find((targetParts) => {
|
|
201
|
+
const fuzzyTarget = domainPartsToFuzzyForm(targetParts);
|
|
202
|
+
const dist = distance(fuzzyForm, fuzzyTarget);
|
|
203
|
+
return dist <= tolerance;
|
|
204
|
+
});
|
|
205
|
+
if (levenshteinMatched) {
|
|
206
|
+
const match = domainPartsToDomain(levenshteinMatched);
|
|
207
|
+
return {
|
|
208
|
+
name,
|
|
209
|
+
match,
|
|
210
|
+
result: true,
|
|
211
|
+
type: PhishingDetectorResultType.Fuzzy,
|
|
212
|
+
version: version === undefined ? version : String(version),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// matched nothing, PASS
|
|
218
|
+
return { result: false, type: PhishingDetectorResultType.All };
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Runs a regex match to determine if a string is a IPFS CID
|
|
222
|
+
* @returns Regex string for IPFS CID
|
|
223
|
+
*/
|
|
224
|
+
function ipfsCidRegex() {
|
|
225
|
+
// regex from https://stackoverflow.com/a/67176726
|
|
226
|
+
const reg = 'Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}';
|
|
227
|
+
return new RegExp(reg, 'u');
|
|
228
|
+
}
|
|
9
229
|
//# sourceMappingURL=PhishingDetector.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"file":"PhishingDetector.mjs","sourceRoot":"","sources":["../src/PhishingDetector.ts"],"names":[],"mappings":"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","sourcesContent":["import { distance } from 'fastest-levenshtein';\n\nimport {\n PhishingDetectorResultType,\n type PhishingDetectorResult,\n} from './types';\nimport {\n domainPartsToDomain,\n domainPartsToFuzzyForm,\n domainToParts,\n getDefaultPhishingDetectorConfig,\n matchPartsAgainstList,\n processConfigs,\n sha256Hash,\n} from './utils';\n\nexport type LegacyPhishingDetectorList = {\n whitelist?: string[];\n blacklist?: string[];\n c2DomainBlocklist?: string[];\n} & FuzzyTolerance;\n\nexport type PhishingDetectorList = {\n allowlist?: string[];\n blocklist?: string[];\n c2DomainBlocklist?: string[];\n name?: string;\n version?: string | number;\n tolerance?: number;\n} & FuzzyTolerance;\n\nexport type FuzzyTolerance =\n | {\n tolerance?: number;\n fuzzylist: string[];\n }\n | {\n tolerance?: never;\n fuzzylist?: never;\n };\n\nexport type PhishingDetectorOptions =\n | LegacyPhishingDetectorList\n | PhishingDetectorList[];\n\nexport type PhishingDetectorConfiguration = {\n name?: string;\n version?: number | string;\n allowlist: string[][];\n blocklist: string[][];\n c2DomainBlocklist?: string[];\n fuzzylist: string[][];\n tolerance: number;\n};\n\nexport class PhishingDetector {\n #configs: PhishingDetectorConfiguration[];\n\n #legacyConfig: boolean;\n\n /**\n * Construct a phishing detector, which can check whether origins are known\n * to be malicious or similar to common phishing targets.\n *\n * A list of configurations is accepted. Each origin checked is processed\n * using each configuration in sequence, so the order defines which\n * configurations take precedence.\n *\n * @param opts - Phishing detection options\n */\n constructor(opts: PhishingDetectorOptions) {\n // recommended configuration\n if (Array.isArray(opts)) {\n this.#configs = processConfigs(opts);\n this.#legacyConfig = false;\n // legacy configuration\n } else {\n this.#configs = [\n getDefaultPhishingDetectorConfig({\n allowlist: opts.whitelist,\n blocklist: opts.blacklist,\n c2DomainBlocklist: opts.c2DomainBlocklist,\n fuzzylist: opts.fuzzylist,\n tolerance: opts.tolerance,\n }),\n ];\n this.#legacyConfig = true;\n }\n }\n\n /**\n * Check if a url is known to be malicious or similar to a common phishing\n * target. This will check the hostname and IPFS CID that is sometimes\n * located in the path.\n *\n * @param url - The url to check.\n * @returns The result of the check.\n */\n check(url: string): PhishingDetectorResult {\n const result = this.#check(url);\n\n if (this.#legacyConfig) {\n let legacyType = result.type;\n if (legacyType === PhishingDetectorResultType.Allowlist) {\n legacyType = PhishingDetectorResultType.Whitelist;\n } else if (legacyType === PhishingDetectorResultType.Blocklist) {\n legacyType = PhishingDetectorResultType.Blacklist;\n }\n return {\n match: result.match,\n result: result.result,\n type: legacyType,\n };\n }\n return result;\n }\n\n #check(url: string): PhishingDetectorResult {\n const ipfsCidMatch = url.match(ipfsCidRegex());\n\n // Check for IPFS CID related blocklist entries\n if (ipfsCidMatch !== null) {\n // there is a cID string somewhere\n // Determine if any of the entries are ipfs cids\n // Depending on the gateway, the CID is in the path OR a subdomain, so we do a regex match on it all\n const cID = ipfsCidMatch[0];\n for (const { blocklist, name, version } of this.#configs) {\n const blocklistMatch = blocklist\n .filter((entries) => entries.length === 1)\n .find((entries) => {\n return entries[0] === cID;\n });\n if (blocklistMatch) {\n return {\n name,\n match: cID,\n result: true,\n type: PhishingDetectorResultType.Blocklist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n }\n\n let domain;\n try {\n domain = new URL(url).hostname;\n } catch (error) {\n return {\n result: false,\n type: PhishingDetectorResultType.All,\n };\n }\n\n const fqdn = domain.endsWith('.') ? domain.slice(0, -1) : domain;\n\n const source = domainToParts(fqdn);\n\n for (const { allowlist, name, version } of this.#configs) {\n // if source matches allowlist hostname (or subdomain thereof), PASS\n const allowlistMatch = matchPartsAgainstList(source, allowlist);\n if (allowlistMatch) {\n const match = domainPartsToDomain(allowlistMatch);\n return {\n match,\n name,\n result: false,\n type: PhishingDetectorResultType.Allowlist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n\n for (const { blocklist, fuzzylist, name, tolerance, version } of this\n .#configs) {\n // if source matches blocklist hostname (or subdomain thereof), FAIL\n const blocklistMatch = matchPartsAgainstList(source, blocklist);\n if (blocklistMatch) {\n const match = domainPartsToDomain(blocklistMatch);\n return {\n match,\n name,\n result: true,\n type: PhishingDetectorResultType.Blocklist,\n version: version === undefined ? version : String(version),\n };\n }\n\n if (tolerance > 0) {\n // check if near-match of whitelist domain, FAIL\n let fuzzyForm = domainPartsToFuzzyForm(source);\n // strip www\n fuzzyForm = fuzzyForm.replace(/^www\\./u, '');\n // check against fuzzylist\n const levenshteinMatched = fuzzylist.find((targetParts) => {\n const fuzzyTarget = domainPartsToFuzzyForm(targetParts);\n const dist = distance(fuzzyForm, fuzzyTarget);\n return dist <= tolerance;\n });\n if (levenshteinMatched) {\n const match = domainPartsToDomain(levenshteinMatched);\n return {\n name,\n match,\n result: true,\n type: PhishingDetectorResultType.Fuzzy,\n version: version === undefined ? version : String(version),\n };\n }\n }\n }\n\n // matched nothing, PASS\n return { result: false, type: PhishingDetectorResultType.All };\n }\n\n /**\n * Checks if a URL is blocked against the hashed request blocklist.\n * This is done by hashing the URL's hostname and checking it against the hashed request blocklist.\n *\n *\n * @param urlString - The URL to check.\n * @returns An object indicating if the URL is blocked and relevant metadata.\n */\n isMaliciousC2Domain(urlString: string): PhishingDetectorResult {\n let hostname;\n try {\n hostname = new URL(urlString).hostname;\n } catch (error) {\n return {\n result: false,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n };\n }\n\n const fqdn = hostname.endsWith('.') ? hostname.slice(0, -1) : hostname;\n\n const source = domainToParts(fqdn);\n\n for (const { allowlist, name, version } of this.#configs) {\n // if source matches allowlist hostname (or subdomain thereof), PASS\n const allowlistMatch = matchPartsAgainstList(source, allowlist);\n if (allowlistMatch) {\n const match = domainPartsToDomain(allowlistMatch);\n return {\n match,\n name,\n result: false,\n type: PhishingDetectorResultType.Allowlist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n\n for (const { c2DomainBlocklist, name, version } of this.#configs) {\n const hash = sha256Hash(hostname.toLowerCase());\n const blocked = c2DomainBlocklist?.includes(hash) ?? false;\n\n if (blocked) {\n return {\n name,\n result: true,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n version: version === undefined ? version : String(version),\n };\n }\n }\n // did not match, PASS\n return {\n result: false,\n type: PhishingDetectorResultType.C2DomainBlocklist,\n };\n }\n}\n\n/**\n * Runs a regex match to determine if a string is a IPFS CID\n * @returns Regex string for IPFS CID\n */\nfunction ipfsCidRegex() {\n // regex from https://stackoverflow.com/a/67176726\n const reg =\n 'Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,}';\n return new RegExp(reg, 'u');\n}\n"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.PhishingDetectorResultType = exports.PhishingDetector = void 0;
|
|
18
|
+
__exportStar(require("./PhishingController.cjs"), exports);
|
|
19
|
+
var PhishingDetector_1 = require("./PhishingDetector.cjs");
|
|
20
|
+
Object.defineProperty(exports, "PhishingDetector", { enumerable: true, get: function () { return PhishingDetector_1.PhishingDetector; } });
|
|
21
|
+
var types_1 = require("./types.cjs");
|
|
22
|
+
Object.defineProperty(exports, "PhishingDetectorResultType", { enumerable: true, get: function () { return types_1.PhishingDetectorResultType; } });
|
|
23
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2DAAqC;AAQrC,2DAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,qCAAqD;AAA5C,mHAAA,0BAA0B,OAAA","sourcesContent":["export * from './PhishingController';\nexport type {\n LegacyPhishingDetectorList,\n PhishingDetectorList,\n FuzzyTolerance,\n PhishingDetectorOptions,\n PhishingDetectorConfiguration,\n} from './PhishingDetector';\nexport { PhishingDetector } from './PhishingDetector';\nexport { PhishingDetectorResultType } from './types';\n"]}
|