crowdsec/cmd/crowdsec-cli/clicapi/capi.go
mmetc bfed861ba7
don't ask user to reload systemd service when running in docker (#3434)
* don't ask user to reload systemd service when running in docker

* refactor + give appropriate message if terminal is attached

* remove explicit filetype
2025-01-31 10:15:28 +00:00

276 lines
6.8 KiB
Go

package clicapi
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"os"
"github.com/fatih/color"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
type configGetter = func() *csconfig.Config
type cliCapi struct {
cfg configGetter
}
func New(cfg configGetter) *cliCapi {
return &cliCapi{
cfg: cfg,
}
}
func (cli *cliCapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "capi [action]",
Short: "Manage interaction with Central API (CAPI)",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err
}
return require.CAPI(cfg)
},
}
cmd.AddCommand(cli.newRegisterCmd())
cmd.AddCommand(cli.newStatusCmd())
return cmd
}
func (cli *cliCapi) register(ctx context.Context, capiUserPrefix string, outputFile string) error {
cfg := cli.cfg()
capiUser, err := idgen.GenerateMachineID(capiUserPrefix)
if err != nil {
return fmt.Errorf("unable to generate machine id: %w", err)
}
pstr, err := idgen.GeneratePassword(idgen.PasswordLength)
if err != nil {
return err
}
password := strfmt.Password(pstr)
apiurl, err := url.Parse(types.CAPIBaseURL)
if err != nil {
return fmt.Errorf("unable to parse api url %s: %w", types.CAPIBaseURL, err)
}
_, err = apiclient.RegisterClient(ctx, &apiclient.Config{
MachineID: capiUser,
Password: password,
URL: apiurl,
VersionPrefix: "v3",
}, nil)
if err != nil {
return fmt.Errorf("api client register ('%s'): %w", types.CAPIBaseURL, err)
}
log.Infof("Successfully registered to Central API (CAPI)")
var dumpFile string
switch {
case outputFile != "":
dumpFile = outputFile
case cfg.API.Server.OnlineClient.CredentialsFilePath != "":
dumpFile = cfg.API.Server.OnlineClient.CredentialsFilePath
default:
dumpFile = ""
}
apiCfg := csconfig.ApiCredentialsCfg{
Login: capiUser,
Password: password.String(),
URL: types.CAPIBaseURL,
}
apiConfigDump, err := yaml.Marshal(apiCfg)
if err != nil {
return fmt.Errorf("unable to serialize api credentials: %w", err)
}
if dumpFile != "" {
err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
if err != nil {
return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err)
}
log.Infof("Central API credentials written to '%s'", dumpFile)
} else {
fmt.Println(string(apiConfigDump))
}
if msg := reload.UserMessage(); msg != "" {
log.Warning(msg)
}
return nil
}
func (cli *cliCapi) newRegisterCmd() *cobra.Command {
var (
capiUserPrefix string
outputFile string
)
cmd := &cobra.Command{
Use: "register",
Short: "Register to Central API (CAPI)",
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.register(cmd.Context(), capiUserPrefix, outputFile)
},
}
cmd.Flags().StringVarP(&outputFile, "file", "f", "", "output file destination")
cmd.Flags().StringVar(&capiUserPrefix, "schmilblick", "", "set a schmilblick (use in tests only)")
_ = cmd.Flags().MarkHidden("schmilblick")
return cmd
}
// queryCAPIStatus checks if the Central API is reachable, and if the credentials are correct. It then checks if the instance is enrolle in the console.
func queryCAPIStatus(ctx context.Context, hub *cwhub.Hub, credURL string, login string, password string) (bool, bool, error) {
apiURL, err := url.Parse(credURL)
if err != nil {
return false, false, err
}
itemsForAPI := hub.GetInstalledListForAPI()
if len(itemsForAPI) == 0 {
return false, false, errors.New("no scenarios or appsec-rules installed, abort")
}
passwd := strfmt.Password(password)
client, err := apiclient.NewClient(&apiclient.Config{
MachineID: login,
Password: passwd,
Scenarios: itemsForAPI,
URL: apiURL,
// I don't believe papi is neede to check enrollement
// PapiURL: papiURL,
VersionPrefix: "v3",
UpdateScenario: func(_ context.Context) ([]string, error) {
return itemsForAPI, nil
},
})
if err != nil {
return false, false, err
}
pw := strfmt.Password(password)
t := models.WatcherAuthRequest{
MachineID: &login,
Password: &pw,
Scenarios: itemsForAPI,
}
authResp, _, err := client.Auth.AuthenticateWatcher(ctx, t)
if err != nil {
return false, false, err
}
client.GetClient().Transport.(*apiclient.JWTTransport).Token = authResp.Token
if client.IsEnrolled() {
return true, true, nil
}
return true, false, nil
}
func (cli *cliCapi) Status(ctx context.Context, out io.Writer, hub *cwhub.Hub) error {
cfg := cli.cfg()
if err := require.CAPIRegistered(cfg); err != nil {
return err
}
cred := cfg.API.Server.OnlineClient.Credentials
fmt.Fprintf(out, "Loaded credentials from %s\n", cfg.API.Server.OnlineClient.CredentialsFilePath)
fmt.Fprintf(out, "Trying to authenticate with username %s on %s\n", cred.Login, cred.URL)
auth, enrolled, err := queryCAPIStatus(ctx, hub, cred.URL, cred.Login, cred.Password)
if err != nil {
return fmt.Errorf("failed to authenticate to Central API (CAPI): %w", err)
}
if auth {
fmt.Fprint(out, "You can successfully interact with Central API (CAPI)\n")
}
if enrolled {
fmt.Fprint(out, "Your instance is enrolled in the console\n")
}
switch *cfg.API.Server.OnlineClient.Sharing {
case true:
fmt.Fprint(out, "Sharing signals is enabled\n")
case false:
fmt.Fprint(out, "Sharing signals is disabled\n")
}
switch *cfg.API.Server.OnlineClient.PullConfig.Community {
case true:
fmt.Fprint(out, "Pulling community blocklist is enabled\n")
case false:
fmt.Fprint(out, "Pulling community blocklist is disabled\n")
}
switch *cfg.API.Server.OnlineClient.PullConfig.Blocklists {
case true:
fmt.Fprint(out, "Pulling blocklists from the console is enabled\n")
case false:
fmt.Fprint(out, "Pulling blocklists from the console is disabled\n")
}
return nil
}
func (cli *cliCapi) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Check status with the Central API (CAPI)",
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
hub, err := require.Hub(cli.cfg(), nil)
if err != nil {
return err
}
return cli.Status(cmd.Context(), color.Output, hub)
},
}
return cmd
}