diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index baf7287..0baea1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - members: read steps: - name: Validate Team Membership For Sensitive Changes env: diff --git a/src/commands/cfg/config_test.go b/src/commands/cfg/config_test.go index a8868cd..fec770e 100644 --- a/src/commands/cfg/config_test.go +++ b/src/commands/cfg/config_test.go @@ -546,6 +546,67 @@ func TestResetAll_PreservesRoot(t *testing.T) { } } +func captureStderr(t *testing.T, fn func() error) (string, error) { + t.Helper() + + oldStderr := os.Stderr + reader, writer, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() failed: %v", err) + } + os.Stderr = writer + + errCh := make(chan error, 1) + var buffer bytes.Buffer + go func() { + _, copyErr := io.Copy(&buffer, reader) + errCh <- copyErr + }() + + runErr := fn() + writer.Close() + os.Stderr = oldStderr + copyErr := <-errCh + reader.Close() + if copyErr != nil { + t.Fatalf("failed capturing stderr: %v", copyErr) + } + + return buffer.String(), runErr +} + +func TestSet_SkipMalwareChecksWarns(t *testing.T) { + bindTestRegistry(t) + + out, err := captureStderr(t, func() error { + return runSetCfg(t, "skip_malware_checks=true") + }) + if err != nil { + t.Fatalf("skip_malware_checks=true: %v", err) + } + if got := getSetting(t, "skip_malware_checks"); got != true { + t.Fatalf("skip_malware_checks: expected true, got %v", got) + } + wantOff := "Your system is no longer attempting to block malicious package installations" + if !strings.Contains(out, wantOff) { + t.Fatalf("skip on stderr = %q, want %q", out, wantOff) + } + + out, err = captureStderr(t, func() error { + return runSetCfg(t, "skip_malware_checks=false") + }) + if err != nil { + t.Fatalf("skip_malware_checks=false: %v", err) + } + if got := getSetting(t, "skip_malware_checks"); got != false { + t.Fatalf("skip_malware_checks: expected false, got %v", got) + } + wantOn := "Your system is now attempting to block malicious package installations" + if !strings.Contains(out, wantOn) { + t.Fatalf("skip off stderr = %q, want %q", out, wantOn) + } +} + func TestResetAll_SkipsBlockedKeys(t *testing.T) { bindTestRegistry(t) root := filepath.Join(os.TempDir(), "nvm_config_reset_all_blocked") diff --git a/src/commands/cfg/set.go b/src/commands/cfg/set.go index d01d92c..d4ebd4a 100644 --- a/src/commands/cfg/set.go +++ b/src/commands/cfg/set.go @@ -99,6 +99,9 @@ func (s *Set) Run() error { } log.LogConfigurationChanged(key, newValue, oldValue, log.OutcomeSucceeded, "") + if key == "skip_malware_checks" { + warnSkipMalwareChecks(value) + } } if len(errs) > 0 { @@ -152,6 +155,24 @@ func displayValue(name string, value interface{}) string { } } +func warnSkipMalwareChecks(value string) { + skip := false + switch strings.ToLower(strings.TrimSpace(value)) { + case "1", "true", "yes", "on": + skip = true + case "0", "false", "no", "off": + skip = false + default: + return + } + msg := "Your system is now attempting to block malicious package installations" + if skip { + msg = "Your system is no longer attempting to block malicious package installations" + } + log.Warn(msg) + fmt.Fprintln(os.Stderr, msg) +} + func setAnnouncements(enabled bool) error { if enabled { log.Log("Project and release announcements have been enabled.") diff --git a/src/go.mod b/src/go.mod index 427a169..83c09a4 100644 --- a/src/go.mod +++ b/src/go.mod @@ -40,6 +40,8 @@ replace common/mirrorauth v1.0.0 => ../../common/mirrorauth replace common/token v1.0.0 => ../../common/token +replace common/malwaredb v1.0.0 => ../../common/malwaredb + replace common/cose v1.0.0 => ../../common/cose replace common/license v1.0.0 => ../../common/licensing @@ -54,6 +56,7 @@ require ( common/http v1.0.0 common/inspect v1.0.0 common/license v1.0.0 + common/malwaredb v1.0.0 common/mirrorauth v1.0.0 common/modulefirewall v1.0.0 common/notify v1.0.0 diff --git a/src/installer/installer.go b/src/installer/installer.go index fcc1efe..bb27423 100644 --- a/src/installer/installer.go +++ b/src/installer/installer.go @@ -27,8 +27,11 @@ import ( "golang.org/x/sys/windows/registry" "common/acl" + "common/malwaredb" "common/notify" + "nvm/bootstrap" "nvm/log" + "nvm/prompt" ) type InstallConfig struct { @@ -144,6 +147,10 @@ func Install(cfg InstallConfig) error { } } + if err := seedMalwareDefinitions(status); err != nil { + return err + } + status.Done() if failures.Load() > 0 { return fmt.Errorf("%d version(s) failed to install", failures.Load()) @@ -627,6 +634,35 @@ func allowInsecureDownloads(cfg InstallConfig) bool { return cfg.AllowInsecure } +func seedMalwareDefinitions(status *Status) error { + root, err := bootstrap.DataRoot() + if err != nil || root == "" { + return nil + } + if _, err := os.Stat(filepath.Join(root, "malware.db")); err == nil { + return nil + } + _, err = malwaredb.Update(malwaredb.UpdateOptions{ + Root: root, + License: malwaredb.LicenseHeader(), + }) + if err == nil { + return nil + } + status.Alert(fmt.Errorf("malware definitions not downloaded: %v", err)) + if !system.IsAppInForeground() { + return nil + } + ok, confirmErr := prompt.Confirm("Malicious package definitions could not be acquired. Package installs will be blocked until definitions download. Continue?", "n") + if confirmErr != nil { + return confirmErr + } + if !ok { + return fmt.Errorf("malware definitions not downloaded: %v", err) + } + return nil +} + func getRoot(version string) string { target := expand(settings.Global().Root) return filepath.Join(target, fmt.Sprintf("v%s", version))