Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions src/commands/firewall/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,50 @@ func (c *CheckRemoteTrust) Run() error {
return nil
}

// PromptTrust asks whether to trust a module after a self-update.
// promptTimeoutExit is returned when the trust prompt gets no answer
// (toast timeout or no console). Distinct from 1 (explicit decline).
const promptTimeoutExit = 2

// afterTrustHook re-signs launchers and reshims after TrustedModules is updated.
// Windows sets this in init; other platforms leave it as a no-op.
var afterTrustHook = func(entries []string) {}

func launcherStem(entry string) string {
e := strings.TrimSpace(entry)
if e == "" {
return ""
}
upper := strings.ToUpper(e)
if upper == "ALL" || strings.HasPrefix(upper, "NOT ") || strings.HasPrefix(e, "!") {
return ""
}
if strings.Contains(e, "://") || strings.ContainsAny(e, "*?") {
return ""
}
if i := strings.LastIndexAny(e, `/\`); i >= 0 {
e = e[i+1:]
}
return strings.TrimSpace(e)
}

// trustNoAnswerMessage is the console and silent-toast text when a prompt
// times out or there is no console.
func trustNoAnswerMessage(name, why string) string {
name = strings.TrimSpace(name)
why = strings.TrimSpace(why)
if why == "" {
why = "no answer"
}
return fmt.Sprintf("No trust answer for '%s' (%s). NVM moved on. If you trust this: nvm firewall trust module %s", name, why, name)
}

func notifyTrustNoAnswer(name, why string) {
msg := trustNoAnswerMessage(name, why)
fmt.Fprintln(os.Stderr, msg)
_ = notify.Send(settings.AppId, "NVM Firewall", msg)
}

// PromptTrust asks whether to trust a module on a blocked launch.
// Foreground console: interactive y/N only (no toast).
// Background: native toast with Trust / Cancel (protocol actions); wait for answer.
type PromptTrust struct {
Expand Down Expand Up @@ -403,13 +446,13 @@ func logUntrustedModuleChanged(module, outcome, handler, via string) {
}

func promptTrustConsole(name string) error {
msg := fmt.Sprintf("Untrusted module '%s' changed after running. Do you trust this module?", name)
msg := fmt.Sprintf("Untrusted module '%s' changed. Do you trust this module?", name)
fmt.Fprintf(os.Stderr, "NVM Firewall: %s [y/N]: ", msg)
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "%s is not trusted\n", name)
os.Exit(1)
notifyTrustNoAnswer(name, "no console")
os.Exit(promptTimeoutExit)
return nil
}
line = strings.TrimSpace(line)
Expand All @@ -436,7 +479,7 @@ func promptTrustToast(name string) error {
}
defer os.Remove(rspPath)

msg := fmt.Sprintf("Untrusted module '%s' changed after running. Do you trust this module?", name)
msg := fmt.Sprintf("Untrusted module '%s' changed. Do you trust this module?", name)
_ = notify.Send(
settings.AppId,
"NVM Firewall",
Expand Down Expand Up @@ -467,8 +510,8 @@ func promptTrustToast(name string) error {
time.Sleep(400 * time.Millisecond)
}

fmt.Fprintf(os.Stderr, "Trust prompt timed out for %s; reshim skipped.\n", name)
os.Exit(1)
notifyTrustNoAnswer(name, "toast timeout")
os.Exit(promptTimeoutExit)
return nil
}

Expand Down
10 changes: 7 additions & 3 deletions src/commands/firewall/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@ type DenyModule struct {

type TrustModule struct {
List TrustModuleList `cmd:"list" aliases:"ls" help:"List trusted modules."`
Add TrustModuleAdd `cmd:"add" default:"withargs" help:"Add modules to TrustedModules (self-update auto-reshim allow list)."`
Add TrustModuleAdd `cmd:"add" default:"withargs" help:"Add modules to TrustedModules and re-sign their launchers."`
}

type TrustModuleAdd struct {
Machine bool `name:"machine" help:"Trust modules for the entire machine."`
Entries []string `arg:"" name:"entry" help:"Module patterns to trust for auto-reshim."`
Entries []string `arg:"" name:"entry" help:"Module patterns to trust. Re-signs matching launchers; a separate nvm reshim is not required."`
}

type TrustModuleList struct {
Expand Down Expand Up @@ -573,7 +573,11 @@ func (t *TrustModuleAdd) Run() error {
if len(t.Entries) == 0 {
return fmt.Errorf("entry required (or use: nvm firewall trust module list)")
}
return appendTrusted(t.Entries, t.Machine)
if err := appendTrusted(t.Entries, t.Machine); err != nil {
return err
}
afterTrustHook(t.Entries)
return nil
}

func (t *TrustModuleList) Run() error {
Expand Down
85 changes: 85 additions & 0 deletions src/commands/firewall/trust_after_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//go:build windows

package firewall

import (
"common/settings"
"common/verifycache"
"fmt"
"os"
"path/filepath"
"strings"

"nvm/bootstrap"
)

func init() {
afterTrustHook = resignTrustedLaunchersAndReshim
}

func resignTrustedLaunchersAndReshim(entries []string) {
if err := resignTrustedLaunchers(entries); err != nil {
fmt.Fprintf(os.Stderr, "NVM Firewall: trusted, but could not re-sign launchers: %v\n", err)
}
if err := bootstrap.RunReshim("--silent"); err != nil {
fmt.Fprintf(os.Stderr, "NVM Firewall: trusted, but reshim failed: %v\nRun nvm reshim after the files unlock.\n", err)
}
}

func resignTrustedLaunchers(entries []string) error {
var first error
for _, entry := range entries {
name := launcherStem(entry)
if name == "" {
continue
}
for _, path := range findModuleLaunchers(name) {
if err := verifycache.SignScript(path); err != nil && first == nil {
first = err
}
}
}
return first
}

func findModuleLaunchers(name string) []string {
seen := map[string]struct{}{}
var out []string
add := func(path string) {
path = filepath.Clean(path)
key := strings.ToLower(path)
if _, ok := seen[key]; ok {
return
}
if _, err := os.Stat(path); err != nil {
return
}
seen[key] = struct{}{}
out = append(out, path)
}

root := settings.Expand(settings.Global().Root)
if root != "" && root != "." {
entries, err := os.ReadDir(root)
if err == nil {
for _, entry := range entries {
if !entry.IsDir() {
continue
}
n := entry.Name()
if len(n) < 2 || (n[0] != 'v' && n[0] != 'V') {
continue
}
dir := filepath.Join(root, n)
add(filepath.Join(dir, name+".cmd"))
add(filepath.Join(dir, name+".bat"))
}
}
}

if appdata, err := os.UserConfigDir(); err == nil {
add(filepath.Join(appdata, "npm", name+".cmd"))
add(filepath.Join(appdata, "npm", name+".bat"))
}
return out
}
28 changes: 28 additions & 0 deletions src/commands/firewall/trust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ func TestEntryMatchesRemove(t *testing.T) {
}
}

func TestTrustNoAnswerMessage(t *testing.T) {
got := trustNoAnswerMessage("opencode", "toast timeout")
want := "No trust answer for 'opencode' (toast timeout). NVM moved on. If you trust this: nvm firewall trust module opencode"
if got != want {
t.Fatalf("got %q, want %q", got, want)
}
}

func TestLauncherStem(t *testing.T) {
tests := []struct {
in, want string
}{
{"opencode", "opencode"},
{" opencode ", "opencode"},
{"ALL", ""},
{"NOT ALL", ""},
{"!opencode", ""},
{"https://policy.example/trust", ""},
{"@org/*", ""},
{`C:\tools\opencode`, "opencode"},
}
for _, tt := range tests {
if got := launcherStem(tt.in); got != tt.want {
t.Fatalf("launcherStem(%q)=%q, want %q", tt.in, got, tt.want)
}
}
}

func TestStripNegation(t *testing.T) {
if got := stripNegation("NOT eslint"); got != "eslint" {
t.Fatalf("got %q", got)
Expand Down
Loading