diff --git a/cmd/crowdsec-cli/capi.go b/cmd/crowdsec-cli/capi.go index b5180d0505a..b89d9c7edb0 100644 --- a/cmd/crowdsec-cli/capi.go +++ b/cmd/crowdsec-cli/capi.go @@ -155,23 +155,11 @@ func (cli *cliCapi) newRegisterCmd() *cobra.Command { return cmd } -func (cli *cliCapi) status() error { - cfg := cli.cfg() - - if err := require.CAPIRegistered(cfg); err != nil { - return err - } - - password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password) - - apiurl, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL) +// QueryCAPIStatus checks if the Local API is reachable, and if the credentials are correct +func QueryCAPIStatus(hub *cwhub.Hub, credURL string, login string, password string) error { + apiURL, err := url.Parse(credURL) if err != nil { - return fmt.Errorf("parsing api url ('%s'): %w", cfg.API.Server.OnlineClient.Credentials.URL, err) - } - - hub, err := require.Hub(cfg, nil, nil) - if err != nil { - return err + return fmt.Errorf("parsing api url: %w", err) } scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS) @@ -183,22 +171,48 @@ func (cli *cliCapi) status() error { return errors.New("no scenarios installed, abort") } - Client, err = apiclient.NewDefaultClient(apiurl, CAPIURLPrefix, fmt.Sprintf("crowdsec/%s", version.String()), nil) + Client, err = apiclient.NewDefaultClient(apiURL, + CAPIURLPrefix, + fmt.Sprintf("crowdsec/%s", version.String()), + nil) if err != nil { return fmt.Errorf("init default client: %w", err) } + pw := strfmt.Password(password) + t := models.WatcherAuthRequest{ - MachineID: &cfg.API.Server.OnlineClient.Credentials.Login, - Password: &password, + MachineID: &login, + Password: &pw, Scenarios: scenarios, } - log.Infof("Loaded credentials from %s", cfg.API.Server.OnlineClient.CredentialsFilePath) - log.Infof("Trying to authenticate with username %s on %s", cfg.API.Server.OnlineClient.Credentials.Login, apiurl) - _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t) if err != nil { + return err + } + + return nil +} + +func (cli *cliCapi) status() error { + cfg := cli.cfg() + + if err := require.CAPIRegistered(cfg); err != nil { + return err + } + + cred := cfg.API.Server.OnlineClient.Credentials + + hub, err := require.Hub(cfg, nil, nil) + if err != nil { + return err + } + + log.Infof("Loaded credentials from %s", cfg.API.Server.OnlineClient.CredentialsFilePath) + log.Infof("Trying to authenticate with username %s on %s", cred.Login, cred.URL) + + if err := QueryCAPIStatus(hub, cred.URL, cred.Login, cred.Password); err != nil { return fmt.Errorf("failed to authenticate to Central API (CAPI): %w", err) } diff --git a/cmd/crowdsec-cli/lapi.go b/cmd/crowdsec-cli/lapi.go index 369de5b426b..7cffd7ffc7f 100644 --- a/cmd/crowdsec-cli/lapi.go +++ b/cmd/crowdsec-cli/lapi.go @@ -39,23 +39,13 @@ func NewCLILapi(cfg configGetter) *cliLapi { } } -func (cli *cliLapi) status() error { - cfg := cli.cfg() - password := strfmt.Password(cfg.API.Client.Credentials.Password) - login := cfg.API.Client.Credentials.Login - - origURL := cfg.API.Client.Credentials.URL - - apiURL, err := url.Parse(origURL) +// QueryLAPIStatus checks if the Local API is reachable, and if the credentials are correct +func QueryLAPIStatus(hub *cwhub.Hub, credURL string, login string, password string) error { + apiURL, err := url.Parse(credURL) if err != nil { return fmt.Errorf("parsing api url: %w", err) } - hub, err := require.Hub(cfg, nil, nil) - if err != nil { - return err - } - scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS) if err != nil { return fmt.Errorf("failed to get scenarios: %w", err) @@ -69,18 +59,36 @@ func (cli *cliLapi) status() error { return fmt.Errorf("init default client: %w", err) } + pw := strfmt.Password(password) + t := models.WatcherAuthRequest{ MachineID: &login, - Password: &password, + Password: &pw, Scenarios: scenarios, } - log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath) - // use the original string because apiURL would print 'http://unix/' - log.Infof("Trying to authenticate with username %s on %s", login, origURL) - _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t) if err != nil { + return err + } + + return nil +} + +func (cli *cliLapi) status() error { + cfg := cli.cfg() + + cred := cfg.API.Client.Credentials + + hub, err := require.Hub(cfg, nil, nil) + if err != nil { + return err + } + + log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath) + log.Infof("Trying to authenticate with username %s on %s", cred.Login, cred.URL) + + if err := QueryLAPIStatus(hub, cred.URL, cred.Login, cred.Password); err != nil { return fmt.Errorf("failed to authenticate to Local API (LAPI): %w", err) } diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index 95c528f20b5..3881818123f 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -258,7 +258,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall cmd.AddCommand(NewCLIExplain(cli.cfg).NewCommand()) cmd.AddCommand(NewCLIHubTest(cli.cfg).NewCommand()) cmd.AddCommand(NewCLINotifications(cli.cfg).NewCommand()) - cmd.AddCommand(NewCLISupport().NewCommand()) + cmd.AddCommand(NewCLISupport(cli.cfg).NewCommand()) cmd.AddCommand(NewCLIPapi(cli.cfg).NewCommand()) cmd.AddCommand(NewCLICollection(cli.cfg).NewCommand()) cmd.AddCommand(NewCLIParser(cli.cfg).NewCommand()) diff --git a/cmd/crowdsec-cli/support.go b/cmd/crowdsec-cli/support.go index 5890061f502..54b2e7ad9ad 100644 --- a/cmd/crowdsec-cli/support.go +++ b/cmd/crowdsec-cli/support.go @@ -7,52 +7,67 @@ import ( "errors" "fmt" "io" + "net" "net/http" - "net/url" "os" "path/filepath" "regexp" + "strconv" "strings" "time" "github.com/blackfireio/osinfo" - "github.com/go-openapi/strfmt" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/crowdsecurity/go-cs-lib/trace" - "github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" - "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/fflag" - "github.com/crowdsecurity/crowdsec/pkg/models" ) const ( - SUPPORT_METRICS_HUMAN_PATH = "metrics/metrics.human" - SUPPORT_METRICS_PROMETHEUS_PATH = "metrics/metrics.prometheus" - SUPPORT_VERSION_PATH = "version.txt" - SUPPORT_FEATURES_PATH = "features.txt" - SUPPORT_OS_INFO_PATH = "osinfo.txt" - SUPPORT_PARSERS_PATH = "hub/parsers.txt" - SUPPORT_SCENARIOS_PATH = "hub/scenarios.txt" - SUPPORT_CONTEXTS_PATH = "hub/scenarios.txt" - SUPPORT_COLLECTIONS_PATH = "hub/collections.txt" - SUPPORT_POSTOVERFLOWS_PATH = "hub/postoverflows.txt" - SUPPORT_BOUNCERS_PATH = "lapi/bouncers.txt" - SUPPORT_AGENTS_PATH = "lapi/agents.txt" - SUPPORT_CROWDSEC_CONFIG_PATH = "config/crowdsec.yaml" - SUPPORT_LAPI_STATUS_PATH = "lapi_status.txt" - SUPPORT_CAPI_STATUS_PATH = "capi_status.txt" - SUPPORT_ACQUISITION_CONFIG_BASE_PATH = "config/acquis/" - SUPPORT_CROWDSEC_PROFILE_PATH = "config/profiles.yaml" - SUPPORT_CRASH_PATH = "crash/" + SUPPORT_METRICS_DIR = "metrics/" + SUPPORT_VERSION_PATH = "version.txt" + SUPPORT_FEATURES_PATH = "features.txt" + SUPPORT_OS_INFO_PATH = "osinfo.txt" + SUPPORT_HUB_DIR = "hub/" + SUPPORT_BOUNCERS_PATH = "lapi/bouncers.txt" + SUPPORT_AGENTS_PATH = "lapi/agents.txt" + SUPPORT_CROWDSEC_CONFIG_PATH = "config/crowdsec.yaml" + SUPPORT_LAPI_STATUS_PATH = "lapi_status.txt" + SUPPORT_CAPI_STATUS_PATH = "capi_status.txt" + SUPPORT_ACQUISITION_DIR = "config/acquis/" + SUPPORT_CROWDSEC_PROFILE_PATH = "config/profiles.yaml" + SUPPORT_CRASH_DIR = "crash/" + SUPPORT_LOG_DIR = "log/" + SUPPORT_PPROF_DIR = "pprof/" ) +// StringHook collects log entries in a string +type StringHook struct { + LogBuilder strings.Builder + LogLevels []log.Level +} + +func (hook *StringHook) Levels() []log.Level { + return hook.LogLevels +} + +func (hook *StringHook) Fire(entry *log.Entry) error { + logEntry, err := entry.String() + if err != nil { + return err + } + + hook.LogBuilder.WriteString(logEntry) + + return nil +} + // from https://github.com/acarl005/stripansi var reStripAnsi = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") @@ -61,75 +76,76 @@ func stripAnsiString(str string) string { return reStripAnsi.ReplaceAllString(str, "") } -func collectMetrics() ([]byte, []byte, error) { +func (cli *cliSupport) dumpMetrics(ctx context.Context, zw *zip.Writer) error { log.Info("Collecting prometheus metrics") - if csConfig.Cscli.PrometheusUrl == "" { - log.Warn("No Prometheus URL configured, metrics will not be collected") - return nil, nil, errors.New("prometheus_uri is not set") + cfg := cli.cfg() + + if cfg.Cscli.PrometheusUrl == "" { + log.Warn("can't collect metrics: prometheus_uri is not set") } - humanMetrics := bytes.NewBuffer(nil) + humanMetrics := new(bytes.Buffer) ms := NewMetricStore() - if err := ms.Fetch(csConfig.Cscli.PrometheusUrl); err != nil { - return nil, nil, fmt.Errorf("could not fetch prometheus metrics: %w", err) + if err := ms.Fetch(cfg.Cscli.PrometheusUrl); err != nil { + return err } if err := ms.Format(humanMetrics, nil, "human", false); err != nil { - return nil, nil, err + return fmt.Errorf("could not format prometheus metrics: %w", err) } - req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Cscli.PrometheusUrl, nil) if err != nil { - return nil, nil, fmt.Errorf("could not create requests to prometheus endpoint: %w", err) + return fmt.Errorf("could not create request to prometheus endpoint: %w", err) } client := &http.Client{} resp, err := client.Do(req) if err != nil { - return nil, nil, fmt.Errorf("could not get metrics from prometheus endpoint: %w", err) + return fmt.Errorf("could not get metrics from prometheus endpoint: %w", err) } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, fmt.Errorf("could not read metrics from prometheus endpoint: %w", err) - } + cli.writeToZip(zw, SUPPORT_METRICS_DIR+"metrics.prometheus", time.Now(), resp.Body) - return humanMetrics.Bytes(), body, nil + stripped := stripAnsiString(humanMetrics.String()) + + cli.writeToZip(zw, SUPPORT_METRICS_DIR+"metrics.human", time.Now(), strings.NewReader(stripped)) + + return nil } -func collectVersion() []byte { +func (cli *cliSupport) dumpVersion(zw *zip.Writer) { log.Info("Collecting version") - return []byte(cwversion.ShowStr()) + + cli.writeToZip(zw, SUPPORT_VERSION_PATH, time.Now(), strings.NewReader(cwversion.ShowStr())) } -func collectFeatures() []byte { +func (cli *cliSupport) dumpFeatures(zw *zip.Writer) { log.Info("Collecting feature flags") - enabledFeatures := fflag.Crowdsec.GetEnabledFeatures() - - w := bytes.NewBuffer(nil) - for _, k := range enabledFeatures { - fmt.Fprintf(w, "%s\n", k) + w := new(bytes.Buffer) + for _, k := range fflag.Crowdsec.GetEnabledFeatures() { + fmt.Fprintln(w, k) } - return w.Bytes() + cli.writeToZip(zw, SUPPORT_FEATURES_PATH, time.Now(), w) } -func collectOSInfo() ([]byte, error) { +func (cli *cliSupport) dumpOSInfo(zw *zip.Writer) error { log.Info("Collecting OS info") info, err := osinfo.GetOSInfo() if err != nil { - return nil, err + return err } - w := bytes.NewBuffer(nil) + w := new(bytes.Buffer) fmt.Fprintf(w, "Architecture: %s\n", info.Architecture) fmt.Fprintf(w, "Family: %s\n", info.Family) fmt.Fprintf(w, "ID: %s\n", info.ID) @@ -138,155 +154,251 @@ func collectOSInfo() ([]byte, error) { fmt.Fprintf(w, "Version: %s\n", info.Version) fmt.Fprintf(w, "Build: %s\n", info.Build) - return w.Bytes(), nil + cli.writeToZip(zw, SUPPORT_OS_INFO_PATH, time.Now(), w) + + return nil } -func collectHubItems(hub *cwhub.Hub, itemType string) []byte { +func (cli *cliSupport) dumpHubItems(zw *zip.Writer, hub *cwhub.Hub, itemType string) error { var err error - out := bytes.NewBuffer(nil) + out := new(bytes.Buffer) - log.Infof("Collecting %s list", itemType) + log.Infof("Collecting hub: %s", itemType) items := make(map[string][]*cwhub.Item) if items[itemType], err = selectItems(hub, itemType, nil, true); err != nil { - log.Warnf("could not collect %s list: %s", itemType, err) + return fmt.Errorf("could not collect %s list: %w", itemType, err) } if err := listItems(out, []string{itemType}, items, false, "human"); err != nil { - log.Warnf("could not collect %s list: %s", itemType, err) + return fmt.Errorf("could not list %s: %w", itemType, err) } - return out.Bytes() + stripped := stripAnsiString(out.String()) + + cli.writeToZip(zw, SUPPORT_HUB_DIR+itemType+".txt", time.Now(), strings.NewReader(stripped)) + + return nil } -func collectBouncers(dbClient *database.Client) ([]byte, error) { - out := bytes.NewBuffer(nil) +func (cli *cliSupport) dumpBouncers(zw *zip.Writer, db *database.Client) error { + log.Info("Collecting bouncers") + + if db == nil { + return errors.New("no database connection") + } + + out := new(bytes.Buffer) - bouncers, err := dbClient.ListBouncers() + bouncers, err := db.ListBouncers() if err != nil { - return nil, fmt.Errorf("unable to list bouncers: %w", err) + return fmt.Errorf("unable to list bouncers: %w", err) } getBouncersTable(out, bouncers) - return out.Bytes(), nil + stripped := stripAnsiString(out.String()) + + cli.writeToZip(zw, SUPPORT_BOUNCERS_PATH, time.Now(), strings.NewReader(stripped)) + + return nil } -func collectAgents(dbClient *database.Client) ([]byte, error) { - out := bytes.NewBuffer(nil) +func (cli *cliSupport) dumpAgents(zw *zip.Writer, db *database.Client) error { + log.Info("Collecting agents") + + if db == nil { + return errors.New("no database connection") + } - machines, err := dbClient.ListMachines() + out := new(bytes.Buffer) + + machines, err := db.ListMachines() if err != nil { - return nil, fmt.Errorf("unable to list machines: %w", err) + return fmt.Errorf("unable to list machines: %w", err) } getAgentsTable(out, machines) - return out.Bytes(), nil + stripped := stripAnsiString(out.String()) + + cli.writeToZip(zw, SUPPORT_AGENTS_PATH, time.Now(), strings.NewReader(stripped)) + + return nil } -func collectAPIStatus(login string, password string, endpoint string, prefix string, hub *cwhub.Hub) []byte { - if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil { - return []byte("No agent credentials found, are we LAPI ?") - } +func (cli *cliSupport) dumpLAPIStatus(zw *zip.Writer, hub *cwhub.Hub) error { + log.Info("Collecting LAPI status") - pwd := strfmt.Password(password) + cfg := cli.cfg() + cred := cfg.API.Client.Credentials - apiurl, err := url.Parse(endpoint) - if err != nil { - return []byte(fmt.Sprintf("cannot parse API URL: %s", err)) - } + out := new(bytes.Buffer) - scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS) - if err != nil { - return []byte(fmt.Sprintf("could not collect scenarios: %s", err)) - } + fmt.Fprintf(out, "LAPI credentials file: %s\n", cfg.API.Client.CredentialsFilePath) + fmt.Fprintf(out, "LAPI URL: %s\n", cred.URL) + fmt.Fprintf(out, "LAPI username: %s\n", cred.Login) - Client, err = apiclient.NewDefaultClient(apiurl, - prefix, - fmt.Sprintf("crowdsec/%s", version.String()), - nil) - if err != nil { - return []byte(fmt.Sprintf("could not init client: %s", err)) + if err := QueryLAPIStatus(hub, cred.URL, cred.Login, cred.Password); err != nil { + return fmt.Errorf("could not authenticate to Local API (LAPI): %w", err) } - t := models.WatcherAuthRequest{ - MachineID: &login, - Password: &pwd, - Scenarios: scenarios, - } + fmt.Fprintln(out, "You can successfully interact with Local API (LAPI)") - _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t) - if err != nil { - return []byte(fmt.Sprintf("Could not authenticate to API: %s", err)) - } else { - return []byte("Successfully authenticated to LAPI") + cli.writeToZip(zw, SUPPORT_LAPI_STATUS_PATH, time.Now(), out) + + return nil +} + +func (cli *cliSupport) dumpCAPIStatus(zw *zip.Writer, hub *cwhub.Hub) error { + log.Info("Collecting CAPI status") + + cfg := cli.cfg() + cred := cfg.API.Server.OnlineClient.Credentials + + out := new(bytes.Buffer) + + fmt.Fprintf(out, "CAPI credentials file: %s\n", cfg.API.Server.OnlineClient.CredentialsFilePath) + fmt.Fprintf(out, "CAPI URL: %s\n", cred.URL) + fmt.Fprintf(out, "CAPI username: %s\n", cred.Login) + + if err := QueryCAPIStatus(hub, cred.URL, cred.Login, cred.Password); err != nil { + return fmt.Errorf("could not authenticate to Central API (CAPI): %w", err) } + + fmt.Fprintln(out, "You can successfully interact with Central API (CAPI)") + + cli.writeToZip(zw, SUPPORT_CAPI_STATUS_PATH, time.Now(), out) + + return nil } -func collectCrowdsecConfig() []byte { +func (cli *cliSupport) dumpConfigYAML(zw *zip.Writer) error { log.Info("Collecting crowdsec config") - config, err := os.ReadFile(*csConfig.FilePath) + cfg := cli.cfg() + + config, err := os.ReadFile(*cfg.FilePath) if err != nil { - return []byte(fmt.Sprintf("could not read config file: %s", err)) + return fmt.Errorf("could not read config file: %w", err) } r := regexp.MustCompile(`(\s+password:|\s+user:|\s+host:)\s+.*`) - return r.ReplaceAll(config, []byte("$1 ****REDACTED****")) + redacted := r.ReplaceAll(config, []byte("$1 ****REDACTED****")) + + cli.writeToZip(zw, SUPPORT_CROWDSEC_CONFIG_PATH, time.Now(), bytes.NewReader(redacted)) + + return nil } -func collectCrowdsecProfile() []byte { - log.Info("Collecting crowdsec profile") +func (cli *cliSupport) dumpPprof(ctx context.Context, zw *zip.Writer, endpoint string) error { + log.Infof("Collecting pprof/%s data", endpoint) + + ctx, cancel := context.WithTimeout(ctx, 120*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf( + "http://%s/debug/pprof/%s?debug=1", + net.JoinHostPort( + csConfig.Prometheus.ListenAddr, + strconv.Itoa(csConfig.Prometheus.ListenPort), + ), + endpoint, + ), + nil, + ) + if err != nil { + return fmt.Errorf("could not create request to pprof endpoint: %w", err) + } - config, err := os.ReadFile(csConfig.API.Server.ProfilesPath) + client := &http.Client{} + + resp, err := client.Do(req) if err != nil { - return []byte(fmt.Sprintf("could not read profile file: %s", err)) + return fmt.Errorf("could not get pprof data from endpoint: %w", err) } - return config + defer resp.Body.Close() + + cli.writeToZip(zw, SUPPORT_PPROF_DIR+endpoint+".pprof", time.Now(), resp.Body) + + return nil } -func collectAcquisitionConfig() map[string][]byte { +func (cli *cliSupport) dumpProfiles(zw *zip.Writer) { + log.Info("Collecting crowdsec profile") + + cfg := cli.cfg() + cli.writeFileToZip(zw, SUPPORT_CROWDSEC_PROFILE_PATH, cfg.API.Server.ProfilesPath) +} + +func (cli *cliSupport) dumpAcquisitionConfig(zw *zip.Writer) { log.Info("Collecting acquisition config") - ret := make(map[string][]byte) + cfg := cli.cfg() - for _, filename := range csConfig.Crowdsec.AcquisitionFiles { - fileContent, err := os.ReadFile(filename) - if err != nil { - ret[filename] = []byte(fmt.Sprintf("could not read file: %s", err)) - } else { - ret[filename] = fileContent - } + for _, filename := range cfg.Crowdsec.AcquisitionFiles { + fname := strings.ReplaceAll(filename, string(filepath.Separator), "___") + cli.writeFileToZip(zw, SUPPORT_ACQUISITION_DIR+fname, filename) + } +} + +func (cli *cliSupport) dumpLogs(zw *zip.Writer) error { + log.Info("Collecting CrowdSec logs") + + cfg := cli.cfg() + + logDir := cfg.Common.LogDir + + logFiles, err := filepath.Glob(filepath.Join(logDir, "crowdsec*.log")) + if err != nil { + return fmt.Errorf("could not list log files: %w", err) } - return ret + for _, filename := range logFiles { + cli.writeFileToZip(zw, SUPPORT_LOG_DIR+filepath.Base(filename), filename) + } + + return nil } -func collectCrash() ([]string, error) { +func (cli *cliSupport) dumpCrash(zw *zip.Writer) error { log.Info("Collecting crash dumps") - return trace.List() + + traceFiles, err := trace.List() + if err != nil { + return fmt.Errorf("could not list crash dumps: %w", err) + } + + for _, filename := range traceFiles { + cli.writeFileToZip(zw, SUPPORT_CRASH_DIR+filepath.Base(filename), filename) + } + + return nil } -type cliSupport struct{} +type cliSupport struct { + cfg configGetter +} -func NewCLISupport() *cliSupport { - return &cliSupport{} +func NewCLISupport(cfg configGetter) *cliSupport { + return &cliSupport{ + cfg: cfg, + } } -func (cli cliSupport) NewCommand() *cobra.Command { +func (cli *cliSupport) NewCommand() *cobra.Command { cmd := &cobra.Command{ Use: "support [action]", Short: "Provide commands to help during support", Args: cobra.MinimumNArgs(1), DisableAutoGenTag: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return nil - }, } cmd.AddCommand(cli.NewDumpCmd()) @@ -294,198 +406,217 @@ func (cli cliSupport) NewCommand() *cobra.Command { return cmd } -func (cli cliSupport) NewDumpCmd() *cobra.Command { - var outFile string +// writeToZip adds a file to the zip archive, from a reader +func (cli *cliSupport) writeToZip(zipWriter *zip.Writer, filename string, mtime time.Time, reader io.Reader) { + header := &zip.FileHeader{ + Name: filename, + Method: zip.Deflate, + Modified: mtime, + } - cmd := &cobra.Command{ - Use: "dump", - Short: "Dump all your configuration to a zip file for easier support", - Long: `Dump the following informations: -- Crowdsec version -- OS version -- Installed collections list -- Installed parsers list -- Installed scenarios list -- Installed postoverflows list -- Installed context list -- Bouncers list -- Machines list -- CAPI status -- LAPI status -- Crowdsec config (sensitive information like username and password are redacted) -- Crowdsec metrics`, - Example: `cscli support dump -cscli support dump -f /tmp/crowdsec-support.zip -`, - Args: cobra.NoArgs, - DisableAutoGenTag: true, - RunE: func(_ *cobra.Command, _ []string) error { - var err error - var skipHub, skipDB, skipCAPI, skipLAPI, skipAgent bool - infos := map[string][]byte{ - SUPPORT_VERSION_PATH: collectVersion(), - SUPPORT_FEATURES_PATH: collectFeatures(), - } + fw, err := zipWriter.CreateHeader(header) + if err != nil { + log.Errorf("could not add zip entry for %s: %s", filename, err) + return + } - if outFile == "" { - outFile = "/tmp/crowdsec-support.zip" - } + _, err = io.Copy(fw, reader) + if err != nil { + log.Errorf("could not write zip entry for %s: %s", filename, err) + } +} - dbClient, err := database.NewClient(csConfig.DbConfig) - if err != nil { - log.Warnf("Could not connect to database: %s", err) - skipDB = true - infos[SUPPORT_BOUNCERS_PATH] = []byte(err.Error()) - infos[SUPPORT_AGENTS_PATH] = []byte(err.Error()) - } +// writeToZip adds a file to the zip archive, from a file, and retains the mtime +func (cli *cliSupport) writeFileToZip(zw *zip.Writer, filename string, fromFile string) { + mtime := time.Now() - if err = csConfig.LoadAPIServer(true); err != nil { - log.Warnf("could not load LAPI, skipping CAPI check") - skipLAPI = true - infos[SUPPORT_CAPI_STATUS_PATH] = []byte(err.Error()) - } + fi, err := os.Stat(fromFile) + if err == nil { + mtime = fi.ModTime() + } - if err = csConfig.LoadCrowdsec(); err != nil { - log.Warnf("could not load agent config, skipping crowdsec config check") - skipAgent = true - } + fin, err := os.Open(fromFile) + if err != nil { + log.Errorf("could not open file %s: %s", fromFile, err) + return + } + defer fin.Close() - hub, err := require.Hub(csConfig, nil, nil) - if err != nil { - log.Warn("Could not init hub, running on LAPI ? Hub related information will not be collected") - skipHub = true - infos[SUPPORT_PARSERS_PATH] = []byte(err.Error()) - infos[SUPPORT_SCENARIOS_PATH] = []byte(err.Error()) - infos[SUPPORT_POSTOVERFLOWS_PATH] = []byte(err.Error()) - infos[SUPPORT_CONTEXTS_PATH] = []byte(err.Error()) - infos[SUPPORT_COLLECTIONS_PATH] = []byte(err.Error()) - } + cli.writeToZip(zw, filename, mtime, fin) +} - if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil { - log.Warn("no agent credentials found, skipping LAPI connectivity check") - if _, ok := infos[SUPPORT_LAPI_STATUS_PATH]; ok { - infos[SUPPORT_LAPI_STATUS_PATH] = append(infos[SUPPORT_LAPI_STATUS_PATH], []byte("\nNo LAPI credentials found")...) - } - skipLAPI = true - } +func (cli *cliSupport) dump(ctx context.Context, outFile string) error { + var skipCAPI, skipLAPI, skipAgent bool - if csConfig.API.Server == nil || csConfig.API.Server.OnlineClient == nil || csConfig.API.Server.OnlineClient.Credentials == nil { - log.Warn("no CAPI credentials found, skipping CAPI connectivity check") - skipCAPI = true - } + collector := &StringHook{ + LogLevels: log.AllLevels, + } + log.AddHook(collector) - infos[SUPPORT_METRICS_HUMAN_PATH], infos[SUPPORT_METRICS_PROMETHEUS_PATH], err = collectMetrics() - if err != nil { - log.Warnf("could not collect prometheus metrics information: %s", err) - infos[SUPPORT_METRICS_HUMAN_PATH] = []byte(err.Error()) - infos[SUPPORT_METRICS_PROMETHEUS_PATH] = []byte(err.Error()) - } + cfg := cli.cfg() - infos[SUPPORT_OS_INFO_PATH], err = collectOSInfo() - if err != nil { - log.Warnf("could not collect OS information: %s", err) - infos[SUPPORT_OS_INFO_PATH] = []byte(err.Error()) - } + if outFile == "" { + outFile = filepath.Join(os.TempDir(), "crowdsec-support.zip") + } - infos[SUPPORT_CROWDSEC_CONFIG_PATH] = collectCrowdsecConfig() + w := bytes.NewBuffer(nil) + zipWriter := zip.NewWriter(w) - if !skipHub { - infos[SUPPORT_PARSERS_PATH] = collectHubItems(hub, cwhub.PARSERS) - infos[SUPPORT_SCENARIOS_PATH] = collectHubItems(hub, cwhub.SCENARIOS) - infos[SUPPORT_POSTOVERFLOWS_PATH] = collectHubItems(hub, cwhub.POSTOVERFLOWS) - infos[SUPPORT_CONTEXTS_PATH] = collectHubItems(hub, cwhub.POSTOVERFLOWS) - infos[SUPPORT_COLLECTIONS_PATH] = collectHubItems(hub, cwhub.COLLECTIONS) - } + db, err := database.NewClient(cfg.DbConfig) + if err != nil { + log.Warnf("Could not connect to database: %s", err) + } - if !skipDB { - infos[SUPPORT_BOUNCERS_PATH], err = collectBouncers(dbClient) - if err != nil { - log.Warnf("could not collect bouncers information: %s", err) - infos[SUPPORT_BOUNCERS_PATH] = []byte(err.Error()) - } - - infos[SUPPORT_AGENTS_PATH], err = collectAgents(dbClient) - if err != nil { - log.Warnf("could not collect agents information: %s", err) - infos[SUPPORT_AGENTS_PATH] = []byte(err.Error()) - } - } + if err = cfg.LoadAPIServer(true); err != nil { + log.Warnf("could not load LAPI, skipping CAPI check") - if !skipCAPI { - log.Info("Collecting CAPI status") - infos[SUPPORT_CAPI_STATUS_PATH] = collectAPIStatus(csConfig.API.Server.OnlineClient.Credentials.Login, - csConfig.API.Server.OnlineClient.Credentials.Password, - csConfig.API.Server.OnlineClient.Credentials.URL, - CAPIURLPrefix, - hub) - } + skipCAPI = true + } - if !skipLAPI { - log.Info("Collection LAPI status") - infos[SUPPORT_LAPI_STATUS_PATH] = collectAPIStatus(csConfig.API.Client.Credentials.Login, - csConfig.API.Client.Credentials.Password, - csConfig.API.Client.Credentials.URL, - LAPIURLPrefix, - hub) - infos[SUPPORT_CROWDSEC_PROFILE_PATH] = collectCrowdsecProfile() - } + if err = cfg.LoadCrowdsec(); err != nil { + log.Warnf("could not load agent config, skipping crowdsec config check") - if !skipAgent { - acquis := collectAcquisitionConfig() + skipAgent = true + } - for filename, content := range acquis { - fname := strings.ReplaceAll(filename, string(filepath.Separator), "___") - infos[SUPPORT_ACQUISITION_CONFIG_BASE_PATH+fname] = content - } - } + hub, err := require.Hub(cfg, nil, nil) + if err != nil { + log.Warn("Could not init hub, running on LAPI ? Hub related information will not be collected") + // XXX: lapi status check requires scenarios, will return an error + } - crash, err := collectCrash() - if err != nil { - log.Errorf("could not collect crash dumps: %s", err) - } + if cfg.API.Client == nil || cfg.API.Client.Credentials == nil { + log.Warn("no agent credentials found, skipping LAPI connectivity check") - for _, filename := range crash { - content, err := os.ReadFile(filename) - if err != nil { - log.Errorf("could not read crash dump %s: %s", filename, err) - } + skipLAPI = true + } - infos[SUPPORT_CRASH_PATH+filepath.Base(filename)] = content - } + if cfg.API.Server == nil || cfg.API.Server.OnlineClient == nil || cfg.API.Server.OnlineClient.Credentials == nil { + log.Warn("no CAPI credentials found, skipping CAPI connectivity check") - w := bytes.NewBuffer(nil) - zipWriter := zip.NewWriter(w) - - for filename, data := range infos { - header := &zip.FileHeader{ - Name: filename, - Method: zip.Deflate, - // TODO: retain mtime where possible (esp. trace) - Modified: time.Now(), - } - fw, err := zipWriter.CreateHeader(header) - if err != nil { - log.Errorf("Could not add zip entry for %s: %s", filename, err) - continue - } - fw.Write([]byte(stripAnsiString(string(data)))) - } + skipCAPI = true + } - err = zipWriter.Close() - if err != nil { - return fmt.Errorf("could not finalize zip file: %s", err) - } + if err = cli.dumpMetrics(ctx, zipWriter); err != nil { + log.Warn(err) + } - if outFile == "-" { - _, err = os.Stdout.Write(w.Bytes()) - return err - } - err = os.WriteFile(outFile, w.Bytes(), 0o600) - if err != nil { - return fmt.Errorf("could not write zip file to %s: %s", outFile, err) + if err = cli.dumpOSInfo(zipWriter); err != nil { + log.Warnf("could not collect OS information: %s", err) + } + + if err = cli.dumpConfigYAML(zipWriter); err != nil { + log.Warnf("could not collect main config file: %s", err) + } + + if hub != nil { + for _, itemType := range cwhub.ItemTypes { + if err = cli.dumpHubItems(zipWriter, hub, itemType); err != nil { + log.Warnf("could not collect %s information: %s", itemType, err) } - log.Infof("Written zip file to %s", outFile) - return nil + } + } + + if err = cli.dumpBouncers(zipWriter, db); err != nil { + log.Warnf("could not collect bouncers information: %s", err) + } + + if err = cli.dumpAgents(zipWriter, db); err != nil { + log.Warnf("could not collect agents information: %s", err) + } + + if !skipCAPI { + if err = cli.dumpCAPIStatus(zipWriter, hub); err != nil { + log.Warnf("could not collect CAPI status: %s", err) + } + } + + if !skipLAPI { + if err = cli.dumpLAPIStatus(zipWriter, hub); err != nil { + log.Warnf("could not collect LAPI status: %s", err) + } + + // call pprof separately, one might fail for timeout + + if err = cli.dumpPprof(ctx, zipWriter, "goroutine"); err != nil { + log.Warnf("could not collect pprof goroutine data: %s", err) + } + + if err = cli.dumpPprof(ctx, zipWriter, "heap"); err != nil { + log.Warnf("could not collect pprof heap data: %s", err) + } + + if err = cli.dumpPprof(ctx, zipWriter, "profile"); err != nil { + log.Warnf("could not collect pprof cpu data: %s", err) + } + + cli.dumpProfiles(zipWriter) + } + + if !skipAgent { + cli.dumpAcquisitionConfig(zipWriter) + } + + if err = cli.dumpCrash(zipWriter); err != nil { + log.Warnf("could not collect crash dumps: %s", err) + } + + if err = cli.dumpLogs(zipWriter); err != nil { + log.Warnf("could not collect log files: %s", err) + } + + cli.dumpVersion(zipWriter) + cli.dumpFeatures(zipWriter) + + // log of the dump process, without color codes + collectedOutput := stripAnsiString(collector.LogBuilder.String()) + + cli.writeToZip(zipWriter, "dump.log", time.Now(), strings.NewReader(collectedOutput)) + + err = zipWriter.Close() + if err != nil { + return fmt.Errorf("could not finalize zip file: %w", err) + } + + if outFile == "-" { + _, err = os.Stdout.Write(w.Bytes()) + return err + } + + err = os.WriteFile(outFile, w.Bytes(), 0o600) + if err != nil { + return fmt.Errorf("could not write zip file to %s: %w", outFile, err) + } + + log.Infof("Written zip file to %s", outFile) + + return nil +} + +func (cli *cliSupport) NewDumpCmd() *cobra.Command { + var outFile string + + cmd := &cobra.Command{ + Use: "dump", + Short: "Dump all your configuration to a zip file for easier support", + Long: `Dump the following information: +- Crowdsec version +- OS version +- Enabled feature flags +- Latest Crowdsec logs (log processor, LAPI, remediation components) +- Installed collections, parsers, scenarios... +- Bouncers and machines list +- CAPI/LAPI status +- Crowdsec config (sensitive information like username and password are redacted) +- Crowdsec metrics +- Stack trace in case of process crash`, + Example: `cscli support dump +cscli support dump -f /tmp/crowdsec-support.zip +`, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.dump(cmd.Context(), outFile) }, } diff --git a/pkg/database/database.go b/pkg/database/database.go index 96a495f6731..357077e7d6f 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -68,7 +68,7 @@ func NewClient(config *csconfig.DatabaseCfg) (*Client, error) { typ, dia, err := config.ConnectionDialect() if err != nil { - return nil, err // unsupported database caught here + return nil, err //unsupported database caught here } if config.Type == "sqlite" { diff --git a/test/bats/01_cscli.bats b/test/bats/01_cscli.bats index 4c7ce7fbc2c..7e74f6f9714 100644 --- a/test/bats/01_cscli.bats +++ b/test/bats/01_cscli.bats @@ -263,7 +263,7 @@ teardown() { rune -1 cscli lapi status -o json rune -0 jq -r '.msg' <(stderr) - assert_output 'parsing api url: parse "http://127.0.0.1:-80/": invalid port ":-80" after host' + assert_output 'failed to authenticate to Local API (LAPI): parsing api url: parse "http://127.0.0.1:-80/": invalid port ":-80" after host' } @test "cscli - bad LAPI password" {