1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02:00

Update the PhishingController to v2 and update phishing warning page (#17835)

The PhishingController has been updated to v2. This release should
dramatically reduce network traffic and double the update speed of the
phishing list.

This was accomplished by combining both of our phishing configurations
into one list (the "stalelist"), then creating a separate list of the
changes just the past few days (the "hotlist"). Now users will download
a smaller list more frequently (every 30 minutes rather than every
hour), whereas the full list is only updated every 4 days.

The combined configuration means that we no longer know which list was
responsible for each block. The phishing warning page has been updated
to dynamically look this information up, to ensure users are still
directed to the correct place to dispute a block. This update to the
phishing warning page also includes the recent redesign.
This commit is contained in:
PeterYinusa 2023-03-06 18:34:18 +00:00
parent 353c9927b4
commit ceab2a0b11
16 changed files with 395 additions and 108 deletions

View File

@ -605,16 +605,13 @@ function notifyInpageOfStreamFailure() {
/**
* Redirects the current page to a phishing information page
*
* @param data
*/
function redirectToPhishingWarning(data = {}) {
function redirectToPhishingWarning() {
console.debug('MetaMask: Routing to Phishing Warning page.');
const { hostname, href } = window.location;
const { newIssueUrl } = data;
const baseUrl = process.env.PHISHING_WARNING_PAGE_URL;
const querystring = new URLSearchParams({ hostname, href, newIssueUrl });
const querystring = new URLSearchParams({ hostname, href });
window.location.href = `${baseUrl}#${querystring}`;
}

View File

@ -62,19 +62,25 @@ describe('MetaMaskController', function () {
beforeEach(function () {
nock('https://static.metafi.codefi.network')
.persist()
.get('/api/v1/lists/eth_phishing_detect_config.json')
.get('/api/v1/lists/stalelist.json')
.reply(
200,
JSON.stringify({
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: ['127.0.0.1'],
allowlist: [],
blocklist: ['127.0.0.1'],
lastUpdated: 0,
}),
)
.get('/api/v1/lists/phishfort_hotlist.json')
.reply(200, JSON.stringify(['127.0.0.1']));
.get('/api/v1/lists/hotlist.json')
.reply(
200,
JSON.stringify([
{ url: '127.0.0.1', targetList: 'blocklist', timestamp: 0 },
]),
);
metamaskController = new MetaMaskController({
showUserConfirmation: noop,
encryptor: {

View File

@ -69,7 +69,6 @@ import {
TransactionStatus,
TransactionType,
} from '../../shared/constants/transaction';
import { PHISHING_NEW_ISSUE_URLS } from '../../shared/constants/phishing';
import {
GAS_API_BASE_URL,
GAS_DEV_API_BASE_URL,
@ -509,10 +508,11 @@ export default class MetamaskController extends EventEmitter {
initState.PhishingController,
);
this.phishingController.maybeUpdatePhishingLists();
this.phishingController.maybeUpdateState();
if (process.env.IN_TEST) {
this.phishingController.setRefreshInterval(5 * SECOND);
this.phishingController.setHotlistRefreshInterval(5 * SECOND);
this.phishingController.setStalelistRefreshInterval(30 * SECOND);
}
this.announcementController = new AnnouncementController(
@ -3601,15 +3601,11 @@ export default class MetamaskController extends EventEmitter {
if (sender.url) {
const { hostname } = new URL(sender.url);
this.phishingController.maybeUpdatePhishingLists();
this.phishingController.maybeUpdateState();
// Check if new connection is blocked if phishing detection is on
const phishingTestResponse = this.phishingController.test(hostname);
if (usePhishDetect && phishingTestResponse?.result) {
this.sendPhishingWarning(
connectionStream,
hostname,
phishingTestResponse,
);
this.sendPhishingWarning(connectionStream, hostname);
this.metaMetricsController.trackEvent({
event: EVENT_NAMES.PHISHING_PAGE_DISPLAYED,
category: EVENT.CATEGORIES.PHISHING,
@ -3694,15 +3690,11 @@ export default class MetamaskController extends EventEmitter {
* @param {*} connectionStream - The duplex stream to the per-page script,
* for sending the reload attempt to.
* @param {string} hostname - The hostname that triggered the suspicion.
* @param {object} phishingTestResponse - Result of calling `phishingController.test`,
* which is the result of calling eth-phishing-detects detector.check method https://github.com/MetaMask/eth-phishing-detect/blob/master/src/detector.js#L55-L112
*/
sendPhishingWarning(connectionStream, hostname, phishingTestResponse) {
const newIssueUrl = PHISHING_NEW_ISSUE_URLS[phishingTestResponse?.name];
sendPhishingWarning(connectionStream, hostname) {
const mux = setupMultiplex(connectionStream);
const phishingStream = mux.createStream('phishing');
phishingStream.write({ hostname, newIssueUrl });
phishingStream.write({ hostname });
}
/**

View File

@ -121,19 +121,25 @@ describe('MetaMaskController', function () {
.reply(200, '{"JPY":12415.9}');
nock('https://static.metafi.codefi.network')
.persist()
.get('/api/v1/lists/eth_phishing_detect_config.json')
.get('/api/v1/lists/stalelist.json')
.reply(
200,
JSON.stringify({
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: ['127.0.0.1'],
allowlist: [],
blocklist: ['127.0.0.1'],
lastUpdated: 0,
}),
)
.get('/api/v1/lists/phishfort_hotlist.json')
.reply(200, JSON.stringify(['127.0.0.1']));
.get('/api/v1/lists/hotlist.json')
.reply(
200,
JSON.stringify([
{ url: '127.0.0.1', targetList: 'blocklist', timestamp: 0 },
]),
);
sandbox.replace(browser, 'runtime', {
sendMessage: sandbox.stub().rejects(),

View File

@ -0,0 +1,38 @@
import { cloneDeep } from 'lodash';
import { hasProperty, isObject } from '@metamask/utils';
export const version = 78;
/**
* The`@metamask/phishing-controller` state was updated in v2.0.0.
*
* @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist.
* @param originalVersionedData.meta - State metadata.
* @param originalVersionedData.meta.version - The current state version.
* @param originalVersionedData.data - The persisted MetaMask state, keyed by controller.
* @returns Updated versioned MetaMask extension state.
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
versionedData.data = transformState(versionedData.data);
return versionedData;
},
};
function transformState(state) {
if (
!hasProperty(state, 'PhishingController') ||
!isObject(state.PhishingController)
) {
return state;
}
const { PhishingController } = state;
delete PhishingController.phishing;
delete PhishingController.lastFetched;
return state;
}

View File

@ -0,0 +1,108 @@
import { migrate, version } from './078';
describe('migration #78', () => {
it('updates the version metadata', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {},
};
const newStorage = await migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version,
});
});
it('does not change the state if the phishing controller state does not exist', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: { test: '123' },
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual(oldStorage.data);
});
const nonObjects = [undefined, null, 'test', 1, ['test']];
for (const invalidState of nonObjects) {
it(`does not change the state if the phishing controller state is ${invalidState}`, async () => {
const oldStorage = {
meta: {
version: 77,
},
data: { PhishingController: invalidState },
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual(oldStorage.data);
});
}
it('does not change the state if the phishing controller state does not include "phishing" or "lastFetched" properties', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: { PhishingController: { test: '123' } },
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual(oldStorage.data);
});
it('deletes the "phishing" property', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: { PhishingController: { test: '123', phishing: [] } },
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
PhishingController: { test: '123' },
});
});
it('deletes the "lastFetched" property', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: { PhishingController: { test: '123', lastFetched: 100 } },
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
PhishingController: { test: '123' },
});
});
it('deletes the "phishing" and "lastFetched" properties', async () => {
const oldStorage = {
meta: {
version: 77,
},
data: {
PhishingController: { test: '123', lastFetched: 100, phishing: [] },
},
};
const newStorage = await migrate(oldStorage);
expect(newStorage.data).toStrictEqual({
PhishingController: { test: '123' },
});
});
});

View File

@ -81,6 +81,7 @@ import m074 from './074';
import m075 from './075';
import m076 from './076';
import m077 from './077';
import m078 from './078';
const migrations = [
m002,
@ -159,6 +160,7 @@ const migrations = [
m075,
m076,
m077,
m078,
];
export default migrations;

View File

@ -1266,7 +1266,7 @@
"@metamask/base-controller": true,
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>eth-phishing-detect": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
@ -1285,7 +1285,7 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>eth-phishing-detect": {
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
}

View File

@ -1218,11 +1218,11 @@
"@metamask/base-controller": true,
"@metamask/controller-utils": true,
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/phishing-controller>eth-phishing-detect": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
"@metamask/phishing-controller>eth-phishing-detect": {
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
}

View File

@ -1278,7 +1278,7 @@
"@metamask/base-controller": true,
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>eth-phishing-detect": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
@ -1297,7 +1297,7 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>eth-phishing-detect": {
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
}

View File

@ -1266,7 +1266,7 @@
"@metamask/base-controller": true,
"@metamask/controller-utils>isomorphic-fetch": true,
"@metamask/phishing-controller>@metamask/controller-utils": true,
"@metamask/phishing-controller>eth-phishing-detect": true,
"@metamask/phishing-warning>eth-phishing-detect": true,
"punycode": true
}
},
@ -1285,7 +1285,7 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/phishing-controller>eth-phishing-detect": {
"@metamask/phishing-warning>eth-phishing-detect": {
"packages": {
"eslint>optionator>fast-levenshtein": true
}

View File

@ -242,7 +242,7 @@
"@metamask/notification-controller": "^1.0.0",
"@metamask/obs-store": "^5.0.0",
"@metamask/permission-controller": "^1.0.0",
"@metamask/phishing-controller": "^1.1.2",
"@metamask/phishing-controller": "^2.0.0",
"@metamask/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.2.1",
"@metamask/rate-limit-controller": "^1.0.0",
@ -371,7 +371,7 @@
"@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1",
"@metamask/forwarder": "^1.1.0",
"@metamask/phishing-warning": "^1.2.1",
"@metamask/phishing-warning": "^2.0.1",
"@metamask/test-dapp": "^5.4.0",
"@sentry/cli": "^1.58.0",
"@storybook/addon-a11y": "^6.5.13",

View File

@ -1,4 +0,0 @@
export const PHISHING_NEW_ISSUE_URLS = {
MetaMask: 'https://github.com/metamask/eth-phishing-detect/issues/new',
PhishFort: 'https://github.com/phishfort/phishfort-lists/issues/new',
};

View File

@ -5,6 +5,21 @@ const blacklistedHosts = [
'sepolia.infura.io',
];
const HOTLIST_URL =
'https://static.metafi.codefi.network/api/v1/lists/hotlist.json';
const STALELIST_URL =
'https://static.metafi.codefi.network/api/v1/lists/stalelist.json';
const emptyHotlist = [];
const emptyStalelist = {
version: 2,
tolerance: 2,
fuzzylist: [],
allowlist: [],
blocklist: [],
lastUpdated: 0,
};
async function setupMocking(server, testSpecificMock) {
await server.forAnyRequest().thenPassThrough({
beforeRequest: (req) => {
@ -341,6 +356,22 @@ async function setupMocking(server, testSpecificMock) {
});
testSpecificMock(server);
// Mocks below this line can be overridden by test-specific mocks
await server.forGet(STALELIST_URL).thenCallback(() => {
return {
statusCode: 200,
json: emptyStalelist,
};
});
await server.forGet(HOTLIST_URL).thenCallback(() => {
return {
statusCode: 200,
json: emptyHotlist,
};
});
}
module.exports = { setupMocking };

View File

@ -2,36 +2,87 @@ const { strict: assert } = require('assert');
const { convertToHexValue, withFixtures } = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
const PHISHFORT_CDN_URL =
'https://static.metafi.codefi.network/api/v1/lists/phishfort_hotlist.json';
const STALELIST_URL =
'https://static.metafi.codefi.network/api/v1/lists/stalelist.json';
describe('Phishing Detection', function () {
async function mockPhishingDetection(mockServer) {
await mockServer
.forGet(
'https://static.metafi.codefi.network/api/v1/lists/eth_phishing_detect_config.json',
)
.thenCallback(() => {
return {
statusCode: 200,
json: {
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: ['127.0.0.1'],
},
};
});
}
async function mockPhishfortPhishingDetection(mockServer) {
await mockServer.forGet(PHISHFORT_CDN_URL).thenCallback(() => {
const emptyHtmlPage = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>title</title>
</head>
<body>
Empty page
</body>
</html>`;
/**
* Setup fetch mocks for the phishing detection feature.
*
* The mock configuration will show that "127.0.0.1" is blocked. The dynamic lookup on the warning
* page can be customized, so that we can test both the MetaMask and PhishFort block cases.
*
* @param {import('mockttp').Mockttp} mockServer - The mock server.
* @param {object} metamaskPhishingConfigResponse - The response for the dynamic phishing
* configuration lookup performed by the warning page.
*/
async function setupPhishingDetectionMocks(
mockServer,
metamaskPhishingConfigResponse,
) {
await mockServer.forGet(STALELIST_URL).thenCallback(() => {
return {
statusCode: 200,
json: {
version: 2,
tolerance: 2,
fuzzylist: [],
allowlist: [],
blocklist: ['127.0.0.1'],
lastUpdated: 0,
},
};
});
await mockServer
.forGet('https://github.com/MetaMask/eth-phishing-detect/issues/new')
.thenCallback(() => {
return {
statusCode: 200,
json: ['127.0.0.1'],
body: emptyHtmlPage,
};
});
await mockServer
.forGet('https://github.com/phishfort/phishfort-lists/issues/new')
.thenCallback(() => {
return {
statusCode: 200,
body: emptyHtmlPage,
};
});
await mockServer
.forGet(
'https://raw.githubusercontent.com/MetaMask/eth-phishing-detect/master/src/config.json',
)
.thenCallback(() => metamaskPhishingConfigResponse);
}
describe('Phishing Detection', function () {
function mockPhishingDetection(mockServer) {
setupPhishingDetectionMocks(mockServer, {
statusCode: 200,
json: {
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: ['127.0.0.1'],
lastUpdated: 0,
},
});
}
const ganacheOptions = {
accounts: [
{
@ -41,6 +92,7 @@ describe('Phishing Detection', function () {
},
],
};
it('should display the MetaMask Phishing Detection page and take the user to the blocked page if they continue', async function () {
await withFixtures(
{
@ -57,7 +109,7 @@ describe('Phishing Detection', function () {
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://127.0.0.1:8080');
await driver.clickElement({
text: 'continuing at your own risk',
text: 'continue to the site.',
});
const header = await driver.findElement('h1');
assert.equal(await header.getText(), 'E2E Test Dapp');
@ -93,7 +145,7 @@ describe('Phishing Detection', function () {
});
await driver.switchToWindowWithTitle('MetaMask Phishing Detection');
await driver.clickElement({
text: 'continuing at your own risk',
text: 'continue to the site.',
});
const header = await driver.findElement('h1');
assert.equal(await header.getText(), 'E2E Test Dapp');
@ -131,7 +183,7 @@ describe('Phishing Detection', function () {
});
await driver.switchToWindowWithTitle('MetaMask Phishing Detection');
await driver.clickElement({
text: 'continuing at your own risk',
text: 'continue to the site.',
});
// Ensure we're not on the wallet home page
@ -140,13 +192,15 @@ describe('Phishing Detection', function () {
);
});
it('should display the MetaMask Phishing Detection page with the correct new issue link if the issue was detected from the phishfort list', async function () {
it('should navigate the user to eth-phishing-detect to dispute a block if the phishing warning page fails to identify the source', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
title: this.test.title,
testSpecificMock: mockPhishfortPhishingDetection,
testSpecificMock: (mockServer) => {
setupPhishingDetectionMocks(mockServer, { statusCode: 500 });
},
dapp: true,
failOnConsoleError: false,
},
@ -155,10 +209,83 @@ describe('Phishing Detection', function () {
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://127.0.0.1:8080');
const newIssueLink = await driver.findElements(
"a[href='https://github.com/phishfort/phishfort-lists/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%3A8080%2F']",
await driver.clickElement({ text: 'report a detection problem.' });
// wait for page to load before checking URL.
await driver.findElement({ text: 'Empty page' });
assert.equal(
await driver.getCurrentUrl(),
`https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%3A8080%2F`,
);
},
);
});
it('should navigate the user to eth-phishing-detect to dispute a block from MetaMask', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
title: this.test.title,
testSpecificMock: mockPhishingDetection,
dapp: true,
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://127.0.0.1:8080');
await driver.clickElement({ text: 'report a detection problem.' });
// wait for page to load before checking URL.
await driver.findElement({ text: 'Empty page' });
assert.equal(
await driver.getCurrentUrl(),
`https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%3A8080%2F`,
);
},
);
});
it('should navigate the user to PhishFort to dispute a block from MetaMask', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
title: this.test.title,
testSpecificMock: (mockServer) => {
setupPhishingDetectionMocks(mockServer, {
statusCode: 200,
json: {
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: [],
lastUpdated: 0,
},
});
},
dapp: true,
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://127.0.0.1:8080');
await driver.clickElement({ text: 'report a detection problem.' });
// wait for page to load before checking URL.
await driver.findElement({ text: 'Empty page' });
assert.equal(
await driver.getCurrentUrl(),
`https://github.com/phishfort/phishfort-lists/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%3A8080%2F`,
);
assert.equal(newIssueLink.length, 1);
},
);
});

View File

@ -4015,9 +4015,9 @@ __metadata:
languageName: node
linkType: hard
"@metamask/phishing-controller@npm:^1.1.2":
version: 1.1.2
resolution: "@metamask/phishing-controller@npm:1.1.2"
"@metamask/phishing-controller@npm:^2.0.0":
version: 2.0.0
resolution: "@metamask/phishing-controller@npm:2.0.0"
dependencies:
"@metamask/base-controller": ^1.1.2
"@metamask/controller-utils": ^2.0.0
@ -4025,31 +4025,24 @@ __metadata:
eth-phishing-detect: ^1.2.0
isomorphic-fetch: ^3.0.0
punycode: ^2.1.1
checksum: a85427c5c0adab2651c9fb0207cae9501597b820d0807c35177615533bba2626a4c2458b4863f0bef987cc5dfbd523d70a60d3c48bd332ebc8bd6b5a8f640c4e
checksum: 8ad20a7cdac8fc5f450bb157d19b0b780d82490571f4d33e1afde871ace078cf4f885e2e5be4abc6e1e551986d5f44a3200b076d5545764f5492dfaf005419e4
languageName: node
linkType: hard
"@metamask/phishing-warning@npm:^1.2.1":
version: 1.2.1
resolution: "@metamask/phishing-warning@npm:1.2.1"
"@metamask/phishing-warning@npm:^2.0.1":
version: 2.0.1
resolution: "@metamask/phishing-warning@npm:2.0.1"
dependencies:
"@metamask/design-tokens": ^1.6.0
"@metamask/post-message-stream": ^5.1.0
"@metamask/post-message-stream": ^6.0.0
"@types/punycode": ^2.1.0
eth-phishing-detect: ^1.2.0
globalthis: 1.0.1
obj-multiplex: ^1.0.0
pump: ^3.0.0
ses: 0.12.4
checksum: 5cdf2f5fb12dbf774c6d54670051ecd02c489f3f0a5ae68a3facbb2135347be3fe308e260fb44e1a34310fe20cb8e26c3b7b0a3addfdd70ca26ff6ba22f22d92
languageName: node
linkType: hard
"@metamask/post-message-stream@npm:^5.1.0":
version: 5.1.0
resolution: "@metamask/post-message-stream@npm:5.1.0"
dependencies:
"@metamask/utils": ^2.0.0
readable-stream: 2.3.3
checksum: d6c66d82b94970a0689f2d78e4011b891a48edb0f397b54d657c10506cfc066298f3198203a6e8ec090ba87705d62c0db5ba409fede209e1612c8028160059da
punycode: ^2.1.1
ses: ^0.18.1
checksum: caa3e596c3a67188e457307b43724c89121d60734353922d369932093f8618f96465ba7613b194dc2c57754399783dcdf1777c900afeff21bd5137f02688b686
languageName: node
linkType: hard
@ -4286,15 +4279,6 @@ __metadata:
languageName: node
linkType: hard
"@metamask/utils@npm:^2.0.0":
version: 2.1.0
resolution: "@metamask/utils@npm:2.1.0"
dependencies:
fast-deep-equal: ^3.1.3
checksum: 50970fe28cbf98fbc34fb4f69d9bc90f5db94929c69ab532f57b48f42163ea77fb080ab31854efd984361c3e29e67831a78d94d1211ac3bcc6b9557769c73127
languageName: node
linkType: hard
"@metamask/utils@npm:^3.0.1, @metamask/utils@npm:^3.0.3, @metamask/utils@npm:^3.3.0, @metamask/utils@npm:^3.3.1, @metamask/utils@npm:^3.4.0, @metamask/utils@npm:^3.4.1":
version: 3.4.1
resolution: "@metamask/utils@npm:3.4.1"
@ -24197,8 +24181,8 @@ __metadata:
"@metamask/notification-controller": ^1.0.0
"@metamask/obs-store": ^5.0.0
"@metamask/permission-controller": ^1.0.0
"@metamask/phishing-controller": ^1.1.2
"@metamask/phishing-warning": ^1.2.1
"@metamask/phishing-controller": ^2.0.0
"@metamask/phishing-warning": ^2.0.1
"@metamask/post-message-stream": ^6.0.0
"@metamask/providers": ^10.2.1
"@metamask/rate-limit-controller": ^1.0.0
@ -30872,7 +30856,7 @@ __metadata:
languageName: node
linkType: hard
"ses@npm:0.12.4, ses@npm:^0.12.4":
"ses@npm:^0.12.4":
version: 0.12.4
resolution: "ses@npm:0.12.4"
dependencies: