@metamask-previews/phishing-controller 17.0.0-preview-17c76be24 → 17.0.0-preview-3ce0062c4
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/CHANGELOG.md +1 -19
- package/dist/PhishingController.cjs +286 -302
- package/dist/PhishingController.cjs.map +1 -1
- package/dist/PhishingController.d.cts +26 -27
- package/dist/PhishingController.d.cts.map +1 -1
- package/dist/PhishingController.d.mts +26 -27
- package/dist/PhishingController.d.mts.map +1 -1
- package/dist/PhishingController.mjs +286 -302
- package/dist/PhishingController.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +0 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -4
- package/dist/PhishingController-method-action-types.cjs +0 -7
- package/dist/PhishingController-method-action-types.cjs.map +0 -1
- package/dist/PhishingController-method-action-types.d.cts +0 -108
- package/dist/PhishingController-method-action-types.d.cts.map +0 -1
- package/dist/PhishingController-method-action-types.d.mts +0 -108
- package/dist/PhishingController-method-action-types.d.mts.map +0 -1
- package/dist/PhishingController-method-action-types.mjs +0 -6
- package/dist/PhishingController-method-action-types.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,29 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- Expose missing public `PhishingController` methods through its messenger ([#8269](https://github.com/MetaMask/core/pull/8269))
|
|
13
|
-
- The following actions are now available:
|
|
14
|
-
- `PhishingController:bypass`
|
|
15
|
-
- `PhishingController:isBlockedRequest`
|
|
16
|
-
- `PhishingController:scanUrl`
|
|
17
|
-
- Corresponding action types (e.g. `PhishingControllerBypassAction`) are available as well.
|
|
18
|
-
|
|
19
10
|
### Changed
|
|
20
11
|
|
|
21
|
-
- Deprecate `test` method in favor of `testOrigin` ([#8269](https://github.com/MetaMask/core/pull/8269))
|
|
22
|
-
- The `test` method is now renamed to `testOrigin` to better reflect its purpose of testing a domain origin for phishing.
|
|
23
|
-
- The old `test` method is still present but is now marked as deprecated and will be removed in a future release.
|
|
24
|
-
- Deprecate action types in favor of `PhishingController...Action` types ([#8269](https://github.com/MetaMask/core/pull/8269))
|
|
25
|
-
- The following action types have been renamed:
|
|
26
|
-
- `TestOrigin` is now `PhishingControllerTestOriginAction`.
|
|
27
|
-
- `MaybeUpdateState` is now `PhishingControllerMaybeUpdateStateAction`.
|
|
28
|
-
- The old types are still exported but are now marked as deprecated and will
|
|
29
|
-
be removed in a future release.
|
|
30
12
|
- `PhishingController` no longer advances `c2DomainBlocklistLastFetched` when the C2 domain blocklist fetch fails, allowing the blocklist to be retried on the next update cycle ([#8250](https://github.com/MetaMask/core/pull/8250))
|
|
31
13
|
- Reduce default cache TTL for `DEFAULT_URL_SCAN_CACHE_TTL`, `DEFAULT_TOKEN_SCAN_CACHE_TTL`, and `DEFAULT_ADDRESS_SCAN_CACHE_TTL` from 15 minutes to 1 minute ([#8254](https://github.com/MetaMask/core/pull/8254))
|
|
32
|
-
- Bump `@metamask/transaction-controller` from `^63.0.0` to `^63.
|
|
14
|
+
- Bump `@metamask/transaction-controller` from `^63.0.0` to `^63.1.0` ([#8272](https://github.com/MetaMask/core/pull/8272))
|
|
33
15
|
|
|
34
16
|
## [17.0.0]
|
|
35
17
|
|
|
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
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
11
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
12
|
};
|
|
13
|
-
var _PhishingController_instances, _PhishingController_detector, _PhishingController_stalelistRefreshInterval, _PhishingController_hotlistRefreshInterval, _PhishingController_c2DomainBlocklistRefreshInterval, _PhishingController_urlScanCache, _PhishingController_tokenScanCache, _PhishingController_addressScanCache, _PhishingController_inProgressHotlistUpdate, _PhishingController_inProgressStalelistUpdate, _PhishingController_isProgressC2DomainBlocklistUpdate, _PhishingController_transactionControllerStateChangeHandler, _PhishingController_subscribeToTransactionControllerStateChange, _PhishingController_isTransactionPatch, _PhishingController_isSimulationDataPatch, _PhishingController_onTransactionControllerStateChange, _PhishingController_getTokensFromTransaction, _PhishingController_scanTokensByChain, _PhishingController_fetchTokenScanBulkResults, _PhishingController_processBatch, _PhishingController_updateStalelist, _PhishingController_updateHotlist, _PhishingController_updateC2DomainBlocklist, _PhishingController_queryConfig;
|
|
13
|
+
var _PhishingController_instances, _PhishingController_detector, _PhishingController_stalelistRefreshInterval, _PhishingController_hotlistRefreshInterval, _PhishingController_c2DomainBlocklistRefreshInterval, _PhishingController_urlScanCache, _PhishingController_tokenScanCache, _PhishingController_addressScanCache, _PhishingController_inProgressHotlistUpdate, _PhishingController_inProgressStalelistUpdate, _PhishingController_isProgressC2DomainBlocklistUpdate, _PhishingController_transactionControllerStateChangeHandler, _PhishingController_subscribeToTransactionControllerStateChange, _PhishingController_registerMessageHandlers, _PhishingController_isTransactionPatch, _PhishingController_isSimulationDataPatch, _PhishingController_onTransactionControllerStateChange, _PhishingController_getTokensFromTransaction, _PhishingController_scanTokensByChain, _PhishingController_fetchTokenScanBulkResults, _PhishingController_processBatch, _PhishingController_updateStalelist, _PhishingController_updateHotlist, _PhishingController_updateC2DomainBlocklist, _PhishingController_queryConfig;
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.PhishingController = exports.phishingListKeyNameMap = exports.ListNames = exports.ListKeys = exports.C2_DOMAIN_BLOCKLIST_URL = exports.METAMASK_HOTLIST_DIFF_URL = exports.METAMASK_STALELIST_URL = exports.STALELIST_REFRESH_INTERVAL = exports.HOTLIST_REFRESH_INTERVAL = exports.C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL = exports.DEFAULT_ADDRESS_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_ADDRESS_SCAN_CACHE_TTL = exports.DEFAULT_TOKEN_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_TOKEN_SCAN_CACHE_TTL = exports.DEFAULT_URL_SCAN_CACHE_MAX_SIZE = exports.DEFAULT_URL_SCAN_CACHE_TTL = exports.ADDRESS_SCAN_ENDPOINT = exports.TOKEN_BULK_SCANNING_ENDPOINT = exports.SECURITY_ALERTS_BASE_URL = exports.PHISHING_DETECTION_BULK_SCAN_ENDPOINT = exports.PHISHING_DETECTION_SCAN_ENDPOINT = exports.PHISHING_DETECTION_BASE_URL = exports.C2_DOMAIN_BLOCKLIST_ENDPOINT = exports.CLIENT_SIDE_DETECION_BASE_URL = exports.METAMASK_HOTLIST_DIFF_FILE = exports.METAMASK_STALELIST_FILE = exports.PHISHING_CONFIG_BASE_URL = void 0;
|
|
16
16
|
const base_controller_1 = require("@metamask/base-controller");
|
|
@@ -149,16 +149,6 @@ const getDefaultState = () => {
|
|
|
149
149
|
addressScanCache: {},
|
|
150
150
|
};
|
|
151
151
|
};
|
|
152
|
-
const MESSENGER_EXPOSED_METHODS = [
|
|
153
|
-
'maybeUpdateState',
|
|
154
|
-
'testOrigin',
|
|
155
|
-
'isBlockedRequest',
|
|
156
|
-
'bypass',
|
|
157
|
-
'scanUrl',
|
|
158
|
-
'bulkScanUrls',
|
|
159
|
-
'bulkScanTokens',
|
|
160
|
-
'scanAddress',
|
|
161
|
-
];
|
|
162
152
|
/**
|
|
163
153
|
* Controller that manages community-maintained lists of approved and unapproved website origins.
|
|
164
154
|
*/
|
|
@@ -204,17 +194,151 @@ class PhishingController extends base_controller_1.BaseController {
|
|
|
204
194
|
_PhishingController_isProgressC2DomainBlocklistUpdate.set(this, void 0);
|
|
205
195
|
_PhishingController_transactionControllerStateChangeHandler.set(this, void 0);
|
|
206
196
|
/**
|
|
207
|
-
*
|
|
197
|
+
* Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports
|
|
198
|
+
* web URLs.
|
|
208
199
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
200
|
+
* @param url - The URL to scan.
|
|
201
|
+
* @returns The phishing detection scan result.
|
|
202
|
+
*/
|
|
203
|
+
this.scanUrl = async (url) => {
|
|
204
|
+
const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
|
|
205
|
+
if (!ok) {
|
|
206
|
+
return {
|
|
207
|
+
hostname: '',
|
|
208
|
+
recommendedAction: types_1.RecommendedAction.None,
|
|
209
|
+
fetchError: 'url is not a valid web URL',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
|
|
213
|
+
if (cachedResult) {
|
|
214
|
+
return cachedResult;
|
|
215
|
+
}
|
|
216
|
+
const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
|
|
217
|
+
const res = await fetch(`${exports.PHISHING_DETECTION_BASE_URL}/${exports.PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(hostname)}`, {
|
|
218
|
+
method: 'GET',
|
|
219
|
+
headers: {
|
|
220
|
+
Accept: 'application/json',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
return {
|
|
225
|
+
error: `${res.status} ${res.statusText}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
return data;
|
|
230
|
+
}, true, 8000);
|
|
231
|
+
// Need to do it this way because safelyExecuteWithTimeout returns undefined for both timeouts and errors.
|
|
232
|
+
if (!apiResponse) {
|
|
233
|
+
return {
|
|
234
|
+
hostname: '',
|
|
235
|
+
recommendedAction: types_1.RecommendedAction.None,
|
|
236
|
+
fetchError: 'timeout of 8000ms exceeded',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else if ('error' in apiResponse) {
|
|
240
|
+
return {
|
|
241
|
+
hostname: '',
|
|
242
|
+
recommendedAction: types_1.RecommendedAction.None,
|
|
243
|
+
fetchError: apiResponse.error,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const result = {
|
|
247
|
+
hostname,
|
|
248
|
+
recommendedAction: apiResponse.recommendedAction,
|
|
249
|
+
};
|
|
250
|
+
__classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
|
|
251
|
+
return result;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Scan multiple URLs for phishing in bulk. It will only scan the hostnames of the URLs.
|
|
255
|
+
* It also only supports web URLs.
|
|
212
256
|
*
|
|
213
|
-
* @param
|
|
214
|
-
* @returns
|
|
215
|
-
* @deprecated Use {@link testOrigin} instead. This method is exposed for backward compatibility and will be removed in a future release.
|
|
257
|
+
* @param urls - The URLs to scan.
|
|
258
|
+
* @returns A mapping of URLs to their phishing detection scan results and errors.
|
|
216
259
|
*/
|
|
217
|
-
this.
|
|
260
|
+
this.bulkScanUrls = async (urls) => {
|
|
261
|
+
if (!urls || urls.length === 0) {
|
|
262
|
+
return {
|
|
263
|
+
results: {},
|
|
264
|
+
errors: {},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// we are arbitrarily limiting the number of URLs to 250
|
|
268
|
+
const MAX_TOTAL_URLS = 250;
|
|
269
|
+
if (urls.length > MAX_TOTAL_URLS) {
|
|
270
|
+
return {
|
|
271
|
+
results: {},
|
|
272
|
+
errors: {
|
|
273
|
+
too_many_urls: [
|
|
274
|
+
`Maximum of ${MAX_TOTAL_URLS} URLs allowed per request`,
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const MAX_URL_LENGTH = 2048;
|
|
280
|
+
const combinedResponse = {
|
|
281
|
+
results: {},
|
|
282
|
+
errors: {},
|
|
283
|
+
};
|
|
284
|
+
// Extract hostnames from URLs and check for validity and length constraints
|
|
285
|
+
const urlsToHostnames = {};
|
|
286
|
+
const urlsToFetch = [];
|
|
287
|
+
for (const url of urls) {
|
|
288
|
+
if (url.length > MAX_URL_LENGTH) {
|
|
289
|
+
combinedResponse.errors[url] = [
|
|
290
|
+
`URL length must not exceed ${MAX_URL_LENGTH} characters`,
|
|
291
|
+
];
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
|
|
295
|
+
if (!ok) {
|
|
296
|
+
combinedResponse.errors[url] = ['url is not a valid web URL'];
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Check if result is already in cache
|
|
300
|
+
const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
|
|
301
|
+
if (cachedResult) {
|
|
302
|
+
// Use cached result
|
|
303
|
+
combinedResponse.results[url] = cachedResult;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// Add to list of URLs to fetch
|
|
307
|
+
urlsToHostnames[url] = hostname;
|
|
308
|
+
urlsToFetch.push(url);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// If there are URLs to fetch, process them in batches
|
|
312
|
+
if (urlsToFetch.length > 0) {
|
|
313
|
+
// The API has a limit of 50 URLs per request, so we batch the requests
|
|
314
|
+
const MAX_URLS_PER_BATCH = 50;
|
|
315
|
+
const batches = [];
|
|
316
|
+
for (let i = 0; i < urlsToFetch.length; i += MAX_URLS_PER_BATCH) {
|
|
317
|
+
batches.push(urlsToFetch.slice(i, i + MAX_URLS_PER_BATCH));
|
|
318
|
+
}
|
|
319
|
+
// Process each batch in parallel
|
|
320
|
+
const batchResults = await Promise.all(batches.map((batchUrls) => __classPrivateFieldGet(this, _PhishingController_processBatch, "f").call(this, batchUrls)));
|
|
321
|
+
// Merge results and errors from all batches
|
|
322
|
+
batchResults.forEach((batchResponse) => {
|
|
323
|
+
// Add results to cache and combine with response
|
|
324
|
+
Object.entries(batchResponse.results).forEach(([url, result]) => {
|
|
325
|
+
const hostname = urlsToHostnames[url];
|
|
326
|
+
if (hostname) {
|
|
327
|
+
__classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
|
|
328
|
+
}
|
|
329
|
+
combinedResponse.results[url] = result;
|
|
330
|
+
});
|
|
331
|
+
// Combine errors
|
|
332
|
+
Object.entries(batchResponse.errors).forEach(([key, messages]) => {
|
|
333
|
+
combinedResponse.errors[key] = [
|
|
334
|
+
...(combinedResponse.errors[key] || []),
|
|
335
|
+
...messages,
|
|
336
|
+
];
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return combinedResponse;
|
|
341
|
+
};
|
|
218
342
|
/**
|
|
219
343
|
* Fetch bulk token scan results from the security alerts API.
|
|
220
344
|
*
|
|
@@ -258,6 +382,141 @@ class PhishingController extends base_controller_1.BaseController {
|
|
|
258
382
|
}
|
|
259
383
|
return apiResponse;
|
|
260
384
|
});
|
|
385
|
+
/**
|
|
386
|
+
* Scan an address for security alerts.
|
|
387
|
+
*
|
|
388
|
+
* @param chainId - The chain ID in hex format (e.g., '0x1' for Ethereum).
|
|
389
|
+
* @param address - The address to scan.
|
|
390
|
+
* @returns The address scan result.
|
|
391
|
+
*/
|
|
392
|
+
this.scanAddress = async (chainId, address) => {
|
|
393
|
+
if (!address || !chainId) {
|
|
394
|
+
return {
|
|
395
|
+
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
396
|
+
label: '',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const normalizedChainId = chainId.toLowerCase();
|
|
400
|
+
const normalizedAddress = address.toLowerCase();
|
|
401
|
+
const chain = (0, utils_1.resolveChainName)(normalizedChainId);
|
|
402
|
+
if (!chain) {
|
|
403
|
+
return {
|
|
404
|
+
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
405
|
+
label: '',
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress);
|
|
409
|
+
const cachedResult = __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").get(cacheKey);
|
|
410
|
+
if (cachedResult) {
|
|
411
|
+
return {
|
|
412
|
+
result_type: cachedResult.result_type,
|
|
413
|
+
label: cachedResult.label,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
|
|
417
|
+
const res = await fetch(`${exports.SECURITY_ALERTS_BASE_URL}${exports.ADDRESS_SCAN_ENDPOINT}`, {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: {
|
|
420
|
+
Accept: 'application/json',
|
|
421
|
+
'Content-Type': 'application/json',
|
|
422
|
+
},
|
|
423
|
+
body: JSON.stringify({
|
|
424
|
+
chain,
|
|
425
|
+
address: normalizedAddress,
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
if (!res.ok) {
|
|
429
|
+
return {
|
|
430
|
+
error: `${res.status} ${res.statusText}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const data = await res.json();
|
|
434
|
+
return data;
|
|
435
|
+
}, true, 5000);
|
|
436
|
+
if (!apiResponse) {
|
|
437
|
+
return {
|
|
438
|
+
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
439
|
+
label: '',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
else if ('error' in apiResponse) {
|
|
443
|
+
return {
|
|
444
|
+
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
445
|
+
label: '',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const result = {
|
|
449
|
+
result_type: apiResponse.result_type,
|
|
450
|
+
label: apiResponse.label,
|
|
451
|
+
};
|
|
452
|
+
__classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").set(cacheKey, result);
|
|
453
|
+
return {
|
|
454
|
+
result_type: apiResponse.result_type,
|
|
455
|
+
label: apiResponse.label,
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Scan multiple tokens for malicious activity in bulk.
|
|
460
|
+
*
|
|
461
|
+
* @param request - The bulk scan request containing chainId and tokens.
|
|
462
|
+
* @param request.chainId - The chain identifier. Accepts a hex chain ID for
|
|
463
|
+
* EVM chains (e.g. `'0x1'` for Ethereum) or a chain name for non-EVM chains
|
|
464
|
+
* (e.g. `'solana'`).
|
|
465
|
+
* @param request.tokens - Array of token addresses to scan.
|
|
466
|
+
* @returns A mapping of token addresses to their scan results. For EVM chains,
|
|
467
|
+
* addresses are lowercased; for non-EVM chains, original casing is preserved.
|
|
468
|
+
* Tokens that fail to scan are omitted.
|
|
469
|
+
*/
|
|
470
|
+
this.bulkScanTokens = async (request) => {
|
|
471
|
+
const { chainId, tokens } = request;
|
|
472
|
+
if (!tokens || tokens.length === 0) {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
const MAX_TOKENS_PER_REQUEST = 100;
|
|
476
|
+
if (tokens.length > MAX_TOKENS_PER_REQUEST) {
|
|
477
|
+
console.warn(`Maximum of ${MAX_TOKENS_PER_REQUEST} tokens allowed per request`);
|
|
478
|
+
return {};
|
|
479
|
+
}
|
|
480
|
+
const normalizedChainId = chainId.toLowerCase();
|
|
481
|
+
const chain = (0, utils_1.resolveChainName)(normalizedChainId);
|
|
482
|
+
if (!chain) {
|
|
483
|
+
console.warn(`Unknown chain ID: ${chainId}`);
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
// EVM addresses are case-insensitive; non-EVM addresses (e.g. Solana
|
|
487
|
+
// base58) are case-sensitive and must not be lowercased.
|
|
488
|
+
const caseSensitive = !normalizedChainId.startsWith('0x');
|
|
489
|
+
// Split tokens into cached results and tokens that need to be fetched
|
|
490
|
+
const { cachedResults, tokensToFetch } = (0, utils_1.splitCacheHits)(__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f"), normalizedChainId, tokens, caseSensitive);
|
|
491
|
+
const results = { ...cachedResults };
|
|
492
|
+
// If there are tokens to fetch, call the bulk token scan API
|
|
493
|
+
if (tokensToFetch.length > 0) {
|
|
494
|
+
const apiResponse = await __classPrivateFieldGet(this, _PhishingController_fetchTokenScanBulkResults, "f").call(this, chain, tokensToFetch);
|
|
495
|
+
if (apiResponse?.results) {
|
|
496
|
+
// Process API results and update cache
|
|
497
|
+
for (const tokenAddress of tokensToFetch) {
|
|
498
|
+
const normalizedAddress = caseSensitive
|
|
499
|
+
? tokenAddress
|
|
500
|
+
: tokenAddress.toLowerCase();
|
|
501
|
+
const tokenResult = apiResponse.results[normalizedAddress];
|
|
502
|
+
if (tokenResult?.result_type) {
|
|
503
|
+
const result = {
|
|
504
|
+
result_type: tokenResult.result_type,
|
|
505
|
+
chain: tokenResult.chain || normalizedChainId,
|
|
506
|
+
address: tokenResult.address || normalizedAddress,
|
|
507
|
+
};
|
|
508
|
+
// Update cache
|
|
509
|
+
const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress, caseSensitive);
|
|
510
|
+
__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f").set(cacheKey, {
|
|
511
|
+
result_type: tokenResult.result_type,
|
|
512
|
+
});
|
|
513
|
+
results[normalizedAddress] = result;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return results;
|
|
519
|
+
};
|
|
261
520
|
/**
|
|
262
521
|
* Process a batch of URLs (up to 50) for phishing detection.
|
|
263
522
|
*
|
|
@@ -340,7 +599,7 @@ class PhishingController extends base_controller_1.BaseController {
|
|
|
340
599
|
});
|
|
341
600
|
},
|
|
342
601
|
}), "f");
|
|
343
|
-
|
|
602
|
+
__classPrivateFieldGet(this, _PhishingController_instances, "m", _PhishingController_registerMessageHandlers).call(this);
|
|
344
603
|
this.updatePhishingDetector();
|
|
345
604
|
__classPrivateFieldGet(this, _PhishingController_instances, "m", _PhishingController_subscribeToTransactionControllerStateChange).call(this);
|
|
346
605
|
}
|
|
@@ -410,7 +669,7 @@ class PhishingController extends base_controller_1.BaseController {
|
|
|
410
669
|
* @param origin - Domain origin of a website.
|
|
411
670
|
* @returns Whether the origin is an unapproved origin.
|
|
412
671
|
*/
|
|
413
|
-
|
|
672
|
+
test(origin) {
|
|
414
673
|
const punycodeOrigin = (0, punycode_js_1.toASCII)(origin);
|
|
415
674
|
const hostname = (0, utils_1.getHostnameFromUrl)(punycodeOrigin);
|
|
416
675
|
const hostnameWithPaths = hostname + (0, utils_1.getPathnameFromUrl)(origin);
|
|
@@ -524,291 +783,16 @@ class PhishingController extends base_controller_1.BaseController {
|
|
|
524
783
|
__classPrivateFieldSet(this, _PhishingController_inProgressStalelistUpdate, undefined, "f");
|
|
525
784
|
}
|
|
526
785
|
}
|
|
527
|
-
/**
|
|
528
|
-
* Scan a URL for phishing. It will only scan the hostname of the URL. It also only supports
|
|
529
|
-
* web URLs.
|
|
530
|
-
*
|
|
531
|
-
* @param url - The URL to scan.
|
|
532
|
-
* @returns The phishing detection scan result.
|
|
533
|
-
*/
|
|
534
|
-
async scanUrl(url) {
|
|
535
|
-
const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
|
|
536
|
-
if (!ok) {
|
|
537
|
-
return {
|
|
538
|
-
hostname: '',
|
|
539
|
-
recommendedAction: types_1.RecommendedAction.None,
|
|
540
|
-
fetchError: 'url is not a valid web URL',
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
|
|
544
|
-
if (cachedResult) {
|
|
545
|
-
return cachedResult;
|
|
546
|
-
}
|
|
547
|
-
const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
|
|
548
|
-
const res = await fetch(`${exports.PHISHING_DETECTION_BASE_URL}/${exports.PHISHING_DETECTION_SCAN_ENDPOINT}?url=${encodeURIComponent(hostname)}`, {
|
|
549
|
-
method: 'GET',
|
|
550
|
-
headers: {
|
|
551
|
-
Accept: 'application/json',
|
|
552
|
-
},
|
|
553
|
-
});
|
|
554
|
-
if (!res.ok) {
|
|
555
|
-
return {
|
|
556
|
-
error: `${res.status} ${res.statusText}`,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
const data = await res.json();
|
|
560
|
-
return data;
|
|
561
|
-
}, true, 8000);
|
|
562
|
-
// Need to do it this way because safelyExecuteWithTimeout returns undefined for both timeouts and errors.
|
|
563
|
-
if (!apiResponse) {
|
|
564
|
-
return {
|
|
565
|
-
hostname: '',
|
|
566
|
-
recommendedAction: types_1.RecommendedAction.None,
|
|
567
|
-
fetchError: 'timeout of 8000ms exceeded',
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
else if ('error' in apiResponse) {
|
|
571
|
-
return {
|
|
572
|
-
hostname: '',
|
|
573
|
-
recommendedAction: types_1.RecommendedAction.None,
|
|
574
|
-
fetchError: apiResponse.error,
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
const result = {
|
|
578
|
-
hostname,
|
|
579
|
-
recommendedAction: apiResponse.recommendedAction,
|
|
580
|
-
};
|
|
581
|
-
__classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
|
|
582
|
-
return result;
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Scan multiple URLs for phishing in bulk. It will only scan the hostnames of the URLs.
|
|
586
|
-
* It also only supports web URLs.
|
|
587
|
-
*
|
|
588
|
-
* @param urls - The URLs to scan.
|
|
589
|
-
* @returns A mapping of URLs to their phishing detection scan results and errors.
|
|
590
|
-
*/
|
|
591
|
-
async bulkScanUrls(urls) {
|
|
592
|
-
if (!urls || urls.length === 0) {
|
|
593
|
-
return {
|
|
594
|
-
results: {},
|
|
595
|
-
errors: {},
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
// we are arbitrarily limiting the number of URLs to 250
|
|
599
|
-
const MAX_TOTAL_URLS = 250;
|
|
600
|
-
if (urls.length > MAX_TOTAL_URLS) {
|
|
601
|
-
return {
|
|
602
|
-
results: {},
|
|
603
|
-
errors: {
|
|
604
|
-
too_many_urls: [
|
|
605
|
-
`Maximum of ${MAX_TOTAL_URLS} URLs allowed per request`,
|
|
606
|
-
],
|
|
607
|
-
},
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
const MAX_URL_LENGTH = 2048;
|
|
611
|
-
const combinedResponse = {
|
|
612
|
-
results: {},
|
|
613
|
-
errors: {},
|
|
614
|
-
};
|
|
615
|
-
// Extract hostnames from URLs and check for validity and length constraints
|
|
616
|
-
const urlsToHostnames = {};
|
|
617
|
-
const urlsToFetch = [];
|
|
618
|
-
for (const url of urls) {
|
|
619
|
-
if (url.length > MAX_URL_LENGTH) {
|
|
620
|
-
combinedResponse.errors[url] = [
|
|
621
|
-
`URL length must not exceed ${MAX_URL_LENGTH} characters`,
|
|
622
|
-
];
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
const [hostname, ok] = (0, utils_1.getHostnameFromWebUrl)(url);
|
|
626
|
-
if (!ok) {
|
|
627
|
-
combinedResponse.errors[url] = ['url is not a valid web URL'];
|
|
628
|
-
continue;
|
|
629
|
-
}
|
|
630
|
-
// Check if result is already in cache
|
|
631
|
-
const cachedResult = __classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").get(hostname);
|
|
632
|
-
if (cachedResult) {
|
|
633
|
-
// Use cached result
|
|
634
|
-
combinedResponse.results[url] = cachedResult;
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
// Add to list of URLs to fetch
|
|
638
|
-
urlsToHostnames[url] = hostname;
|
|
639
|
-
urlsToFetch.push(url);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// If there are URLs to fetch, process them in batches
|
|
643
|
-
if (urlsToFetch.length > 0) {
|
|
644
|
-
// The API has a limit of 50 URLs per request, so we batch the requests
|
|
645
|
-
const MAX_URLS_PER_BATCH = 50;
|
|
646
|
-
const batches = [];
|
|
647
|
-
for (let i = 0; i < urlsToFetch.length; i += MAX_URLS_PER_BATCH) {
|
|
648
|
-
batches.push(urlsToFetch.slice(i, i + MAX_URLS_PER_BATCH));
|
|
649
|
-
}
|
|
650
|
-
// Process each batch in parallel
|
|
651
|
-
const batchResults = await Promise.all(batches.map((batchUrls) => __classPrivateFieldGet(this, _PhishingController_processBatch, "f").call(this, batchUrls)));
|
|
652
|
-
// Merge results and errors from all batches
|
|
653
|
-
batchResults.forEach((batchResponse) => {
|
|
654
|
-
// Add results to cache and combine with response
|
|
655
|
-
Object.entries(batchResponse.results).forEach(([url, result]) => {
|
|
656
|
-
const hostname = urlsToHostnames[url];
|
|
657
|
-
if (hostname) {
|
|
658
|
-
__classPrivateFieldGet(this, _PhishingController_urlScanCache, "f").set(hostname, result);
|
|
659
|
-
}
|
|
660
|
-
combinedResponse.results[url] = result;
|
|
661
|
-
});
|
|
662
|
-
// Combine errors
|
|
663
|
-
Object.entries(batchResponse.errors).forEach(([key, messages]) => {
|
|
664
|
-
combinedResponse.errors[key] = [
|
|
665
|
-
...(combinedResponse.errors[key] || []),
|
|
666
|
-
...messages,
|
|
667
|
-
];
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
return combinedResponse;
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Scan an address for security alerts.
|
|
675
|
-
*
|
|
676
|
-
* @param chainId - The chain ID in hex format (e.g., '0x1' for Ethereum).
|
|
677
|
-
* @param address - The address to scan.
|
|
678
|
-
* @returns The address scan result.
|
|
679
|
-
*/
|
|
680
|
-
async scanAddress(chainId, address) {
|
|
681
|
-
if (!address || !chainId) {
|
|
682
|
-
return {
|
|
683
|
-
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
684
|
-
label: '',
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
const normalizedChainId = chainId.toLowerCase();
|
|
688
|
-
const normalizedAddress = address.toLowerCase();
|
|
689
|
-
const chain = (0, utils_1.resolveChainName)(normalizedChainId);
|
|
690
|
-
if (!chain) {
|
|
691
|
-
return {
|
|
692
|
-
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
693
|
-
label: '',
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress);
|
|
697
|
-
const cachedResult = __classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").get(cacheKey);
|
|
698
|
-
if (cachedResult) {
|
|
699
|
-
return {
|
|
700
|
-
result_type: cachedResult.result_type,
|
|
701
|
-
label: cachedResult.label,
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const apiResponse = await (0, controller_utils_1.safelyExecuteWithTimeout)(async () => {
|
|
705
|
-
const res = await fetch(`${exports.SECURITY_ALERTS_BASE_URL}${exports.ADDRESS_SCAN_ENDPOINT}`, {
|
|
706
|
-
method: 'POST',
|
|
707
|
-
headers: {
|
|
708
|
-
Accept: 'application/json',
|
|
709
|
-
'Content-Type': 'application/json',
|
|
710
|
-
},
|
|
711
|
-
body: JSON.stringify({
|
|
712
|
-
chain,
|
|
713
|
-
address: normalizedAddress,
|
|
714
|
-
}),
|
|
715
|
-
});
|
|
716
|
-
if (!res.ok) {
|
|
717
|
-
return {
|
|
718
|
-
error: `${res.status} ${res.statusText}`,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
const data = await res.json();
|
|
722
|
-
return data;
|
|
723
|
-
}, true, 5000);
|
|
724
|
-
if (!apiResponse) {
|
|
725
|
-
return {
|
|
726
|
-
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
727
|
-
label: '',
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
else if ('error' in apiResponse) {
|
|
731
|
-
return {
|
|
732
|
-
result_type: types_1.AddressScanResultType.ErrorResult,
|
|
733
|
-
label: '',
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
const result = {
|
|
737
|
-
result_type: apiResponse.result_type,
|
|
738
|
-
label: apiResponse.label,
|
|
739
|
-
};
|
|
740
|
-
__classPrivateFieldGet(this, _PhishingController_addressScanCache, "f").set(cacheKey, result);
|
|
741
|
-
return {
|
|
742
|
-
result_type: apiResponse.result_type,
|
|
743
|
-
label: apiResponse.label,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Scan multiple tokens for malicious activity in bulk.
|
|
748
|
-
*
|
|
749
|
-
* @param request - The bulk scan request containing chainId and tokens.
|
|
750
|
-
* @param request.chainId - The chain identifier. Accepts a hex chain ID for
|
|
751
|
-
* EVM chains (e.g. `'0x1'` for Ethereum) or a chain name for non-EVM chains
|
|
752
|
-
* (e.g. `'solana'`).
|
|
753
|
-
* @param request.tokens - Array of token addresses to scan.
|
|
754
|
-
* @returns A mapping of token addresses to their scan results. For EVM chains,
|
|
755
|
-
* addresses are lowercased; for non-EVM chains, original casing is preserved.
|
|
756
|
-
* Tokens that fail to scan are omitted.
|
|
757
|
-
*/
|
|
758
|
-
async bulkScanTokens(request) {
|
|
759
|
-
const { chainId, tokens } = request;
|
|
760
|
-
if (!tokens || tokens.length === 0) {
|
|
761
|
-
return {};
|
|
762
|
-
}
|
|
763
|
-
const MAX_TOKENS_PER_REQUEST = 100;
|
|
764
|
-
if (tokens.length > MAX_TOKENS_PER_REQUEST) {
|
|
765
|
-
console.warn(`Maximum of ${MAX_TOKENS_PER_REQUEST} tokens allowed per request`);
|
|
766
|
-
return {};
|
|
767
|
-
}
|
|
768
|
-
const normalizedChainId = chainId.toLowerCase();
|
|
769
|
-
const chain = (0, utils_1.resolveChainName)(normalizedChainId);
|
|
770
|
-
if (!chain) {
|
|
771
|
-
console.warn(`Unknown chain ID: ${chainId}`);
|
|
772
|
-
return {};
|
|
773
|
-
}
|
|
774
|
-
// EVM addresses are case-insensitive; non-EVM addresses (e.g. Solana
|
|
775
|
-
// base58) are case-sensitive and must not be lowercased.
|
|
776
|
-
const caseSensitive = !normalizedChainId.startsWith('0x');
|
|
777
|
-
// Split tokens into cached results and tokens that need to be fetched
|
|
778
|
-
const { cachedResults, tokensToFetch } = (0, utils_1.splitCacheHits)(__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f"), normalizedChainId, tokens, caseSensitive);
|
|
779
|
-
const results = { ...cachedResults };
|
|
780
|
-
// If there are tokens to fetch, call the bulk token scan API
|
|
781
|
-
if (tokensToFetch.length > 0) {
|
|
782
|
-
const apiResponse = await __classPrivateFieldGet(this, _PhishingController_fetchTokenScanBulkResults, "f").call(this, chain, tokensToFetch);
|
|
783
|
-
if (apiResponse?.results) {
|
|
784
|
-
// Process API results and update cache
|
|
785
|
-
for (const tokenAddress of tokensToFetch) {
|
|
786
|
-
const normalizedAddress = caseSensitive
|
|
787
|
-
? tokenAddress
|
|
788
|
-
: tokenAddress.toLowerCase();
|
|
789
|
-
const tokenResult = apiResponse.results[normalizedAddress];
|
|
790
|
-
if (tokenResult?.result_type) {
|
|
791
|
-
const result = {
|
|
792
|
-
result_type: tokenResult.result_type,
|
|
793
|
-
chain: tokenResult.chain || normalizedChainId,
|
|
794
|
-
address: tokenResult.address || normalizedAddress,
|
|
795
|
-
};
|
|
796
|
-
// Update cache
|
|
797
|
-
const cacheKey = (0, utils_1.buildCacheKey)(normalizedChainId, normalizedAddress, caseSensitive);
|
|
798
|
-
__classPrivateFieldGet(this, _PhishingController_tokenScanCache, "f").set(cacheKey, {
|
|
799
|
-
result_type: tokenResult.result_type,
|
|
800
|
-
});
|
|
801
|
-
results[normalizedAddress] = result;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return results;
|
|
807
|
-
}
|
|
808
786
|
}
|
|
809
787
|
exports.PhishingController = PhishingController;
|
|
810
788
|
_PhishingController_detector = new WeakMap(), _PhishingController_stalelistRefreshInterval = new WeakMap(), _PhishingController_hotlistRefreshInterval = new WeakMap(), _PhishingController_c2DomainBlocklistRefreshInterval = new WeakMap(), _PhishingController_urlScanCache = new WeakMap(), _PhishingController_tokenScanCache = new WeakMap(), _PhishingController_addressScanCache = new WeakMap(), _PhishingController_inProgressHotlistUpdate = new WeakMap(), _PhishingController_inProgressStalelistUpdate = new WeakMap(), _PhishingController_isProgressC2DomainBlocklistUpdate = new WeakMap(), _PhishingController_transactionControllerStateChangeHandler = new WeakMap(), _PhishingController_fetchTokenScanBulkResults = new WeakMap(), _PhishingController_processBatch = new WeakMap(), _PhishingController_instances = new WeakSet(), _PhishingController_subscribeToTransactionControllerStateChange = function _PhishingController_subscribeToTransactionControllerStateChange() {
|
|
811
789
|
this.messenger.subscribe('TransactionController:stateChange', __classPrivateFieldGet(this, _PhishingController_transactionControllerStateChangeHandler, "f"));
|
|
790
|
+
}, _PhishingController_registerMessageHandlers = function _PhishingController_registerMessageHandlers() {
|
|
791
|
+
this.messenger.registerActionHandler(`${controllerName}:maybeUpdateState`, this.maybeUpdateState.bind(this));
|
|
792
|
+
this.messenger.registerActionHandler(`${controllerName}:testOrigin`, this.test.bind(this));
|
|
793
|
+
this.messenger.registerActionHandler(`${controllerName}:bulkScanUrls`, this.bulkScanUrls.bind(this));
|
|
794
|
+
this.messenger.registerActionHandler(`${controllerName}:bulkScanTokens`, this.bulkScanTokens.bind(this));
|
|
795
|
+
this.messenger.registerActionHandler(`${controllerName}:scanAddress`, this.scanAddress.bind(this));
|
|
812
796
|
}, _PhishingController_isTransactionPatch = function _PhishingController_isTransactionPatch(patch) {
|
|
813
797
|
const { path } = patch;
|
|
814
798
|
return (path.length === 2 &&
|