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 }