@metamask-previews/phishing-controller 11.0.0-preview-02e7ff99 → 12.0.0-preview-04657153

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 CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [12.0.0]
11
+
12
+ ### Added
13
+
14
+ - Add allowlist functionality to the C2 domain detection system ([#4464](https://github.com/MetaMask/core/pull/4644))
15
+ - Add `PhishingController` functionality for blocking client-side C2 requests by managing a hashed C2 request blocklist ([#4526](https://github.com/MetaMask/core/pull/4526))
16
+ - Add `requestBlocklist` type to `ListTypes`.
17
+ - Add `isBlockedRequest` method to `PhishingController`.
18
+ - Add `isMaliciousRequestDomain` method to `PhishingDetector`.
19
+ - Add handling of `requestBlocklist` in `PhishingDetector` configuration.
20
+ - Add logic to update and check `requestBlocklist` when updating a stale list.
21
+ - Add `sha256Hash` function to generate SHA-256 hash of a domain.
22
+ - Define and export new types: `PhishingControllerGetStateAction`, `PhishingControllerStateChangeEvent`, `PhishingControllerEvents` ([#4633](https://github.com/MetaMask/core/pull/4633))
23
+
24
+ ### Changed
25
+
26
+ - **BREAKING:** Add `@noble/hashes` `^1.4.0` as dependency ([#4526](https://github.com/MetaMask/core/pull/4526))
27
+ - **BREAKING:**: Add `ethereum-cryptography` `^2.1.2` as dependency ([#4526](https://github.com/MetaMask/core/pull/4526))
28
+ - **BREAKING:** `PhishingControllerMessenger` must allow internal events defined in the `PhishingControllerEvents` type ([#4633](https://github.com/MetaMask/core/pull/4633))
29
+ - Widen `PhishingControllerActions` to include the `PhishingController:getState` action ([#4633](https://github.com/MetaMask/core/pull/4633))
30
+ - Bump `@metamask/base-controller` from `^6.0.2` to `^6.0.3` ([#4625](https://github.com/MetaMask/core/pull/4625))
31
+ - Bump `@metamask/controller-utils` from `^11.0.2` to `^11.1.0` ([#4639](https://github.com/MetaMask/core/pull/4639))
32
+
10
33
  ## [11.0.0]
11
34
 
12
35
  ### Changed
@@ -228,7 +251,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
228
251
 
229
252
  All changes listed after this point were applied to this package following the monorepo conversion.
230
253
 
231
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@11.0.0...HEAD
254
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@12.0.0...HEAD
255
+ [12.0.0]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@11.0.0...@metamask/phishing-controller@12.0.0
232
256
  [11.0.0]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@10.1.1...@metamask/phishing-controller@11.0.0
233
257
  [10.1.1]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@10.1.0...@metamask/phishing-controller@10.1.1
234
258
  [10.1.0]: https://github.com/MetaMask/core/compare/@metamask/phishing-controller@10.0.0...@metamask/phishing-controller@10.1.0
@@ -11,7 +11,12 @@
11
11
 
12
12
 
13
13
 
14
- var _chunkDPHXQJ4Ejs = require('./chunk-DPHXQJ4E.js');
14
+
15
+
16
+
17
+
18
+ var _chunkXZNT2KYXjs = require('./chunk-XZNT2KYX.js');
19
+ require('./chunk-5NDIXQG5.js');
15
20
  require('./chunk-Z4BLTVTB.js');
16
21
 
17
22
 
@@ -26,5 +31,9 @@ require('./chunk-Z4BLTVTB.js');
26
31
 
27
32
 
28
33
 
29
- exports.HOTLIST_REFRESH_INTERVAL = _chunkDPHXQJ4Ejs.HOTLIST_REFRESH_INTERVAL; exports.ListKeys = _chunkDPHXQJ4Ejs.ListKeys; exports.ListNames = _chunkDPHXQJ4Ejs.ListNames; exports.METAMASK_HOTLIST_DIFF_FILE = _chunkDPHXQJ4Ejs.METAMASK_HOTLIST_DIFF_FILE; exports.METAMASK_HOTLIST_DIFF_URL = _chunkDPHXQJ4Ejs.METAMASK_HOTLIST_DIFF_URL; exports.METAMASK_STALELIST_FILE = _chunkDPHXQJ4Ejs.METAMASK_STALELIST_FILE; exports.METAMASK_STALELIST_URL = _chunkDPHXQJ4Ejs.METAMASK_STALELIST_URL; exports.PHISHING_CONFIG_BASE_URL = _chunkDPHXQJ4Ejs.PHISHING_CONFIG_BASE_URL; exports.PhishingController = _chunkDPHXQJ4Ejs.PhishingController; exports.STALELIST_REFRESH_INTERVAL = _chunkDPHXQJ4Ejs.STALELIST_REFRESH_INTERVAL; exports.default = _chunkDPHXQJ4Ejs.PhishingController_default; exports.phishingListKeyNameMap = _chunkDPHXQJ4Ejs.phishingListKeyNameMap;
34
+
35
+
36
+
37
+
38
+ exports.C2_DOMAIN_BLOCKLIST_ENDPOINT = _chunkXZNT2KYXjs.C2_DOMAIN_BLOCKLIST_ENDPOINT; exports.C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL = _chunkXZNT2KYXjs.C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL; exports.C2_DOMAIN_BLOCKLIST_URL = _chunkXZNT2KYXjs.C2_DOMAIN_BLOCKLIST_URL; exports.CLIENT_SIDE_DETECION_BASE_URL = _chunkXZNT2KYXjs.CLIENT_SIDE_DETECION_BASE_URL; exports.HOTLIST_REFRESH_INTERVAL = _chunkXZNT2KYXjs.HOTLIST_REFRESH_INTERVAL; exports.ListKeys = _chunkXZNT2KYXjs.ListKeys; exports.ListNames = _chunkXZNT2KYXjs.ListNames; exports.METAMASK_HOTLIST_DIFF_FILE = _chunkXZNT2KYXjs.METAMASK_HOTLIST_DIFF_FILE; exports.METAMASK_HOTLIST_DIFF_URL = _chunkXZNT2KYXjs.METAMASK_HOTLIST_DIFF_URL; exports.METAMASK_STALELIST_FILE = _chunkXZNT2KYXjs.METAMASK_STALELIST_FILE; exports.METAMASK_STALELIST_URL = _chunkXZNT2KYXjs.METAMASK_STALELIST_URL; exports.PHISHING_CONFIG_BASE_URL = _chunkXZNT2KYXjs.PHISHING_CONFIG_BASE_URL; exports.PhishingController = _chunkXZNT2KYXjs.PhishingController; exports.STALELIST_REFRESH_INTERVAL = _chunkXZNT2KYXjs.STALELIST_REFRESH_INTERVAL; exports.default = _chunkXZNT2KYXjs.PhishingController_default; exports.phishingListKeyNameMap = _chunkXZNT2KYXjs.phishingListKeyNameMap;
30
39
  //# sourceMappingURL=PhishingController.js.map
@@ -1,4 +1,8 @@
1
1
  import {
2
+ C2_DOMAIN_BLOCKLIST_ENDPOINT,
3
+ C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL,
4
+ C2_DOMAIN_BLOCKLIST_URL,
5
+ CLIENT_SIDE_DETECION_BASE_URL,
2
6
  HOTLIST_REFRESH_INTERVAL,
3
7
  ListKeys,
4
8
  ListNames,
@@ -11,9 +15,14 @@ import {
11
15
  PhishingController_default,
12
16
  STALELIST_REFRESH_INTERVAL,
13
17
  phishingListKeyNameMap
14
- } from "./chunk-Q42CM6EP.mjs";
18
+ } from "./chunk-F7CZ3O4X.mjs";
19
+ import "./chunk-G2RL74NS.mjs";
15
20
  import "./chunk-XUI43LEZ.mjs";
16
21
  export {
22
+ C2_DOMAIN_BLOCKLIST_ENDPOINT,
23
+ C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL,
24
+ C2_DOMAIN_BLOCKLIST_URL,
25
+ CLIENT_SIDE_DETECION_BASE_URL,
17
26
  HOTLIST_REFRESH_INTERVAL,
18
27
  ListKeys,
19
28
  ListNames,
@@ -1,8 +1,9 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkDPHXQJ4Ejs = require('./chunk-DPHXQJ4E.js');
3
+ var _chunkXZNT2KYXjs = require('./chunk-XZNT2KYX.js');
4
+ require('./chunk-5NDIXQG5.js');
4
5
  require('./chunk-Z4BLTVTB.js');
5
6
 
6
7
 
7
- exports.PhishingDetector = _chunkDPHXQJ4Ejs.PhishingDetector;
8
+ exports.PhishingDetector = _chunkXZNT2KYXjs.PhishingDetector;
8
9
  //# sourceMappingURL=PhishingDetector.js.map
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  PhishingDetector
3
- } from "./chunk-Q42CM6EP.mjs";
3
+ } from "./chunk-F7CZ3O4X.mjs";
4
+ import "./chunk-G2RL74NS.mjs";
4
5
  import "./chunk-XUI43LEZ.mjs";
5
6
  export {
6
7
  PhishingDetector
@@ -0,0 +1,16 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/types.ts
2
+ var PhishingDetectorResultType = /* @__PURE__ */ ((PhishingDetectorResultType2) => {
3
+ PhishingDetectorResultType2["All"] = "all";
4
+ PhishingDetectorResultType2["Fuzzy"] = "fuzzy";
5
+ PhishingDetectorResultType2["Blocklist"] = "blocklist";
6
+ PhishingDetectorResultType2["Allowlist"] = "allowlist";
7
+ PhishingDetectorResultType2["Blacklist"] = "blacklist";
8
+ PhishingDetectorResultType2["Whitelist"] = "whitelist";
9
+ PhishingDetectorResultType2["C2DomainBlocklist"] = "c2DomainBlocklist";
10
+ return PhishingDetectorResultType2;
11
+ })(PhishingDetectorResultType || {});
12
+
13
+
14
+
15
+ exports.PhishingDetectorResultType = PhishingDetectorResultType;
16
+ //# sourceMappingURL=chunk-5NDIXQG5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"names":["PhishingDetectorResultType"],"mappings":";AAyCO,IAAK,6BAAL,kBAAKA,gCAAL;AAIL,EAAAA,4BAAA,SAAM;AAIN,EAAAA,4BAAA,WAAQ;AAIR,EAAAA,4BAAA,eAAY;AAIZ,EAAAA,4BAAA,eAAY;AAKZ,EAAAA,4BAAA,eAAY;AAKZ,EAAAA,4BAAA,eAAY;AAIZ,EAAAA,4BAAA,uBAAoB;AA9BV,SAAAA;AAAA,GAAA","sourcesContent":["/**\n * Represents the result of checking a domain.\n */\nexport type PhishingDetectorResult = {\n /**\n * The name of the configuration object in which the domain was found within\n * an allowlist, blocklist, or fuzzylist.\n */\n name?: string;\n /**\n * The version associated with the configuration object in which the domain\n * was found within an allowlist, blocklist, or fuzzylist.\n */\n version?: string;\n /**\n * Whether the domain is regarded as allowed (true) or not (false).\n */\n result: boolean;\n /**\n * A normalized version of the domain, which is only constructed if the domain\n * is found within a list.\n */\n match?: string;\n /**\n * Which type of list in which the domain was found.\n *\n * - \"allowlist\" means that the domain was found in the allowlist.\n * - \"blocklist\" means that the domain was found in the blocklist.\n * - \"fuzzy\" means that the domain was found in the fuzzylist.\n * - \"blacklist\" means that the domain was found in a blacklist of a legacy\n * configuration object.\n * - \"whitelist\" means that the domain was found in a whitelist of a legacy\n * configuration object.\n * - \"all\" means that the domain was not found in any list.\n */\n type: PhishingDetectorResultType;\n};\n\n/**\n * The type of list in which the domain was found.\n */\nexport enum PhishingDetectorResultType {\n /*\n * \"all\" means that the domain was not found in any list.\n */\n All = 'all',\n /*\n * \"fuzzy\" means that the domain was found in the fuzzylist.\n */\n Fuzzy = 'fuzzy',\n /*\n * \"blocklist\" means that the domain was found in the blocklist.\n */\n Blocklist = 'blocklist',\n /*\n * \"allowlist\" means that the domain was found in the allowlist.\n */\n Allowlist = 'allowlist',\n /*\n * \"blacklist\" means that the domain was found in a blacklist of a legacy\n * configuration object.\n */\n Blacklist = 'blacklist',\n /*\n * \"whitelist\" means that the domain was found in a whitelist of a legacy\n * configuration object.\n */\n Whitelist = 'whitelist',\n /*\n * \"c2DomainBlocklist\" means that the domain was found in the C2 domain blocklist.\n */\n C2DomainBlocklist = 'c2DomainBlocklist',\n}\n"]}
@@ -5,6 +5,10 @@ import {
5
5
  __privateSet
6
6
  } from "./chunk-XUI43LEZ.mjs";
7
7
 
8
+ // src/utils.ts
9
+ import { bytesToHex } from "@noble/hashes/utils";
10
+ import { sha256 } from "ethereum-cryptography/sha256";
11
+
8
12
  // src/PhishingController.ts
9
13
  import { BaseController } from "@metamask/base-controller";
10
14
  import { safelyExecute } from "@metamask/controller-utils";
@@ -36,6 +40,7 @@ var PhishingDetector = class {
36
40
  getDefaultPhishingDetectorConfig({
37
41
  allowlist: opts.whitelist,
38
42
  blocklist: opts.blacklist,
43
+ c2DomainBlocklist: opts.c2DomainBlocklist,
39
44
  fuzzylist: opts.fuzzylist,
40
45
  tolerance: opts.tolerance
41
46
  })
@@ -55,10 +60,10 @@ var PhishingDetector = class {
55
60
  const result = __privateMethod(this, _check, check_fn).call(this, url);
56
61
  if (__privateGet(this, _legacyConfig)) {
57
62
  let legacyType = result.type;
58
- if (legacyType === "allowlist") {
59
- legacyType = "whitelist";
60
- } else if (legacyType === "blocklist") {
61
- legacyType = "blacklist";
63
+ if (legacyType === "allowlist" /* Allowlist */) {
64
+ legacyType = "whitelist" /* Whitelist */;
65
+ } else if (legacyType === "blocklist" /* Blocklist */) {
66
+ legacyType = "blacklist" /* Blacklist */;
62
67
  }
63
68
  return {
64
69
  match: result.match,
@@ -68,12 +73,88 @@ var PhishingDetector = class {
68
73
  }
69
74
  return result;
70
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
+ } catch (error) {
89
+ return {
90
+ result: false,
91
+ type: "c2DomainBlocklist" /* C2DomainBlocklist */
92
+ };
93
+ }
94
+ const fqdn = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
95
+ const source = domainToParts(fqdn);
96
+ for (const { allowlist, name, version } of __privateGet(this, _configs)) {
97
+ const allowlistMatch = matchPartsAgainstList(source, allowlist);
98
+ if (allowlistMatch) {
99
+ const match = domainPartsToDomain(allowlistMatch);
100
+ return {
101
+ match,
102
+ name,
103
+ result: false,
104
+ type: "allowlist" /* Allowlist */,
105
+ version: version === void 0 ? version : String(version)
106
+ };
107
+ }
108
+ }
109
+ for (const { c2DomainBlocklist, name, version } of __privateGet(this, _configs)) {
110
+ const hash = sha256Hash(hostname.toLowerCase());
111
+ const blocked = c2DomainBlocklist?.includes(hash) ?? false;
112
+ if (blocked) {
113
+ return {
114
+ name,
115
+ result: true,
116
+ type: "c2DomainBlocklist" /* C2DomainBlocklist */,
117
+ version: version === void 0 ? version : String(version)
118
+ };
119
+ }
120
+ }
121
+ return {
122
+ result: false,
123
+ type: "c2DomainBlocklist" /* C2DomainBlocklist */
124
+ };
125
+ }
71
126
  };
72
127
  _configs = new WeakMap();
73
128
  _legacyConfig = new WeakMap();
74
129
  _check = new WeakSet();
75
130
  check_fn = function(url) {
76
- const domain = new URL(url).hostname;
131
+ const ipfsCidMatch = url.match(ipfsCidRegex());
132
+ if (ipfsCidMatch !== null) {
133
+ const cID = ipfsCidMatch[0];
134
+ for (const { blocklist, name, version } of __privateGet(this, _configs)) {
135
+ const blocklistMatch = blocklist.filter((entries) => entries.length === 1).find((entries) => {
136
+ return entries[0] === cID;
137
+ });
138
+ if (blocklistMatch) {
139
+ return {
140
+ name,
141
+ match: cID,
142
+ result: true,
143
+ type: "blocklist" /* Blocklist */,
144
+ version: version === void 0 ? version : String(version)
145
+ };
146
+ }
147
+ }
148
+ }
149
+ let domain;
150
+ try {
151
+ domain = new URL(url).hostname;
152
+ } catch (error) {
153
+ return {
154
+ result: false,
155
+ type: "all" /* All */
156
+ };
157
+ }
77
158
  const fqdn = domain.endsWith(".") ? domain.slice(0, -1) : domain;
78
159
  const source = domainToParts(fqdn);
79
160
  for (const { allowlist, name, version } of __privateGet(this, _configs)) {
@@ -84,7 +165,7 @@ check_fn = function(url) {
84
165
  match,
85
166
  name,
86
167
  result: false,
87
- type: "allowlist",
168
+ type: "allowlist" /* Allowlist */,
88
169
  version: version === void 0 ? version : String(version)
89
170
  };
90
171
  }
@@ -97,7 +178,7 @@ check_fn = function(url) {
97
178
  match,
98
179
  name,
99
180
  result: true,
100
- type: "blocklist",
181
+ type: "blocklist" /* Blocklist */,
101
182
  version: version === void 0 ? version : String(version)
102
183
  };
103
184
  }
@@ -115,31 +196,13 @@ check_fn = function(url) {
115
196
  name,
116
197
  match,
117
198
  result: true,
118
- type: "fuzzy",
199
+ type: "fuzzy" /* Fuzzy */,
119
200
  version: version === void 0 ? version : String(version)
120
201
  };
121
202
  }
122
203
  }
123
204
  }
124
- const ipfsCidMatch = url.match(ipfsCidRegex());
125
- if (ipfsCidMatch !== null) {
126
- const cID = ipfsCidMatch[0];
127
- for (const { blocklist, name, version } of __privateGet(this, _configs)) {
128
- const blocklistMatch = blocklist.filter((entries) => entries.length === 1).find((entries) => {
129
- return entries[0] === cID;
130
- });
131
- if (blocklistMatch) {
132
- return {
133
- name,
134
- match: cID,
135
- result: true,
136
- type: "blocklist",
137
- version: version === void 0 ? version : String(version)
138
- };
139
- }
140
- }
141
- }
142
- return { result: false, type: "all" };
205
+ return { result: false, type: "all" /* All */ };
143
206
  };
144
207
  function ipfsCidRegex() {
145
208
  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,}";
@@ -150,10 +213,14 @@ function ipfsCidRegex() {
150
213
  var PHISHING_CONFIG_BASE_URL = "https://phishing-detection.api.cx.metamask.io";
151
214
  var METAMASK_STALELIST_FILE = "/v1/stalelist";
152
215
  var METAMASK_HOTLIST_DIFF_FILE = "/v1/diffsSince";
216
+ var CLIENT_SIDE_DETECION_BASE_URL = "https://client-side-detection.api.cx.metamask.io";
217
+ var C2_DOMAIN_BLOCKLIST_ENDPOINT = "/v1/request-blocklist";
218
+ var C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL = 15 * 60;
153
219
  var HOTLIST_REFRESH_INTERVAL = 5 * 60;
154
220
  var STALELIST_REFRESH_INTERVAL = 30 * 24 * 60 * 60;
155
221
  var METAMASK_STALELIST_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_STALELIST_FILE}`;
156
222
  var METAMASK_HOTLIST_DIFF_URL = `${PHISHING_CONFIG_BASE_URL}${METAMASK_HOTLIST_DIFF_FILE}`;
223
+ var C2_DOMAIN_BLOCKLIST_URL = `${CLIENT_SIDE_DETECION_BASE_URL}${C2_DOMAIN_BLOCKLIST_ENDPOINT}`;
157
224
  var ListKeys = /* @__PURE__ */ ((ListKeys2) => {
158
225
  ListKeys2["EthPhishingDetectConfig"] = "eth_phishing_detect_config";
159
226
  return ListKeys2;
@@ -173,17 +240,19 @@ var metadata = {
173
240
  phishingLists: { persist: true, anonymous: false },
174
241
  whitelist: { persist: true, anonymous: false },
175
242
  hotlistLastFetched: { persist: true, anonymous: false },
176
- stalelistLastFetched: { persist: true, anonymous: false }
243
+ stalelistLastFetched: { persist: true, anonymous: false },
244
+ c2DomainBlocklistLastFetched: { persist: true, anonymous: false }
177
245
  };
178
246
  var getDefaultState = () => {
179
247
  return {
180
248
  phishingLists: [],
181
249
  whitelist: [],
182
250
  hotlistLastFetched: 0,
183
- stalelistLastFetched: 0
251
+ stalelistLastFetched: 0,
252
+ c2DomainBlocklistLastFetched: 0
184
253
  };
185
254
  };
186
- var _detector, _stalelistRefreshInterval, _hotlistRefreshInterval, _inProgressHotlistUpdate, _inProgressStalelistUpdate, _registerMessageHandlers, registerMessageHandlers_fn, _updateStalelist, updateStalelist_fn, _updateHotlist, updateHotlist_fn, _queryConfig, queryConfig_fn;
255
+ var _detector, _stalelistRefreshInterval, _hotlistRefreshInterval, _c2DomainBlocklistRefreshInterval, _inProgressHotlistUpdate, _inProgressStalelistUpdate, _isProgressC2DomainBlocklistUpdate, _registerMessageHandlers, registerMessageHandlers_fn, _updateStalelist, updateStalelist_fn, _updateHotlist, updateHotlist_fn, _updateC2DomainBlocklist, updateC2DomainBlocklist_fn, _queryConfig, queryConfig_fn;
187
256
  var PhishingController = class extends BaseController {
188
257
  /**
189
258
  * Construct a Phishing Controller.
@@ -191,12 +260,14 @@ var PhishingController = class extends BaseController {
191
260
  * @param config - Initial options used to configure this controller.
192
261
  * @param config.stalelistRefreshInterval - Polling interval used to fetch stale list.
193
262
  * @param config.hotlistRefreshInterval - Polling interval used to fetch hotlist diff list.
263
+ * @param config.c2DomainBlocklistRefreshInterval - Polling interval used to fetch c2 domain blocklist.
194
264
  * @param config.messenger - The controller restricted messenger.
195
265
  * @param config.state - Initial state to set on this controller.
196
266
  */
197
267
  constructor({
198
268
  stalelistRefreshInterval = STALELIST_REFRESH_INTERVAL,
199
269
  hotlistRefreshInterval = HOTLIST_REFRESH_INTERVAL,
270
+ c2DomainBlocklistRefreshInterval = C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL,
200
271
  messenger,
201
272
  state = {}
202
273
  }) {
@@ -228,16 +299,26 @@ var PhishingController = class extends BaseController {
228
299
  * this function that prevents redundant configuration updates.
229
300
  */
230
301
  __privateAdd(this, _updateHotlist);
302
+ /**
303
+ * Update the C2 domain blocklist.
304
+ *
305
+ * This should only be called from the `updateC2DomainBlocklist` function, which is a wrapper around
306
+ * this function that prevents redundant configuration updates.
307
+ */
308
+ __privateAdd(this, _updateC2DomainBlocklist);
231
309
  __privateAdd(this, _queryConfig);
232
310
  // TODO: Replace `any` with type
233
311
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
234
312
  __privateAdd(this, _detector, void 0);
235
313
  __privateAdd(this, _stalelistRefreshInterval, void 0);
236
314
  __privateAdd(this, _hotlistRefreshInterval, void 0);
315
+ __privateAdd(this, _c2DomainBlocklistRefreshInterval, void 0);
237
316
  __privateAdd(this, _inProgressHotlistUpdate, void 0);
238
317
  __privateAdd(this, _inProgressStalelistUpdate, void 0);
318
+ __privateAdd(this, _isProgressC2DomainBlocklistUpdate, void 0);
239
319
  __privateSet(this, _stalelistRefreshInterval, stalelistRefreshInterval);
240
320
  __privateSet(this, _hotlistRefreshInterval, hotlistRefreshInterval);
321
+ __privateSet(this, _c2DomainBlocklistRefreshInterval, c2DomainBlocklistRefreshInterval);
241
322
  __privateMethod(this, _registerMessageHandlers, registerMessageHandlers_fn).call(this);
242
323
  this.updatePhishingDetector();
243
324
  }
@@ -267,6 +348,16 @@ var PhishingController = class extends BaseController {
267
348
  setHotlistRefreshInterval(interval) {
268
349
  __privateSet(this, _hotlistRefreshInterval, interval);
269
350
  }
351
+ /**
352
+ * Set the interval at which the C2 domain blocklist will be refetched.
353
+ * Fetching will only occur on the next call to test/bypass.
354
+ * For immediate update to the phishing list, call {@link updateHotlist} directly.
355
+ *
356
+ * @param interval - the new interval, in ms.
357
+ */
358
+ setC2DomainBlocklistRefreshInterval(interval) {
359
+ __privateSet(this, _c2DomainBlocklistRefreshInterval, interval);
360
+ }
270
361
  /**
271
362
  * Determine if an update to the stalelist configuration is needed.
272
363
  *
@@ -283,12 +374,20 @@ var PhishingController = class extends BaseController {
283
374
  isHotlistOutOfDate() {
284
375
  return fetchTimeNow() - this.state.hotlistLastFetched >= __privateGet(this, _hotlistRefreshInterval);
285
376
  }
377
+ /**
378
+ * Determine if an update to the C2 domain blocklist is needed.
379
+ *
380
+ * @returns Whether an update is needed
381
+ */
382
+ isC2DomainBlocklistOutOfDate() {
383
+ return fetchTimeNow() - this.state.c2DomainBlocklistLastFetched >= __privateGet(this, _c2DomainBlocklistRefreshInterval);
384
+ }
286
385
  /**
287
386
  * Conditionally update the phishing configuration.
288
387
  *
289
388
  * If the stalelist configuration is out of date, this function will call `updateStalelist`
290
389
  * to update the configuration. This will automatically grab the hotlist,
291
- * so it isn't necessary to continue on to download the hotlist.
390
+ * so it isn't necessary to continue on to download the hotlist and the c2 domain blocklist.
292
391
  *
293
392
  */
294
393
  async maybeUpdateState() {
@@ -301,6 +400,10 @@ var PhishingController = class extends BaseController {
301
400
  if (hotlistOutOfDate) {
302
401
  await this.updateHotlist();
303
402
  }
403
+ const c2DomainBlocklistOutOfDate = this.isC2DomainBlocklistOutOfDate();
404
+ if (c2DomainBlocklistOutOfDate) {
405
+ await this.updateC2DomainBlocklist();
406
+ }
304
407
  }
305
408
  /**
306
409
  * Determines if a given origin is unapproved.
@@ -315,10 +418,24 @@ var PhishingController = class extends BaseController {
315
418
  test(origin) {
316
419
  const punycodeOrigin = toASCII(origin);
317
420
  if (this.state.whitelist.includes(punycodeOrigin)) {
318
- return { result: false, type: "all" };
421
+ return { result: false, type: "all" /* All */ };
319
422
  }
320
423
  return __privateGet(this, _detector).check(punycodeOrigin);
321
424
  }
425
+ /**
426
+ * Checks if a request URL's domain is blocked against the request blocklist.
427
+ *
428
+ * This method is used to determine if a specific request URL is associated with a malicious
429
+ * command and control (C2) domain. The URL's hostname is hashed and checked against a configured
430
+ * blocklist of known malicious domains.
431
+ *
432
+ * @param origin - The full request URL to be checked.
433
+ * @returns An object indicating whether the URL's domain is blocked and relevant metadata.
434
+ */
435
+ isBlockedRequest(origin) {
436
+ const punycodeOrigin = toASCII(origin);
437
+ return __privateGet(this, _detector).isMaliciousC2Domain(punycodeOrigin);
438
+ }
322
439
  /**
323
440
  * Temporarily marks a given origin as approved.
324
441
  *
@@ -334,6 +451,24 @@ var PhishingController = class extends BaseController {
334
451
  draftState.whitelist.push(punycodeOrigin);
335
452
  });
336
453
  }
454
+ /**
455
+ * Update the C2 domain blocklist.
456
+ *
457
+ * If an update is in progress, no additional update will be made. Instead this will wait until
458
+ * the in-progress update has finished.
459
+ */
460
+ async updateC2DomainBlocklist() {
461
+ if (__privateGet(this, _isProgressC2DomainBlocklistUpdate)) {
462
+ await __privateGet(this, _isProgressC2DomainBlocklistUpdate);
463
+ return;
464
+ }
465
+ try {
466
+ __privateSet(this, _isProgressC2DomainBlocklistUpdate, __privateMethod(this, _updateC2DomainBlocklist, updateC2DomainBlocklist_fn).call(this));
467
+ await __privateGet(this, _isProgressC2DomainBlocklistUpdate);
468
+ } finally {
469
+ __privateSet(this, _isProgressC2DomainBlocklistUpdate, void 0);
470
+ }
471
+ }
337
472
  /**
338
473
  * Update the hotlist.
339
474
  *
@@ -374,8 +509,10 @@ var PhishingController = class extends BaseController {
374
509
  _detector = new WeakMap();
375
510
  _stalelistRefreshInterval = new WeakMap();
376
511
  _hotlistRefreshInterval = new WeakMap();
512
+ _c2DomainBlocklistRefreshInterval = new WeakMap();
377
513
  _inProgressHotlistUpdate = new WeakMap();
378
514
  _inProgressStalelistUpdate = new WeakMap();
515
+ _isProgressC2DomainBlocklistUpdate = new WeakMap();
379
516
  _registerMessageHandlers = new WeakSet();
380
517
  registerMessageHandlers_fn = function() {
381
518
  this.messagingSystem.registerActionHandler(
@@ -389,10 +526,16 @@ registerMessageHandlers_fn = function() {
389
526
  };
390
527
  _updateStalelist = new WeakSet();
391
528
  updateStalelist_fn = async function() {
392
- let stalelistResponse;
393
- let hotlistDiffsResponse;
529
+ let stalelistResponse = null;
530
+ let hotlistDiffsResponse = null;
531
+ let c2DomainBlocklistResponse = null;
394
532
  try {
395
- stalelistResponse = await __privateMethod(this, _queryConfig, queryConfig_fn).call(this, METAMASK_STALELIST_URL).then((d) => d);
533
+ const stalelistPromise = __privateMethod(this, _queryConfig, queryConfig_fn).call(this, METAMASK_STALELIST_URL);
534
+ const c2DomainBlocklistPromise = __privateMethod(this, _queryConfig, queryConfig_fn).call(this, C2_DOMAIN_BLOCKLIST_URL);
535
+ [stalelistResponse, c2DomainBlocklistResponse] = await Promise.all([
536
+ stalelistPromise,
537
+ c2DomainBlocklistPromise
538
+ ]);
396
539
  if (stalelistResponse?.data && stalelistResponse.data.lastUpdated > 0) {
397
540
  hotlistDiffsResponse = await __privateMethod(this, _queryConfig, queryConfig_fn).call(this, `${METAMASK_HOTLIST_DIFF_URL}/${stalelistResponse.data.lastUpdated}`);
398
541
  }
@@ -401,6 +544,7 @@ updateStalelist_fn = async function() {
401
544
  this.update((draftState) => {
402
545
  draftState.stalelistLastFetched = timeNow;
403
546
  draftState.hotlistLastFetched = timeNow;
547
+ draftState.c2DomainBlocklistLastFetched = timeNow;
404
548
  });
405
549
  }
406
550
  if (!stalelistResponse || !hotlistDiffsResponse) {
@@ -410,6 +554,7 @@ updateStalelist_fn = async function() {
410
554
  const metamaskListState = {
411
555
  ...eth_phishing_detect_config,
412
556
  ...partialState,
557
+ c2DomainBlocklist: c2DomainBlocklistResponse ? c2DomainBlocklistResponse.recentlyAdded : [],
413
558
  name: phishingListKeyNameMap.eth_phishing_detect_config
414
559
  };
415
560
  const newMetaMaskListState = applyDiffs(
@@ -439,13 +584,48 @@ updateHotlist_fn = async function() {
439
584
  return;
440
585
  }
441
586
  const hotlist = hotlistResponse.data;
442
- const newPhishingLists = this.state.phishingLists.map(
443
- (phishingList) => applyDiffs(
587
+ const newPhishingLists = this.state.phishingLists.map((phishingList) => {
588
+ const updatedList = applyDiffs(
444
589
  phishingList,
445
590
  hotlist,
446
- phishingListNameKeyMap[phishingList.name]
447
- )
448
- );
591
+ phishingListNameKeyMap[phishingList.name],
592
+ [],
593
+ []
594
+ );
595
+ return updatedList;
596
+ });
597
+ this.update((draftState) => {
598
+ draftState.phishingLists = newPhishingLists;
599
+ });
600
+ this.updatePhishingDetector();
601
+ };
602
+ _updateC2DomainBlocklist = new WeakSet();
603
+ updateC2DomainBlocklist_fn = async function() {
604
+ let c2DomainBlocklistResponse = null;
605
+ try {
606
+ c2DomainBlocklistResponse = await __privateMethod(this, _queryConfig, queryConfig_fn).call(this, `${C2_DOMAIN_BLOCKLIST_URL}?timestamp=${roundToNearestMinute(
607
+ this.state.c2DomainBlocklistLastFetched
608
+ )}`);
609
+ } finally {
610
+ this.update((draftState) => {
611
+ draftState.c2DomainBlocklistLastFetched = fetchTimeNow();
612
+ });
613
+ }
614
+ if (!c2DomainBlocklistResponse) {
615
+ return;
616
+ }
617
+ const recentlyAddedC2Domains = c2DomainBlocklistResponse.recentlyAdded;
618
+ const recentlyRemovedC2Domains = c2DomainBlocklistResponse.recentlyRemoved;
619
+ const newPhishingLists = this.state.phishingLists.map((phishingList) => {
620
+ const updatedList = applyDiffs(
621
+ phishingList,
622
+ [],
623
+ phishingListNameKeyMap[phishingList.name],
624
+ recentlyAddedC2Domains,
625
+ recentlyRemovedC2Domains
626
+ );
627
+ return updatedList;
628
+ });
449
629
  this.update((draftState) => {
450
630
  draftState.phishingLists = newPhishingLists;
451
631
  });
@@ -471,6 +651,9 @@ var PhishingController_default = PhishingController;
471
651
  // src/utils.ts
472
652
  var DEFAULT_TOLERANCE = 3;
473
653
  var fetchTimeNow = () => Math.round(Date.now() / 1e3);
654
+ function roundToNearestMinute(unixTimestamp) {
655
+ return Math.floor(unixTimestamp / 60) * 60;
656
+ }
474
657
  var splitStringByPeriod = (stringToSplit) => {
475
658
  const periodIndex = stringToSplit.indexOf(".");
476
659
  return [
@@ -478,7 +661,7 @@ var splitStringByPeriod = (stringToSplit) => {
478
661
  stringToSplit.slice(periodIndex + 1)
479
662
  ];
480
663
  };
481
- var applyDiffs = (listState, hotlistDiffs, listKey) => {
664
+ var applyDiffs = (listState, hotlistDiffs, listKey, recentlyAddedC2Domains = [], recentlyRemovedC2Domains = []) => {
482
665
  const diffsToApply = hotlistDiffs.filter(
483
666
  ({ timestamp, targetList }) => timestamp > listState.lastUpdated && splitStringByPeriod(targetList)[0] === listKey
484
667
  );
@@ -486,7 +669,8 @@ var applyDiffs = (listState, hotlistDiffs, listKey) => {
486
669
  const listSets = {
487
670
  allowlist: new Set(listState.allowlist),
488
671
  blocklist: new Set(listState.blocklist),
489
- fuzzylist: new Set(listState.fuzzylist)
672
+ fuzzylist: new Set(listState.fuzzylist),
673
+ c2DomainBlocklist: new Set(listState.c2DomainBlocklist)
490
674
  };
491
675
  for (const { isRemoval, targetList, url, timestamp } of diffsToApply) {
492
676
  const targetListType = splitStringByPeriod(targetList)[1];
@@ -499,7 +683,16 @@ var applyDiffs = (listState, hotlistDiffs, listKey) => {
499
683
  listSets[targetListType].add(url);
500
684
  }
501
685
  }
686
+ if (listKey === "eth_phishing_detect_config" /* EthPhishingDetectConfig */) {
687
+ for (const hash of recentlyAddedC2Domains) {
688
+ listSets.c2DomainBlocklist.add(hash);
689
+ }
690
+ for (const hash of recentlyRemovedC2Domains) {
691
+ listSets.c2DomainBlocklist.delete(hash);
692
+ }
693
+ }
502
694
  return {
695
+ c2DomainBlocklist: Array.from(listSets.c2DomainBlocklist),
503
696
  allowlist: Array.from(listSets.allowlist),
504
697
  blocklist: Array.from(listSets.blocklist),
505
698
  fuzzylist: Array.from(listSets.fuzzylist),
@@ -564,9 +757,14 @@ var matchPartsAgainstList = (source, list) => {
564
757
  return target.every((part, index) => source[index] === part);
565
758
  });
566
759
  };
760
+ var sha256Hash = (hostname) => {
761
+ const hashBuffer = sha256(new TextEncoder().encode(hostname.toLowerCase()));
762
+ return bytesToHex(hashBuffer);
763
+ };
567
764
 
568
765
  export {
569
766
  fetchTimeNow,
767
+ roundToNearestMinute,
570
768
  applyDiffs,
571
769
  validateConfig,
572
770
  domainToParts,
@@ -576,18 +774,23 @@ export {
576
774
  domainPartsToDomain,
577
775
  domainPartsToFuzzyForm,
578
776
  matchPartsAgainstList,
777
+ sha256Hash,
579
778
  PhishingDetector,
580
779
  PHISHING_CONFIG_BASE_URL,
581
780
  METAMASK_STALELIST_FILE,
582
781
  METAMASK_HOTLIST_DIFF_FILE,
782
+ CLIENT_SIDE_DETECION_BASE_URL,
783
+ C2_DOMAIN_BLOCKLIST_ENDPOINT,
784
+ C2_DOMAIN_BLOCKLIST_REFRESH_INTERVAL,
583
785
  HOTLIST_REFRESH_INTERVAL,
584
786
  STALELIST_REFRESH_INTERVAL,
585
787
  METAMASK_STALELIST_URL,
586
788
  METAMASK_HOTLIST_DIFF_URL,
789
+ C2_DOMAIN_BLOCKLIST_URL,
587
790
  ListKeys,
588
791
  ListNames,
589
792
  phishingListKeyNameMap,
590
793
  PhishingController,
591
794
  PhishingController_default
592
795
  };
593
- //# sourceMappingURL=chunk-Q42CM6EP.mjs.map
796
+ //# sourceMappingURL=chunk-F7CZ3O4X.mjs.map