From 67b1971d3bac0d2dfb3f569206f2dda93d5ceebc Mon Sep 17 00:00:00 2001 From: kevin olson Date: Thu, 21 Mar 2024 18:31:01 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20interactivity=20and=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 6 + go.sum | 16 ++ pkg/cmd/auth/login/interact.go | 105 +++++++++++++ pkg/cmd/auth/login/login.go | 99 ++---------- pkg/cmd/auth/logout/logout.go | 3 +- pkg/cmd/index/index.go | 48 ++++++ pkg/cmd/indices/indices.go | 5 +- pkg/cmd/root/root.go | 12 +- pkg/config/config.go | 7 + .../login_test.go => config/config_test.go} | 2 +- pkg/session/session.go | 11 +- pkg/ui/ui.go | 39 ++++- pkg/ui/viewport.go | 145 ++++++++++++++++++ pkg/util/util.go | 25 --- 14 files changed, 390 insertions(+), 133 deletions(-) create mode 100644 pkg/cmd/auth/login/interact.go create mode 100644 pkg/cmd/index/index.go rename pkg/{cmd/auth/login/login_test.go => config/config_test.go} (97%) create mode 100644 pkg/ui/viewport.go delete mode 100644 pkg/util/util.go diff --git a/go.mod b/go.mod index 05dde7f..dfe4918 100644 --- a/go.mod +++ b/go.mod @@ -17,11 +17,16 @@ require ( ) require ( + github.com/alecthomas/chroma/v2 v2.13.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.2.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -34,6 +39,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/octoper/go-ray v0.1.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect diff --git a/go.sum b/go.sum index ecdefcb..edb741b 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,12 @@ github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= @@ -23,12 +26,19 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 h1:nWU6p08f1VgIalT6iZyqXi4o5cZsz4X6qa87nusfcsc= +github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -58,6 +68,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/octoper/go-ray v0.1.5 h1:tTN+4HptuzSnw60E0wM71wegKsy8wYhnQ2THjMvXBGQ= +github.com/octoper/go-ray v0.1.5/go.mod h1:Y1I9cUEZ4oD94H0/M+xwHhvGVbFu1o/dW3fDrknELk8= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -87,9 +99,12 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -101,6 +116,7 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= diff --git a/pkg/cmd/auth/login/interact.go b/pkg/cmd/auth/login/interact.go new file mode 100644 index 0000000..ef3c1e9 --- /dev/null +++ b/pkg/cmd/auth/login/interact.go @@ -0,0 +1,105 @@ +package login + +import ( + "fmt" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/huh/spinner" + "github.com/spf13/cobra" + "github.com/vulncheck-oss/cli/pkg/config" + "github.com/vulncheck-oss/cli/pkg/session" + "github.com/vulncheck-oss/cli/pkg/ui" + "github.com/vulncheck-oss/sdk" +) + +func chooseAuthMethod() (string, error) { + + var choice string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Select an authentication method"). + Options( + huh.NewOption("Login with a web browser", "web"), + huh.NewOption("Paste an authentication token", "token"), + ).Value(&choice), + ), + ) + + err := form.Run() + if err != nil { + return "", ui.Error("Failed to select authentication method: %v", err) + } + + return choice, nil +} + +func existingToken() error { + logoutChoice := true + confirm := huh.NewForm(huh.NewGroup(huh.NewConfirm(). + Title("You currently have a token saved. Do you want to invalidate it first?"). + Affirmative("Yes"). + Negative("No"). + Value(&logoutChoice))).WithTheme(huh.ThemeDracula()) + confirm.Run() + + if logoutChoice { + if _, err := session.InvalidateToken(config.Token()); err != nil { + if err := config.RemoveToken(); err != nil { + return ui.Error("Failed to remove token from config") + } + ui.Info("Token was not valid, removing from config") + } else { + if err := config.RemoveToken(); err != nil { + return ui.Error("Failed to remove token from config") + } + ui.Success("Token invalidated successfully") + } + } else { + return nil + } + + return nil +} + +func cmdToken(cmd *cobra.Command, args []string) error { + + var token string + + input := huh. + NewInput(). + Title("Enter your authentication token"). + Password(true). + Placeholder("vulncheck_******************"). + Value(&token) + + if err := input.Run(); err != nil { + return ui.Error("Token verification failed: %v", err) + } + + if !config.ValidToken(token) { + return ui.Error("Invalid token specified") + } + + return SaveToken(token) +} + +func SaveToken(token string) error { + + var res *sdk.UserResponse + var err error + + _ = spinner.New(). + Style(ui.Pantone). + Title(" Verifying token...").Action(func() { + res, err = session.CheckToken(token) + }).Run() + + if err != nil { + return ui.Error("Token verification failed: %v", err) + } + if err := config.SaveToken(token); err != nil { + return ui.Error("Failed to save token: %v", err) + } + ui.Success(fmt.Sprintf("Authenticated as %s (%s)", res.Data.Name, res.Data.Email)) + return nil +} diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 028293f..9faad6b 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -1,16 +1,11 @@ package login import ( - "fmt" "github.com/MakeNowJust/heredoc/v2" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/huh/spinner" "github.com/spf13/cobra" "github.com/vulncheck-oss/cli/pkg/config" "github.com/vulncheck-oss/cli/pkg/session" "github.com/vulncheck-oss/cli/pkg/ui" - "github.com/vulncheck-oss/cli/pkg/util" - "github.com/vulncheck-oss/sdk" ) type CmdCopy struct { @@ -43,55 +38,29 @@ func Command() *cobra.Command { `), RunE: func(cmd *cobra.Command, args []string) error { + if config.IsCI() { + return ui.Error("This command is interactive and cannot be run in a CI environment, use the VC_TOKEN environment variable instead") + } + if config.HasConfig() && config.HasToken() { - logoutChoice := true - confirm := huh.NewForm(huh.NewGroup(huh.NewConfirm(). - Title("You currently have a token saved. Do you want to invalidate it first?"). - Affirmative("Yes"). - Negative("No"). - Value(&logoutChoice))).WithTheme(huh.ThemeDracula()) - confirm.Run() - - if logoutChoice { - if _, err := session.InvalidateToken(config.Token()); err != nil { - if err := config.RemoveToken(); err != nil { - return ui.Danger("Failed to remove token from config") - } - return ui.Info("Token was not valid, removing from config") - } else { - if err := config.RemoveToken(); err != nil { - return ui.Danger("Failed to remove token from config") - } - ui.Success("Token invalidated successfully") - } - } else { - return nil + if err := existingToken(); err != nil { + return err } - } - var choice string - form := huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Title("Select an authentication method"). - Options( - huh.NewOption("Login with a web browser", "web"), - huh.NewOption("Paste an authentication token", "token"), - ).Value(&choice), - ), - ) - - err := form.Run() + choice, err := chooseAuthMethod() + if err != nil { - return util.FlagErrorf("Failed to select authentication method: %v", err) + return err } switch choice { case "token": return cmdToken(cmd, args) + case "web": + return ui.Error("Command currently under construction") default: - return util.FlagErrorf("Invalid choice") + return ui.Error("Invalid choice") } }, } @@ -106,7 +75,7 @@ func Command() *cobra.Command { Use: "web", Short: "Log in with a VulnCheck account using a web browser", RunE: func(cmd *cobra.Command, args []string) error { - return util.FlagErrorf("web login is not yet implemented") + return ui.Error("web login is not yet implemented") }, } @@ -115,45 +84,3 @@ func Command() *cobra.Command { session.DisableAuthCheck(cmd) return cmd } - -func cmdToken(cmd *cobra.Command, args []string) error { - - var token string - - input := huh. - NewInput(). - Title("Enter your authentication token"). - Password(true). - Placeholder("vulncheck_******************"). - Value(&token) - - if err := input.Run(); err != nil { - return ui.Danger(fmt.Sprintf("Token verification failed: %v", err)) - } - - if !config.ValidToken(token) { - return util.FlagErrorf("Invalid token specified") - } - - return SaveToken(token) -} - -func SaveToken(token string) error { - - var res *sdk.UserResponse - var err error - - _ = spinner.New(). - Style(ui.Pantone). - Title(" Verifying token...").Action(func() { - res, err = session.CheckToken(token) - }).Run() - - if err != nil { - return ui.Danger(fmt.Sprintf("Token verification failed: %v", err)) - } - if err := config.SaveToken(token); err != nil { - return util.FlagErrorf("Failed to save token: %v", err) - } - return ui.Success(fmt.Sprintf("Authenticated as %s (%s)", res.Data.Name, res.Data.Email)) -} diff --git a/pkg/cmd/auth/logout/logout.go b/pkg/cmd/auth/logout/logout.go index 3bd26a7..0efac39 100644 --- a/pkg/cmd/auth/logout/logout.go +++ b/pkg/cmd/auth/logout/logout.go @@ -24,7 +24,8 @@ func Command() *cobra.Command { if err := config.RemoveToken(); err != nil { return ui.Danger("Failed to remove token") } - return ui.Success("Token successfully invalidated") + ui.Success("Token successfully invalidated") + return nil } if errors.Is(err, sdk.ErrorUnauthorized) { if err := config.RemoveToken(); err != nil { diff --git a/pkg/cmd/index/index.go b/pkg/cmd/index/index.go new file mode 100644 index 0000000..9592744 --- /dev/null +++ b/pkg/cmd/index/index.go @@ -0,0 +1,48 @@ +package index + +import ( + "github.com/spf13/cobra" + "github.com/vulncheck-oss/cli/pkg/config" + "github.com/vulncheck-oss/cli/pkg/environment" + "github.com/vulncheck-oss/cli/pkg/ui" + "github.com/vulncheck-oss/sdk" +) + +type indexOptions struct { + Json bool + Browse bool +} + +func Command() *cobra.Command { + + opts := &indexOptions{} + + cmd := &cobra.Command{ + Use: "index ", + Short: "", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return ui.Error("index name is required") + } + response, err := sdk.Connect(environment.Env.API, config.Token()).GetIndex(args[0]) + if err != nil { + return err + } + + if opts.Json { + ui.Json(response.GetData()) + return nil + } + + if opts.Browse { + ui.Viewport(args[0], response.GetData()) + } + return nil + }, + } + + cmd.Flags().BoolVar(&opts.Json, "json", false, "Output as JSON") + cmd.Flags().BoolVar(&opts.Browse, "browse", false, "Browse the index in a pager") + + return cmd +} diff --git a/pkg/cmd/indices/indices.go b/pkg/cmd/indices/indices.go index cff50f9..6a739f1 100644 --- a/pkg/cmd/indices/indices.go +++ b/pkg/cmd/indices/indices.go @@ -2,6 +2,7 @@ package indices import ( "fmt" + "github.com/octoper/go-ray" "github.com/spf13/cobra" "github.com/vulncheck-oss/cli/pkg/config" "github.com/vulncheck-oss/cli/pkg/environment" @@ -32,9 +33,11 @@ func Browse() *cobra.Command { } if len(args) > 0 && args[0] != "" { indices := response.GetData() - _ = ui.Info(fmt.Sprintf("Browsing %d indices searching for \"%s\"", len(ui.IndicesRows(indices, args[0])), args[0])) + ui.Info(fmt.Sprintf("Browsing %d indices searching for \"%s\"", len(ui.IndicesRows(indices, args[0])), args[0])) return ui.Indices(indices, args[0]) } + + ray.Ray(response.String()) ui.Info(fmt.Sprintf("Browsing %d indices", len(response.GetData()))) return ui.Indices(response.GetData(), "") }, diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 93fba9b..b8846af 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -9,6 +9,7 @@ import ( "github.com/vulncheck-oss/cli/pkg/build" "github.com/vulncheck-oss/cli/pkg/cmd/ascii" "github.com/vulncheck-oss/cli/pkg/cmd/auth" + "github.com/vulncheck-oss/cli/pkg/cmd/index" "github.com/vulncheck-oss/cli/pkg/cmd/indices" cmdVersion "github.com/vulncheck-oss/cli/pkg/cmd/version" "github.com/vulncheck-oss/cli/pkg/config" @@ -56,7 +57,7 @@ func NewCmdRoot() *cobra.Command { if session.IsAuthCheckEnabled(cmd) && !session.CheckAuth() { fmt.Println(authHelp()) - return &AuthError{} + return ui.Error("No valid token found") } return nil @@ -64,6 +65,7 @@ func NewCmdRoot() *cobra.Command { }, } + cmd.SilenceUsage = true cmd.SilenceErrors = true cmd.PersistentFlags().Bool("help", false, "Show help for command") @@ -77,6 +79,7 @@ func NewCmdRoot() *cobra.Command { cmd.AddCommand(ascii.Command()) cmd.AddCommand(auth.Command()) cmd.AddCommand(indices.Command()) + cmd.AddCommand(index.Command()) return cmd } @@ -84,10 +87,11 @@ func NewCmdRoot() *cobra.Command { func Execute() { if err := NewCmdRoot().Execute(); err != nil { if errors.Is(err, sdk.ErrorUnauthorized) { - fmt.Println(ui.Danger("Error: %v, Try authenticating with: vc auth login", err.Error())) + fmt.Println(ui.Danger("Error: Unauthorized, Try authenticating with: vc auth login")) + } else { + fmt.Println(ui.Danger(err.Error())) } + os.Exit(1) - // fmt.Println(err) - // os.Exit(1) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 7af38a3..2e5f9a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -122,3 +122,10 @@ func ValidToken(token string) bool { } return true } + +// IsCI based on https://github.com/watson/ci-info/blob/HEAD/index.js +func IsCI() bool { + return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari + os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity + os.Getenv("RUN_ID") != "" // TaskCluster, dsari +} diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/config/config_test.go similarity index 97% rename from pkg/cmd/auth/login/login_test.go rename to pkg/config/config_test.go index 3dd16f1..5b1dcfa 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/config/config_test.go @@ -1,4 +1,4 @@ -package login +package config import ( "testing" diff --git a/pkg/session/session.go b/pkg/session/session.go index 610b84c..d88a4c4 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -5,7 +5,6 @@ import ( "github.com/vulncheck-oss/cli/pkg/config" "github.com/vulncheck-oss/cli/pkg/environment" "github.com/vulncheck-oss/sdk" - "os" ) type MeResponse struct { @@ -21,17 +20,11 @@ type Me struct { } func CheckAuth() bool { - token := os.Getenv("VC_TOKEN") + token := config.Token() if token != "" && config.ValidToken(token) { return true } - if token != "" && !config.ValidToken(token) { - return false - } - - token = config.Token() - - return true + return false } func CheckToken(token string) (response *sdk.UserResponse, err error) { diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 9c6406c..2482d18 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "encoding/json" "fmt" "github.com/charmbracelet/lipgloss" ) @@ -12,28 +13,54 @@ var White = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")) var Emerald = lipgloss.NewStyle().Foreground(lipgloss.Color("#34d399")) var Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000")) -func Success(str string) error { +func Success(str string) { fmt.Printf( format, Emerald.Render("✓"), White.Render(str), ) - return nil } -func Info(str string) error { +func Info(str string) { fmt.Printf( format, Pantone.Render("i"), White.Render(str), ) - return nil } -func Danger(str string, a ...any) error { +func Danger(str string) error { return fmt.Errorf( format, Red.Render("✗"), - White.Render(fmt.Sprintf(str, a)), + White.Render(str), ) } + +func Json(data interface{}) { + marshaled, err := json.MarshalIndent(data, "", " ") + if err != nil { + panic(err) + } + fmt.Println(string(marshaled)) +} + +type FlagError struct { + // Note: not struct{error}: only *FlagError should satisfy error. + err error +} + +func (fe *FlagError) Error() string { + return fe.err.Error() +} + +func (fe *FlagError) Unwrap() error { + return fe.err +} + +func Error(format string, args ...interface{}) error { + return FlagErrorWrap(fmt.Errorf(format, args...)) +} + +// FlagErrorWrap FlagError returns a new FlagError that wraps the specified error. +func FlagErrorWrap(err error) error { return &FlagError{err} } diff --git a/pkg/ui/viewport.go b/pkg/ui/viewport.go new file mode 100644 index 0000000..33177c7 --- /dev/null +++ b/pkg/ui/viewport.go @@ -0,0 +1,145 @@ +package ui + +import ( + "encoding/json" + "fmt" + "github.com/alecthomas/chroma/v2/quick" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "strings" +) + +const useHighPerformanceRenderer = false + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + infoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return titleStyle.Copy().BorderStyle(b) + }() +) + +type model struct { + index string + content string + ready bool + viewport viewport.Model +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + cmds []tea.Cmd + ) + + switch msg := msg.(type) { + case tea.KeyMsg: + if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { + return m, tea.Quit + } + + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + + if !m.ready { + // Since this program is using the full size of the viewport we + // need to wait until we've received the window dimensions before + // we can initialize the viewport. The initial dimensions come in + // quickly, though asynchronously, which is why we wait for them + // here. + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.viewport.YPosition = headerHeight + m.viewport.HighPerformanceRendering = useHighPerformanceRenderer + m.viewport.SetContent(m.content) + m.ready = true + + // This is only necessary for high performance rendering, which in + // most cases you won't need. + // + // Render the viewport one line below the header. + m.viewport.YPosition = headerHeight + 1 + } else { + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + } + + if useHighPerformanceRenderer { + // Render (or re-render) the whole viewport. Necessary both to + // initialize the viewport and when the window is resized. + // + // This is needed for high-performance rendering only. + cmds = append(cmds, viewport.Sync(m.viewport)) + } + } + + // Handle keyboard and mouse events in the viewport + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + if !m.ready { + return "\n Initializing..." + } + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) +} + +func (m model) headerView() string { + title := titleStyle.Render("Browsing index: " + m.index) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, line) +} + +func (m model) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func Viewport(index string, data interface{}) { + + marshaled, err := json.MarshalIndent(data, "", " ") + if err != nil { + panic(err) + } + + var buf strings.Builder + + err = quick.Highlight(&buf, string(marshaled), "json", "terminal16m", "nord") + + if err != nil { + panic(err) + } + + p := tea.NewProgram( + model{index: index, content: buf.String()}, + tea.WithAltScreen(), + tea.WithMouseCellMotion(), + ) + + if _, err := p.Run(); err != nil { + fmt.Println("could not run program:", err) + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go deleted file mode 100644 index d042c10..0000000 --- a/pkg/util/util.go +++ /dev/null @@ -1,25 +0,0 @@ -package util - -import ( - "fmt" -) - -type FlagError struct { - // Note: not struct{error}: only *FlagError should satisfy error. - err error -} - -func (fe *FlagError) Error() string { - return fe.err.Error() -} - -func (fe *FlagError) Unwrap() error { - return fe.err -} - -func FlagErrorf(format string, args ...interface{}) error { - return FlagErrorWrap(fmt.Errorf(format, args...)) -} - -// FlagErrorWrap FlagError returns a new FlagError that wraps the specified error. -func FlagErrorWrap(err error) error { return &FlagError{err} }