Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mip-sandbox

Package Overview
Dependencies
Maintainers
1
Versions
42
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mip-sandbox - npm Package Compare versions

Comparing version 1.0.4 to 1.0.5

lib/global-mark.js

4

lib/generate-lite.js

@@ -9,5 +9,5 @@ /**

module.exports = function (code) {
var ast = replace(code)
module.exports = function (code, keywords, options) {
var ast = replace(code, keywords, options && options.prefix)
return escodegen.generate(ast)
}

@@ -9,5 +9,5 @@ /**

module.exports = function (code, options) {
var ast = replace(code)
return escodegen.generate(ast, options)
module.exports = function (code, keywords, options) {
var ast = replace(code, keywords, options && options.prefix)
return escodegen.generate(ast, options && options.escodegen)
}

@@ -6,3 +6,3 @@ /**

var esprima = require('esprima')
var mark = require('./global-mark')
var estraverse = require('estraverse')

@@ -12,10 +12,9 @@ var is = require('./utils/is')

module.exports = function (code, fn, type) {
var ast = esprima.parseModule(code, {
range: true,
loc: true
})
var ast
if (typeof code === 'string') {
ast = mark(code)
} else {
ast = code
}
mark(ast)
scope(ast)
estraverse[type || 'traverse'](ast, {

@@ -46,180 +45,2 @@ enter: function (node, parent) {

function mark (ast) {
// 作用域内变量定义 Identifier case
// 1. import
// 1. import a from 'xxx' 的 a
// 2. import {a as b} from 'xxx' 的 b
// 3. import * as c from 'xxx' 的 c
// 2. var/let/const
// 1. var a = 1 的 a
// 2. var {a: b} = {a: 1} 中的 b
// 3. var [a, b] 中的 a, b
// 4. var [a, ...b] 中的 b
// 5. var {a = 1} 中的 a
// 3. FunctionDeclaration
// 1. function a() {} 中的 a
// 4. ClassDeclaration
// 1. Class A extends B {} 中的 A
// 5. Function params
// 1. function a(args) {} 中的 args
// 2. var a = function (args) {} 中的 args
// 3. var b = (args) => {} 中的 args
// 6. CatchClause
// 1. try {} catch (e) {} 的 e
// 2. 这个 e 有可能会以解构的形式去写
// 无需关心的 Identifier case
// 1. MemberExpression
// 1. a.b.c 中的 b、c 但 a[b].c 中只有 c 不需要关心
// 2. Property 包括解构赋值和普通的 Object 定义
// 1. {a: b} 中的 a 但 {[a]: b} 中的 a 需要关心
// 3. MethodDefinition
// 1. class A {a() {}} 中的 a 但 class A {[a]() {}} 中的 a 需要关心
// 4. FunctionExpression
// 1. var a = function b() {} 中的 b 因为 b() 是无效的
// 5. import
// 1. import {a as b} from 'xxx' 中的 a
// 变量提升
// 1. var
// var a = 1 中的 a
// var {a: b} = {} 中的 b
// var [a, {b: {c: [d]}}, f = 1, ...e] = [] 中的 a d f e
// 2. function
// function a() {} 中的 a
estraverse.traverse(ast, {
enter: function (node, parent) {
// 标记变量声明
if (is(node, /^Import\w*Specifier$/)) {
node.local.isVar = true
if (node.imported && is(node.imported, 'Identifier')) {
node.imported.isIgnore = true
}
} else if (is(node, 'VariableDeclaration')) {
if (node.kind === 'var') {
node.declarations.forEach(elem => {
elem.isLift = true
})
}
} else if (is(node, 'VariableDeclarator')) {
if (is(node.id, 'Identifier')) {
node.id.isVar = true
}
node.id.isLift = node.isLift
} else if (is(node, 'ObjectPattern')) {
node.properties.forEach(function (elem) {
if (is(elem.value, 'Identifier')) {
elem.value.isVar = true
}
elem.value.isLift = node.isLift
})
} else if (is(node, 'ArrayPattern')) {
node.elements.forEach(function (elem) {
if (is(elem, 'Identifier')) {
elem.isVar = true
}
elem.isLift = node.isLift
})
} else if (is(node, 'AssignmentPattern')) {
node.left.isVar = true
node.left.isLift = node.isLift
} else if (is(node, 'RestElement')) {
if (is(node.argument, 'Identifier')) {
node.argument.isVar = true
}
node.argument.isLift = node.isLift
} else if (is(node, /Function/)) {
if (node.id) {
if (node.type === 'FunctionDeclaration') {
node.id.isVar = true
node.id.isLift = true
} else {
// FunctionExpression 的 id 没用
// var a = function b () {} 这个没法在最下面使用 b()
node.id.isIgnore = true
}
node.id.isVar = true
}
node.params.forEach(function (elem) {
if (is(elem, 'Identifier')) {
elem.isVar = true
}
})
} else if (is(node, 'ClassDeclaration')) {
node.id.isVar = true
} else if (is(node, 'CatchClause')) {
if (is(node.param, 'Identifier')) {
node.param.isVar = true
}
} else if (is(node, 'MemberExpression')) {
// a.b.c 的 b c 忽略
if (is(node.property, 'Identifier') && !node.computed) {
node.property.isIgnore = true
}
} else if (is(node, 'Property')) {
if (is(node.key, 'Identifier') && !node.computed) {
node.key.isIgnore = true
}
} else if (is(node, 'MethodDefinition') && !node.computed) {
node.key.isIgnore = true
}
}
})
}
function scope (ast, parentAst) {
// 标记 scope case
// 1. Program 是 top scope
// 2. *Function* 其中 ArrowFunctionExpression 的 body 不一定带 BlockStatement
// 1. FunctionDeclaration
// 2. FunctionExpression
// 3. ArrowFunctionExpression
// 3. BlockStatement 只会影响 const/let
// 4. ForStatement 只会影响 const/let
parentAst = parentAst || []
estraverse.traverse(ast, {
enter: function (node) {
if (node === ast) {
return
}
if (is(node, /Function/) ||
is(node, 'BlockStatement') ||
is(node, 'ForStatement') ||
is(node, 'CatchClause')
) {
scope(node, parentAst.concat(ast))
this.skip()
return
}
if (!is(node, 'Identifier') ||
!node.isVar
) {
return
}
if (node.isLift && parentAst.length) {
for (var i = parentAst.length - 1; i > -1; i--) {
if (is(parentAst[i], 'Program') || is(parentAst[i], /Function/)) {
parentAst[i].vars = parentAst[i].vars || []
parentAst[i].vars.push(node.name)
break
}
}
} else {
ast.vars = ast.vars || []
ast.vars.push(node.name)
}
}
})
}
function hasBinding (name, context) {

@@ -226,0 +47,0 @@ var parents = context.parents()

@@ -5,4 +5,5 @@ /**

*/
var keys = require('./utils/keys')
var WINDOW_ORIGINAL = [
var ORIGINAL = [
'Array',

@@ -16,5 +17,2 @@ 'ArrayBuffer',

'Error',
'File',
'FileList',
'FileReader',
'Float32Array',

@@ -24,4 +22,2 @@ 'Float64Array',

'Headers',
'Image',
'ImageBitmap',
'Infinity',

@@ -34,5 +30,3 @@ 'Int16Array',

'Math',
'MutationObserver',
'NaN',
'Notification',
'Number',

@@ -61,8 +55,5 @@ 'Object',

'WritableStream',
'addEventListener',
'cancelAnimationFrame',
'clearInterval',
'clearTimeout',
'console',
'createImageBitmap',
'decodeURI',

@@ -76,4 +67,2 @@ 'decodeURIComponent',

'getComputedStyle',
// 待定
'history',
'innerHeight',

@@ -85,4 +74,2 @@ 'innerWidth',

'localStorage',
// 待定
'location',
'length',

@@ -95,4 +82,2 @@ 'matchMedia',

'parseInt',
'removeEventListener',
'requestAnimationFrame',
'screen',

@@ -103,8 +88,4 @@ 'screenLeft',

'screenY',
'scroll',
'scrollBy',
'scrollTo',
'scrollX',
'scrollY',
'scrollbars',
'sessionStorage',

@@ -114,16 +95,8 @@ 'setInterval',

'undefined',
'unescape',
'webkitCancelAnimationFrame',
'webkitRequestAnimationFrame'
'unescape'
]
var WINDOW_CUSTOM = [
'document',
'window',
'MIP'
]
var RESERVED = [
'arguments',
'MIP',
// 'MIP',
'require',

@@ -135,24 +108,116 @@ 'module',

var DOCUMENT_ORIGINAL = [
'head',
'body',
'title',
'cookie',
'referrer',
'readyState',
'documentElement',
'createElement',
'createDcoumentFragment',
'getElementById',
'getElementsByClassName',
'getElementsByTagName',
'querySelector',
'querySelectorAll'
]
var SANDBOX_STRICT = {
name: 'strict',
access: 'readyonly',
host: 'window',
mount: 'MIP.sandbox.strict',
children: ORIGINAL.concat([
{
name: 'document',
host: 'document',
children: [
'cookie'
]
},
{
name: 'location',
host: 'location',
access: 'readonly',
children: [
'href',
'protocol',
'host',
'hostname',
'port',
'pathname',
'search',
'hash',
'origin'
]
},
{
name: 'MIP',
host: 'MIP',
children: [
'watch',
'setData',
'viewPort',
'util',
'sandbox'
]
},
{
name: 'window',
host: 'MIP.sandbox.strict'
}
])
}
var SANDBOX = {
access: 'readonly',
host: 'window',
mount: 'MIP.sandbox',
children: ORIGINAL.concat([
'File',
'FileList',
'FileReader',
'Image',
'ImageBitmap',
'MutationObserver',
'Notification',
'addEventListener',
'cancelAnimationFrame',
'createImageBitmap',
// 待定
'history',
// 待定
'location',
'removeEventListener',
'requestAnimationFrame',
'scrollBy',
'scrollTo',
'scroll',
'scrollbars',
'webkitCancelAnimationFrame',
'webkitRequestAnimationFrame',
{
name: 'document',
host: 'document',
children: [
'head',
'body',
'title',
'cookie',
'referrer',
'readyState',
'documentElement',
'createElement',
'createDcoumentFragment',
'getElementById',
'getElementsByClassName',
'getElementsByTagName',
'querySelector',
'querySelectorAll'
]
},
{
name: 'window',
host: 'MIP.sandbox'
},
{
name: 'MIP',
host: 'MIP'
},
SANDBOX_STRICT
])
}
module.exports = {
WINDOW_ORIGINAL: WINDOW_ORIGINAL,
WINDOW_CUSTOM: WINDOW_CUSTOM,
DOCUMENT_ORIGINAL: DOCUMENT_ORIGINAL,
RESERVED: RESERVED
ORIGINAL: ORIGINAL,
RESERVED: RESERVED,
SANDBOX: SANDBOX,
SANDBOX_STRICT: SANDBOX_STRICT,
WHITELIST: keys(SANDBOX.children).concat(RESERVED),
WHITELIST_STRICT: keys(SANDBOX_STRICT.children).concat(RESERVED),
WHITELIST_RESERVED: ORIGINAL.concat(RESERVED)
}

@@ -11,13 +11,4 @@ /**

var sandbox = {}
var sandbox = defUtils.traverse(keywords.SANDBOX)
defUtils.defs(sandbox, keywords.WINDOW_ORIGINAL)
defUtils.def(sandbox, 'window', sandbox)
var sandboxDocument = {}
defUtils.defs(sandboxDocument, keywords.DOCUMENT_ORIGINAL, {host: document, setter: true})
defUtils.def(sandbox, 'document', sandboxDocument)
defUtils.def(sandbox, 'MIP', window.MIP)
/**

@@ -33,8 +24,21 @@ * this sandbox,避免诸如

* @param {Object} that this
* @return {Object} safe this
* @return {Function} 返回 safe this 的方法
*/
defUtils.def(sandbox, 'this', function (that) {
return that === window ? sandbox : that === document ? sandbox.document : that
})
function safeThis (sandbox) {
return function (that) {
return that === window ? sandbox : that === document ? sandbox.document : that
}
}
defUtils.def(sandbox, 'this', safeThis(sandbox))
defUtils.def(sandbox.strict, 'this', safeThis(sandbox.strict))
defUtils.def(sandbox, 'WHITELIST', keywords.WHITELIST)
defUtils.def(sandbox, 'WHITELIST_STRICT', keywords.WHITELIST_STRICT)
defUtils.def(sandbox, 'WHITELIST_RESERVED', keywords.WHITELIST_RESERVED)
defUtils.def(sandbox.strict, 'WHITELIST', keywords.WHITELIST)
defUtils.def(sandbox.strict, 'WHITELIST_STRICT', keywords.WHITELIST_STRICT)
defUtils.def(sandbox.strict, 'WHITELIST_RESERVED', keywords.WHITELIST_RESERVED)
module.exports = sandbox

@@ -7,11 +7,7 @@ /**

var detect = require('./global-detect')
var keywords = require('./keywords')
var is = require('./utils/is')
var WINDOW_SAFE_KEYWORDS = keywords.WINDOW_ORIGINAL
.concat(keywords.RESERVED)
.concat(keywords.WINDOW_CUSTOM)
module.exports = function (code) {
module.exports = function (code, keywords) {
var unsafeList = []
keywords = keywords || []

@@ -23,10 +19,7 @@ detect(code, function (node, parent, ast) {

if (WINDOW_SAFE_KEYWORDS.indexOf(node.name) === -1) {
if (keywords.indexOf(node.name) === -1) {
unsafeList.push(node)
}
})
if (unsafeList.length) {
return unsafeList
}
return unsafeList
}

@@ -7,22 +7,17 @@ /**

var detect = require('./global-detect')
var keywords = require('./keywords')
var is = require('./utils/is')
var t = require('./utils/type')
var WINDOW_SAFE_KEYWORDS = keywords.WINDOW_ORIGINAL
.concat(keywords.RESERVED)
function sandboxExpression (name) {
return t.memberExpression(
t.memberExpression(
t.identifier('MIP'),
t.identifier('sandbox')
),
t.identifier(name)
)
function memberExpression (name) {
var keys = name.split('.')
var expression = t.identifier(keys[0])
for (var i = 1; i < keys.length; i++) {
expression = t.memberExpression(expression, t.identifier(keys[i]))
}
return expression
}
function safeThisExpression () {
function safeThisExpression (prefix) {
return t.callExpression(
sandboxExpression('this'),
memberExpression(prefix + '.this'),
[t.thisExpression()]

@@ -32,3 +27,6 @@ )

module.exports = function (code) {
module.exports = function (code, keywords, prefix) {
keywords = keywords || []
prefix = prefix || 'MIP.sandbox'
return detect(

@@ -39,6 +37,6 @@ code,

this.skip()
return safeThisExpression()
return safeThisExpression(prefix)
}
if (WINDOW_SAFE_KEYWORDS.indexOf(node.name) === -1) {
if (keywords.indexOf(node.name) === -1) {
this.skip()

@@ -48,3 +46,3 @@ if (is(parent, 'Property', {shorthand: true})) {

}
return sandboxExpression(node.name)
return memberExpression(prefix + '.' + node.name)
}

@@ -51,0 +49,0 @@ },

@@ -6,49 +6,128 @@ /**

module.exports = {
defs: function (obj, props, {host = window, writable = false} = {}) {
Object.defineProperties(
obj,
props.reduce(function (obj, key) {
obj[key] = {
enumberable: true,
configurable: false
}
var utils = {
// 方便测试用
globals: typeof window === 'object' ? window : {},
traverse: traverse,
def: def
}
if (typeof host[key] === 'function') {
if (/^[A-Z]/.test(key)) {
// class
obj[key].value = host[key]
obj[key].writable = false
} else {
// 不然直接 MIP.sandbox.setTimeout(() => {}) 会报错
obj[key].get = function () {
return host[key].bind(host)
}
}
} else {
obj[key].get = function () {
return host[key]
}
function prop (obj, name) {
var keys = name.split('.')
for (var i = 0; i < keys.length; i++) {
if (!obj) {
return
}
obj = obj[keys[i]]
}
return obj
}
obj[key].set = function (val) {
// 只是防止用户篡改而不是不让用户写
if (writable) {
host[key] = val
}
}
function merge (a, b, exclude) {
var keys = Object.keys(b)
for (var i = 0; i < keys.length; i++) {
if (!exclude || exclude.indexOf(keys[i]) === -1) {
a[keys[i]] = b[keys[i]]
}
}
return a
}
function traverse (node, parent, mount) {
mount = mount || {}
var host
if (typeof node.host === 'string') {
host = prop(utils.globals, node.host)
if (host == null) {
host = mount[node.host]
}
if (host == null) {
throw Error('host ' + node.host + ' not found.')
}
} else {
host = utils.globals
}
if (!node.children && parent) {
def(parent, node.name, host)
return
}
var options = merge({}, node, ['children'])
merge(options, {host: host})
var obj = {}
if (node.mount) {
mount[node.mount] = obj
}
node.children.forEach(function (child) {
if (typeof child === 'string') {
def(obj, child, child, options)
} else {
traverse(child, obj, mount)
}
})
if (node.name) {
def(parent, node.name, obj)
return
}
return obj
}
function def (obj, name, props, options) {
options = options || {}
var descriptor
if (options.type === 'raw') {
descriptor = props
} else if (typeof props === 'string' && options.type !== 'getter') {
descriptor = {
enumerable: true,
configurable: false
}
var host = options.host || utils.globals
if (typeof host[name] === 'function') {
if (/^[A-Z]/.test(name)) {
// class
descriptor.value = host[name]
descriptor.writable = false
} else {
// 不然直接 MIP.sandbox.setTimeout(() => {}) 会报错
descriptor.get = function () {
return host[name].bind(host)
}
}
} else {
descriptor.get = function () {
return host[name]
}
return obj
}, {})
)
},
def: function (obj, prop, getter) {
Object.defineProperty(obj, prop, {
enumberable: true,
descriptor.set = function (val) {
// 只是防止用户篡改而不是不让用户写
if (options.access !== 'readonly') {
host[name] = val
}
}
}
} else {
descriptor = {
enumerable: true,
get: function () {
return getter
return props
}
})
}
}
Object.defineProperty(obj, name, descriptor)
}
module.exports = utils
{
"name": "mip-sandbox",
"version": "1.0.4",
"version": "1.0.5",
"description": "sandbox tools for MIP project",
"main": "lib/sandbox.js",
"scripts": {
"test": "mocha test/*.spec.js",
"test": "sh test/test.sh",
"release": "sh publish.sh"

@@ -29,2 +29,3 @@ },

"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"source-map": "~0.6.1"

@@ -34,4 +35,11 @@ },

"chai": "^4.1.2",
"mocha": "^5.2.0"
"karma": "^2.0.2",
"karma-chrome-launcher": "^2.2.0",
"karma-mocha": "^1.3.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"mocha": "^5.2.0",
"webpack": "^4.12.0"
}
}

@@ -31,4 +31,21 @@ # mip-sandbox

使用 `mip-sandbox/lib/unsafe-detect` 方法进行不安全全局变量检测,该函数的定义如下
```javascript
/**
* 不安全全局变量检测
*
* @params {string|AST} code 代码字符串或代码 AST
* @params {Array=} keywords 安全全局变量声明列表,
* 在默认情况下,所有全局变量包括 window document 等均认为不安全,
* 需要传入该参数进行条件过滤
* @return {Array.<ASTNode>} 不安全全局变量列表
*/
```
使用例子如下:
```javascript
var detect = require('mip-sandbox/lib/unsafe-detect')
var keywords = require('mip-sandbox/lib/keywords')

@@ -40,3 +57,5 @@ var code = `

var results = detect(code)
// 严格模式 请使用 keywords.WHITLIST_STRICT
// 在前端使用时,可通过 MIP.sandbox.WHITELIST 去拿该列表
var results = detect(code, keywords.WHITELIST)

@@ -58,4 +77,24 @@ console.log(result)

使用 `mip-sandbox/lib/generate` 方法进行不安全全局变量替换,该函数的定义如下
```javascript
/**
* 不安全全局变量替换
*
* @param {string|AST} code 代码字符串或代码 AST
* @param {Array=} keywords 安全全局变量声明列表,
* 在默认情况下,所有全局变量包括 window document 等均认为不安全,
* 需要传入该参数进行条件过滤
* @param {Object=} options options
* @param {string=} options.prefix 默认全局变量注入的前缀,默认为 MIP.sandbox
* @param {Object=} options.escodegen 透传给 escodegen 的参数
* @return {string} 替换后的代码字符串
*/
```
使用例子如下:
```javascript
var generate = require('mip-sandbox/lib/generate')
var keywords = require('mip-sandbox/lib/keywords')

@@ -68,3 +107,3 @@ var code = `

var result = generate(code)
var result = generate(code, keywords.WHITELIST)

@@ -76,3 +115,16 @@ console.log(result)

// MIP.sandbox.window.console.log(a)
```
对于严格模式下 window 需要替换成 `MIP.sandbox.strict.window` 在这种情况下,需要传入第三个参数:
**options.prefix**
默认的 options.prefix === 'MIP.sandbox',在严格下可以传入 MIP.sandbox.strict,得到的结果将如下所示:
```javascript
var result = generate(code, keywords.WHITELIST, {prefix: 'MIP.sandbox.strict'})
// var a = 1
// console.log(MIP.sandbox.b)
// MIP.sandbox.strict.window.console.log(a)
```

@@ -82,3 +134,3 @@

该方法的第二个参数 options 将会透传给 escodegen 因此比如需要返回 sourcemap 的话,请于第二个参数传入 sourcemap 相关参数
该方法的第三个参数 options.escodegen 将会透传给 escodegen 因此比如需要返回 sourcemap 的话,请于第二个参数传入 sourcemap 相关参数

@@ -88,5 +140,7 @@ 如:

```javascript
var output = generate(code, {
sourceMap: 'name',
sourceMapWithCode: true
var output = generate(code, keywords.WHITELIST, {
escodegen: {
sourceMap: 'name',
sourceMapWithCode: true
}
})

@@ -98,9 +152,39 @@

对于不需要生成 sourceMap 的情况,可以使用 generate-lite 来去掉 source-map 相关代码以减小打包体积:
对于不需要生成 sourceMap 的情况,可以使用 generate-lite 来去掉 source-map 相关代码以减小打包体积。
该方法的定义如下:
```javascript
/**
* 不安全全局变量替换
*
* @param {string|AST} code 代码字符串或代码 AST
* @param {Array=} keywords 安全全局变量声明列表,
* 在默认情况下,所有全局变量包括 window document 等均认为不安全,
* 需要传入该参数进行条件过滤
* @return {string} 替换后的代码字符串
*/
```
```javascript
var generate = require('mip-sandbox/lib/generate-lite')
var code = generate(code)
var keywords = require('mip-sandbox/lib/keywords')
var code = generate(code, keywords.WHITELIST)
```
### 沙盒检测替换优化
在某些场景下需要同时使用 detect 和 generate 去实现功能,这时,如果对这两个方法传入的 code 都是字符串的话,就需要对字符串做两次 ast 解析和标记,为了解决这个问题,可以调用 global-mark 生成解析标记好的 ast,再将 ast 传入 detect 和 generate 中,从而提高效率:
```javascript
var mark = require('mip-sandbox/lib/global-mark')
var detect = require('mip-sandbox/lib/unsafe-detect')
var generate = require('mip-sandbox/lib/generate')
var keywords = require('mip-sandbox/lib/keywords')
var ast - mark(code)
var unsafeList = detect(ast, keywords.WHITELIST)
var generated = generated(ast, keywords.WHITELIST)
```
## 沙盒替换规则

@@ -150,2 +234,8 @@

## 严格模式
在 mip-script 中,理论上只允许进行数据运算和发请求等等操作,不允许直接操作 DOM ,因此在 mip-script 中写的 js 将会以沙盒的严格模式进行全局变量替换,比如 window 会被替换成 `MIP.sandbox.strict.window`、 this 将会替换成 `MIP..strict.this(this)`。
其中 MIP.sandbox.strict 是 MIP.sandbox 的子集。
## 可用全局变量

@@ -292,3 +382,2 @@

```

@@ -295,0 +384,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc