github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/lapi.go (about) 1 package main 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/url" 8 "os" 9 "slices" 10 "sort" 11 "strings" 12 13 "github.com/go-openapi/strfmt" 14 log "github.com/sirupsen/logrus" 15 "github.com/spf13/cobra" 16 "gopkg.in/yaml.v2" 17 18 "github.com/crowdsecurity/go-cs-lib/version" 19 20 "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" 21 "github.com/crowdsecurity/crowdsec/pkg/alertcontext" 22 "github.com/crowdsecurity/crowdsec/pkg/apiclient" 23 "github.com/crowdsecurity/crowdsec/pkg/csconfig" 24 "github.com/crowdsecurity/crowdsec/pkg/cwhub" 25 "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" 26 "github.com/crowdsecurity/crowdsec/pkg/models" 27 "github.com/crowdsecurity/crowdsec/pkg/parser" 28 ) 29 30 const LAPIURLPrefix = "v1" 31 32 type cliLapi struct { 33 cfg configGetter 34 } 35 36 func NewCLILapi(cfg configGetter) *cliLapi { 37 return &cliLapi{ 38 cfg: cfg, 39 } 40 } 41 42 func (cli *cliLapi) status() error { 43 cfg := cli.cfg() 44 password := strfmt.Password(cfg.API.Client.Credentials.Password) 45 login := cfg.API.Client.Credentials.Login 46 47 origURL := cfg.API.Client.Credentials.URL 48 49 apiURL, err := url.Parse(origURL) 50 if err != nil { 51 return fmt.Errorf("parsing api url: %w", err) 52 } 53 54 hub, err := require.Hub(cfg, nil, nil) 55 if err != nil { 56 return err 57 } 58 59 scenarios, err := hub.GetInstalledItemNames(cwhub.SCENARIOS) 60 if err != nil { 61 return fmt.Errorf("failed to get scenarios: %w", err) 62 } 63 64 Client, err = apiclient.NewDefaultClient(apiURL, 65 LAPIURLPrefix, 66 fmt.Sprintf("crowdsec/%s", version.String()), 67 nil) 68 if err != nil { 69 return fmt.Errorf("init default client: %w", err) 70 } 71 72 t := models.WatcherAuthRequest{ 73 MachineID: &login, 74 Password: &password, 75 Scenarios: scenarios, 76 } 77 78 log.Infof("Loaded credentials from %s", cfg.API.Client.CredentialsFilePath) 79 // use the original string because apiURL would print 'http://unix/' 80 log.Infof("Trying to authenticate with username %s on %s", login, origURL) 81 82 _, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t) 83 if err != nil { 84 return fmt.Errorf("failed to authenticate to Local API (LAPI): %w", err) 85 } 86 87 log.Infof("You can successfully interact with Local API (LAPI)") 88 89 return nil 90 } 91 92 func (cli *cliLapi) register(apiURL string, outputFile string, machine string) error { 93 var err error 94 95 lapiUser := machine 96 cfg := cli.cfg() 97 98 if lapiUser == "" { 99 lapiUser, err = generateID("") 100 if err != nil { 101 return fmt.Errorf("unable to generate machine id: %w", err) 102 } 103 } 104 105 password := strfmt.Password(generatePassword(passwordLength)) 106 107 apiurl, err := prepareAPIURL(cfg.API.Client, apiURL) 108 if err != nil { 109 return fmt.Errorf("parsing api url: %w", err) 110 } 111 112 _, err = apiclient.RegisterClient(&apiclient.Config{ 113 MachineID: lapiUser, 114 Password: password, 115 UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), 116 URL: apiurl, 117 VersionPrefix: LAPIURLPrefix, 118 }, nil) 119 120 if err != nil { 121 return fmt.Errorf("api client register: %w", err) 122 } 123 124 log.Printf("Successfully registered to Local API (LAPI)") 125 126 var dumpFile string 127 128 if outputFile != "" { 129 dumpFile = outputFile 130 } else if cfg.API.Client.CredentialsFilePath != "" { 131 dumpFile = cfg.API.Client.CredentialsFilePath 132 } else { 133 dumpFile = "" 134 } 135 136 apiCfg := csconfig.ApiCredentialsCfg{ 137 Login: lapiUser, 138 Password: password.String(), 139 URL: apiURL, 140 } 141 142 apiConfigDump, err := yaml.Marshal(apiCfg) 143 if err != nil { 144 return fmt.Errorf("unable to marshal api credentials: %w", err) 145 } 146 147 if dumpFile != "" { 148 err = os.WriteFile(dumpFile, apiConfigDump, 0o600) 149 if err != nil { 150 return fmt.Errorf("write api credentials to '%s' failed: %w", dumpFile, err) 151 } 152 153 log.Printf("Local API credentials written to '%s'", dumpFile) 154 } else { 155 fmt.Printf("%s\n", string(apiConfigDump)) 156 } 157 158 log.Warning(ReloadMessage()) 159 160 return nil 161 } 162 163 // prepareAPIURL checks/fixes a LAPI connection url (http, https or socket) and returns an URL struct 164 func prepareAPIURL(clientCfg *csconfig.LocalApiClientCfg, apiURL string) (*url.URL, error) { 165 if apiURL == "" { 166 if clientCfg == nil || clientCfg.Credentials == nil || clientCfg.Credentials.URL == "" { 167 return nil, errors.New("no Local API URL. Please provide it in your configuration or with the -u parameter") 168 } 169 170 apiURL = clientCfg.Credentials.URL 171 } 172 173 // URL needs to end with /, but user doesn't care 174 if !strings.HasSuffix(apiURL, "/") { 175 apiURL += "/" 176 } 177 178 // URL needs to start with http://, but user doesn't care 179 if !strings.HasPrefix(apiURL, "http://") && !strings.HasPrefix(apiURL, "https://") && !strings.HasPrefix(apiURL, "/") { 180 apiURL = "http://" + apiURL 181 } 182 183 return url.Parse(apiURL) 184 } 185 186 func (cli *cliLapi) newStatusCmd() *cobra.Command { 187 cmdLapiStatus := &cobra.Command{ 188 Use: "status", 189 Short: "Check authentication to Local API (LAPI)", 190 Args: cobra.MinimumNArgs(0), 191 DisableAutoGenTag: true, 192 RunE: func(_ *cobra.Command, _ []string) error { 193 return cli.status() 194 }, 195 } 196 197 return cmdLapiStatus 198 } 199 200 func (cli *cliLapi) newRegisterCmd() *cobra.Command { 201 var ( 202 apiURL string 203 outputFile string 204 machine string 205 ) 206 207 cmd := &cobra.Command{ 208 Use: "register", 209 Short: "Register a machine to Local API (LAPI)", 210 Long: `Register your machine to the Local API (LAPI). 211 Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`, 212 Args: cobra.MinimumNArgs(0), 213 DisableAutoGenTag: true, 214 RunE: func(_ *cobra.Command, _ []string) error { 215 return cli.register(apiURL, outputFile, machine) 216 }, 217 } 218 219 flags := cmd.Flags() 220 flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)") 221 flags.StringVarP(&outputFile, "file", "f", "", "output file destination") 222 flags.StringVar(&machine, "machine", "", "Name of the machine to register with") 223 224 return cmd 225 } 226 227 func (cli *cliLapi) NewCommand() *cobra.Command { 228 cmd := &cobra.Command{ 229 Use: "lapi [action]", 230 Short: "Manage interaction with Local API (LAPI)", 231 Args: cobra.MinimumNArgs(1), 232 DisableAutoGenTag: true, 233 PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 234 if err := cli.cfg().LoadAPIClient(); err != nil { 235 return fmt.Errorf("loading api client: %w", err) 236 } 237 return nil 238 }, 239 } 240 241 cmd.AddCommand(cli.newRegisterCmd()) 242 cmd.AddCommand(cli.newStatusCmd()) 243 cmd.AddCommand(cli.newContextCmd()) 244 245 return cmd 246 } 247 248 func (cli *cliLapi) addContext(key string, values []string) error { 249 cfg := cli.cfg() 250 251 if err := alertcontext.ValidateContextExpr(key, values); err != nil { 252 return fmt.Errorf("invalid context configuration: %w", err) 253 } 254 255 if _, ok := cfg.Crowdsec.ContextToSend[key]; !ok { 256 cfg.Crowdsec.ContextToSend[key] = make([]string, 0) 257 258 log.Infof("key '%s' added", key) 259 } 260 261 data := cfg.Crowdsec.ContextToSend[key] 262 263 for _, val := range values { 264 if !slices.Contains(data, val) { 265 log.Infof("value '%s' added to key '%s'", val, key) 266 data = append(data, val) 267 } 268 269 cfg.Crowdsec.ContextToSend[key] = data 270 } 271 272 if err := cfg.Crowdsec.DumpContextConfigFile(); err != nil { 273 return err 274 } 275 276 return nil 277 } 278 279 func (cli *cliLapi) newContextAddCmd() *cobra.Command { 280 var ( 281 keyToAdd string 282 valuesToAdd []string 283 ) 284 285 cmd := &cobra.Command{ 286 Use: "add", 287 Short: "Add context to send with alerts. You must specify the output key with the expr value you want", 288 Example: `cscli lapi context add --key source_ip --value evt.Meta.source_ip 289 cscli lapi context add --key file_source --value evt.Line.Src 290 cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user 291 `, 292 DisableAutoGenTag: true, 293 RunE: func(_ *cobra.Command, _ []string) error { 294 hub, err := require.Hub(cli.cfg(), nil, nil) 295 if err != nil { 296 return err 297 } 298 299 if err = alertcontext.LoadConsoleContext(cli.cfg(), hub); err != nil { 300 return fmt.Errorf("while loading context: %w", err) 301 } 302 303 if keyToAdd != "" { 304 if err := cli.addContext(keyToAdd, valuesToAdd); err != nil { 305 return err 306 } 307 return nil 308 } 309 310 for _, v := range valuesToAdd { 311 keySlice := strings.Split(v, ".") 312 key := keySlice[len(keySlice)-1] 313 value := []string{v} 314 if err := cli.addContext(key, value); err != nil { 315 return err 316 } 317 } 318 319 return nil 320 }, 321 } 322 323 flags := cmd.Flags() 324 flags.StringVarP(&keyToAdd, "key", "k", "", "The key of the different values to send") 325 flags.StringSliceVar(&valuesToAdd, "value", []string{}, "The expr fields to associate with the key") 326 cmd.MarkFlagRequired("value") 327 328 return cmd 329 } 330 331 func (cli *cliLapi) newContextStatusCmd() *cobra.Command { 332 cmd := &cobra.Command{ 333 Use: "status", 334 Short: "List context to send with alerts", 335 DisableAutoGenTag: true, 336 RunE: func(_ *cobra.Command, _ []string) error { 337 cfg := cli.cfg() 338 hub, err := require.Hub(cfg, nil, nil) 339 if err != nil { 340 return err 341 } 342 343 if err = alertcontext.LoadConsoleContext(cfg, hub); err != nil { 344 return fmt.Errorf("while loading context: %w", err) 345 } 346 347 if len(cfg.Crowdsec.ContextToSend) == 0 { 348 fmt.Println("No context found on this agent. You can use 'cscli lapi context add' to add context to your alerts.") 349 return nil 350 } 351 352 dump, err := yaml.Marshal(cfg.Crowdsec.ContextToSend) 353 if err != nil { 354 return fmt.Errorf("unable to show context status: %w", err) 355 } 356 357 fmt.Print(string(dump)) 358 359 return nil 360 }, 361 } 362 363 return cmd 364 } 365 366 func (cli *cliLapi) newContextDetectCmd() *cobra.Command { 367 var detectAll bool 368 369 cmd := &cobra.Command{ 370 Use: "detect", 371 Short: "Detect available fields from the installed parsers", 372 Example: `cscli lapi context detect --all 373 cscli lapi context detect crowdsecurity/sshd-logs 374 `, 375 DisableAutoGenTag: true, 376 RunE: func(cmd *cobra.Command, args []string) error { 377 cfg := cli.cfg() 378 if !detectAll && len(args) == 0 { 379 log.Infof("Please provide parsers to detect or --all flag.") 380 printHelp(cmd) 381 } 382 383 // to avoid all the log.Info from the loaders functions 384 log.SetLevel(log.WarnLevel) 385 386 if err := exprhelpers.Init(nil); err != nil { 387 return fmt.Errorf("failed to init expr helpers: %w", err) 388 } 389 390 hub, err := require.Hub(cfg, nil, nil) 391 if err != nil { 392 return err 393 } 394 395 csParsers := parser.NewParsers(hub) 396 if csParsers, err = parser.LoadParsers(cfg, csParsers); err != nil { 397 return fmt.Errorf("unable to load parsers: %w", err) 398 } 399 400 fieldByParsers := make(map[string][]string) 401 for _, node := range csParsers.Nodes { 402 if !detectAll && !slices.Contains(args, node.Name) { 403 continue 404 } 405 if !detectAll { 406 args = removeFromSlice(node.Name, args) 407 } 408 fieldByParsers[node.Name] = make([]string, 0) 409 fieldByParsers[node.Name] = detectNode(node, *csParsers.Ctx) 410 411 subNodeFields := detectSubNode(node, *csParsers.Ctx) 412 for _, field := range subNodeFields { 413 if !slices.Contains(fieldByParsers[node.Name], field) { 414 fieldByParsers[node.Name] = append(fieldByParsers[node.Name], field) 415 } 416 } 417 } 418 419 fmt.Printf("Acquisition :\n\n") 420 fmt.Printf(" - evt.Line.Module\n") 421 fmt.Printf(" - evt.Line.Raw\n") 422 fmt.Printf(" - evt.Line.Src\n") 423 fmt.Println() 424 425 parsersKey := make([]string, 0) 426 for k := range fieldByParsers { 427 parsersKey = append(parsersKey, k) 428 } 429 sort.Strings(parsersKey) 430 431 for _, k := range parsersKey { 432 if len(fieldByParsers[k]) == 0 { 433 continue 434 } 435 fmt.Printf("%s :\n\n", k) 436 values := fieldByParsers[k] 437 sort.Strings(values) 438 for _, value := range values { 439 fmt.Printf(" - %s\n", value) 440 } 441 fmt.Println() 442 } 443 444 if len(args) > 0 { 445 for _, parserNotFound := range args { 446 log.Errorf("parser '%s' not found, can't detect fields", parserNotFound) 447 } 448 } 449 450 return nil 451 }, 452 } 453 cmd.Flags().BoolVarP(&detectAll, "all", "a", false, "Detect evt field for all installed parser") 454 455 return cmd 456 } 457 458 func (cli *cliLapi) newContextDeleteCmd() *cobra.Command { 459 cmd := &cobra.Command{ 460 Use: "delete", 461 DisableAutoGenTag: true, 462 RunE: func(_ *cobra.Command, _ []string) error { 463 filePath := cli.cfg().Crowdsec.ConsoleContextPath 464 if filePath == "" { 465 filePath = "the context file" 466 } 467 fmt.Printf("Command 'delete' is deprecated, please manually edit %s.", filePath) 468 469 return nil 470 }, 471 } 472 473 return cmd 474 } 475 476 func (cli *cliLapi) newContextCmd() *cobra.Command { 477 cmd := &cobra.Command{ 478 Use: "context [command]", 479 Short: "Manage context to send with alerts", 480 DisableAutoGenTag: true, 481 PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 482 cfg := cli.cfg() 483 if err := cfg.LoadCrowdsec(); err != nil { 484 fileNotFoundMessage := fmt.Sprintf("failed to open context file: open %s: no such file or directory", cfg.Crowdsec.ConsoleContextPath) 485 if err.Error() != fileNotFoundMessage { 486 return fmt.Errorf("unable to load CrowdSec agent configuration: %w", err) 487 } 488 } 489 if cfg.DisableAgent { 490 return errors.New("agent is disabled and lapi context can only be used on the agent") 491 } 492 493 return nil 494 }, 495 Run: func(cmd *cobra.Command, _ []string) { 496 printHelp(cmd) 497 }, 498 } 499 500 cmd.AddCommand(cli.newContextAddCmd()) 501 cmd.AddCommand(cli.newContextStatusCmd()) 502 cmd.AddCommand(cli.newContextDetectCmd()) 503 cmd.AddCommand(cli.newContextDeleteCmd()) 504 505 return cmd 506 } 507 508 func detectStaticField(grokStatics []parser.ExtraField) []string { 509 ret := make([]string, 0) 510 511 for _, static := range grokStatics { 512 if static.Parsed != "" { 513 fieldName := fmt.Sprintf("evt.Parsed.%s", static.Parsed) 514 if !slices.Contains(ret, fieldName) { 515 ret = append(ret, fieldName) 516 } 517 } 518 519 if static.Meta != "" { 520 fieldName := fmt.Sprintf("evt.Meta.%s", static.Meta) 521 if !slices.Contains(ret, fieldName) { 522 ret = append(ret, fieldName) 523 } 524 } 525 526 if static.TargetByName != "" { 527 fieldName := static.TargetByName 528 if !strings.HasPrefix(fieldName, "evt.") { 529 fieldName = "evt." + fieldName 530 } 531 532 if !slices.Contains(ret, fieldName) { 533 ret = append(ret, fieldName) 534 } 535 } 536 } 537 538 return ret 539 } 540 541 func detectNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { 542 ret := make([]string, 0) 543 544 if node.Grok.RunTimeRegexp != nil { 545 for _, capturedField := range node.Grok.RunTimeRegexp.Names() { 546 fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField) 547 if !slices.Contains(ret, fieldName) { 548 ret = append(ret, fieldName) 549 } 550 } 551 } 552 553 if node.Grok.RegexpName != "" { 554 grokCompiled, err := parserCTX.Grok.Get(node.Grok.RegexpName) 555 // ignore error (parser does not exist?) 556 if err == nil { 557 for _, capturedField := range grokCompiled.Names() { 558 fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField) 559 if !slices.Contains(ret, fieldName) { 560 ret = append(ret, fieldName) 561 } 562 } 563 } 564 } 565 566 if len(node.Grok.Statics) > 0 { 567 staticsField := detectStaticField(node.Grok.Statics) 568 for _, staticField := range staticsField { 569 if !slices.Contains(ret, staticField) { 570 ret = append(ret, staticField) 571 } 572 } 573 } 574 575 if len(node.Statics) > 0 { 576 staticsField := detectStaticField(node.Statics) 577 for _, staticField := range staticsField { 578 if !slices.Contains(ret, staticField) { 579 ret = append(ret, staticField) 580 } 581 } 582 } 583 584 return ret 585 } 586 587 func detectSubNode(node parser.Node, parserCTX parser.UnixParserCtx) []string { 588 var ret = make([]string, 0) 589 590 for _, subnode := range node.LeavesNodes { 591 if subnode.Grok.RunTimeRegexp != nil { 592 for _, capturedField := range subnode.Grok.RunTimeRegexp.Names() { 593 fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField) 594 if !slices.Contains(ret, fieldName) { 595 ret = append(ret, fieldName) 596 } 597 } 598 } 599 600 if subnode.Grok.RegexpName != "" { 601 grokCompiled, err := parserCTX.Grok.Get(subnode.Grok.RegexpName) 602 if err == nil { 603 // ignore error (parser does not exist?) 604 for _, capturedField := range grokCompiled.Names() { 605 fieldName := fmt.Sprintf("evt.Parsed.%s", capturedField) 606 if !slices.Contains(ret, fieldName) { 607 ret = append(ret, fieldName) 608 } 609 } 610 } 611 } 612 613 if len(subnode.Grok.Statics) > 0 { 614 staticsField := detectStaticField(subnode.Grok.Statics) 615 for _, staticField := range staticsField { 616 if !slices.Contains(ret, staticField) { 617 ret = append(ret, staticField) 618 } 619 } 620 } 621 622 if len(subnode.Statics) > 0 { 623 staticsField := detectStaticField(subnode.Statics) 624 for _, staticField := range staticsField { 625 if !slices.Contains(ret, staticField) { 626 ret = append(ret, staticField) 627 } 628 } 629 } 630 } 631 632 return ret 633 }