diff --git a/README.md b/README.md index 01321d3..7a68339 100644 --- a/README.md +++ b/README.md @@ -225,4 +225,6 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui ## Telemetry PMG collects anonymous usage data to improve project stability and reliability. -To disable: `export PMG_DISABLE_TELEMETRY=true`. +To disable, either: +- Set `disable_telemetry: true` in your PMG config file, or +- Export `PMG_DISABLE_TELEMETRY=true`. diff --git a/cmd/cloud/sync.go b/cmd/cloud/sync.go index 8f87805..b503f9d 100644 --- a/cmd/cloud/sync.go +++ b/cmd/cloud/sync.go @@ -31,7 +31,7 @@ func runSync(cmd *cobra.Command, args []string) error { cfg := config.Get() if analytics.IsDisabled() { - ui.Infof("Cloud sync is disabled because telemetry is disabled (PMG_DISABLE_TELEMETRY)") + ui.Infof("Cloud sync is disabled because telemetry is disabled (disable_telemetry or PMG_DISABLE_TELEMETRY)") return nil } diff --git a/cmd/setup/info.go b/cmd/setup/info.go index 84e2889..4e70cb7 100644 --- a/cmd/setup/info.go +++ b/cmd/setup/info.go @@ -8,6 +8,7 @@ import ( "github.com/safedep/pmg/config" "github.com/safedep/pmg/internal/alias" + "github.com/safedep/pmg/internal/analytics" "github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/version" "github.com/spf13/cobra" @@ -38,6 +39,7 @@ func executeSetupInfo() error { configEntries := make(map[string]string) configEntries["Config File"] = cfg.ConfigFilePath() configEntries["Proxy Mode"] = strconv.FormatBool(cfg.IsProxyModeEnabled()) + configEntries["Proxy Install Only"] = strconv.FormatBool(cfg.Config.ProxyInstallOnly) ui.PrintInfoSection("Configuration", configEntries) // Shell Integration section @@ -89,6 +91,7 @@ func executeSetupInfo() error { securityEntries["Dependency Cooldown"] = strconv.FormatBool(cfg.Config.DependencyCooldown.Enabled) securityEntries["Dependency Cooldown Days"] = strconv.Itoa(cfg.Config.DependencyCooldown.Days) + securityEntries["Telemetry"] = strconv.FormatBool(!analytics.IsDisabled()) securityEntries["Event Logging"] = strconv.FormatBool(!cfg.Config.SkipEventLogging) securityEntries["Event Log Directory"] = cfg.EventLogDir() diff --git a/config/config.go b/config/config.go index a8b9b3c..1e39d6e 100644 --- a/config/config.go +++ b/config/config.go @@ -53,6 +53,9 @@ type Config struct { // Paranoid enables high-security defaults (e.g., treating suspicious behavior as malicious). Paranoid bool `mapstructure:"paranoid"` + // DisableTelemetry allows turning off telemetry collection. + DisableTelemetry bool `mapstructure:"disable_telemetry"` + // TrustedPackages allows for trusting a suspicious package and ignoring the suspicious behaviour for the package in future installations TrustedPackages []TrustedPackage `mapstructure:"trusted_packages"` @@ -242,6 +245,7 @@ func DefaultConfig() RuntimeConfig { TransitiveDepth: 5, IncludeDevDependencies: false, Paranoid: false, + DisableTelemetry: false, EventLogRetentionDays: 7, SkipEventLogging: false, ExperimentalProxyMode: false, diff --git a/config/config.template.yml b/config/config.template.yml index 763baf9..897f951 100644 --- a/config/config.template.yml +++ b/config/config.template.yml @@ -20,6 +20,9 @@ verbosity: normal # as malicious packages paranoid: false +# Disable anonymous telemetry. Default is false. +disable_telemetry: false + # Skip event logging. Default is false. # When skip_event_logging is false, all events will be logged to file. These events are useful for audit # trail and incident response on systems using PMG. Set this config to true to disable event logging. diff --git a/config/config_template_test.go b/config/config_template_test.go index d442785..46bd537 100644 --- a/config/config_template_test.go +++ b/config/config_template_test.go @@ -30,6 +30,7 @@ func TestTemplateParsesAsYAML(t *testing.T) { assert.Equal(t, 5, cfg.TransitiveDepth, "expected TransitiveDepth 5") assert.False(t, false, cfg.IncludeDevDependencies, "expected IncludeDevDependencies false") assert.False(t, false, cfg.Paranoid, "expected Paranoid false") + assert.False(t, cfg.DisableTelemetry, "expected DisableTelemetry false") assert.False(t, false, cfg.SkipEventLogging, "expected SkipEventLogging false") assert.Equal(t, 7, cfg.EventLogRetentionDays, "expected EventLogRetentionDays 7") assert.Len(t, cfg.TrustedPackages, 1) @@ -52,6 +53,7 @@ func TestTemplateMatchesDefaults(t *testing.T) { assert.Equal(t, def.TransitiveDepth, parsed.TransitiveDepth, "transitive_depth mismatch") assert.Equal(t, def.IncludeDevDependencies, parsed.IncludeDevDependencies, "include_dev_dependencies mismatch") assert.Equal(t, def.Paranoid, parsed.Paranoid, "paranoid mismatch") + assert.Equal(t, def.DisableTelemetry, parsed.DisableTelemetry, "disable_telemetry mismatch") assert.Equal(t, def.SkipEventLogging, parsed.SkipEventLogging, "skip_event_logging mismatch") assert.Equal(t, def.EventLogRetentionDays, parsed.EventLogRetentionDays, "event_log_retention_days mismatch") assert.Equal(t, def.Verbosity, parsed.Verbosity, "verbosity mismatch") diff --git a/config/config_test.go b/config/config_test.go index 97f0320..891a22f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -154,6 +154,19 @@ func TestConfigPrecedence(t *testing.T) { assert.Equal(t, true, Get().Config.ProxyInstallOnly, "config file should override default") }) + t.Run("telemetry can be disabled via config", func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("PMG_CONFIG_DIR", tmpDir) + t.Setenv("PMG_DISABLE_TELEMETRY", "") + + configPath := filepath.Join(tmpDir, "config.yml") + err := os.WriteFile(configPath, []byte("disable_telemetry: true\n"), 0o644) + require.NoError(t, err) + + initConfig() + assert.Equal(t, true, Get().Config.DisableTelemetry, "config file should disable telemetry") + }) + t.Run("env var works when key is absent from config file", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PMG_CONFIG_DIR", tmpDir) diff --git a/internal/analytics/analytics.go b/internal/analytics/analytics.go index 01a16ce..2bb6cde 100644 --- a/internal/analytics/analytics.go +++ b/internal/analytics/analytics.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/posthog/posthog-go" + "github.com/safedep/pmg/config" ) const ( @@ -47,6 +48,10 @@ func init() { } func isTelemetryDisabled() bool { + if config.Get().Config.DisableTelemetry { + return true + } + val := os.Getenv(telemetryDisableEnvKey) if booleanVal, err := strconv.ParseBool(val); err == nil { return booleanVal diff --git a/internal/analytics/analytics_test.go b/internal/analytics/analytics_test.go index 30fc503..d99009a 100644 --- a/internal/analytics/analytics_test.go +++ b/internal/analytics/analytics_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/safedep/pmg/config" "github.com/stretchr/testify/assert" ) @@ -16,8 +17,18 @@ func TestIsDisabled(t *testing.T) { }) t.Run("returns false if PMG_DISABLE_TELEMETRY is not set", func(t *testing.T) { + config.Get().Config.DisableTelemetry = false assert.False(t, IsDisabled()) }) + + t.Run("returns true if telemetry is disabled in config", func(t *testing.T) { + config.Get().Config.DisableTelemetry = true + t.Cleanup(func() { + config.Get().Config.DisableTelemetry = false + }) + + assert.True(t, IsDisabled()) + }) } func TestCloseIsImmutable(t *testing.T) {