Skip to content
61 changes: 50 additions & 11 deletions cmd/crowdsec-cli/cliconsole/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"slices"
"strconv"
"strings"
"time"

"github.com/fatih/color"
"github.com/go-openapi/strfmt"
Expand Down Expand Up @@ -65,7 +66,7 @@ func (cli *cliConsole) NewCommand() *cobra.Command {
return cmd
}

func (cli *cliConsole) enroll(ctx context.Context, key string, name string, overwrite bool, tags []string, opts []string) error {
func (cli *cliConsole) enroll(ctx context.Context, key string, name string, overwrite bool, tags []string, opts []string, autoEnroll bool) error {
cfg := cli.cfg()
password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)

Expand All @@ -89,14 +90,31 @@ func (cli *cliConsole) enroll(ctx context.Context, key string, name string, over
},
})

resp, err := c.Auth.EnrollWatcher(ctx, key, name, tags, overwrite)
autoResp, rawResp, err := c.Auth.EnrollWatcher(ctx, key, name, tags, overwrite, autoEnroll)
if err != nil {
return fmt.Errorf("could not enroll instance: %w", err)
}

if resp.Response.StatusCode == http.StatusOK && !overwrite {
log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll")
return nil
if rawResp.Response.StatusCode == http.StatusOK {
if autoEnroll {
log.Warn("The instance is already enrolled in an organization; transfer it using https://app.crowdsec.net/.")
return nil
}
if !overwrite {
log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll")
return nil
}
}

if autoEnroll {
bold := color.New(color.Bold)
log.Infof("Please visit the following URL to enroll your instance: %s", bold.Sprint(autoResp.Url))
log.Infof("This link is valid for the next %s.", time.Until(time.UnixMilli(autoResp.ExpiresAt)).Round(time.Minute))
log.Info("Please restart crowdsec after accepting the enrollment.")

} else {
log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.")
log.Info("Please restart crowdsec after accepting the enrollment.")
}

if err := cli.setConsoleOpts(opts, true); err != nil {
Expand All @@ -107,9 +125,6 @@ func (cli *cliConsole) enroll(ctx context.Context, key string, name string, over
log.Infof("Enabled %s : %s", opt, csconfig.CONSOLE_CONFIGS_HELP[opt])
}

log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.")
log.Info("Please restart crowdsec after accepting the enrollment.")

return nil
}

Expand Down Expand Up @@ -170,9 +185,20 @@ func optionFilterDisable(opts []string, disableOpts []string) ([]string, error)
return opts, nil
}

func (*cliConsole) getDefaultInstanceName() string {
hostname, err := os.Hostname()
if err != nil {
log.Warnf("Could not get machine hostname: %s. Defaulting to machine id as instance name", err)
return ""
}

return hostname
}

func (cli *cliConsole) newEnrollCmd() *cobra.Command {
name := ""
overwrite := false
quickEnroll := false
tags := []string{}
enableOpts := []string{}
disableOpts := []string{}
Expand All @@ -186,15 +212,27 @@ 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: fmt.Sprintf(`cscli console enroll YOUR-ENROLL-KEY
cscli console enroll --quick
cscli console enroll --quick --name [instance_name]
cscli console enroll --name [instance_name] YOUR-ENROLL-KEY
cscli console enroll --name [instance_name] --tags [tag_1] --tags [tag_2] YOUR-ENROLL-KEY
cscli console enroll --enable console_management YOUR-ENROLL-KEY
cscli console enroll --disable context YOUR-ENROLL-KEY

valid options are : %s,all (see 'cscli console status' for details)`, strings.Join(csconfig.CONSOLE_CONFIGS, ",")),
Args: args.ExactArgs(1),
Args: args.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 && !quickEnroll {
return cmd.Usage()
}
if len(args) > 0 && quickEnroll {
return errors.New("enroll key cannot be specified when using quick enroll")
}
key := ""
if len(args) > 0 {
key = args[0]
}
opts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS, csconfig.SEND_CONTEXT}

opts, err := optionFilterEnable(opts, enableOpts)
Expand All @@ -207,16 +245,17 @@ valid options are : %s,all (see 'cscli console status' for details)`, strings.Jo
return err
}

return cli.enroll(cmd.Context(), args[0], name, overwrite, tags, opts)
return cli.enroll(cmd.Context(), key, name, overwrite, tags, opts, quickEnroll)
},
}

flags := cmd.Flags()
flags.StringVarP(&name, "name", "n", "", "Name to display in the console")
flags.StringVarP(&name, "name", "n", cli.getDefaultInstanceName(), "Name to display in the console")
flags.BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
flags.StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
flags.StringSliceVarP(&enableOpts, "enable", "e", enableOpts, "Enable console options")
flags.StringSliceVarP(&disableOpts, "disable", "d", disableOpts, "Disable console options")
flags.BoolVarP(&quickEnroll, "quick", "q", false, "Enrolls the instance without an enroll key by visiting a link to the CrowdSec console.")

return cmd
}
Expand Down
37 changes: 27 additions & 10 deletions pkg/apiclient/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ 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"`
Name string `json:"name"`
Tags []string `json:"tags"`
Overwrite bool `json:"overwrite"`
EnrollKey string `json:"attachment_key,omitempty"`
Name string `json:"name"`
Tags []string `json:"tags"`
Overwrite bool `json:"overwrite,omitempty"`
AutoEnroll bool `json:"autoenroll"`
}

type autoEnrollResponse struct {
Url string `json:"url"`
ExpiresAt int64 `json:"expire_at"`
}

func (s *AuthService) UnregisterWatcher(ctx context.Context) (*Response, error) {
Expand Down Expand Up @@ -68,18 +74,29 @@ func (s *AuthService) AuthenticateWatcher(ctx context.Context, auth models.Watch
return authResp, resp, nil
}

func (s *AuthService) EnrollWatcher(ctx context.Context, enrollKey string, name string, tags []string, overwrite bool) (*Response, error) {
func (s *AuthService) EnrollWatcher(ctx context.Context, enrollKey string, name string, tags []string, overwrite bool, autoEnroll bool) (autoEnrollResponse, *Response, error) {
u := fmt.Sprintf("%s/watchers/enroll", s.client.URLPrefix)

req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &enrollRequest{EnrollKey: enrollKey, Name: name, Tags: tags, Overwrite: overwrite})
b := &enrollRequest{EnrollKey: enrollKey, Name: name, Tags: tags, AutoEnroll: autoEnroll}
// Console refuses enroll requests if overwrite or key is set even to falsy values with autoenroll, so we only set them if they are needed
if overwrite {
b.Overwrite = true
}
if enrollKey != "" {
b.EnrollKey = enrollKey
}

req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, b)
if err != nil {
return nil, err
return autoEnrollResponse{}, nil, err
}

resp, err := s.client.Do(ctx, req, nil)
r := autoEnrollResponse{}

resp, err := s.client.Do(ctx, req, &r)
if err != nil {
return resp, err
return autoEnrollResponse{}, resp, err
}

return resp, nil
return r, resp, nil
}
16 changes: 12 additions & 4 deletions pkg/apiclient/auth_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,17 @@ func TestWatcherEnroll(t *testing.T) {
_, _ = buf.ReadFrom(r.Body)
newStr := buf.String()
log.Debugf("body -> %s", newStr)
parsedBody := map[string]any{}
_ = json.Unmarshal([]byte(newStr), &parsedBody)

if newStr == `{"attachment_key":"goodkey","name":"","tags":[],"overwrite":false}
` {
if parsedBody["attachment_key"] == "goodkey" && !parsedBody["autoenroll"].(bool) {
log.Print("good key")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"statusCode": 200, "message": "OK"}`)
} else if parsedBody["attachment_key"] == nil && parsedBody["autoenroll"].(bool) {
log.Print("autoenroll")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"url": "https://example.com/enroll"}`)
} else {
log.Print("bad key")
w.WriteHeader(http.StatusForbidden)
Expand Down Expand Up @@ -287,9 +292,12 @@ func TestWatcherEnroll(t *testing.T) {

client := NewClient(mycfg)

_, err = client.Auth.EnrollWatcher(ctx, "goodkey", "", []string{}, false)
_, _, err = client.Auth.EnrollWatcher(ctx, "goodkey", "", []string{}, false, false)
require.NoError(t, err)

_, _, err = client.Auth.EnrollWatcher(ctx, "", "", []string{}, false, true)
require.NoError(t, err)

_, err = client.Auth.EnrollWatcher(ctx, "badkey", "", []string{}, false)
_, _, err = client.Auth.EnrollWatcher(ctx, "badkey", "", []string{}, false, false)
assert.Contains(t, err.Error(), "the attachment key provided is not valid", "got %s", err.Error())
}
Loading