diff --git a/Makefile b/Makefile index 8862c69..103f36b 100644 --- a/Makefile +++ b/Makefile @@ -27,3 +27,6 @@ test: update: go get -u ./... && go mod tidy + +lint: + @golangci-lint run diff --git a/go.mod b/go.mod index 6c8a3b6..03aba9f 100644 --- a/go.mod +++ b/go.mod @@ -12,9 +12,10 @@ require ( github.com/charmbracelet/huh v0.3.0 github.com/charmbracelet/huh/spinner v0.0.0-20240328185852-590ecabc34b9 github.com/charmbracelet/lipgloss v0.10.0 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 - github.com/vulncheck-oss/sdk v1.2.5 + github.com/vulncheck-oss/sdk v1.2.6 golang.org/x/term v0.18.0 ) diff --git a/go.sum b/go.sum index 436fddc..e42a879 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,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/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -111,8 +113,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vulncheck-oss/sdk v1.2.5 h1:gJgUm+dojHTC9fUSl/doq9IGS5QqtH5BGCDgTkcIjU8= -github.com/vulncheck-oss/sdk v1.2.5/go.mod h1:ufLXRGtv47jpjt2B7FcwUXhexX7F4lj4LaXl3ZyNiKA= +github.com/vulncheck-oss/sdk v1.2.6 h1:5KVRqHs7nnQ9mJNeEUEDwZq7E2V9oGrre0XGBlwG/Ao= +github.com/vulncheck-oss/sdk v1.2.6/go.mod h1:ufLXRGtv47jpjt2B7FcwUXhexX7F4lj4LaXl3ZyNiKA= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index 640e946..42dfd13 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -2,7 +2,8 @@ package auth import ( "github.com/spf13/cobra" - "github.com/vulncheck-oss/cli/pkg/cmd/auth/login" + + cmdLogin "github.com/vulncheck-oss/cli/pkg/cmd/auth/login" "github.com/vulncheck-oss/cli/pkg/cmd/auth/logout" "github.com/vulncheck-oss/cli/pkg/cmd/auth/status" "github.com/vulncheck-oss/cli/pkg/i18n" @@ -18,7 +19,7 @@ func Command() *cobra.Command { session.DisableAuthCheck(cmd) - cmd.AddCommand(login.Command()) + cmd.AddCommand(cmdLogin.Command()) cmd.AddCommand(status.Command()) cmd.AddCommand(logout.Command()) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 5816282..981f326 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -2,8 +2,11 @@ package login import ( "github.com/spf13/cobra" + "github.com/vulncheck-oss/cli/pkg/cmd/auth/login/token" + "github.com/vulncheck-oss/cli/pkg/cmd/auth/login/web" "github.com/vulncheck-oss/cli/pkg/config" "github.com/vulncheck-oss/cli/pkg/i18n" + pkgLogin "github.com/vulncheck-oss/cli/pkg/login" "github.com/vulncheck-oss/cli/pkg/session" "github.com/vulncheck-oss/cli/pkg/ui" ) @@ -27,12 +30,12 @@ func Command() *cobra.Command { } if config.HasConfig() && config.HasToken() { - if err := existingToken(); err != nil { + if err := pkgLogin.ExistingToken(); err != nil { return err } } - choice, err := chooseAuthMethod() + choice, err := pkgLogin.ChooseAuthMethod() if err != nil { return err @@ -40,30 +43,16 @@ func Command() *cobra.Command { switch choice { case "token": - return cmdToken(cmd, args) + return token.CmdToken(cmd, args) case "web": - return ui.Error("Command currently under construction") + return web.CmdWeb(cmd, args) default: return ui.Error("Invalid choice") } }, } - token := &cobra.Command{ - Use: "token", - Short: i18n.C.AuthLoginToken, - RunE: cmdToken, - } - - web := &cobra.Command{ - Use: "web", - Short: i18n.C.AuthLoginWeb, - RunE: func(cmd *cobra.Command, args []string) error { - return ui.Error("web login is not yet implemented") - }, - } - - cmd.AddCommand(web, token) + cmd.AddCommand(web.Command(), token.Command()) session.DisableAuthCheck(cmd) return cmd diff --git a/pkg/cmd/auth/login/token/token.go b/pkg/cmd/auth/login/token/token.go new file mode 100644 index 0000000..322a886 --- /dev/null +++ b/pkg/cmd/auth/login/token/token.go @@ -0,0 +1,40 @@ +package token + +import ( + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "github.com/vulncheck-oss/cli/pkg/config" + "github.com/vulncheck-oss/cli/pkg/i18n" + "github.com/vulncheck-oss/cli/pkg/login" + "github.com/vulncheck-oss/cli/pkg/ui" +) + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "token", + Short: i18n.C.AuthLoginToken, + RunE: CmdToken, + } +} + +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 login.SaveToken(token) +} diff --git a/pkg/cmd/auth/login/web/web.go b/pkg/cmd/auth/login/web/web.go new file mode 100644 index 0000000..c335d22 --- /dev/null +++ b/pkg/cmd/auth/login/web/web.go @@ -0,0 +1,131 @@ +package web + +import ( + "encoding/json" + "fmt" + "github.com/charmbracelet/huh/spinner" + "github.com/pkg/browser" + "github.com/spf13/cobra" + "github.com/vulncheck-oss/cli/pkg/config" + "github.com/vulncheck-oss/cli/pkg/environment" + "github.com/vulncheck-oss/cli/pkg/i18n" + "github.com/vulncheck-oss/cli/pkg/login" + "github.com/vulncheck-oss/cli/pkg/session" + "github.com/vulncheck-oss/cli/pkg/ui" + "os/exec" + "runtime" + "strings" + "time" +) + +/** +step 1. generate an inquiry. +step 2. prompt the user to visit the inquiry URL. +step 3. loop and sleep waiting for an inquiry response. +*/ + +type Inquiry struct { + Hash string + Token string + Name string + IP string + Agent string + Location string + Coordinate string + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type InquiryResponse struct { + Benchmark float64 `json:"_benchmark"` + Message string `json:"message"` + Data Inquiry `json:"data"` +} + +type InquiryPingResponse struct { + Benchmark float64 `json:"_benchmark"` + Data Inquiry `json:"data"` +} + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "web", + Short: i18n.C.AuthLoginWeb, + RunE: CmdWeb, + } +} + +func CmdWeb(cmd *cobra.Command, args []string) error { + var responseJSON *InquiryResponse + response, err := session.Connect(config.Token()).Form("name", GetName()).Request("POST", "/inquiry") + if err != nil { + return err + } + defer response.Body.Close() + _ = json.NewDecoder(response.Body).Decode(&responseJSON) + + ui.Info("Attempting to launch vulncheck.com in your browser...") + if err := browser.OpenURL(fmt.Sprintf("%s/inquiry/%s", environment.Env.WEB, responseJSON.Data.Hash)); err != nil { + return err + } + + var errorResponse error + var pingResponse *InquiryPingResponse + + _ = spinner.New(). + Style(ui.Pantone). + Title(" Awaiting Verification...").Action(func() { + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + timeout := time.After(30 * time.Second) + + for { + select { + case <-ticker.C: + var responsePing *InquiryPingResponse + response, err := session.Connect(config.Token()).Request("GET", fmt.Sprintf("/inquiry/ping/%s", responseJSON.Data.Hash)) + if err != nil { + errorResponse = err + return + } + defer response.Body.Close() + _ = json.NewDecoder(response.Body).Decode(&responsePing) + if config.ValidToken(responsePing.Data.Token) { + pingResponse = responsePing + return + } + case <-timeout: + return + } + } + + }).Run() + + if errorResponse != nil { + return errorResponse + } + + if pingResponse != nil { + return login.SaveToken(pingResponse.Data.Token) + } + return nil +} + +// GetName returns the ComputerName and/or hostname of the machine +func GetName() string { + var out []byte + var err error + + if strings.HasPrefix(runtime.GOOS, "darwin") { + out, err = exec.Command("scutil", "--get", "ComputerName").Output() + } else { + out, err = exec.Command("hostname").Output() + } + if err != nil { + return "" + } + + return strings.TrimSpace(string(out)) +} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 49b0390..6029808 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -3,9 +3,9 @@ package status import ( "fmt" "github.com/spf13/cobra" - "github.com/vulncheck-oss/cli/pkg/cmd/auth/login" "github.com/vulncheck-oss/cli/pkg/config" "github.com/vulncheck-oss/cli/pkg/i18n" + "github.com/vulncheck-oss/cli/pkg/login" "github.com/vulncheck-oss/cli/pkg/session" ) diff --git a/pkg/cmd/backup/backup.go b/pkg/cmd/backup/backup.go index 4398e2b..53b2fc8 100644 --- a/pkg/cmd/backup/backup.go +++ b/pkg/cmd/backup/backup.go @@ -12,6 +12,10 @@ import ( "time" ) +type UrlOptions struct { + Json bool +} + func Command() *cobra.Command { cmd := &cobra.Command{ @@ -19,6 +23,10 @@ func Command() *cobra.Command { Short: i18n.C.BackupShort, } + opts := &UrlOptions{ + Json: false, + } + cmdUrl := &cobra.Command{ Use: "url ", Short: i18n.C.BackupUrlShort, @@ -30,10 +38,19 @@ func Command() *cobra.Command { if err != nil { return err } - ui.Json(response.GetData()[0]) + if opts.Json { + ui.Json(response.GetData()[0]) + return nil + } + + ui.Stat("Filename", response.GetData()[0].Filename) + ui.Stat("SHA256", response.GetData()[0].Sha256) + ui.Stat("Date Added", response.GetData()[0].DateAdded) + ui.Stat("URL", response.GetData()[0].URL) return nil }, } + cmdUrl.Flags().BoolVarP(&opts.Json, "json", "j", false, "Output as JSON") cmdDownload := &cobra.Command{ Use: "download ", @@ -49,6 +66,10 @@ func Command() *cobra.Command { file, err := extractFile(response.GetData()[0].URL) + if err != nil { + return err + } + date := parseDate(response.GetData()[0].DateAdded) ui.Info(fmt.Sprintf(i18n.C.BackupDownloadInfo, args[0], date)) diff --git a/pkg/cmd/cpe/cpe.go b/pkg/cmd/cpe/cpe.go index 353e5e2..07a9304 100644 --- a/pkg/cmd/cpe/cpe.go +++ b/pkg/cmd/cpe/cpe.go @@ -9,10 +9,20 @@ import ( "github.com/vulncheck-oss/cli/pkg/ui" ) +type Options struct { + Json bool +} + func Command() *cobra.Command { - return &cobra.Command{ - Use: "cpe ", - Short: i18n.C.CpeShort, + + opts := &Options{ + Json: false, + } + + cmd := &cobra.Command{ + Use: "cpe ", + Short: i18n.C.CpeShort, + Example: fmt.Sprintf(i18n.C.CpeExample, "cpe:2.3:a:sap:businessobjects_business_intelligence_platform:4.2:-:*"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return ui.Error(i18n.C.ErrorCpeSchemeRequired) @@ -21,6 +31,11 @@ func Command() *cobra.Command { if err != nil { return err } + + if opts.Json { + ui.Json(response.GetData()) + return nil + } cves := response.GetData() if err := ui.CpeMeta(response.CpeMeta()); err != nil { return err @@ -34,4 +49,8 @@ func Command() *cobra.Command { return nil }, } + + cmd.Flags().BoolVarP(&opts.Json, "json", "j", false, "Output as JSON") + + return cmd } diff --git a/pkg/cmd/purl/purl.go b/pkg/cmd/purl/purl.go index 23d4757..64b23e8 100644 --- a/pkg/cmd/purl/purl.go +++ b/pkg/cmd/purl/purl.go @@ -9,10 +9,19 @@ import ( "github.com/vulncheck-oss/cli/pkg/ui" ) +type Options struct { + Json bool +} + func Command() *cobra.Command { - return &cobra.Command{ - Use: "purl ", - Short: i18n.C.PurlShort, + opts := &Options{ + Json: false, + } + + cmd := &cobra.Command{ + Use: "purl ", + Short: i18n.C.PurlShort, + Example: fmt.Sprintf(i18n.C.PurlExample, "pkg:hackage/aeson@0.3.2.8"), RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return ui.Error(i18n.C.ErrorPurlSchemeRequired) @@ -21,6 +30,11 @@ func Command() *cobra.Command { if err != nil { return err } + + if opts.Json { + ui.Json(response.GetData()) + return nil + } cves := response.Cves() if err := ui.PurlMeta(response.PurlMeta()); err != nil { return err @@ -34,4 +48,8 @@ func Command() *cobra.Command { return nil }, } + + cmd.Flags().BoolVarP(&opts.Json, "json", "j", false, "Output as JSON") + + return cmd } diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 2bf11b6..b62aca8 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -27,15 +27,6 @@ type AuthError struct { err error } -type exitCode int - -const ( - exitOK exitCode = 0 - exitError exitCode = 1 - exitCancel exitCode = 2 - exitAuthError exitCode = 3 -) - func (ae *AuthError) Error() string { return ae.err.Error() } diff --git a/pkg/cmd/root/root_test.go b/pkg/cmd/root/root_test.go index 35e7fef..2ddcc22 100644 --- a/pkg/cmd/root/root_test.go +++ b/pkg/cmd/root/root_test.go @@ -106,9 +106,7 @@ func setRootActual(args ...string) (*bytes.Buffer, *cobra.Command) { root.SetOut(actual) root.SetErr(actual) var argsArray []string - for _, arg := range args { - argsArray = append(argsArray, arg) - } + argsArray = append(argsArray, args...) root.SetArgs(argsArray) return actual, root } diff --git a/pkg/config/config.go b/pkg/config/config.go index 2e5f9a7..0f59206 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -52,6 +52,7 @@ func saveConfig(config *Config) error { } viper.AddConfigPath(dir) viper.SetConfigName("vc") + viper.SetConfigPermissions(0600) viper.SetConfigType("yaml") viper.Set("Token", config.Token) return viper.WriteConfigAs(fmt.Sprintf("%s/vc.yaml", dir)) @@ -72,12 +73,8 @@ func configDir() (string, error) { } func HasConfig() bool { - _, err := loadConfig() - if err != nil { - return false - } - return true + return err == nil } func Token() string { diff --git a/pkg/i18n/en_US.go b/pkg/i18n/en_US.go index 479c0d4..3e11d3c 100644 --- a/pkg/i18n/en_US.go +++ b/pkg/i18n/en_US.go @@ -65,10 +65,12 @@ var En = Copy{ BackupDownloadComplete: "Backup downloaded successfully", CpeShort: "Look up a specified cpe for any related CVEs", + CpeExample: "vc cpe \"%s\"", CpeNoCves: "No CVEs were found for cpe %s", CpeCvesFound: "%d CVEs were found for cpe %s", - PurlShort: "Look up a specified PURL for any CVES or vulnerabilities", + PurlShort: "Look up a specified PURL for any CVES or vulnerabilities", + PurlExample: "vc purl \"%s\"", PurlNoCves: "No CVEs were found for purl %s", PurlCvesFound: "%d CVEs were found for purl %s", diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 93d98b1..12eb316 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -1,7 +1,5 @@ package i18n -var lang = "en_US" - type Copy struct { AboutInfo string InteractiveOnly string @@ -44,10 +42,12 @@ type Copy struct { BackupDownloadComplete string CpeShort string + CpeExample string CpeNoCves string CpeCvesFound string PurlShort string + PurlExample string PurlNoCves string PurlCvesFound string diff --git a/pkg/cmd/auth/login/interact.go b/pkg/login/login.go similarity index 75% rename from pkg/cmd/auth/login/interact.go rename to pkg/login/login.go index ef3c1e9..5e0ad77 100644 --- a/pkg/cmd/auth/login/interact.go +++ b/pkg/login/login.go @@ -4,14 +4,13 @@ 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) { +func ChooseAuthMethod() (string, error) { var choice string form := huh.NewForm( @@ -33,14 +32,16 @@ func chooseAuthMethod() (string, error) { return choice, nil } -func existingToken() error { +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() + Value(&logoutChoice))).WithTheme(huh.ThemeCatppuccin()) + if err := confirm.Run(); err != nil { + return err + } if logoutChoice { if _, err := session.InvalidateToken(config.Token()); err != nil { @@ -61,28 +62,6 @@ func existingToken() error { 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 diff --git a/pkg/ui/table.go b/pkg/ui/table.go index 0627df3..af1f585 100644 --- a/pkg/ui/table.go +++ b/pkg/ui/table.go @@ -36,7 +36,9 @@ func (m tableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "q", "ctrl+c": return m, tea.Quit case "enter": - m.action(m.table.SelectedRow()[0]) + if err := m.action(m.table.SelectedRow()[0]); err != nil { + return m, tea.Quit + } return m, tea.Quit /* return m, tea.Batch( diff --git a/pkg/ui/ui.go b/pkg/ui/ui.go index 2482d18..accd89a 100644 --- a/pkg/ui/ui.go +++ b/pkg/ui/ui.go @@ -7,9 +7,11 @@ import ( ) var format = "%s %s\n" +var statFormat = "%s %s: %s\n" var Pantone = lipgloss.NewStyle().Foreground(lipgloss.Color("#6667ab")) var White = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")) +var Gray = lipgloss.NewStyle().Foreground(lipgloss.Color("#dddddd")) var Emerald = lipgloss.NewStyle().Foreground(lipgloss.Color("#34d399")) var Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000")) @@ -29,6 +31,15 @@ func Info(str string) { ) } +func Stat(label string, value string) { + fmt.Printf( + statFormat, + Pantone.Render("i"), + Gray.Render(label), + White.Render(value), + ) +} + func Danger(str string) error { return fmt.Errorf( format,