Skip to content

Commit f57740f

Browse files
committed
feat: add support for socket.dev firewall client
1 parent 8e921c2 commit f57740f

23 files changed

Lines changed: 410 additions & 18 deletions

File tree

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- [Commands](#commands)
2929
- [Registries](#registries)
3030
- [Screenshots](#screenshots)
31+
- [Firewall (socket.dev)](#firewall-socketdev)
3132
- [Configuration](#configuration)
3233

3334
## Introduction
@@ -134,6 +135,39 @@ functions to ensure you have the latest package information before retrieving pa
134135
| <img alt="Main window" src="https://github.com/user-attachments/assets/b9a57d21-f551-45ad-a1e5-a9fd66291510"> | <img alt="Language search" src="https://github.com/user-attachments/assets/3d24fb7b-2c57-4948-923b-0a42bb627cbe"> | <img alt="Language filter" src="https://github.com/user-attachments/assets/c0ca5818-3c74-4071-bc41-427a2cd1056d"> |
135136
| <img alt="Package information" src="https://github.com/user-attachments/assets/6f9f6819-ac97-483d-a77c-8f6c6131ac85"> | <img alt="New package versions" src="https://github.com/user-attachments/assets/ff1adc4d-2fcc-46df-ab4c-291c891efa50"> | <img alt="Help window" src="https://github.com/user-attachments/assets/1fbe75e4-fe69-4417-83e3-82329e1c236e"> |
136137

138+
## Firewall (socket.dev)
139+
140+
> Socket Firewall is a free tool that blocks malicious packages at install time, giving developers proactive protection
141+
> against rising supply chain attacks.
142+
143+
`mason.nvim` supports the [Socket.dev firewall](https://socket.dev/). To enable the firewall, turn it on in the configuration:
144+
145+
```lua
146+
require("mason").setup {
147+
firewall = {
148+
enabled = true
149+
}
150+
}
151+
```
152+
153+
By default, `mason.nvim` will automatically install and update the Socket Firewall client. If you want to manage the
154+
client manually, set `auto_managed` to `false` (this requires the `sfw` binary to be available in your `PATH`):
155+
156+
```lua
157+
require("mason").setup {
158+
firewall = {
159+
enabled = true,
160+
auto_managed = false
161+
}
162+
}
163+
```
164+
165+
For more information refer to the [Socket.dev](https://socket.dev/) documentation.
166+
167+
> [!NOTE]
168+
> If you already use the Socket.dev firewall in a proxy service configuration you don't need to enable the firewall in
169+
> `mason.nvim`.
170+
137171
## Configuration
138172

139173
> [`:h mason-settings`][help-mason-settings]
@@ -215,6 +249,18 @@ local DEFAULT_SETTINGS = {
215249
"github:mason-org/mason-system-registry",
216250
},
217251

252+
firewall = {
253+
---@since 2.3.0
254+
-- Whether to enable the socket.dev firewall (sfw) for supported package sources.
255+
-- For more information, refer to https://socket.dev.
256+
enabled = false,
257+
258+
---@since 2.3.0
259+
-- Whether mason.nvim should automatically install and update the Socket Firewall client.
260+
-- If false, the sfw binary must exist in PATH if the firewall is enabled.
261+
auto_managed = true,
262+
},
263+
218264
---@since 1.0.0
219265
-- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions).
220266
-- Accepts multiple entries, where later entries will be used as fallback should prior providers fail.

doc/mason.txt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,33 @@ See also ~
136136
Launch an embedded terminal: |terminal|.
137137
Launch background jobs: |jobstart| & |uv.spawn()| (via |vim.loop|)
138138

139+
==============================================================================
140+
MASON FIREWALL *mason-firewall*
141+
142+
`mason.nvim` supports the Socket.dev firewall. To enable the firewall, turn it
143+
on in the configuration:
144+
>lua
145+
require("mason").setup {
146+
firewall = {
147+
enabled = true
148+
}
149+
}
150+
<
151+
152+
By default, `mason.nvim` will automatically install and update the Socket
153+
Firewall client. If you want to manage the client manually, set `auto_managed`
154+
to `false` (this requires the `sfw` binary to be available in your `PATH`):
155+
>lua
156+
require("mason").setup {
157+
firewall = {
158+
enabled = true,
159+
auto_managed = false
160+
}
161+
}
162+
<
163+
164+
For more information refer to the [Socket.dev](https://socket.dev/) documentation.
165+
139166
==============================================================================
140167
COMMANDS *mason-commands*
141168

@@ -258,6 +285,18 @@ Example:
258285
"github:mason-org/mason-system-registry",
259286
},
260287

288+
firewall = {
289+
---@since 2.3.0
290+
-- Whether to enable the socket.dev firewall (sfw) for supported package sources.
291+
-- For more information, refer to https://socket.dev.
292+
enabled = false,
293+
294+
---@since 2.3.0
295+
-- Whether mason.nvim should automatically install and update the Socket Firewall client.
296+
-- If false, the sfw binary must exist in PATH if the firewall is enabled.
297+
auto_managed = true,
298+
},
299+
261300
---@since 1.0.0
262301
-- The provider implementations to use for resolving supplementary package metadata (e.g., all available versions).
263302
-- Accepts multiple entries, where later entries will be used as fallback should prior providers fail.

lua/mason-core/async/init.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ local function new_execution_context(suspend_fn, callback, ...)
7878
if cancelled or not thread then
7979
return
8080
end
81-
local ok, promise_or_result = co.resume(thread, ...)
81+
local results = { co.resume(thread, ...) }
82+
local ok, promise_or_result = results[1], results[2]
8283
if cancelled or not thread then
8384
return
8485
end
@@ -88,7 +89,7 @@ local function new_execution_context(suspend_fn, callback, ...)
8889
promise_or_result(step)
8990
else
9091
-- yield to parent coroutine
91-
step(coroutine.yield(promise_or_result))
92+
step(coroutine.yield(promise_or_result, unpack(results, 3)))
9293
end
9394
else
9495
callback(true, promise_or_result)

lua/mason-core/installer/InstallHandle.lua

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@ local uv = vim.loop
2020
---@field pid integer
2121
---@field cmd string
2222
---@field args string[]
23+
---@field firewall boolean
2324
local InstallHandleSpawnHandle = {}
2425
InstallHandleSpawnHandle.__index = InstallHandleSpawnHandle
2526

2627
---@param luv_handle luv_handle
2728
---@param pid integer
2829
---@param cmd string
2930
---@param args string[]
30-
function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args)
31+
---@param firewall boolean
32+
function InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args, firewall)
3133
---@type InstallHandleSpawnHandle
3234
local instance = {}
3335
setmetatable(instance, InstallHandleSpawnHandle)
3436
instance.uv_handle = luv_handle
3537
instance.pid = pid
3638
instance.cmd = cmd
3739
instance.args = args
40+
instance.firewall = firewall
3841
return instance
3942
end
4043

@@ -73,8 +76,9 @@ end
7376
---@param pid integer
7477
---@param cmd string
7578
---@param args string[]
76-
function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args)
77-
local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args)
79+
---@param firewall boolean
80+
function InstallHandle:register_spawn_handle(luv_handle, pid, cmd, args, firewall)
81+
local spawn_handles = InstallHandleSpawnHandle:new(luv_handle, pid, cmd, args, firewall)
7882
log.fmt_trace("Pushing spawn_handles stack for %s: %s (pid: %s)", self, spawn_handles, pid)
7983
self.spawn_handles[#self.spawn_handles + 1] = spawn_handles
8084
self:emit "spawn_handles:change"

lua/mason-core/installer/InstallRunner.lua

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ function InstallRunner:execute(opts, callback)
3939
local handle = self.handle
4040
log.fmt_info("Executing installer for %s %s", handle.package, opts)
4141

42-
local context = InstallContext:new(handle, opts)
42+
local context = InstallContext:new(handle, opts, {
43+
suspend = function()
44+
self:suspend()
45+
end,
46+
resume = function()
47+
self:resume()
48+
end,
49+
})
4350

4451
local tailed_output = {}
4552

@@ -212,6 +219,20 @@ function InstallRunner:acquire_permit()
212219
return Result.success(channel)
213220
end
214221

222+
function InstallRunner:suspend()
223+
if self.global_permit then
224+
self.global_permit:forget()
225+
self.global_permit = nil
226+
end
227+
end
228+
229+
---@async
230+
function InstallRunner:resume()
231+
self.handle:set_state "QUEUED"
232+
self.global_permit = self.global_semaphore:acquire()
233+
self.handle:set_state "ACTIVE"
234+
end
235+
215236
---@private
216237
function InstallRunner:release_permit()
217238
if self.global_permit then

lua/mason-core/installer/context/InstallContextSpawn.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ end
2222

2323
---@param cmd string
2424
function InstallContextSpawn:__index(cmd)
25-
---@param args JobSpawnOpts
25+
---@param args SpawnArgs
2626
return function(args)
2727
args.cwd = args.cwd or self.cwd:get()
2828
args.stdio_sink = args.stdio_sink or self.handle.stdio_sink
2929
local on_spawn = args.on_spawn
3030
local captured_handle
3131
args.on_spawn = function(handle, stdio, pid, ...)
3232
captured_handle = handle
33-
self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args))
33+
self.handle:register_spawn_handle(handle, pid, cmd, spawn._flatten_cmd_args(args), args.firewall == true)
3434
if on_spawn then
3535
on_spawn(handle, stdio, pid, ...)
3636
end

lua/mason-core/installer/context/init.lua

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,22 @@ local receipt = require "mason-core.receipt"
2121
---@field cwd InstallContextCwd
2222
---@field opts PackageInstallOpts
2323
---@field stdio_sink StdioSink
24+
---@field runner { suspend: fun(), resume: async fun() }
2425
---@field links { bin: table<string, string>, share: table<string, string>, opt: table<string, string> }
2526
local InstallContext = {}
2627
InstallContext.__index = InstallContext
2728

2829
---@param handle InstallHandle
2930
---@param opts PackageInstallOpts
30-
function InstallContext:new(handle, opts)
31+
---@param runner { suspend: fun(), resume: async fun() }
32+
function InstallContext:new(handle, opts, runner)
3133
local cwd = InstallContextCwd:new(handle)
3234
local spawn = InstallContextSpawn:new(handle, cwd, false)
3335
local fs = InstallContextFs:new(cwd)
3436
return setmetatable({
3537
cwd = cwd,
3638
spawn = spawn,
39+
runner = runner,
3740
handle = handle,
3841
location = handle.location, -- for convenience
3942
package = handle.package, -- for convenience
@@ -264,6 +267,7 @@ function InstallContext:link_bin(executable, rel_path)
264267
end
265268

266269
InstallContext.CONTEXT_REQUEST = {}
270+
InstallContext.ABORT = {}
267271

268272
---@generic T
269273
---@param fn fun(context: InstallContext): T
@@ -277,14 +281,17 @@ function InstallContext:execute(fn)
277281
local step
278282
local ret_val
279283
step = function(...)
280-
local ok, result = coroutine.resume(thread, ...)
284+
local results = { coroutine.resume(thread, ...) }
285+
local ok, result = results[1], results[2]
281286
if not ok then
282287
error(result, 0)
283288
elseif result == InstallContext.CONTEXT_REQUEST then
284289
step(self)
290+
elseif result == InstallContext.ABORT then
291+
ret_val = Result.failure(results[3])
285292
elseif coroutine.status(thread) == "suspended" then
286293
-- yield to parent coroutine
287-
step(coroutine.yield(result))
294+
step(coroutine.yield(result, unpack(results, 3)))
288295
else
289296
ret_val = result
290297
end
@@ -293,6 +300,31 @@ function InstallContext:execute(fn)
293300
return ret_val
294301
end
295302

303+
---@async
304+
---@param system_pkg SystemPackage
305+
function InstallContext:require(system_pkg)
306+
local result = Result.try(function(try)
307+
if try(system_pkg:needs_install()) then
308+
self.stdio_sink:stdout("Installing dependency " .. system_pkg.name .. ".\n")
309+
self.runner.suspend()
310+
try(system_pkg:install():on_failure(function()
311+
if self.opts.force then
312+
self.runner.resume()
313+
end
314+
end))
315+
self.runner.resume()
316+
end
317+
end)
318+
if result:is_failure() then
319+
if not self.opts.force then
320+
self.stdio_sink:stderr "Run with :MasonInstall --force to attempt installation anyway.\n"
321+
coroutine.yield(InstallContext.ABORT, result:err_or_nil())
322+
else
323+
self.stdio_sink:stderr(result:err_or_nil() .. "\n")
324+
end
325+
end
326+
end
327+
296328
---@async
297329
function InstallContext:build_receipt()
298330
log.fmt_debug("Building receipt for %s", self.package)

lua/mason-core/installer/managers/cargo.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local Result = require "mason-core.result"
2+
local SystemPackage = require "mason-core.system-package"
23
local _ = require "mason-core.functional"
34
local installer = require "mason-core.installer"
45
local log = require "mason-core.log"
@@ -15,6 +16,7 @@ function M.install(crate, version, opts)
1516
opts = opts or {}
1617
log.fmt_debug("cargo: install %s %s %s", crate, version, opts)
1718
local ctx = installer.context()
19+
ctx:require(SystemPackage.sfw)
1820
ctx.stdio_sink:stdout(("Installing crate %s@%s…\n"):format(crate, version))
1921
return ctx.spawn.cargo {
2022
"install",
@@ -29,6 +31,7 @@ function M.install(crate, version, opts)
2931
opts.features and { "--features", opts.features } or vim.NIL,
3032
opts.locked and "--locked" or vim.NIL,
3133
crate,
34+
firewall = true,
3235
}
3336
end
3437

lua/mason-core/installer/managers/npm.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local Result = require "mason-core.result"
2+
local SystemPackage = require "mason-core.system-package"
23
local _ = require "mason-core.functional"
34
local installer = require "mason-core.installer"
45
local log = require "mason-core.log"
@@ -62,12 +63,14 @@ function M.install(pkg, version, opts)
6263
opts = opts or {}
6364
log.fmt_debug("npm: install %s %s %s", pkg, version, opts)
6465
local ctx = installer.context()
66+
ctx:require(SystemPackage.sfw)
6567
ctx.stdio_sink:stdout(("Installing npm package %s@%s…\n"):format(pkg, version))
6668
return ctx.spawn.npm {
6769
"install",
6870
("%s@%s"):format(pkg, version),
6971
opts.extra_packages or vim.NIL,
7072
opts.install_extra_args or vim.NIL,
73+
firewall = true,
7174
}
7275
end
7376

lua/mason-core/installer/managers/pypi.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local Optional = require "mason-core.optional"
22
local Result = require "mason-core.result"
3+
local SystemPackage = require "mason-core.system-package"
34
local _ = require "mason-core.functional"
45
local a = require "mason-core.async"
56
local installer = require "mason-core.installer"
@@ -173,6 +174,8 @@ end
173174
---@param pkgs string[]
174175
---@param extra_args? string[]
175176
local function pip_install(pkgs, extra_args)
177+
local ctx = installer.context()
178+
ctx:require(SystemPackage.sfw)
176179
return venv_python {
177180
"-m",
178181
"pip",
@@ -182,6 +185,7 @@ local function pip_install(pkgs, extra_args)
182185
"--ignore-installed",
183186
extra_args or vim.NIL,
184187
pkgs,
188+
firewall = true,
185189
}
186190
end
187191

0 commit comments

Comments
 (0)