github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/support.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/blackfireio/osinfo"
    17  	"github.com/go-openapi/strfmt"
    18  	log "github.com/sirupsen/logrus"
    19  	"github.com/spf13/cobra"
    20  
    21  	"github.com/crowdsecurity/go-cs-lib/version"
    22  
    23  	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
    24  	"github.com/crowdsecurity/crowdsec/pkg/apiclient"
    25  	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
    26  	"github.com/crowdsecurity/crowdsec/pkg/cwversion"
    27  	"github.com/crowdsecurity/crowdsec/pkg/database"
    28  	"github.com/crowdsecurity/crowdsec/pkg/fflag"
    29  	"github.com/crowdsecurity/crowdsec/pkg/models"
    30  )
    31  
    32  const (
    33  	SUPPORT_METRICS_HUMAN_PATH           = "metrics/metrics.human"
    34  	SUPPORT_METRICS_PROMETHEUS_PATH      = "metrics/metrics.prometheus"
    35  	SUPPORT_VERSION_PATH                 = "version.txt"
    36  	SUPPORT_FEATURES_PATH                = "features.txt"
    37  	SUPPORT_OS_INFO_PATH                 = "osinfo.txt"
    38  	SUPPORT_PARSERS_PATH                 = "hub/parsers.txt"
    39  	SUPPORT_SCENARIOS_PATH               = "hub/scenarios.txt"
    40  	SUPPORT_CONTEXTS_PATH                = "hub/scenarios.txt"
    41  	SUPPORT_COLLECTIONS_PATH             = "hub/collections.txt"
    42  	SUPPORT_POSTOVERFLOWS_PATH           = "hub/postoverflows.txt"
    43  	SUPPORT_BOUNCERS_PATH                = "lapi/bouncers.txt"
    44  	SUPPORT_AGENTS_PATH                  = "lapi/agents.txt"
    45  	SUPPORT_CROWDSEC_CONFIG_PATH         = "config/crowdsec.yaml"
    46  	SUPPORT_LAPI_STATUS_PATH             = "lapi_status.txt"
    47  	SUPPORT_CAPI_STATUS_PATH             = "capi_status.txt"
    48  	SUPPORT_ACQUISITION_CONFIG_BASE_PATH = "config/acquis/"
    49  	SUPPORT_CROWDSEC_PROFILE_PATH        = "config/profiles.yaml"
    50  )
    51  
    52  // from https://github.com/acarl005/stripansi
    53  var reStripAnsi = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
    54  
    55  func stripAnsiString(str string) string {
    56  	// the byte version doesn't strip correctly
    57  	return reStripAnsi.ReplaceAllString(str, "")
    58  }
    59  
    60  func collectMetrics() ([]byte, []byte, error) {
    61  	log.Info("Collecting prometheus metrics")
    62  
    63  	if csConfig.Cscli.PrometheusUrl == "" {
    64  		log.Warn("No Prometheus URL configured, metrics will not be collected")
    65  		return nil, nil, fmt.Errorf("prometheus_uri is not set")
    66  	}
    67  
    68  	humanMetrics := bytes.NewBuffer(nil)
    69  
    70  	ms := NewMetricStore()
    71  
    72  	if err := ms.Fetch(csConfig.Cscli.PrometheusUrl); err != nil {
    73  		return nil, nil, fmt.Errorf("could not fetch prometheus metrics: %s", err)
    74  	}
    75  
    76  	if err := ms.Format(humanMetrics, nil, "human", false); err != nil {
    77  		return nil, nil, err
    78  	}
    79  
    80  	req, err := http.NewRequest(http.MethodGet, csConfig.Cscli.PrometheusUrl, nil)
    81  	if err != nil {
    82  		return nil, nil, fmt.Errorf("could not create requests to prometheus endpoint: %s", err)
    83  	}
    84  
    85  	client := &http.Client{}
    86  
    87  	resp, err := client.Do(req)
    88  	if err != nil {
    89  		return nil, nil, fmt.Errorf("could not get metrics from prometheus endpoint: %s", err)
    90  	}
    91  
    92  	defer resp.Body.Close()
    93  
    94  	body, err := io.ReadAll(resp.Body)
    95  	if err != nil {
    96  		return nil, nil, fmt.Errorf("could not read metrics from prometheus endpoint: %s", err)
    97  	}
    98  
    99  	return humanMetrics.Bytes(), body, nil
   100  }
   101  
   102  func collectVersion() []byte {
   103  	log.Info("Collecting version")
   104  	return []byte(cwversion.ShowStr())
   105  }
   106  
   107  func collectFeatures() []byte {
   108  	log.Info("Collecting feature flags")
   109  
   110  	enabledFeatures := fflag.Crowdsec.GetEnabledFeatures()
   111  
   112  	w := bytes.NewBuffer(nil)
   113  	for _, k := range enabledFeatures {
   114  		fmt.Fprintf(w, "%s\n", k)
   115  	}
   116  
   117  	return w.Bytes()
   118  }
   119  
   120  func collectOSInfo() ([]byte, error) {
   121  	log.Info("Collecting OS info")
   122  
   123  	info, err := osinfo.GetOSInfo()
   124  
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	w := bytes.NewBuffer(nil)
   130  	w.WriteString(fmt.Sprintf("Architecture: %s\n", info.Architecture))
   131  	w.WriteString(fmt.Sprintf("Family: %s\n", info.Family))
   132  	w.WriteString(fmt.Sprintf("ID: %s\n", info.ID))
   133  	w.WriteString(fmt.Sprintf("Name: %s\n", info.Name))
   134  	w.WriteString(fmt.Sprintf("Codename: %s\n", info.Codename))
   135  	w.WriteString(fmt.Sprintf("Version: %s\n", info.Version))
   136  	w.WriteString(fmt.Sprintf("Build: %s\n", info.Build))
   137  
   138  	return w.Bytes(), nil
   139  }
   140  
   141  func collectHubItems(hub *cwhub.Hub, itemType string) []byte {
   142  	var err error
   143  
   144  	out := bytes.NewBuffer(nil)
   145  
   146  	log.Infof("Collecting %s list", itemType)
   147  
   148  	items := make(map[string][]*cwhub.Item)
   149  
   150  	if items[itemType], err = selectItems(hub, itemType, nil, true); err != nil {
   151  		log.Warnf("could not collect %s list: %s", itemType, err)
   152  	}
   153  
   154  	if err := listItems(out, []string{itemType}, items, false); err != nil {
   155  		log.Warnf("could not collect %s list: %s", itemType, err)
   156  	}
   157  
   158  	return out.Bytes()
   159  }
   160  
   161  func collectBouncers(dbClient *database.Client) ([]byte, error) {
   162  	out := bytes.NewBuffer(nil)
   163  
   164  	bouncers, err := dbClient.ListBouncers()
   165  	if err != nil {
   166  		return nil, fmt.Errorf("unable to list bouncers: %s", err)
   167  	}
   168  
   169  	getBouncersTable(out, bouncers)
   170  
   171  	return out.Bytes(), nil
   172  }
   173  
   174  func collectAgents(dbClient *database.Client) ([]byte, error) {
   175  	out := bytes.NewBuffer(nil)
   176  
   177  	machines, err := dbClient.ListMachines()
   178  	if err != nil {
   179  		return nil, fmt.Errorf("unable to list machines: %s", err)
   180  	}
   181  
   182  	getAgentsTable(out, machines)
   183  
   184  	return out.Bytes(), nil
   185  }
   186  
   187  func collectAPIStatus(login string, password string, endpoint string, prefix string, hub *cwhub.Hub) []byte {
   188  	if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil {
   189  		return []byte("No agent credentials found, are we LAPI ?")
   190  	}
   191  
   192  	pwd := strfmt.Password(password)
   193  
   194  	apiurl, err := url.Parse(endpoint)
   195  	if err != nil {
   196  		return []byte(fmt.Sprintf("cannot parse API URL: %s", err))
   197  	}
   198  
   199  	scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS)
   200  	if err != nil {
   201  		return []byte(fmt.Sprintf("could not collect scenarios: %s", err))
   202  	}
   203  
   204  	Client, err = apiclient.NewDefaultClient(apiurl,
   205  		prefix,
   206  		fmt.Sprintf("crowdsec/%s", version.String()),
   207  		nil)
   208  	if err != nil {
   209  		return []byte(fmt.Sprintf("could not init client: %s", err))
   210  	}
   211  
   212  	t := models.WatcherAuthRequest{
   213  		MachineID: &login,
   214  		Password:  &pwd,
   215  		Scenarios: scenarios,
   216  	}
   217  
   218  	_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
   219  	if err != nil {
   220  		return []byte(fmt.Sprintf("Could not authenticate to API: %s", err))
   221  	} else {
   222  		return []byte("Successfully authenticated to LAPI")
   223  	}
   224  }
   225  
   226  func collectCrowdsecConfig() []byte {
   227  	log.Info("Collecting crowdsec config")
   228  
   229  	config, err := os.ReadFile(*csConfig.FilePath)
   230  	if err != nil {
   231  		return []byte(fmt.Sprintf("could not read config file: %s", err))
   232  	}
   233  
   234  	r := regexp.MustCompile(`(\s+password:|\s+user:|\s+host:)\s+.*`)
   235  
   236  	return r.ReplaceAll(config, []byte("$1 ****REDACTED****"))
   237  }
   238  
   239  func collectCrowdsecProfile() []byte {
   240  	log.Info("Collecting crowdsec profile")
   241  
   242  	config, err := os.ReadFile(csConfig.API.Server.ProfilesPath)
   243  	if err != nil {
   244  		return []byte(fmt.Sprintf("could not read profile file: %s", err))
   245  	}
   246  
   247  	return config
   248  }
   249  
   250  func collectAcquisitionConfig() map[string][]byte {
   251  	log.Info("Collecting acquisition config")
   252  
   253  	ret := make(map[string][]byte)
   254  
   255  	for _, filename := range csConfig.Crowdsec.AcquisitionFiles {
   256  		fileContent, err := os.ReadFile(filename)
   257  		if err != nil {
   258  			ret[filename] = []byte(fmt.Sprintf("could not read file: %s", err))
   259  		} else {
   260  			ret[filename] = fileContent
   261  		}
   262  	}
   263  
   264  	return ret
   265  }
   266  
   267  type cliSupport struct{}
   268  
   269  func NewCLISupport() *cliSupport {
   270  	return &cliSupport{}
   271  }
   272  
   273  func (cli cliSupport) NewCommand() *cobra.Command {
   274  	cmd := &cobra.Command{
   275  		Use:               "support [action]",
   276  		Short:             "Provide commands to help during support",
   277  		Args:              cobra.MinimumNArgs(1),
   278  		DisableAutoGenTag: true,
   279  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
   280  			return nil
   281  		},
   282  	}
   283  
   284  	cmd.AddCommand(cli.NewDumpCmd())
   285  
   286  	return cmd
   287  }
   288  
   289  func (cli cliSupport) NewDumpCmd() *cobra.Command {
   290  	var outFile string
   291  
   292  	cmd := &cobra.Command{
   293  		Use:   "dump",
   294  		Short: "Dump all your configuration to a zip file for easier support",
   295  		Long: `Dump the following informations:
   296  - Crowdsec version
   297  - OS version
   298  - Installed collections list
   299  - Installed parsers list
   300  - Installed scenarios list
   301  - Installed postoverflows list
   302  - Installed context list
   303  - Bouncers list
   304  - Machines list
   305  - CAPI status
   306  - LAPI status
   307  - Crowdsec config (sensitive information like username and password are redacted)
   308  - Crowdsec metrics`,
   309  		Example: `cscli support dump
   310  cscli support dump -f /tmp/crowdsec-support.zip
   311  `,
   312  		Args:              cobra.NoArgs,
   313  		DisableAutoGenTag: true,
   314  		Run: func(_ *cobra.Command, _ []string) {
   315  			var err error
   316  			var skipHub, skipDB, skipCAPI, skipLAPI, skipAgent bool
   317  			infos := map[string][]byte{
   318  				SUPPORT_VERSION_PATH:  collectVersion(),
   319  				SUPPORT_FEATURES_PATH: collectFeatures(),
   320  			}
   321  
   322  			if outFile == "" {
   323  				outFile = "/tmp/crowdsec-support.zip"
   324  			}
   325  
   326  			dbClient, err = database.NewClient(csConfig.DbConfig)
   327  			if err != nil {
   328  				log.Warnf("Could not connect to database: %s", err)
   329  				skipDB = true
   330  				infos[SUPPORT_BOUNCERS_PATH] = []byte(err.Error())
   331  				infos[SUPPORT_AGENTS_PATH] = []byte(err.Error())
   332  			}
   333  
   334  			if err = csConfig.LoadAPIServer(true); err != nil {
   335  				log.Warnf("could not load LAPI, skipping CAPI check")
   336  				skipLAPI = true
   337  				infos[SUPPORT_CAPI_STATUS_PATH] = []byte(err.Error())
   338  			}
   339  
   340  			if err = csConfig.LoadCrowdsec(); err != nil {
   341  				log.Warnf("could not load agent config, skipping crowdsec config check")
   342  				skipAgent = true
   343  			}
   344  
   345  			hub, err := require.Hub(csConfig, nil, nil)
   346  			if err != nil {
   347  				log.Warn("Could not init hub, running on LAPI ? Hub related information will not be collected")
   348  				skipHub = true
   349  				infos[SUPPORT_PARSERS_PATH] = []byte(err.Error())
   350  				infos[SUPPORT_SCENARIOS_PATH] = []byte(err.Error())
   351  				infos[SUPPORT_POSTOVERFLOWS_PATH] = []byte(err.Error())
   352  				infos[SUPPORT_CONTEXTS_PATH] = []byte(err.Error())
   353  				infos[SUPPORT_COLLECTIONS_PATH] = []byte(err.Error())
   354  			}
   355  
   356  			if csConfig.API.Client == nil || csConfig.API.Client.Credentials == nil {
   357  				log.Warn("no agent credentials found, skipping LAPI connectivity check")
   358  				if _, ok := infos[SUPPORT_LAPI_STATUS_PATH]; ok {
   359  					infos[SUPPORT_LAPI_STATUS_PATH] = append(infos[SUPPORT_LAPI_STATUS_PATH], []byte("\nNo LAPI credentials found")...)
   360  				}
   361  				skipLAPI = true
   362  			}
   363  
   364  			if csConfig.API.Server == nil || csConfig.API.Server.OnlineClient == nil || csConfig.API.Server.OnlineClient.Credentials == nil {
   365  				log.Warn("no CAPI credentials found, skipping CAPI connectivity check")
   366  				skipCAPI = true
   367  			}
   368  
   369  			infos[SUPPORT_METRICS_HUMAN_PATH], infos[SUPPORT_METRICS_PROMETHEUS_PATH], err = collectMetrics()
   370  			if err != nil {
   371  				log.Warnf("could not collect prometheus metrics information: %s", err)
   372  				infos[SUPPORT_METRICS_HUMAN_PATH] = []byte(err.Error())
   373  				infos[SUPPORT_METRICS_PROMETHEUS_PATH] = []byte(err.Error())
   374  			}
   375  
   376  			infos[SUPPORT_OS_INFO_PATH], err = collectOSInfo()
   377  			if err != nil {
   378  				log.Warnf("could not collect OS information: %s", err)
   379  				infos[SUPPORT_OS_INFO_PATH] = []byte(err.Error())
   380  			}
   381  
   382  			infos[SUPPORT_CROWDSEC_CONFIG_PATH] = collectCrowdsecConfig()
   383  
   384  			if !skipHub {
   385  				infos[SUPPORT_PARSERS_PATH] = collectHubItems(hub, cwhub.PARSERS)
   386  				infos[SUPPORT_SCENARIOS_PATH] = collectHubItems(hub, cwhub.SCENARIOS)
   387  				infos[SUPPORT_POSTOVERFLOWS_PATH] = collectHubItems(hub, cwhub.POSTOVERFLOWS)
   388  				infos[SUPPORT_CONTEXTS_PATH] = collectHubItems(hub, cwhub.POSTOVERFLOWS)
   389  				infos[SUPPORT_COLLECTIONS_PATH] = collectHubItems(hub, cwhub.COLLECTIONS)
   390  			}
   391  
   392  			if !skipDB {
   393  				infos[SUPPORT_BOUNCERS_PATH], err = collectBouncers(dbClient)
   394  				if err != nil {
   395  					log.Warnf("could not collect bouncers information: %s", err)
   396  					infos[SUPPORT_BOUNCERS_PATH] = []byte(err.Error())
   397  				}
   398  
   399  				infos[SUPPORT_AGENTS_PATH], err = collectAgents(dbClient)
   400  				if err != nil {
   401  					log.Warnf("could not collect agents information: %s", err)
   402  					infos[SUPPORT_AGENTS_PATH] = []byte(err.Error())
   403  				}
   404  			}
   405  
   406  			if !skipCAPI {
   407  				log.Info("Collecting CAPI status")
   408  				infos[SUPPORT_CAPI_STATUS_PATH] = collectAPIStatus(csConfig.API.Server.OnlineClient.Credentials.Login,
   409  					csConfig.API.Server.OnlineClient.Credentials.Password,
   410  					csConfig.API.Server.OnlineClient.Credentials.URL,
   411  					CAPIURLPrefix,
   412  					hub)
   413  			}
   414  
   415  			if !skipLAPI {
   416  				log.Info("Collection LAPI status")
   417  				infos[SUPPORT_LAPI_STATUS_PATH] = collectAPIStatus(csConfig.API.Client.Credentials.Login,
   418  					csConfig.API.Client.Credentials.Password,
   419  					csConfig.API.Client.Credentials.URL,
   420  					LAPIURLPrefix,
   421  					hub)
   422  				infos[SUPPORT_CROWDSEC_PROFILE_PATH] = collectCrowdsecProfile()
   423  			}
   424  
   425  			if !skipAgent {
   426  				acquis := collectAcquisitionConfig()
   427  
   428  				for filename, content := range acquis {
   429  					fname := strings.ReplaceAll(filename, string(filepath.Separator), "___")
   430  					infos[SUPPORT_ACQUISITION_CONFIG_BASE_PATH+fname] = content
   431  				}
   432  			}
   433  
   434  			w := bytes.NewBuffer(nil)
   435  			zipWriter := zip.NewWriter(w)
   436  
   437  			for filename, data := range infos {
   438  				fw, err := zipWriter.Create(filename)
   439  				if err != nil {
   440  					log.Errorf("Could not add zip entry for %s: %s", filename, err)
   441  					continue
   442  				}
   443  				fw.Write([]byte(stripAnsiString(string(data))))
   444  			}
   445  
   446  			err = zipWriter.Close()
   447  			if err != nil {
   448  				log.Fatalf("could not finalize zip file: %s", err)
   449  			}
   450  
   451  			err = os.WriteFile(outFile, w.Bytes(), 0o600)
   452  			if err != nil {
   453  				log.Fatalf("could not write zip file to %s: %s", outFile, err)
   454  			}
   455  
   456  			log.Infof("Written zip file to %s", outFile)
   457  		},
   458  	}
   459  
   460  	cmd.Flags().StringVarP(&outFile, "outFile", "f", "", "File to dump the information to")
   461  
   462  	return cmd
   463  }