diff --git a/cmd/crowdsec-cli/capi.go b/cmd/crowdsec-cli/capi.go index 5bf127977..509c085fb 100644 --- a/cmd/crowdsec-cli/capi.go +++ b/cmd/crowdsec-cli/capi.go @@ -166,5 +166,6 @@ func NewCapiCmd() *cobra.Command { }, } cmdCapi.AddCommand(cmdCapiStatus) + return cmdCapi } diff --git a/cmd/crowdsec-cli/console.go b/cmd/crowdsec-cli/console.go new file mode 100644 index 000000000..db14a420b --- /dev/null +++ b/cmd/crowdsec-cli/console.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "fmt" + "net/url" + + "github.com/crowdsecurity/crowdsec/pkg/apiclient" + "github.com/crowdsecurity/crowdsec/pkg/cwhub" + "github.com/crowdsecurity/crowdsec/pkg/cwversion" + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func NewConsoleCmd() *cobra.Command { + var cmdConsole = &cobra.Command{ + Use: "console [action]", + Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)", + Args: cobra.MinimumNArgs(1), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI { + log.Fatal("Local API is disabled, please run this command on the local API machine") + } + if csConfig.API.Server.OnlineClient == nil { + log.Fatalf("no configuration for crowdsec API in '%s'", *csConfig.FilePath) + } + + return nil + }, + } + + cmdEnroll := &cobra.Command{ + Use: "enroll [enroll-key]", + Short: "Enroll this instance to https://app.crowdsec.net [requires local API]", + Long: ` +Enroll this instance to https://app.crowdsec.net + +You can get your enrollment key by creating an account on https://app.crowdsec.net. +After running this command your will need to validate the enrollment in the webapp.`, + Example: "cscli console enroll YOUR-ENROLL-KEY", + Args: cobra.ExactArgs(1), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI { + log.Fatal("Local API is disabled, please run this command on the local API machine") + } + if csConfig.API.Server.OnlineClient == nil { + log.Fatalf("no configuration for crowdsec API in '%s'", *csConfig.FilePath) + } + if csConfig.API.Server.OnlineClient.Credentials == nil { + log.Fatal("You must configure CAPI with `cscli capi register` before enrolling your instance") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + password := strfmt.Password(csConfig.API.Server.OnlineClient.Credentials.Password) + apiURL, err := url.Parse(csConfig.API.Server.OnlineClient.Credentials.URL) + if err != nil { + log.Fatalf("Could not parse CAPI URL : %s", err) + } + + if err := csConfig.LoadHub(); err != nil { + log.Fatalf(err.Error()) + } + + if err := cwhub.GetHubIdx(csConfig.Hub); err != nil { + log.Fatalf("Failed to load hub index : %s", err) + log.Infoln("Run 'sudo cscli hub update' to get the hub index") + } + + scenarios, err := cwhub.GetUpstreamInstalledScenariosAsString() + if err != nil { + log.Fatalf("failed to get scenarios : %s", err.Error()) + } + + if len(scenarios) == 0 { + scenarios = make([]string, 0) + } + + c, _ := apiclient.NewClient(&apiclient.Config{ + MachineID: csConfig.API.Server.OnlineClient.Credentials.Login, + Password: password, + Scenarios: scenarios, + UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()), + URL: apiURL, + VersionPrefix: "v2", + }) + _, err = c.Auth.EnrollWatcher(context.Background(), args[0]) + if err != nil { + log.Fatalf("Could not enroll instance: %s", err) + } + log.Infof("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.") + }, + } + + cmdConsole.AddCommand(cmdEnroll) + return cmdConsole +} diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index d1c211d8d..100e0fa83 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -78,7 +78,9 @@ func initConfig() { } var validArgs = []string{ - "scenarios", "parsers", "collections", "capi", "lapi", "postoverflows", "machines", "metrics", "bouncers", "alerts", "decisions", "simulation", "hub", "dashboard", "config", "completion", "version", + "scenarios", "parsers", "collections", "capi", "lapi", "postoverflows", "machines", + "metrics", "bouncers", "alerts", "decisions", "simulation", "hub", "dashboard", + "config", "completion", "version", "console", } func main() { @@ -148,6 +150,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall rootCmd.AddCommand(NewCapiCmd()) rootCmd.AddCommand(NewLapiCmd()) rootCmd.AddCommand(NewCompletionCmd()) + rootCmd.AddCommand(NewConsoleCmd()) if err := rootCmd.Execute(); err != nil { log.Fatalf("While executing root command : %s", err) } diff --git a/pkg/apiclient/auth_service.go b/pkg/apiclient/auth_service.go index cfb4b4803..632fb8778 100644 --- a/pkg/apiclient/auth_service.go +++ b/pkg/apiclient/auth_service.go @@ -11,6 +11,11 @@ import ( type AuthService service +// Don't add it to the models, as they are used with LAPI, but the enroll endpoint is specific to CAPI +type enrollRequest struct { + EnrollKey string `json:"attachment_key"` +} + func (s *AuthService) UnregisterWatcher(ctx context.Context) (*Response, error) { u := fmt.Sprintf("%s/watchers", s.client.URLPrefix) @@ -55,3 +60,17 @@ func (s *AuthService) AuthenticateWatcher(ctx context.Context, auth models.Watch } return resp, nil } + +func (s *AuthService) EnrollWatcher(ctx context.Context, enrollKey string) (*Response, error) { + u := fmt.Sprintf("%s/watchers/enroll", s.client.URLPrefix) + req, err := s.client.NewRequest("POST", u, &enrollRequest{EnrollKey: enrollKey}) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} diff --git a/pkg/apiclient/auth_service_test.go b/pkg/apiclient/auth_service_test.go index 12019e962..ae18b4c89 100644 --- a/pkg/apiclient/auth_service_test.go +++ b/pkg/apiclient/auth_service_test.go @@ -179,3 +179,60 @@ func TestWatcherUnregister(t *testing.T) { } log.Printf("->%T", client) } + +func TestWatcherEnroll(t *testing.T) { + log.SetLevel(log.DebugLevel) + + mux, urlx, teardown := setup() + defer teardown() + + mux.HandleFunc("/watchers/enroll", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + buf := new(bytes.Buffer) + _, _ = buf.ReadFrom(r.Body) + newStr := buf.String() + log.Debugf("body -> %s", newStr) + if newStr == `{"attachment_key":"goodkey"} +` { + log.Print("good key") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"statusCode": 200, "message": "OK"}`) + } else { + log.Print("bad key") + w.WriteHeader(http.StatusForbidden) + fmt.Fprintf(w, `{"message":"the attachment key provided is not valid"}`) + } + }) + mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{"code":200,"expire":"2029-11-30T14:14:24+01:00","token":"toto"}`) + }) + log.Printf("URL is %s", urlx) + apiURL, err := url.Parse(urlx + "/") + if err != nil { + log.Fatalf("parsing api url: %s", apiURL) + } + + mycfg := &Config{ + MachineID: "test_login", + Password: "test_password", + UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()), + URL: apiURL, + VersionPrefix: "v1", + Scenarios: []string{"crowdsecurity/test"}, + } + client, err := NewClient(mycfg) + + if err != nil { + log.Fatalf("new api client: %s", err.Error()) + } + + _, err = client.Auth.EnrollWatcher(context.Background(), "goodkey") + if err != nil { + t.Fatalf("unexpect auth err: %s", err) + } + + _, err = client.Auth.EnrollWatcher(context.Background(), "badkey") + assert.Contains(t, err.Error(), "the attachment key provided is not valid") +}