diff --git a/packages/snaps-execution-environments/.c8rc.json b/packages/snaps-execution-environments/.c8rc.json index 0337876aaa..032637ed03 100644 --- a/packages/snaps-execution-environments/.c8rc.json +++ b/packages/snaps-execution-environments/.c8rc.json @@ -1,5 +1,5 @@ { - "reporter": ["html", "json-summary", "text", "json"], + "reporter": ["html", "json-summary", "json"], "exclude": ["*.js", "./src/index.ts", "**/*.ava.test.ts"], "report-dir": "./coverage-ava" } diff --git a/packages/snaps-execution-environments/ava.config.js b/packages/snaps-execution-environments/ava.config.js index bd26f77592..b341f9139e 100644 --- a/packages/snaps-execution-environments/ava.config.js +++ b/packages/snaps-execution-environments/ava.config.js @@ -1,9 +1,9 @@ module.exports = () => { return { - concurrency: 5, extensions: ['ts'], require: ['ts-node/register'], verbose: true, files: ['src/**/*.ava.test.ts'], + timeout: '30s', }; }; diff --git a/packages/snaps-execution-environments/jest.config.js b/packages/snaps-execution-environments/jest.config.js index 85daac7887..759caad624 100644 --- a/packages/snaps-execution-environments/jest.config.js +++ b/packages/snaps-execution-environments/jest.config.js @@ -2,20 +2,20 @@ const deepmerge = require('deepmerge'); const baseConfig = require('../../jest.config.base'); +delete baseConfig.coverageThreshold; + module.exports = deepmerge(baseConfig, { - coveragePathIgnorePatterns: ['./src/index.ts', '.ava.test.ts'], - coverageThreshold: { - global: { - branches: 83.93, - functions: 92.25, - lines: 87.07, - statements: 87.18, - }, - }, + coveragePathIgnorePatterns: [ + './src/index.ts', + '.ava.test.ts', + 'update-coverage-thresholds.js', + ], testEnvironment: '/jest.environment.js', testEnvironmentOptions: { customExportConditions: ['node', 'node-addons'], }, testTimeout: 2500, testPathIgnorePatterns: ['.ava.test.ts'], + coverageProvider: 'v8', + coverageReporters: ['html', 'json-summary', 'json'], }); diff --git a/packages/snaps-execution-environments/jest.environment.js b/packages/snaps-execution-environments/jest.environment.js index de208e70d8..dd5774b584 100644 --- a/packages/snaps-execution-environments/jest.environment.js +++ b/packages/snaps-execution-environments/jest.environment.js @@ -13,6 +13,7 @@ module.exports = class CustomTestEnvironment extends TestEnvironment { this.global.TextDecoder = TextDecoder; this.global.ArrayBuffer = ArrayBuffer; this.global.Uint8Array = Uint8Array; + this.global.harden = (param) => param; } } }; diff --git a/packages/snaps-execution-environments/nyc.config.js b/packages/snaps-execution-environments/nyc.config.js new file mode 100644 index 0000000000..d66fed6f4c --- /dev/null +++ b/packages/snaps-execution-environments/nyc.config.js @@ -0,0 +1,10 @@ +/** + * NYC coverage reporter configuration. + */ +module.exports = { + 'check-coverage': true, + branches: 91.02, + lines: 91.36, + functions: 92.85, + statements: 91.36, +}; diff --git a/packages/snaps-execution-environments/package.json b/packages/snaps-execution-environments/package.json index 635ee6cb7f..bc85cbe48c 100644 --- a/packages/snaps-execution-environments/package.json +++ b/packages/snaps-execution-environments/package.json @@ -12,10 +12,10 @@ "dist/" ], "scripts": { - "test": "yarn test:ava && jest && yarn posttest && yarn merge:coverage", - "posttest": "jest-it-up --margin 0.25", + "test": "yarn test:ava && jest && yarn merge:coverage && yarn posttest", + "posttest": "node update-coverage-thresholds.js", "test:ava": "c8 ava", - "merge:coverage": "yarn mkdirp coverage-all && shx cp coverage/coverage-final.json coverage-all/coverage-final-jest.json && shx cp coverage-ava/coverage-final.json coverage-all/coverage-final-ava.json && rimraf 'coverage' 'coverage-ava' && nyc merge coverage-all coverage-merged/merged-coverage.json && nyc report -t coverage-merged --report-dir coverage --reporter=html --reporter=json-summary --reporter=json && rimraf 'coverage-merged' 'coverage-all'", + "merge:coverage": "yarn mkdirp coverage-all && shx cp coverage/coverage-final.json coverage-all/coverage-final-jest.json && shx cp coverage-ava/coverage-final.json coverage-all/coverage-final-ava.json && rimraf 'coverage' 'coverage-ava' && nyc merge coverage-all coverage-merged/merged-coverage.json && nyc report -t coverage-merged --report-dir coverage --reporter=text --reporter=html --reporter=json-summary --reporter=json && rimraf 'coverage-merged' 'coverage-all'", "test:ci": "yarn test", "test:watch": "jest --watch", "lint:eslint": "eslint . --cache --ext js,ts", diff --git a/packages/snaps-execution-environments/src/common/endowments/abortController.ts b/packages/snaps-execution-environments/src/common/endowments/abortController.ts new file mode 100644 index 0000000000..ab16d3a45e --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/abortController.ts @@ -0,0 +1,16 @@ +/** + * Creates AbortController function hardened by SES. + * + * @returns An object with the attenuated `AbortController` function. + */ +const createAbortController = () => { + return { + AbortController: harden(AbortController), + } as const; +}; + +const endowmentModule = { + names: ['AbortController'] as const, + factory: createAbortController, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts b/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts new file mode 100644 index 0000000000..9c50772767 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/abortSignal.ts @@ -0,0 +1,16 @@ +/** + * Creates AbortSignal function hardened by SES. + * + * @returns An object with the attenuated `AbortSignal` function. + */ +const createAbortSignal = () => { + return { + AbortSignal: harden(AbortSignal), + } as const; +}; + +const endowmentModule = { + names: ['AbortSignal'] as const, + factory: createAbortSignal, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts b/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts new file mode 100644 index 0000000000..a705005677 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/arrayBuffer.ts @@ -0,0 +1,16 @@ +/** + * Creates ArrayBuffer function hardened by SES. + * + * @returns An object with the attenuated `ArrayBuffer` function. + */ +const createArrayBuffer = () => { + return { + ArrayBuffer: harden(ArrayBuffer), + } as const; +}; + +const endowmentModule = { + names: ['ArrayBuffer'] as const, + factory: createArrayBuffer, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/atob.ts b/packages/snaps-execution-environments/src/common/endowments/atob.ts new file mode 100644 index 0000000000..d3024020ef --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/atob.ts @@ -0,0 +1,16 @@ +/** + * Creates atob function hardened by SES. + * + * @returns An object with the attenuated `atob` function. + */ +const createAtob = () => { + return { + atob: harden(atob), + } as const; +}; + +const endowmentModule = { + names: ['atob'] as const, + factory: createAtob, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigInt.ts b/packages/snaps-execution-environments/src/common/endowments/bigInt.ts new file mode 100644 index 0000000000..ea5219c279 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigInt.ts @@ -0,0 +1,16 @@ +/** + * Creates BigInt function hardened by SES. + * + * @returns An object with the attenuated `BigInt` function. + */ +const createBigInt = () => { + return { + BigInt: harden(BigInt), + } as const; +}; + +const endowmentModule = { + names: ['BigInt'] as const, + factory: createBigInt, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts b/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts new file mode 100644 index 0000000000..f34be30117 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigInt64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates BigInt64Array function hardened by SES. + * + * @returns An object with the attenuated `BigInt64Array` function. + */ +const createBigInt64Array = () => { + return { + BigInt64Array: harden(BigInt64Array), + } as const; +}; + +const endowmentModule = { + names: ['BigInt64Array'] as const, + factory: createBigInt64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts b/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts new file mode 100644 index 0000000000..3771739251 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/bigUint64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates BigUint64Array function hardened by SES. + * + * @returns An object with the attenuated `BigUint64Array` function. + */ +const createBigUint64Array = () => { + return { + BigUint64Array: harden(BigUint64Array), + } as const; +}; + +const endowmentModule = { + names: ['BigUint64Array'] as const, + factory: createBigUint64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/btoa.ts b/packages/snaps-execution-environments/src/common/endowments/btoa.ts new file mode 100644 index 0000000000..84e0d7af65 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/btoa.ts @@ -0,0 +1,16 @@ +/** + * Creates btoa function hardened by SES. + * + * @returns An object with the attenuated `btoa` function. + */ +const createBtoa = () => { + return { + btoa: harden(btoa), + } as const; +}; + +const endowmentModule = { + names: ['btoa'] as const, + factory: createBtoa, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/crypto.ts b/packages/snaps-execution-environments/src/common/endowments/crypto.ts index cc4f498939..7252e61bed 100644 --- a/packages/snaps-execution-environments/src/common/endowments/crypto.ts +++ b/packages/snaps-execution-environments/src/common/endowments/crypto.ts @@ -16,7 +16,10 @@ const createCrypto = () => { // TODO: Figure out if this is enough long-term or if we should use a polyfill. /* eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires, node/global-require */ const crypto = require('crypto').webcrypto; - return { crypto, SubtleCrypto: crypto.subtle.constructor } as const; + return { + crypto: harden(crypto), + SubtleCrypto: harden(crypto.subtle.constructor), + } as const; }; const endowmentModule = { diff --git a/packages/snaps-execution-environments/src/common/endowments/dataView.ts b/packages/snaps-execution-environments/src/common/endowments/dataView.ts new file mode 100644 index 0000000000..b2e3ed3260 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/dataView.ts @@ -0,0 +1,16 @@ +/** + * Creates DataView function hardened by SES. + * + * @returns An object with the attenuated `DataView` function. + */ +const createDataView = () => { + return { + DataView: harden(DataView), + } as const; +}; + +const endowmentModule = { + names: ['DataView'] as const, + factory: createDataView, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts new file mode 100644 index 0000000000..4e65b01e6a --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/endowmentModules.ava.test.ts @@ -0,0 +1,230 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'ses'; +import test from 'ava'; +// FinalizationRegistry will fix type errors in tests related to network endowment. +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars +import FinalizationRegistry from 'globals'; + +import abortController from './abortController'; +import abortSignal from './abortSignal'; +import arrayBuffer from './arrayBuffer'; +import atobEndowment from './atob'; +import bigInt from './bigInt'; +import bigInt64Array from './bigInt64Array'; +import bigUint64Array from './bigUint64Array'; +import btoaEndowment from './btoa'; +import crypto from './crypto'; +import dataView from './dataView'; +import float32Array from './float32Array'; +import float64Array from './float64Array'; +import int16Array from './int16Array'; +import int32Array from './int32Array'; +import int8Array from './int8Array'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import textDecoder from './textDecoder'; +import textEncoder from './textEncoder'; +import timeout from './timeout'; +import uint16Array from './uint16Array'; +import uint32Array from './uint32Array'; +import uint8array from './uint8array'; +import uint8ClampedArray from './uint8ClampedArray'; +import url from './url'; +import webAssembly from './webAssembly'; + +// Note: harden is only defined after calling lockdown +lockdown({ + domainTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', +}); + +test(`AbortController endowment module has expected properties`, (expect) => { + const { names, factory } = abortController; + + expect.deepEqual(names, ['AbortController']); + expect.is(typeof factory, 'function'); +}); + +test(`AbortSignal endowment module has expected properties`, (expect) => { + const { names, factory } = abortSignal; + + expect.deepEqual(names, ['AbortSignal']); + expect.is(typeof factory, 'function'); +}); + +test(`ArrayBuffer endowment module has expected properties`, (expect) => { + const { names, factory } = arrayBuffer; + + expect.deepEqual(names, ['ArrayBuffer']); + expect.is(typeof factory, 'function'); +}); + +test(`atob endowment module has expected properties`, (expect) => { + const { names, factory } = atobEndowment; + + expect.deepEqual(names, ['atob']); + expect.is(typeof factory, 'function'); +}); + +test(`BigInt endowment module has expected properties`, (expect) => { + const { names, factory } = bigInt; + + expect.deepEqual(names, ['BigInt']); + expect.is(typeof factory, 'function'); +}); + +test(`BigInt64Array endowment module has expected properties`, (expect) => { + const { names, factory } = bigInt64Array; + + expect.deepEqual(names, ['BigInt64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`BigUint64Array endowment module has expected properties`, (expect) => { + const { names, factory } = bigUint64Array; + + expect.deepEqual(names, ['BigUint64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`btoa endowment module has expected properties`, (expect) => { + const { names, factory } = btoaEndowment; + + expect.deepEqual(names, ['btoa']); + expect.is(typeof factory, 'function'); +}); + +test(`crypto endowment module has expected properties`, (expect) => { + const { names, factory } = crypto; + + expect.deepEqual(names, ['crypto', 'SubtleCrypto']); + expect.is(typeof factory, 'function'); +}); + +test(`DataView endowment module has expected properties`, (expect) => { + const { names, factory } = dataView; + + expect.deepEqual(names, ['DataView']); + expect.is(typeof factory, 'function'); +}); + +test(`Float32Array endowment module has expected properties`, (expect) => { + const { names, factory } = float32Array; + + expect.deepEqual(names, ['Float32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Float64Array endowment module has expected properties`, (expect) => { + const { names, factory } = float64Array; + + expect.deepEqual(names, ['Float64Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int8Array endowment module has expected properties`, (expect) => { + const { names, factory } = int8Array; + + expect.deepEqual(names, ['Int8Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int16Array endowment module has expected properties`, (expect) => { + const { names, factory } = int16Array; + + expect.deepEqual(names, ['Int16Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Int32Array endowment module has expected properties`, (expect) => { + const { names, factory } = int32Array; + + expect.deepEqual(names, ['Int32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Interval endowment module has expected properties`, (expect) => { + const { names, factory } = interval; + + expect.deepEqual(names, ['setInterval', 'clearInterval']); + expect.is(typeof factory, 'function'); +}); + +test(`Math endowment module has expected properties`, (expect) => { + const { names, factory } = math; + + expect.deepEqual(names, ['Math']); + expect.is(typeof factory, 'function'); +}); + +test(`Network endowment module has expected properties`, (expect) => { + const { names, factory } = network; + + expect.deepEqual(names, ['fetch', 'WebSocket']); + expect.is(typeof factory, 'function'); +}); + +test(`TextDecoder endowment module has expected properties`, (expect) => { + const { names, factory } = textDecoder; + + expect.deepEqual(names, ['TextDecoder']); + expect.is(typeof factory, 'function'); +}); + +test(`TextEncoder endowment module has expected properties`, (expect) => { + const { names, factory } = textEncoder; + + expect.deepEqual(names, ['TextEncoder']); + expect.is(typeof factory, 'function'); +}); + +test(`Timeout endowment module has expected properties`, (expect) => { + const { names, factory } = timeout; + + expect.deepEqual(names, ['setTimeout', 'clearTimeout']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint8Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint8array; + + expect.deepEqual(names, ['Uint8Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint8ClampedArray endowment module has expected properties`, (expect) => { + const { names, factory } = uint8ClampedArray; + + expect.deepEqual(names, ['Uint8ClampedArray']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint16Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint16Array; + + expect.deepEqual(names, ['Uint16Array']); + expect.is(typeof factory, 'function'); +}); + +test(`Uint32Array endowment module has expected properties`, (expect) => { + const { names, factory } = uint32Array; + + expect.deepEqual(names, ['Uint32Array']); + expect.is(typeof factory, 'function'); +}); + +test(`URL endowment module has expected properties`, (expect) => { + const { names, factory } = url; + + expect.deepEqual(names, ['URL']); + expect.is(typeof factory, 'function'); +}); + +test(`WebAssembly endowment module has expected properties`, (expect) => { + const { names, factory } = webAssembly; + + expect.deepEqual(names, ['WebAssembly']); + expect.is(typeof factory, 'function'); +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts b/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts new file mode 100644 index 0000000000..826fffdc1d --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/endowmentRegistry.ts @@ -0,0 +1,62 @@ +import abortController from './abortController'; +import abortSignal from './abortSignal'; +import arrayBuffer from './arrayBuffer'; +import atobEndowment from './atob'; +import bigInt from './bigInt'; +import bigInt64Array from './bigInt64Array'; +import bigUint64Array from './bigUint64Array'; +import btoaEndowment from './btoa'; +import crypto from './crypto'; +import dataView from './dataView'; +import float32Array from './float32Array'; +import float64Array from './float64Array'; +import int16Array from './int16Array'; +import int32Array from './int32Array'; +import int8Array from './int8Array'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import textDecoder from './textDecoder'; +import textEncoder from './textEncoder'; +import timeout from './timeout'; +import uint16Array from './uint16Array'; +import uint32Array from './uint32Array'; +import uint8array from './uint8array'; +import uint8ClampedArray from './uint8ClampedArray'; +import url from './url'; +import webAssembly from './webAssembly'; + +/** + * Registry of all attenuated and/or hardened endowments. + */ +const registeredEndowments = [ + atobEndowment, + btoaEndowment, + bigInt, + crypto, + math, + timeout, + textDecoder, + textEncoder, + url, + webAssembly, + interval, + int8Array, + uint8array, + uint8ClampedArray, + int16Array, + uint16Array, + int32Array, + uint32Array, + float32Array, + float64Array, + bigInt64Array, + bigUint64Array, + dataView, + arrayBuffer, + abortController, + abortSignal, + network, +]; + +export default registeredEndowments; diff --git a/packages/snaps-execution-environments/src/common/endowments/float32Array.ts b/packages/snaps-execution-environments/src/common/endowments/float32Array.ts new file mode 100644 index 0000000000..2f2c43d0fb --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/float32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Float32Array function hardened by SES. + * + * @returns An object with the attenuated `Float32Array` function. + */ +const createFloat32Array = () => { + return { + Float32Array: harden(Float32Array), + } as const; +}; + +const endowmentModule = { + names: ['Float32Array'] as const, + factory: createFloat32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/float64Array.ts b/packages/snaps-execution-environments/src/common/endowments/float64Array.ts new file mode 100644 index 0000000000..87e327e40a --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/float64Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Float64Array function hardened by SES. + * + * @returns An object with the attenuated `Float64Array` function. + */ +const createFloat64Array = () => { + return { + Float64Array: harden(Float64Array), + } as const; +}; + +const endowmentModule = { + names: ['Float64Array'] as const, + factory: createFloat64Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts new file mode 100644 index 0000000000..b6b3da3d63 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/hardenedEndowments.ava.test.ts @@ -0,0 +1,264 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'ses'; +import test from 'ava'; +// FinalizationRegistry will fix type errors in tests related to network endowment. +// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-unused-vars +import FinalizationRegistry from 'globals'; + +import Crypto from './crypto'; +import registeredEndowments from './endowmentRegistry'; +import interval from './interval'; +import math from './math'; +import network from './network'; +import { walkPropertiesAndSearchForReference } from './security-utils'; +import timeout from './timeout'; + +const globalThis = global; + +// Note: harden is only defined after calling lockdown +lockdown({ + domainTaming: 'unsafe', + errorTaming: 'unsafe', + stackFiltering: 'verbose', +}); + +// Call factory method for each endowment. It will harden each of them. +// This is how endowments are created (see index.ts / createEndowments()). +registeredEndowments.forEach((endowment) => endowment.factory()); + +// Specially attenuated endowments or endowments that require +// to be imported in a different way +const { SubtleCrypto, crypto } = Crypto.factory(); +const { + setTimeout: setTimeoutAttenuated, + clearTimeout: clearTimeoutAttenuated, +} = timeout.factory(); +const { + setInterval: setIntervalAttenuated, + clearInterval: clearIntervalAttenuated, +} = interval.factory(); +const { Math: mathAttenuated } = math.factory(); +const { fetch: fetchAttenuated, WebSocket: WebSocketAttenuated } = + network.factory(); + +// All the endowments to be tested +const testSubjects = { + // --- Constructor functions + BigInt: { + endowments: { BigInt }, + factory: () => BigInt(3), + }, + SubtleCrypto: { + endowments: { SubtleCrypto }, + factory: () => undefined, + }, + TextDecoder: { + endowments: { TextDecoder }, + factory: () => new TextDecoder(), + }, + TextEncoder: { + endowments: { TextEncoder }, + factory: () => new TextEncoder(), + }, + URL: { + endowments: { URL }, + factory: () => new URL('https://metamask.io/snaps/'), + }, + Int8Array: { + endowments: { Int8Array }, + factory: () => new Int8Array(), + }, + Uint8Array: { + endowments: { Uint8Array }, + factory: () => new Uint8Array(), + }, + Uint8ClampedArray: { + endowments: { Uint8ClampedArray }, + factory: () => new Uint8ClampedArray(), + }, + Int16Array: { + endowments: { Int16Array }, + factory: () => new Int16Array(), + }, + Uint16Array: { + endowments: { Uint16Array }, + factory: () => new Uint16Array(), + }, + Int32Array: { + endowments: { Int32Array }, + factory: () => new Int32Array(), + }, + Uint32Array: { + endowments: { Uint32Array }, + factory: () => new Uint32Array(), + }, + Float32Array: { + endowments: { Float32Array }, + factory: () => new Float32Array(), + }, + Float64Array: { + endowments: { Float64Array }, + factory: () => new Float64Array(), + }, + BigInt64Array: { + endowments: { BigInt64Array }, + factory: () => new BigInt64Array(), + }, + BigUint64Array: { + endowments: { BigUint64Array }, + factory: () => new BigUint64Array(), + }, + DataView: { + endowments: { DataView, ArrayBuffer }, + factory: () => new DataView(new ArrayBuffer(64)), + }, + ArrayBuffer: { + endowments: { ArrayBuffer }, + factory: () => new ArrayBuffer(64), + }, + AbortController: { + endowments: { AbortController }, + factory: () => new AbortController(), + }, + AbortSignal: { + endowments: { AbortSignal }, + // @ts-expect-error This is not ok, but will provide access to its prototype + factory: () => AbortSignal.abort(), + }, + WebSocketAttenuated: { + endowments: { WebSocketAttenuated }, + factory: () => undefined, + }, + // --- Objects + console: { + endowments: { console }, + factory: () => console, + }, + crypto: { + endowments: { crypto }, + factory: () => crypto, + }, + mathAttenuated: { + endowments: { mathAttenuated }, + factory: () => mathAttenuated, + }, + WebAssembly: { + endowments: { WebAssembly }, + factory: () => WebAssembly, + }, + // --- Functions + atob: { + endowments: { atob }, + factory: () => atob('U25hcHM='), + }, + btoa: { + endowments: { btoa }, + factory: () => btoa('Snaps'), + }, + setTimeoutAttenuated: { + endowments: { setTimeoutAttenuated }, + factory: () => setTimeoutAttenuated((param: unknown) => param, 1), + }, + clearTimeoutAttenuated: { + endowments: { clearTimeoutAttenuated }, + factory: () => undefined, + }, + setIntervalAttenuated: { + endowments: { setIntervalAttenuated }, + factory: () => setIntervalAttenuated((param: unknown) => param, 100000), + }, + clearIntervalAttenuated: { + endowments: { clearIntervalAttenuated }, + factory: () => undefined, + }, + fetchAttenuated: { + endowments: { fetchAttenuated }, + factory: () => undefined, + }, +}; + +// These are fake types just to make this test work with the TypeScript +type HardenedEndowmentSubject = { + // eslint-disable-next-line @typescript-eslint/naming-convention + __flag: unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + prototype: { __flag: unknown }; +}; +type HardenedEndowmentInstance = { + // eslint-disable-next-line @typescript-eslint/naming-convention + __flag: unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + __proto__: { __flag: unknown }; +}; + +/** + * Test helper function. + * + * @param subject - Test subject (instance, object, function). + * @param factory - Factory that creates an instance using constructor function. + * @returns Array of error messages. + */ +function code( + subject: HardenedEndowmentSubject, + factory: () => HardenedEndowmentInstance, +): unknown[] { + const log = []; + const instance = factory(); + try { + subject.__flag = 'not_secure'; + } catch (error) { + log.push(error.message); + } + try { + if (instance) { + instance.__flag = 'not_secure'; + } + } catch (error) { + log.push(error.message); + } + try { + subject.prototype.__flag = 'not_secure'; + } catch (error) { + log.push(error.message); + } + try { + if (instance) { + // eslint-disable-next-line @typescript-eslint/naming-convention + Object.setPrototypeOf(instance, { __flag: 'not_secure' }); + } + } catch (error) { + log.push(error.message); + } + return log; +} + +Object.entries(testSubjects).forEach(([name, { endowments, factory }]) => { + test(`hardening protects ${name}`, (expect) => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const source = `;(${code})(${name},${factory})`; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const subject = endowments[name]; + const c1 = new Compartment(endowments, {}, {}); + const errors = c1.evaluate(source); + const instance = factory(); + + expect.falsy(subject.__flag, 'flag is leaking via endowed object'); + if (instance) { + expect.falsy(instance.__flag, 'flag is leaking via prototype'); + } + expect.assert(errors.length > 0); + }); + + if (factory()) { + test(`endowment ${name} does not leak global this`, (expect) => { + const instance = factory(); + const searchResult = walkPropertiesAndSearchForReference( + instance, + globalThis, + ); + + expect.is(searchResult, false); + }); + } +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/index.ts b/packages/snaps-execution-environments/src/common/endowments/index.ts index faf49ecb39..09d5df63ce 100644 --- a/packages/snaps-execution-environments/src/common/endowments/index.ts +++ b/packages/snaps-execution-environments/src/common/endowments/index.ts @@ -3,11 +3,7 @@ import { SnapsGlobalObject } from '@metamask/snaps-utils'; import { hasProperty } from '@metamask/utils'; import { rootRealmGlobal } from '../globalObject'; -import crypto from './crypto'; -import interval from './interval'; -import math from './math'; -import network from './network'; -import timeout from './timeout'; +import registeredEndowments from './endowmentRegistry'; type EndowmentFactoryResult = { /** @@ -27,15 +23,12 @@ type EndowmentFactoryResult = { * the same factory function, but we only call each factory once for each snap. * See {@link createEndowments} for details. */ -const endowmentFactories = [timeout, interval, network, crypto, math].reduce( - (factories, builder) => { - builder.names.forEach((name) => { - factories.set(name, builder.factory); - }); - return factories; - }, - new Map EndowmentFactoryResult>(), -); +const endowmentFactories = registeredEndowments.reduce((factories, builder) => { + builder.names.forEach((name) => { + factories.set(name, builder.factory); + }); + return factories; +}, new Map EndowmentFactoryResult>()); /** * Gets the endowments for a particular Snap. Some endowments, like `setTimeout` diff --git a/packages/snaps-execution-environments/src/common/endowments/int16Array.ts b/packages/snaps-execution-environments/src/common/endowments/int16Array.ts new file mode 100644 index 0000000000..3cc05dca56 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int16Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int16Array function hardened by SES. + * + * @returns An object with the attenuated `Int16Array` function. + */ +const createInt16Array = () => { + return { + Int16Array: harden(Int16Array), + } as const; +}; + +const endowmentModule = { + names: ['Int16Array'] as const, + factory: createInt16Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/int32Array.ts b/packages/snaps-execution-environments/src/common/endowments/int32Array.ts new file mode 100644 index 0000000000..009f5fed4b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int32Array function hardened by SES. + * + * @returns An object with the attenuated `Int32Array` function. + */ +const createInt32Array = () => { + return { + Int32Array: harden(Int32Array), + } as const; +}; + +const endowmentModule = { + names: ['Int32Array'] as const, + factory: createInt32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/int8Array.ts b/packages/snaps-execution-environments/src/common/endowments/int8Array.ts new file mode 100644 index 0000000000..824ac60050 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/int8Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Int8Array function hardened by SES. + * + * @returns An object with the attenuated `Int8Array` function. + */ +const createInt8Array = () => { + return { + Int8Array: harden(Int8Array), + } as const; +}; + +const endowmentModule = { + names: ['Int8Array'] as const, + factory: createInt8Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/interval.ts b/packages/snaps-execution-environments/src/common/endowments/interval.ts index bf7753ec6d..76c4b68934 100644 --- a/packages/snaps-execution-environments/src/common/endowments/interval.ts +++ b/packages/snaps-execution-environments/src/common/endowments/interval.ts @@ -17,13 +17,15 @@ const createInterval = () => { `The interval handler must be a function. Received: ${typeof handler}`, ); } + harden(handler); const handle = Object.freeze({}); const platformHandle = setInterval(handler, timeout); registeredHandles.set(handle, platformHandle); - return handle; + return harden(handle); }; const _clearInterval = (handle: unknown): void => { + harden(handle); const platformHandle = registeredHandles.get(handle); if (platformHandle !== undefined) { clearInterval(platformHandle as any); @@ -38,8 +40,8 @@ const createInterval = () => { }; return { - setInterval: _setInterval, - clearInterval: _clearInterval, + setInterval: harden(_setInterval), + clearInterval: harden(_clearInterval), teardownFunction, } as const; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/math.ts b/packages/snaps-execution-environments/src/common/endowments/math.ts index 942d123133..d21446af8c 100644 --- a/packages/snaps-execution-environments/src/common/endowments/math.ts +++ b/packages/snaps-execution-environments/src/common/endowments/math.ts @@ -22,7 +22,7 @@ function createMath() { return { ...target, [key]: rootRealmGlobal.Math[key] }; }, {}); - return { + return harden({ Math: { ...math, random: () => { @@ -45,7 +45,7 @@ function createMath() { return crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32; }, }, - }; + }); } const endowmentModule = { diff --git a/packages/snaps-execution-environments/src/common/endowments/network.ts b/packages/snaps-execution-environments/src/common/endowments/network.ts index 448a23e358..10df5b83e0 100644 --- a/packages/snaps-execution-environments/src/common/endowments/network.ts +++ b/packages/snaps-execution-environments/src/common/endowments/network.ts @@ -174,7 +174,7 @@ const createNetwork = () => { () => openConnections.delete(openBodyConnection), ); } - return res; + return harden(res); }; /** @@ -211,7 +211,7 @@ const createNetwork = () => { } get onclose(): WebSocketCallback | null { - return this.#oncloseOriginal; + return harden(this.#oncloseOriginal); } set onclose(callback: WebSocketCallback | null) { @@ -222,7 +222,7 @@ const createNetwork = () => { } get onerror(): ((this: WebSocket, ev: Event) => any) | null { - return this.#onerrorOriginal; + return harden(this.#onerrorOriginal); } set onerror(callback: ((this: WebSocket, ev: Event) => any) | null) { @@ -231,7 +231,7 @@ const createNetwork = () => { } get onmessage(): ((this: WebSocket, ev: MessageEvent) => any) | null { - return this.#onmessageOriginal; + return harden(this.#onmessageOriginal); } set onmessage( @@ -242,7 +242,7 @@ const createNetwork = () => { } get onopen(): ((this: WebSocket, ev: Event) => any) | null { - return this.#onopenOriginal; + return harden(this.#onopenOriginal); } set onopen(callback: ((this: WebSocket, ev: Event) => any) | null) { @@ -278,7 +278,7 @@ const createNetwork = () => { /* eslint-enable @typescript-eslint/naming-convention */ get binaryType(): BinaryType { - return this.#socket.binaryType; + return harden(this.#socket.binaryType); } set binaryType(value: BinaryType) { @@ -454,8 +454,8 @@ const createNetwork = () => { }; return { - fetch: _fetch, - WebSocket: _WebSocket, + fetch: harden(_fetch), + WebSocket: harden(_WebSocket), teardownFunction, }; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts new file mode 100644 index 0000000000..b560cbdd4b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/index.ts @@ -0,0 +1,5 @@ +/** + * Endowment security related test utilities. + */ + +export * from './object-walker'; diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts new file mode 100644 index 0000000000..21b799361b --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.test.ts @@ -0,0 +1,182 @@ +import { walkPropertiesAndSearchForReference } from './object-walker'; + +const GLOBAL_THIS = global; +const simpleObject = { simple: 'object' }; +const TEST_OBJECT = { + something: { + that: { + is: { + holding: { + nested: { + values: [1, 2, 3], + and: { + strings: ['a', 'b', 'c', 'd'], + }, + or: { + objects: { + one: Date, + two: WebAssembly, + three: {}, + four: { + which: { + is: { + hiding: { + globalThis: {}, // Add global this reference when needed + }, + }, + }, + }, + five: { + is: { + empty: {}, + }, + }, + six: () => true, + }, + }, + }, + }, + }, + }, + }, + whatever: { + can: { + be: { + here: true, + or: undefined, + orMaybe: null, + }, + }, + simple: simpleObject, + simpleAgain: simpleObject, + }, + circular: { + reference: { + lives: { + here: {}, + }, + }, + }, +}; +TEST_OBJECT.circular.reference.lives.here = TEST_OBJECT; + +describe('Object walker', () => { + it('should not detect given reference in a test object', () => { + const justRandomPlainObject = { something: 'something' }; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + justRandomPlainObject, + ); + + expect(result).toBe(false); + }); + + it('should detect WebAssembly reference in a test object', () => { + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + WebAssembly, + ); + + expect(result).toBe(true); + }); + + it('should detect object added to WebAssembly', () => { + const secretObject = { + thisObject: { + is: 'secret', + }, + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + WebAssembly.secretObject = secretObject; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + secretObject, + ); + + expect(result).toBe(true); + }); + + it('should detect newly added object to test object', () => { + const secretObject = new TextDecoder(); + TEST_OBJECT.something.that.is.holding.nested.or.objects.three = + secretObject; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + secretObject, + ); + + expect(result).toBe(true); + }); + + it('should not detect an empty object', () => { + const randomEmptyObject = {}; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + randomEmptyObject, + ); + + expect(result).toBe(false); + }); + + it('should detect global this inside the test object', () => { + TEST_OBJECT.something.that.is.holding.nested.or.objects.four.which.is.hiding.globalThis = + GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this attached to a function', () => { + // @ts-expect-error This is intentional hack to test security features + TEST_OBJECT.something.that.is.holding.nested.or.objects.six.globalThis = + GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + TEST_OBJECT, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this inside the TextDecoder instance', () => { + const textDecoder = new TextDecoder(); + // @ts-expect-error This error is expected because this is security related test + textDecoder.hiddenReference = GLOBAL_THIS; + const result = walkPropertiesAndSearchForReference( + textDecoder, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); + + it('should detect global this inside the object that inherited another one', () => { + const textDecoder = new TextDecoder(); + // @ts-expect-error This error is expected because this is security related test + textDecoder.hiddenReference = GLOBAL_THIS; + const testSubject = Object.create(textDecoder, { + foo: { + writable: true, + configurable: true, + value: 'something_valuable', + }, + }); + const finalTestSubject = Object.create(testSubject, { + foo: { + writable: true, + configurable: true, + value: 'something_final', + }, + }); + const result = walkPropertiesAndSearchForReference( + finalTestSubject, + GLOBAL_THIS, + ); + + expect(result).toBe(true); + }); +}); diff --git a/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts new file mode 100644 index 0000000000..e8a71cb530 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/security-utils/object-walker.ts @@ -0,0 +1,95 @@ +/** + * Object Walker function is testing utility function used to detect when + * a certain object reference is present within the provided object. + * + * Note: Walker will search only for object references, primitive types are omitted. + * This function cannot work with very large nested structures because of + * the call stack size limit. Further optimizations are required. + * + * @param subject - Object-like structure to be searched for a reference. + * @param targetReference - Target reference. + * @returns True if reference to a target is found, false otherwise. + */ +export function walkPropertiesAndSearchForReference( + subject: unknown, + targetReference: unknown, +) { + const seenObjects = new Set(); + + /** + * Recursively walk properties and search for reference. + * + * @param currentValue - Object-like structure to be searched for a reference. + * @param target - Target reference. + * @returns True if reference to a target is found, false otherwise. + */ + function walkAndSearch(currentValue: unknown, target: unknown): boolean { + // Check for nulls or undefined and skip further process + if (currentValue === undefined || currentValue === null) { + return false; + } + // Check value type and stop process if its a primitive + const typeOfValue = typeof currentValue; + if ( + typeOfValue === 'bigint' || + typeOfValue === 'boolean' || + typeOfValue === 'number' || + typeOfValue === 'string' || + typeOfValue === 'symbol' + ) { + return false; + } + + // Circular object detection (handling) + // Check if the same object already exists + if (seenObjects.has(currentValue)) { + return false; + } + // Add new object to the seen objects set + // Only the plain objects should be added (Primitive types are skipped) + seenObjects.add(currentValue); + + // TODO: Investigate and find a reason why this is failing + // for some objects, possibly __proto__ + let objectProperties: [string, any][] = []; + try { + // Extract object properties + objectProperties = Object.entries(currentValue); + // Extract object prototype and add to an array for analysis + const objectProto = Object.getPrototypeOf(currentValue); + if (objectProto) { + objectProperties.push(['__proto__', objectProto]); + } + } catch (error) { + console.log( + `Could not process object entries. Error message: ${error.message}`, + ); + } + + return objectProperties.reduce( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (result, [key, nestedValue]) => { + if (result) { + return true; + } + + if (nestedValue === target) { + return true; + } + + const branchSearchResult = walkAndSearch(nestedValue, target); + + // Circular object detection + // Once a child node is visited and processed remove it from the set. + // This will prevent false positives with the same adjacent objects. + seenObjects.delete(currentValue); + + return branchSearchResult; + }, + // Starting with negative result + false, + ); + } + + return walkAndSearch(subject, targetReference); +} diff --git a/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts b/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts new file mode 100644 index 0000000000..8848449573 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/textDecoder.ts @@ -0,0 +1,16 @@ +/** + * Creates TextDecoder function hardened by SES. + * + * @returns An object with the attenuated `TextDecoder` function. + */ +const createTextDecoder = () => { + return { + TextDecoder: harden(TextDecoder), + } as const; +}; + +const endowmentModule = { + names: ['TextDecoder'] as const, + factory: createTextDecoder, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts b/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts new file mode 100644 index 0000000000..cefb2f3b65 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/textEncoder.ts @@ -0,0 +1,16 @@ +/** + * Creates TextEncoder function hardened by SES. + * + * @returns An object with the attenuated `TextEncoder` function. + */ +const createTextEncoder = () => { + return { + TextEncoder: harden(TextEncoder), + } as const; +}; + +const endowmentModule = { + names: ['TextEncoder'] as const, + factory: createTextEncoder, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts deleted file mode 100644 index 3f1f33665a..0000000000 --- a/packages/snaps-execution-environments/src/common/endowments/timeout.ava.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import test from 'ava'; - -import timeout from './timeout'; - -test('modifying handler should not be allowed and error should be thrown', (expect) => { - const { setTimeout: _setTimeout } = timeout.factory(); - - const handle = _setTimeout((param: unknown) => param, 100); - - expect.throws( - () => { - // @ts-expect-error Ignore because this is supposed to cause an error - handle.whatever = 'something'; - }, - { - message: `Cannot add property whatever, object is not extensible`, - }, - ); -}); diff --git a/packages/snaps-execution-environments/src/common/endowments/timeout.ts b/packages/snaps-execution-environments/src/common/endowments/timeout.ts index b0508cd7d1..360845ee25 100644 --- a/packages/snaps-execution-environments/src/common/endowments/timeout.ts +++ b/packages/snaps-execution-environments/src/common/endowments/timeout.ts @@ -17,7 +17,7 @@ const createTimeout = () => { `The timeout handler must be a function. Received: ${typeof handler}`, ); } - + harden(handler); const handle = Object.freeze({}); const platformHandle = setTimeout(() => { registeredHandles.delete(handle); @@ -25,7 +25,7 @@ const createTimeout = () => { }, timeout); registeredHandles.set(handle, platformHandle); - return handle; + return harden(handle); }; const _clearTimeout = (handle: unknown): void => { @@ -43,8 +43,8 @@ const createTimeout = () => { }; return { - setTimeout: _setTimeout, - clearTimeout: _clearTimeout, + setTimeout: harden(_setTimeout), + clearTimeout: harden(_clearTimeout), teardownFunction, } as const; }; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts b/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts new file mode 100644 index 0000000000..bc25170821 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint16Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint16Array function hardened by SES. + * + * @returns An object with the attenuated `Uint16Array` function. + */ +const createUint16Array = () => { + return { + Uint16Array: harden(Uint16Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint16Array'] as const, + factory: createUint16Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts b/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts new file mode 100644 index 0000000000..0b8cb4a5b6 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint32Array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint32Array function hardened by SES. + * + * @returns An object with the attenuated `Uint32Array` function. + */ +const createUint32Array = () => { + return { + Uint32Array: harden(Uint32Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint32Array'] as const, + factory: createUint32Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts b/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts new file mode 100644 index 0000000000..5fdedd7150 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint8ClampedArray.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint8ClampedArray function hardened by SES. + * + * @returns An object with the attenuated `Uint8ClampedArray` function. + */ +const createUint8ClampedArray = () => { + return { + Uint8ClampedArray: harden(Uint8ClampedArray), + } as const; +}; + +const endowmentModule = { + names: ['Uint8ClampedArray'] as const, + factory: createUint8ClampedArray, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/uint8array.ts b/packages/snaps-execution-environments/src/common/endowments/uint8array.ts new file mode 100644 index 0000000000..3e3071f8c0 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/uint8array.ts @@ -0,0 +1,16 @@ +/** + * Creates Uint8Array function hardened by SES. + * + * @returns An object with the attenuated `Uint8Array` function. + */ +const createUint8Array = () => { + return { + Uint8Array: harden(Uint8Array), + } as const; +}; + +const endowmentModule = { + names: ['Uint8Array'] as const, + factory: createUint8Array, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/url.ts b/packages/snaps-execution-environments/src/common/endowments/url.ts new file mode 100644 index 0000000000..57e5785c05 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/url.ts @@ -0,0 +1,16 @@ +/** + * Creates URL function hardened by SES. + * + * @returns An object with the attenuated `URL` function. + */ +const createURL = () => { + return { + URL: harden(URL), + } as const; +}; + +const endowmentModule = { + names: ['URL'] as const, + factory: createURL, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts b/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts new file mode 100644 index 0000000000..2b648f0df4 --- /dev/null +++ b/packages/snaps-execution-environments/src/common/endowments/webAssembly.ts @@ -0,0 +1,16 @@ +/** + * Creates WebAssembly function hardened by SES. + * + * @returns An object with the attenuated `WebAssembly` function. + */ +const createWebAssembly = () => { + return { + WebAssembly: harden(WebAssembly), + } as const; +}; + +const endowmentModule = { + names: ['WebAssembly'] as const, + factory: createWebAssembly, +}; +export default endowmentModule; diff --git a/packages/snaps-execution-environments/update-coverage-thresholds.js b/packages/snaps-execution-environments/update-coverage-thresholds.js new file mode 100644 index 0000000000..cfa94256f8 --- /dev/null +++ b/packages/snaps-execution-environments/update-coverage-thresholds.js @@ -0,0 +1,76 @@ +// This script is a custom replacement for jest-it-up +// Since two test runners are used in the snaps-execution-environments package, +// it is required that coverage process runs independently based on the results +// from both test runners. +// This script will update changes to coverage thresholds when they're improved. +'use strict'; + +const fs = require('fs'); + +const nycConfig = require('./nyc.config'); + +/** + * Round float number to two decimal places. + * + * @param {number} value - Float number. + * @returns {number} A number rounded to two decimals. + */ +function getRoundedFloat(value) { + return Math.round(value * 100) / 100; +} + +(async () => { + console.log('Checking and updating coverage thresholds...'); + + // Read current coverage report + const rawCoverageData = await fs.promises.readFile( + './coverage/coverage-summary.json', + ); + const coverageAll = JSON.parse(rawCoverageData.toString()); + const coverage = coverageAll.total; + + // Update coverage report + if ( + nycConfig.branches < coverage.branches.pct || + nycConfig.lines < coverage.lines.pct || + nycConfig.functions < coverage.functions.pct || + nycConfig.statements < coverage.statements.pct + ) { + console.log('\nCoverage thresholds are changed. Updating...'); + // Display difference + console.log( + `Branches: +${getRoundedFloat( + coverage.branches.pct - nycConfig.branches, + )}%`, + ); + console.log( + `Lines: +${getRoundedFloat(coverage.lines.pct - nycConfig.lines)}%`, + ); + console.log( + `Functions: +${getRoundedFloat( + coverage.functions.pct - nycConfig.functions, + )}%`, + ); + console.log( + `Statements: +${getRoundedFloat( + coverage.statements.pct - nycConfig.statements, + )}%`, + ); + + // Update file + const updatedNycConfig = `/** + * NYC coverage reporter configuration. + */ +module.exports = { + 'check-coverage': true, + branches: ${getRoundedFloat(coverage.branches.pct)}, + lines: ${getRoundedFloat(coverage.lines.pct)}, + functions: ${getRoundedFloat(coverage.functions.pct)}, + statements: ${getRoundedFloat(coverage.statements.pct)}, +};\n`; + await fs.promises.writeFile('nyc.config.js', updatedNycConfig); + console.log('\nCoverage thresholds updated!'); + } else { + console.log('No changes detected to coverage thresholds.'); + } +})();