Skip to content

Commit d136008

Browse files
Add nvm firewall trust module CLI and prompt-trust helper.
Co-authored-by: Cursor
1 parent 0f35883 commit d136008

4 files changed

Lines changed: 171 additions & 0 deletions

File tree

‎src/commands/firewall/helpers.go‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package firewall
2+
3+
import (
4+
"bufio"
5+
"common/notify"
6+
"common/settings"
7+
"fmt"
8+
"nvm/log"
9+
"os"
10+
"strings"
11+
"syscall"
12+
"unsafe"
13+
14+
"golang.org/x/sys/windows"
15+
)
16+
17+
// PromptTrust dual-channel (console + desktop MessageBox + toast). Either Yes advances.
18+
type PromptTrust struct {
19+
Module string `arg:"" name:"module" help:"Module / command name that changed."`
20+
}
21+
22+
func (p *PromptTrust) Run() error {
23+
name := strings.TrimSpace(p.Module)
24+
if name == "" {
25+
return fmt.Errorf("module name required")
26+
}
27+
msg := fmt.Sprintf("Untrusted module '%s' changed after running. Approve and reshim?", name)
28+
_ = notify.Send(settings.AppId, "NVM Firewall", msg+" Answer Yes in the dialog or type y in the console.")
29+
30+
result := make(chan bool, 2)
31+
32+
go func() {
33+
fmt.Printf("%s [y/N]: ", msg)
34+
reader := bufio.NewReader(os.Stdin)
35+
line, err := reader.ReadString('\n')
36+
if err != nil {
37+
result <- false
38+
return
39+
}
40+
line = strings.TrimSpace(line)
41+
result <- len(line) > 0 && (line[0] == 'y' || line[0] == 'Y')
42+
}()
43+
44+
go func() {
45+
result <- messageBoxYesNo("NVM Firewall", msg)
46+
}()
47+
48+
ok := <-result
49+
if ok {
50+
log.LogStructured("firewall.trust_prompt_accepted", map[string]any{"module": name}, CodePolicyMutate)
51+
return nil
52+
}
53+
log.LogStructured("firewall.trust_prompt_declined", map[string]any{"module": name}, CodePolicyMutate)
54+
os.Exit(1)
55+
return nil
56+
}
57+
58+
func messageBoxYesNo(title, body string) bool {
59+
user32 := windows.NewLazySystemDLL("user32.dll")
60+
proc := user32.NewProc("MessageBoxW")
61+
t, err1 := syscall.UTF16PtrFromString(title)
62+
b, err2 := syscall.UTF16PtrFromString(body)
63+
if err1 != nil || err2 != nil {
64+
return false
65+
}
66+
const mbYesNo = 0x00000004
67+
const mbIconQuestion = 0x00000020
68+
const mbSetForeground = 0x00010000
69+
const mbTopmost = 0x00040000
70+
const idYes = 6
71+
r, _, _ := proc.Call(0, uintptr(unsafe.Pointer(b)), uintptr(unsafe.Pointer(t)), uintptr(mbYesNo|mbIconQuestion|mbSetForeground|mbTopmost))
72+
return r == idYes
73+
}

‎src/commands/firewall/root.go‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package firewall
2+
3+
import (
4+
"common/modulefirewall"
5+
"common/settings"
6+
"common/system"
7+
"fmt"
8+
"nvm/log"
9+
"strings"
10+
)
11+
12+
// Event codes (NVM44xx firewall range).
13+
const (
14+
CodePolicyMutate = 4401
15+
CodeElevationRequired = 4404
16+
CodeInvalidRule = 4405
17+
)
18+
19+
type Root struct {
20+
Trust TrustRoot `cmd:"trust" help:"Manage TrustedModules for self-updating global CLIs."`
21+
PromptTrust PromptTrust `cmd:"prompt-trust" hidden:"true" help:"Internal: dual-channel trust prompt for proxy."`
22+
}
23+
24+
type TrustRoot struct {
25+
Module TrustModule `cmd:"module" help:"Manage TrustedModules (self-update auto-reshim allow list)."`
26+
}
27+
28+
type TrustModule struct {
29+
Entries []string `arg:"" name:"entry" help:"Module patterns to trust for auto-reshim (or NOT to revoke)."`
30+
}
31+
32+
func requireMachine() error {
33+
if err := system.RequireAdministrator(); err != nil {
34+
log.ErrorStructured("firewall.elevation_required", map[string]any{
35+
"error": err.Error(),
36+
}, CodeElevationRequired)
37+
return fmt.Errorf("firewall policy changes require an elevated administrator prompt (NVM%d): %w", CodeElevationRequired, err)
38+
}
39+
return nil
40+
}
41+
42+
func appendSettingsList(cfgKey, regLabel string, add []string, negate bool) error {
43+
if err := requireMachine(); err != nil {
44+
return err
45+
}
46+
cur, _ := settings.Get(cfgKey)
47+
var list []string
48+
switch v := cur.(type) {
49+
case []string:
50+
list = append([]string{}, v...)
51+
case string:
52+
if strings.TrimSpace(v) != "" {
53+
list = []string{v}
54+
}
55+
}
56+
for _, e := range add {
57+
e = strings.TrimSpace(e)
58+
if e == "" {
59+
continue
60+
}
61+
if negate && !strings.HasPrefix(strings.ToLower(e), "not ") && !strings.HasPrefix(e, "!") {
62+
e = "NOT " + e
63+
}
64+
if err := modulefirewall.ValidateRuleEntry(e); err != nil {
65+
log.ErrorStructured("firewall.invalid_rule", map[string]any{
66+
"key": cfgKey,
67+
"entry": e,
68+
"error": err.Error(),
69+
}, CodeInvalidRule)
70+
return fmt.Errorf("invalid firewall entry %q (NVM%d): %w", e, CodeInvalidRule, err)
71+
}
72+
list = append(list, e)
73+
}
74+
if err := settings.PutMachine(cfgKey, list); err != nil {
75+
log.ErrorStructured("firewall.policy_mutate_failed", map[string]any{
76+
"key": cfgKey,
77+
"error": err.Error(),
78+
}, CodePolicyMutate)
79+
return err
80+
}
81+
log.Logf("firewall: updated %s (+%d entries)", regLabel, len(add))
82+
log.LogStructured("firewall.policy_mutated", map[string]any{
83+
"key": cfgKey,
84+
"added": add,
85+
"negate": negate,
86+
"action": "append",
87+
}, CodePolicyMutate)
88+
return nil
89+
}
90+
91+
func (t *TrustModule) Run() error {
92+
return appendSettingsList("trusted_modules", "TrustedModules", t.Entries, false)
93+
}

‎src/commands/root.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"nvm/commands/alias"
55
"nvm/commands/cache"
66
"nvm/commands/cfg"
7+
"nvm/commands/firewall"
78
"nvm/commands/install"
89
"nvm/commands/license"
910
"nvm/commands/list"
@@ -26,6 +27,7 @@ type RootCommand struct {
2627
Env Env `cmd:"env" help:"Display ${app} environment details."`
2728
Cache cache.Root `cmd:"cache" help:"View and manage the ${app} cache."`
2829
Config cfg.Root `cmd:"config" aliases:"cfg" help:"View and manage the ${app} configuration."`
30+
Firewall firewall.Root `cmd:"firewall" help:"Manage NVM trust firewall policy."`
2931
On Toggle `cmd:"on" help:"Manage Node.js with ${app}."`
3032
Off Toggle `cmd:"off" help:"Stop managing Node.js with ${app}."`
3133
Doctor Doctor `cmd:"doctor" help:"Detect and fix common ${app} issues." hidden:"true"`

‎src/go.mod‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ replace common/cose v1.0.0 => ../../common/cose
4444

4545
replace common/license v1.0.0 => ../../common/licensing
4646

47+
replace common/modulefirewall v1.0.0 => ../../common/modulefirewall
48+
4749
require (
4850
common/acl v1.0.0
4951
common/config v1.0.0
@@ -53,6 +55,7 @@ require (
5355
common/inspect v1.0.0
5456
common/license v1.0.0
5557
common/mirrorauth v1.0.0
58+
common/modulefirewall v1.0.0
5659
common/notify v1.0.0
5760
common/preferences v1.0.0
5861
common/registry v1.0.0

0 commit comments

Comments
 (0)