diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 6bbcd066..00000000 --- a/.deepsource.toml +++ /dev/null @@ -1,13 +0,0 @@ -version = 1 - -[[analyzers]] -name = "go" - - [analyzers.meta] - import_path = "github.com/deepsourcelabs/cli" - -[[analyzers]] -name = "secrets" - -[[analyzers]] -name = "test-coverage" \ No newline at end of file diff --git a/command/auth/login/login.go b/command/auth/login/login.go index 22ece504..ec85fb1e 100644 --- a/command/auth/login/login.go +++ b/command/auth/login/login.go @@ -6,6 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/deepsourcelabs/cli/command/cmddeps" + "github.com/deepsourcelabs/cli/command/cmdutil" "github.com/deepsourcelabs/cli/config" "github.com/deepsourcelabs/cli/deepsource" "github.com/deepsourcelabs/cli/internal/cli/args" @@ -59,7 +60,7 @@ func NewCmdLoginWithDeps(deps *cmddeps.Deps) *cobra.Command { Long: doc, Args: args.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return opts.Run() + return opts.Run(cmd) }, } @@ -72,7 +73,7 @@ func NewCmdLoginWithDeps(deps *cmddeps.Deps) *cobra.Command { return cmd } -func (opts *LoginOptions) Run() (err error) { +func (opts *LoginOptions) Run(cmd *cobra.Command) (err error) { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -94,6 +95,9 @@ func (opts *LoginOptions) Run() (err error) { opts.User = cfg.User opts.TokenExpired = cfg.IsExpired() + // Resolve skip-tls-verify: flag > env > config + cfg.SkipTLSVerify = cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify) + opts.verifyTokenWithServer(cfg, svc) if opts.Interactive { diff --git a/command/auth/login/tests/login_test.go b/command/auth/login/tests/login_test.go index 74ceeb26..7393268e 100644 --- a/command/auth/login/tests/login_test.go +++ b/command/auth/login/tests/login_test.go @@ -245,6 +245,69 @@ func TestLoginVerifyTokenServerReject(t *testing.T) { } } +func TestLoginPATSkipTLSVerifyNotPersisted(t *testing.T) { + cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "") + + deps := &cmddeps.Deps{ + ConfigMgr: cfgMgr, + Client: newMockViewerClient(t), + } + + cmd := loginCmd.NewCmdLoginWithDeps(deps) + cmd.SetArgs([]string{"--with-token", "dsp_noskip"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + cfg, err := cfgMgr.Load() + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + if cfg.SkipTLSVerify { + t.Error("expected SkipTLSVerify=false when flag is not set") + } +} + +func TestLoginPATSkipTLSVerifyFromConfig(t *testing.T) { + // Write a config with SkipTLSVerify already set to true + tmpDir := t.TempDir() + fs := adapters.NewOSFileSystem() + cfgMgr := config.NewManager(fs, func() (string, error) { + return tmpDir, nil + }) + + // Use manager.Write to create valid TOML with expired token + SkipTLSVerify + initialCfg := &config.CLIConfig{ + Host: "enterprise.example.com", + Token: "", + SkipTLSVerify: true, + } + if err := cfgMgr.Write(initialCfg); err != nil { + t.Fatalf("failed to write initial config: %v", err) + } + + deps := &cmddeps.Deps{ + ConfigMgr: cfgMgr, + Client: newMockViewerClient(t), + } + + cmd := loginCmd.NewCmdLoginWithDeps(deps) + cmd.SetArgs([]string{"--with-token", "dsp_skipyes"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + cfg, err := cfgMgr.Load() + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + if !cfg.SkipTLSVerify { + t.Error("expected SkipTLSVerify=true to be preserved from config after login") + } +} + func TestLoginPATInvalidToken(t *testing.T) { cfgMgr := testutil.CreateExpiredTestConfigManager(t, "", "deepsource.com", "") diff --git a/command/auth/status/status.go b/command/auth/status/status.go index 2710dfe7..8c8b4b5f 100644 --- a/command/auth/status/status.go +++ b/command/auth/status/status.go @@ -9,6 +9,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/deepsourcelabs/cli/command/cmddeps" + "github.com/deepsourcelabs/cli/command/cmdutil" "github.com/deepsourcelabs/cli/config" "github.com/deepsourcelabs/cli/deepsource" dsuser "github.com/deepsourcelabs/cli/deepsource/user" @@ -16,6 +17,7 @@ import ( "github.com/deepsourcelabs/cli/internal/cli/style" clierrors "github.com/deepsourcelabs/cli/internal/errors" "github.com/spf13/cobra" + ) type AuthStatusOptions struct { @@ -47,13 +49,13 @@ func NewCmdStatusWithDeps(deps *cmddeps.Deps) *cobra.Command { Args: args.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { opts := AuthStatusOptions{deps: deps} - return opts.Run() + return opts.Run(cmd) }, } return cmd } -func (opts *AuthStatusOptions) Run() error { +func (opts *AuthStatusOptions) Run(cmd *cobra.Command) error { cfgMgr := opts.configManager() cfg, err := cfgMgr.Load() if err != nil { @@ -68,7 +70,7 @@ func (opts *AuthStatusOptions) Run() error { return nil } - client, err := opts.apiClient(cfg, cfgMgr) + client, err := opts.apiClient(cmd, cfg, cfgMgr) if err != nil { style.Warnf(opts.stdout(), "Could not connect to DeepSource to verify authentication") return nil @@ -100,14 +102,15 @@ func (opts *AuthStatusOptions) configManager() *config.Manager { return config.DefaultManager() } -func (opts *AuthStatusOptions) apiClient(cfg *config.CLIConfig, cfgMgr *config.Manager) (*deepsource.Client, error) { +func (opts *AuthStatusOptions) apiClient(cmd *cobra.Command, cfg *config.CLIConfig, cfgMgr *config.Manager) (*deepsource.Client, error) { if opts.deps != nil && opts.deps.Client != nil { return opts.deps.Client, nil } return deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) } diff --git a/command/cmdutil/tls.go b/command/cmdutil/tls.go new file mode 100644 index 00000000..59d7c67a --- /dev/null +++ b/command/cmdutil/tls.go @@ -0,0 +1,14 @@ +package cmdutil + +import "github.com/spf13/cobra" + +// ResolveSkipTLSVerify returns true if TLS verification should be skipped. +// Priority: --skip-tls-verify flag > config value (which includes env var). +func ResolveSkipTLSVerify(cmd *cobra.Command, cfgValue bool) bool { + if cmd != nil { + if f := cmd.Root().PersistentFlags().Lookup("skip-tls-verify"); f != nil && f.Changed { + return true + } + } + return cfgValue +} diff --git a/command/cmdutil/tls_test.go b/command/cmdutil/tls_test.go new file mode 100644 index 00000000..61b2d636 --- /dev/null +++ b/command/cmdutil/tls_test.go @@ -0,0 +1,106 @@ +package cmdutil + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestResolveSkipTLSVerify(t *testing.T) { + tests := []struct { + name string + setupCmd func() *cobra.Command + cfgValue bool + want bool + }{ + { + name: "nil cmd, cfgValue false", + setupCmd: func() *cobra.Command { return nil }, + cfgValue: false, + want: false, + }, + { + name: "nil cmd, cfgValue true", + setupCmd: func() *cobra.Command { return nil }, + cfgValue: true, + want: true, + }, + { + name: "cmd without skip-tls-verify flag, cfgValue false", + setupCmd: func() *cobra.Command { + return &cobra.Command{Use: "test"} + }, + cfgValue: false, + want: false, + }, + { + name: "cmd without skip-tls-verify flag, cfgValue true", + setupCmd: func() *cobra.Command { + return &cobra.Command{Use: "test"} + }, + cfgValue: true, + want: true, + }, + { + name: "cmd with flag not set, cfgValue false", + setupCmd: func() *cobra.Command { + root := &cobra.Command{Use: "root"} + root.PersistentFlags().Bool("skip-tls-verify", false, "") + child := &cobra.Command{Use: "child"} + root.AddCommand(child) + return child + }, + cfgValue: false, + want: false, + }, + { + name: "cmd with flag not set, cfgValue true", + setupCmd: func() *cobra.Command { + root := &cobra.Command{Use: "root"} + root.PersistentFlags().Bool("skip-tls-verify", false, "") + child := &cobra.Command{Use: "child"} + root.AddCommand(child) + return child + }, + cfgValue: true, + want: true, + }, + { + name: "cmd with flag set, cfgValue false", + setupCmd: func() *cobra.Command { + root := &cobra.Command{Use: "root"} + root.PersistentFlags().Bool("skip-tls-verify", false, "") + child := &cobra.Command{Use: "child"} + root.AddCommand(child) + // Simulate the flag being set on the command line + root.PersistentFlags().Set("skip-tls-verify", "true") + return child + }, + cfgValue: false, + want: true, + }, + { + name: "cmd with flag set, cfgValue true", + setupCmd: func() *cobra.Command { + root := &cobra.Command{Use: "root"} + root.PersistentFlags().Bool("skip-tls-verify", false, "") + child := &cobra.Command{Use: "child"} + root.AddCommand(child) + root.PersistentFlags().Set("skip-tls-verify", "true") + return child + }, + cfgValue: true, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := tt.setupCmd() + got := ResolveSkipTLSVerify(cmd, tt.cfgValue) + if got != tt.want { + t.Errorf("ResolveSkipTLSVerify() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/command/flags_test.go b/command/flags_test.go index 6bd50a03..9851b315 100644 --- a/command/flags_test.go +++ b/command/flags_test.go @@ -15,6 +15,17 @@ type flagExpectation struct { defaultValue string } +func TestRootSkipTLSVerifyFlag(t *testing.T) { + cmd := NewCmdRoot() + f := cmd.PersistentFlags().Lookup("skip-tls-verify") + if f == nil { + t.Fatal("expected skip-tls-verify persistent flag on root command, got nil") + } + if f.DefValue != "false" { + t.Errorf("expected default value %q, got %q", "false", f.DefValue) + } +} + func TestFlagDefaults(t *testing.T) { tests := []struct { name string diff --git a/command/issues/issues.go b/command/issues/issues.go index fd6255e5..89e3fe7e 100644 --- a/command/issues/issues.go +++ b/command/issues/issues.go @@ -92,7 +92,7 @@ func NewCmdIssuesWithDeps(deps *cmddeps.Deps) *cobra.Command { Short: "View issues in a repository", Long: doc, RunE: func(cmd *cobra.Command, _ []string) error { - return opts.Run(cmd.Context()) + return opts.Run(cmd, cmd.Context()) }, } @@ -203,7 +203,7 @@ func flagUsageLine(f *pflag.Flag) string { return line } -func (opts *IssuesOptions) initClientAndConfig() (*deepsource.Client, *vcs.RemoteData, error) { +func (opts *IssuesOptions) initClientAndConfig(cmd *cobra.Command) (*deepsource.Client, *vcs.RemoteData, error) { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -228,9 +228,10 @@ func (opts *IssuesOptions) initClientAndConfig() (*deepsource.Client, *vcs.Remot return opts.deps.Client, remote, nil } client, err := deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return nil, nil, err @@ -238,8 +239,8 @@ func (opts *IssuesOptions) initClientAndConfig() (*deepsource.Client, *vcs.Remot return client, remote, nil } -func (opts *IssuesOptions) Run(ctx context.Context) error { - client, remote, err := opts.initClientAndConfig() +func (opts *IssuesOptions) Run(cmd *cobra.Command, ctx context.Context) error { + client, remote, err := opts.initClientAndConfig(cmd) if err != nil { return err } diff --git a/command/metrics/metrics.go b/command/metrics/metrics.go index 826fe7ae..9a7ecb41 100644 --- a/command/metrics/metrics.go +++ b/command/metrics/metrics.go @@ -87,7 +87,7 @@ func NewCmdMetricsWithDeps(deps *cmddeps.Deps) *cobra.Command { Short: "View repository metrics", Long: doc, RunE: func(cmd *cobra.Command, _ []string) error { - return opts.Run(cmd.Context()) + return opts.Run(cmd, cmd.Context()) }, } @@ -116,7 +116,7 @@ func NewCmdMetricsWithDeps(deps *cmddeps.Deps) *cobra.Command { return cmd } -func (opts *MetricsOptions) Run(ctx context.Context) error { +func (opts *MetricsOptions) Run(cmd *cobra.Command, ctx context.Context) error { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -142,9 +142,10 @@ func (opts *MetricsOptions) Run(ctx context.Context) error { client = opts.deps.Client } else { client, err = deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return err diff --git a/command/report/report.go b/command/report/report.go index 6881c719..0c9a5146 100644 --- a/command/report/report.go +++ b/command/report/report.go @@ -80,6 +80,18 @@ func NewCmdReportWithDeps(deps *container.Container) *cobra.Command { opts.DeepSourceHostEndpoint = "https://app.deepsource.com" } + // Resolve skip-tls-verify: local --skip-verify | global --skip-tls-verify | config + if !opts.SkipCertificateVerification { + if f := cmd.Root().PersistentFlags().Lookup("skip-tls-verify"); f != nil && f.Changed { + opts.SkipCertificateVerification = true + } + } + if !opts.SkipCertificateVerification { + if cfg, err := deps.Config.Load(); err == nil { + opts.SkipCertificateVerification = cfg.SkipTLSVerify + } + } + svc := reportsvc.NewService(reportsvc.ServiceDeps{ GitClient: deps.GitClient, HTTPClient: deps.HTTPClient, @@ -116,6 +128,7 @@ func NewCmdReportWithDeps(deps *container.Container) *cobra.Command { cmd.Flags().StringVar(&opts.Output, "output", "pretty", "Output format: pretty, json") cmd.Flags().BoolVar(&opts.SkipCertificateVerification, "skip-verify", false, "skip SSL certificate verification while sending the test coverage data") + _ = cmd.Flags().MarkDeprecated("skip-verify", "use the global --skip-tls-verify flag instead") _ = cmd.RegisterFlagCompletionFunc("analyzer", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{ diff --git a/command/report/report_test.go b/command/report/report_test.go new file mode 100644 index 00000000..a8e53609 --- /dev/null +++ b/command/report/report_test.go @@ -0,0 +1,22 @@ +package report + +import "testing" + +func TestReportSkipVerifyFlagExists(t *testing.T) { + cmd := NewCmdReport() + f := cmd.Flags().Lookup("skip-verify") + if f == nil { + t.Fatal("expected skip-verify flag on report command, got nil") + } +} + +func TestReportSkipVerifyFlagDeprecated(t *testing.T) { + cmd := NewCmdReport() + f := cmd.Flags().Lookup("skip-verify") + if f == nil { + t.Fatal("expected skip-verify flag on report command, got nil") + } + if f.Deprecated == "" { + t.Error("expected skip-verify flag to be marked as deprecated") + } +} diff --git a/command/reportcard/reportcard.go b/command/reportcard/reportcard.go index 3dc2febd..17e9cd24 100644 --- a/command/reportcard/reportcard.go +++ b/command/reportcard/reportcard.go @@ -14,6 +14,7 @@ import ( "github.com/deepsourcelabs/cli/command/cmdutil" "github.com/deepsourcelabs/cli/config" "github.com/deepsourcelabs/cli/deepsource" + "github.com/deepsourcelabs/cli/deepsource/runs" "github.com/deepsourcelabs/cli/internal/cli/completion" "github.com/deepsourcelabs/cli/internal/cli/style" @@ -78,7 +79,7 @@ func NewCmdReportCardWithDeps(deps *cmddeps.Deps) *cobra.Command { Short: "View repository report card", Long: doc, RunE: func(cmd *cobra.Command, _ []string) error { - return opts.Run(cmd.Context()) + return opts.Run(cmd, cmd.Context()) }, } @@ -103,7 +104,7 @@ func NewCmdReportCardWithDeps(deps *cmddeps.Deps) *cobra.Command { return cmd } -func (opts *ReportCardOptions) initClientAndRemote() (*deepsource.Client, *vcs.RemoteData, error) { +func (opts *ReportCardOptions) initClientAndRemote(cmd *cobra.Command) (*deepsource.Client, *vcs.RemoteData, error) { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -129,9 +130,10 @@ func (opts *ReportCardOptions) initClientAndRemote() (*deepsource.Client, *vcs.R client = opts.deps.Client } else { client, err = deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return nil, nil, err @@ -140,8 +142,8 @@ func (opts *ReportCardOptions) initClientAndRemote() (*deepsource.Client, *vcs.R return client, remote, nil } -func (opts *ReportCardOptions) Run(ctx context.Context) error { - client, remote, err := opts.initClientAndRemote() +func (opts *ReportCardOptions) Run(cmd *cobra.Command, ctx context.Context) error { + client, remote, err := opts.initClientAndRemote(cmd) if err != nil { return err } diff --git a/command/root.go b/command/root.go index d8dee937..ed15790a 100644 --- a/command/root.go +++ b/command/root.go @@ -89,6 +89,8 @@ func NewCmdRoot() *cobra.Command { completionC.GroupID = "setup" cmd.AddCommand(completionC) + cmd.PersistentFlags().Bool("skip-tls-verify", false, "Skip TLS certificate verification (for self-signed certs)") + cmd.InitDefaultHelpFlag() cmd.InitDefaultVersionFlag() cmd.Flags().Lookup("help").Usage = "Show usage and available commands" diff --git a/command/runs/runs.go b/command/runs/runs.go index e8c72acb..0a26b3fd 100644 --- a/command/runs/runs.go +++ b/command/runs/runs.go @@ -76,9 +76,9 @@ func NewCmdRunsWithDeps(deps *cmddeps.Deps) *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { if opts.commitOid != "" { opts.commitOid = cmdutil.ResolveCommitOid(opts.commitOid) - return opts.runDetail(cmd.Context()) + return opts.runDetail(cmd, cmd.Context()) } - return opts.runList() + return opts.runList(cmd) }, } @@ -103,7 +103,7 @@ func NewCmdRunsWithDeps(deps *cmddeps.Deps) *cobra.Command { } // runList fetches and displays a table of recent analysis runs. -func (opts *RunsOptions) runList() error { +func (opts *RunsOptions) runList(cmd *cobra.Command) error { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -123,9 +123,10 @@ func (opts *RunsOptions) runList() error { client = opts.deps.Client } else { client, err = deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return err @@ -194,7 +195,7 @@ func (opts *RunsOptions) fetchRuns(client *deepsource.Client) ([]runstypes.Analy } // runDetail fetches and displays metadata + issues summary for a single commit. -func (opts *RunsOptions) runDetail(ctx context.Context) error { +func (opts *RunsOptions) runDetail(cmd *cobra.Command, ctx context.Context) error { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -214,9 +215,10 @@ func (opts *RunsOptions) runDetail(ctx context.Context) error { client = opts.deps.Client } else { client, err = deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return err diff --git a/command/vulnerabilities/vulnerabilities.go b/command/vulnerabilities/vulnerabilities.go index 0d04ebe2..65541ef4 100644 --- a/command/vulnerabilities/vulnerabilities.go +++ b/command/vulnerabilities/vulnerabilities.go @@ -89,7 +89,7 @@ func NewCmdVulnerabilitiesWithDeps(deps *cmddeps.Deps) *cobra.Command { Short: "View dependency vulnerabilities", Long: doc, RunE: func(cmd *cobra.Command, _ []string) error { - return opts.Run(cmd.Context()) + return opts.Run(cmd, cmd.Context()) }, } @@ -122,7 +122,7 @@ func NewCmdVulnerabilitiesWithDeps(deps *cmddeps.Deps) *cobra.Command { return cmd } -func (opts *VulnerabilitiesOptions) Run(ctx context.Context) error { +func (opts *VulnerabilitiesOptions) Run(cmd *cobra.Command, ctx context.Context) error { var cfgMgr *config.Manager if opts.deps != nil && opts.deps.ConfigMgr != nil { cfgMgr = opts.deps.ConfigMgr @@ -148,9 +148,10 @@ func (opts *VulnerabilitiesOptions) Run(ctx context.Context) error { client = opts.deps.Client } else { client, err = deepsource.New(deepsource.ClientOpts{ - Token: cfg.Token, - HostName: cfg.Host, - OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), + Token: cfg.Token, + HostName: cfg.Host, + InsecureSkipVerify: cmdutil.ResolveSkipTLSVerify(cmd, cfg.SkipTLSVerify), + OnTokenRefreshed: cfgMgr.TokenRefreshCallback(), }) if err != nil { return err diff --git a/config/config.go b/config/config.go index 27195726..aa72a0f9 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ type CLIConfig struct { User string `toml:"user"` Token string `toml:"token"` TokenExpiresIn time.Time `toml:"token_expires_in,omitempty"` + SkipTLSVerify bool `toml:"skip_tls_verify,omitempty"` TokenFromEnv bool `toml:"-"` } diff --git a/config/manager.go b/config/manager.go index 65b43edc..9a74911e 100644 --- a/config/manager.go +++ b/config/manager.go @@ -80,6 +80,12 @@ func (m *Manager) Load() (*CLIConfig, error) { } } + if !cfg.SkipTLSVerify { + if v := os.Getenv("DEEPSOURCE_SKIP_TLS_VERIFY"); v == "1" || v == "true" { + cfg.SkipTLSVerify = true + } + } + debug.Log("config: host=%q user=%q token_present=%v env=%v", cfg.Host, cfg.User, cfg.Token != "", cfg.TokenFromEnv) return cfg, nil diff --git a/config/manager_test.go b/config/manager_test.go index e434e4f5..0622f708 100644 --- a/config/manager_test.go +++ b/config/manager_test.go @@ -259,3 +259,100 @@ func TestManagerBackfillUserNoop(t *testing.T) { mgr.BackfillUser(cfg, "new@example.com") assert.Equal(t, "existing@example.com", cfg.User) } + +func TestManagerLoadSkipTLSVerifyFromEnv(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + t.Setenv("DEEPSOURCE_SKIP_TLS_VERIFY", "1") + + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + cfg, err := mgr.Load() + require.NoError(t, err) + assert.True(t, cfg.SkipTLSVerify) +} + +func TestManagerLoadSkipTLSVerifyFromEnvTrue(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + t.Setenv("DEEPSOURCE_SKIP_TLS_VERIFY", "true") + + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + cfg, err := mgr.Load() + require.NoError(t, err) + assert.True(t, cfg.SkipTLSVerify) +} + +func TestManagerLoadSkipTLSVerifyEnvIgnoredValues(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + t.Setenv("DEEPSOURCE_SKIP_TLS_VERIFY", "0") + + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + cfg, err := mgr.Load() + require.NoError(t, err) + assert.False(t, cfg.SkipTLSVerify) +} + +func TestManagerLoadSkipTLSVerifyFromFile(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + configDir := filepath.Join(tempDir, buildinfo.ConfigDirName) + require.NoError(t, os.MkdirAll(configDir, 0o700)) + + tomlData := `host = "enterprise.example.com" +token = "tok" +skip_tls_verify = true +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, ConfigFileName), []byte(tomlData), 0o644)) + + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + cfg, err := mgr.Load() + require.NoError(t, err) + assert.True(t, cfg.SkipTLSVerify) +} + +func TestManagerLoadSkipTLSVerifyFileOverEnv(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + configDir := filepath.Join(tempDir, buildinfo.ConfigDirName) + require.NoError(t, os.MkdirAll(configDir, 0o700)) + + tomlData := `host = "enterprise.example.com" +token = "tok" +skip_tls_verify = true +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, ConfigFileName), []byte(tomlData), 0o644)) + + // Env not set — file value should still be true + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + cfg, err := mgr.Load() + require.NoError(t, err) + assert.True(t, cfg.SkipTLSVerify) +} + +func TestManagerWriteSkipTLSVerify(t *testing.T) { + tempDir := t.TempDir() + homeDir := func() (string, error) { return tempDir, nil } + + mgr := NewManager(adapters.NewOSFileSystem(), homeDir) + err := mgr.Write(&CLIConfig{ + Host: "enterprise.example.com", + Token: "tok", + SkipTLSVerify: true, + }) + require.NoError(t, err) + + // Read back raw TOML and verify field is present + path := filepath.Join(tempDir, buildinfo.ConfigDirName, ConfigFileName) + data, err := os.ReadFile(path) + require.NoError(t, err) + + var got CLIConfig + require.NoError(t, toml.Unmarshal(data, &got)) + assert.True(t, got.SkipTLSVerify) +} diff --git a/deepsource/client.go b/deepsource/client.go index 42385675..ca022ff3 100644 --- a/deepsource/client.go +++ b/deepsource/client.go @@ -2,6 +2,7 @@ package deepsource import ( "context" + "crypto/tls" "fmt" "net/http" @@ -34,6 +35,10 @@ type ClientOpts struct { Token string HostName string + // InsecureSkipVerify disables TLS certificate verification. + // Use for self-hosted instances with self-signed certificates. + InsecureSkipVerify bool + // OnTokenRefreshed is called after a successful automatic token refresh. // If set, enables transparent token refresh when API calls fail due to // expired tokens. The callback should persist the new credentials. @@ -62,8 +67,18 @@ func NewWithGraphQLClient(gql graphqlclient.GraphQLClient) *Client { func New(cp ClientOpts) (*Client, error) { apiClientURL := getAPIClientURL(cp.HostName) + + var base http.RoundTripper = http.DefaultTransport + if cp.InsecureSkipVerify { + base = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // user-requested for self-signed certs + MinVersion: tls.VersionTLS12, + }, + } + } httpClient := &http.Client{ - Transport: &graphqlclient.StatusCheckTransport{Base: http.DefaultTransport}, + Transport: &graphqlclient.StatusCheckTransport{Base: base}, } gql := graphql.NewClient(apiClientURL, graphql.WithHTTPClient(httpClient)) diff --git a/deepsource/client_test.go b/deepsource/client_test.go index 1681573a..575fcd11 100644 --- a/deepsource/client_test.go +++ b/deepsource/client_test.go @@ -19,6 +19,34 @@ func TestNormalizeHostName(t *testing.T) { } } +func TestNewClientDefaultTransport(t *testing.T) { + client, err := New(ClientOpts{ + Token: "test-token", + HostName: "deepsource.com", + InsecureSkipVerify: false, + }) + if err != nil { + t.Fatalf("unexpected error creating client: %v", err) + } + if client == nil { + t.Fatal("expected non-nil client") + } +} + +func TestNewClientInsecureTransport(t *testing.T) { + client, err := New(ClientOpts{ + Token: "test-token", + HostName: "enterprise.example.com", + InsecureSkipVerify: true, + }) + if err != nil { + t.Fatalf("unexpected error creating client with InsecureSkipVerify: %v", err) + } + if client == nil { + t.Fatal("expected non-nil client") + } +} + func TestGetAPIClientURL(t *testing.T) { tests := []struct { hostName string diff --git a/internal/services/auth/service.go b/internal/services/auth/service.go index ab22c106..9f46dc12 100644 --- a/internal/services/auth/service.go +++ b/internal/services/auth/service.go @@ -47,7 +47,7 @@ func (s *Service) DeleteConfig() error { } func (s *Service) RegisterDevice(ctx context.Context, cfg *config.CLIConfig) (*dsauth.Device, error) { - client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host}) + client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host, InsecureSkipVerify: cfg.SkipTLSVerify}) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (s *Service) RegisterDevice(ctx context.Context, cfg *config.CLIConfig) (*d } func (s *Service) RequestPAT(ctx context.Context, cfg *config.CLIConfig, deviceCode, description string) (*dsauth.PAT, error) { - client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host}) + client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host, InsecureSkipVerify: cfg.SkipTLSVerify}) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (s *Service) RequestPAT(ctx context.Context, cfg *config.CLIConfig, deviceC } func (s *Service) GetViewer(ctx context.Context, cfg *config.CLIConfig) (*dsuser.User, error) { - client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host}) + client, err := s.newClient(deepsource.ClientOpts{Token: cfg.Token, HostName: cfg.Host, InsecureSkipVerify: cfg.SkipTLSVerify}) if err != nil { return nil, err }