github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/notifications.go (about) 1 package main 2 3 import ( 4 "context" 5 "encoding/csv" 6 "encoding/json" 7 "fmt" 8 "io/fs" 9 "net/url" 10 "os" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/fatih/color" 17 "github.com/go-openapi/strfmt" 18 log "github.com/sirupsen/logrus" 19 "github.com/spf13/cobra" 20 "gopkg.in/tomb.v2" 21 "gopkg.in/yaml.v3" 22 23 "github.com/crowdsecurity/go-cs-lib/ptr" 24 "github.com/crowdsecurity/go-cs-lib/version" 25 26 "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" 27 "github.com/crowdsecurity/crowdsec/pkg/apiclient" 28 "github.com/crowdsecurity/crowdsec/pkg/csconfig" 29 "github.com/crowdsecurity/crowdsec/pkg/csplugin" 30 "github.com/crowdsecurity/crowdsec/pkg/csprofiles" 31 "github.com/crowdsecurity/crowdsec/pkg/models" 32 "github.com/crowdsecurity/crowdsec/pkg/types" 33 ) 34 35 type NotificationsCfg struct { 36 Config csplugin.PluginConfig `json:"plugin_config"` 37 Profiles []*csconfig.ProfileCfg `json:"associated_profiles"` 38 ids []uint 39 } 40 41 type cliNotifications struct { 42 cfg configGetter 43 } 44 45 func NewCLINotifications(cfg configGetter) *cliNotifications { 46 return &cliNotifications{ 47 cfg: cfg, 48 } 49 } 50 51 func (cli *cliNotifications) NewCommand() *cobra.Command { 52 cmd := &cobra.Command{ 53 Use: "notifications [action]", 54 Short: "Helper for notification plugin configuration", 55 Long: "To list/inspect/test notification template", 56 Args: cobra.MinimumNArgs(1), 57 Aliases: []string{"notifications", "notification"}, 58 DisableAutoGenTag: true, 59 PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 60 cfg := cli.cfg() 61 if err := require.LAPI(cfg); err != nil { 62 return err 63 } 64 if err := cfg.LoadAPIClient(); err != nil { 65 return fmt.Errorf("loading api client: %w", err) 66 } 67 if err := require.Notifications(cfg); err != nil { 68 return err 69 } 70 71 return nil 72 }, 73 } 74 75 cmd.AddCommand(cli.NewListCmd()) 76 cmd.AddCommand(cli.NewInspectCmd()) 77 cmd.AddCommand(cli.NewReinjectCmd()) 78 cmd.AddCommand(cli.NewTestCmd()) 79 80 return cmd 81 } 82 83 func (cli *cliNotifications) getPluginConfigs() (map[string]csplugin.PluginConfig, error) { 84 cfg := cli.cfg() 85 pcfgs := map[string]csplugin.PluginConfig{} 86 wf := func(path string, info fs.FileInfo, err error) error { 87 if info == nil { 88 return fmt.Errorf("error while traversing directory %s: %w", path, err) 89 } 90 91 name := filepath.Join(cfg.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice 92 if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) { 93 ts, err := csplugin.ParsePluginConfigFile(name) 94 if err != nil { 95 return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err) 96 } 97 98 for _, t := range ts { 99 csplugin.SetRequiredFields(&t) 100 pcfgs[t.Name] = t 101 } 102 } 103 104 return nil 105 } 106 107 if err := filepath.Walk(cfg.ConfigPaths.NotificationDir, wf); err != nil { 108 return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err) 109 } 110 111 return pcfgs, nil 112 } 113 114 func (cli *cliNotifications) getProfilesConfigs() (map[string]NotificationsCfg, error) { 115 cfg := cli.cfg() 116 // A bit of a tricky stuf now: reconcile profiles and notification plugins 117 pcfgs, err := cli.getPluginConfigs() 118 if err != nil { 119 return nil, err 120 } 121 122 ncfgs := map[string]NotificationsCfg{} 123 for _, pc := range pcfgs { 124 ncfgs[pc.Name] = NotificationsCfg{ 125 Config: pc, 126 } 127 } 128 129 profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles) 130 if err != nil { 131 return nil, fmt.Errorf("while extracting profiles from configuration: %w", err) 132 } 133 134 for profileID, profile := range profiles { 135 for _, notif := range profile.Cfg.Notifications { 136 pc, ok := pcfgs[notif] 137 if !ok { 138 return nil, fmt.Errorf("notification plugin '%s' does not exist", notif) 139 } 140 141 tmp, ok := ncfgs[pc.Name] 142 if !ok { 143 return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name) 144 } 145 146 tmp.Profiles = append(tmp.Profiles, profile.Cfg) 147 tmp.ids = append(tmp.ids, uint(profileID)) 148 ncfgs[pc.Name] = tmp 149 } 150 } 151 152 return ncfgs, nil 153 } 154 155 func (cli *cliNotifications) NewListCmd() *cobra.Command { 156 cmd := &cobra.Command{ 157 Use: "list", 158 Short: "list active notifications plugins", 159 Long: `list active notifications plugins`, 160 Example: `cscli notifications list`, 161 Args: cobra.ExactArgs(0), 162 DisableAutoGenTag: true, 163 RunE: func(_ *cobra.Command, _ []string) error { 164 cfg := cli.cfg() 165 ncfgs, err := cli.getProfilesConfigs() 166 if err != nil { 167 return fmt.Errorf("can't build profiles configuration: %w", err) 168 } 169 170 if cfg.Cscli.Output == "human" { 171 notificationListTable(color.Output, ncfgs) 172 } else if cfg.Cscli.Output == "json" { 173 x, err := json.MarshalIndent(ncfgs, "", " ") 174 if err != nil { 175 return fmt.Errorf("failed to marshal notification configuration: %w", err) 176 } 177 fmt.Printf("%s", string(x)) 178 } else if cfg.Cscli.Output == "raw" { 179 csvwriter := csv.NewWriter(os.Stdout) 180 err := csvwriter.Write([]string{"Name", "Type", "Profile name"}) 181 if err != nil { 182 return fmt.Errorf("failed to write raw header: %w", err) 183 } 184 for _, b := range ncfgs { 185 profilesList := []string{} 186 for _, p := range b.Profiles { 187 profilesList = append(profilesList, p.Name) 188 } 189 err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")}) 190 if err != nil { 191 return fmt.Errorf("failed to write raw content: %w", err) 192 } 193 } 194 csvwriter.Flush() 195 } 196 197 return nil 198 }, 199 } 200 201 return cmd 202 } 203 204 func (cli *cliNotifications) NewInspectCmd() *cobra.Command { 205 cmd := &cobra.Command{ 206 Use: "inspect", 207 Short: "Inspect active notifications plugin configuration", 208 Long: `Inspect active notifications plugin and show configuration`, 209 Example: `cscli notifications inspect <plugin_name>`, 210 Args: cobra.ExactArgs(1), 211 DisableAutoGenTag: true, 212 RunE: func(_ *cobra.Command, args []string) error { 213 cfg := cli.cfg() 214 ncfgs, err := cli.getProfilesConfigs() 215 if err != nil { 216 return fmt.Errorf("can't build profiles configuration: %w", err) 217 } 218 ncfg, ok := ncfgs[args[0]] 219 if !ok { 220 return fmt.Errorf("plugin '%s' does not exist or is not active", args[0]) 221 } 222 if cfg.Cscli.Output == "human" || cfg.Cscli.Output == "raw" { 223 fmt.Printf(" - %15s: %15s\n", "Type", ncfg.Config.Type) 224 fmt.Printf(" - %15s: %15s\n", "Name", ncfg.Config.Name) 225 fmt.Printf(" - %15s: %15s\n", "Timeout", ncfg.Config.TimeOut) 226 fmt.Printf(" - %15s: %15s\n", "Format", ncfg.Config.Format) 227 for k, v := range ncfg.Config.Config { 228 fmt.Printf(" - %15s: %15v\n", k, v) 229 } 230 } else if cfg.Cscli.Output == "json" { 231 x, err := json.MarshalIndent(cfg, "", " ") 232 if err != nil { 233 return fmt.Errorf("failed to marshal notification configuration: %w", err) 234 } 235 fmt.Printf("%s", string(x)) 236 } 237 238 return nil 239 }, 240 } 241 242 return cmd 243 } 244 245 func (cli *cliNotifications) NewTestCmd() *cobra.Command { 246 var ( 247 pluginBroker csplugin.PluginBroker 248 pluginTomb tomb.Tomb 249 alertOverride string 250 ) 251 252 cmd := &cobra.Command{ 253 Use: "test [plugin name]", 254 Short: "send a generic test alert to notification plugin", 255 Long: `send a generic test alert to a notification plugin to test configuration even if is not active`, 256 Example: `cscli notifications test [plugin_name]`, 257 Args: cobra.ExactArgs(1), 258 DisableAutoGenTag: true, 259 PreRunE: func(_ *cobra.Command, args []string) error { 260 cfg := cli.cfg() 261 pconfigs, err := cli.getPluginConfigs() 262 if err != nil { 263 return fmt.Errorf("can't build profiles configuration: %w", err) 264 } 265 pcfg, ok := pconfigs[args[0]] 266 if !ok { 267 return fmt.Errorf("plugin name: '%s' does not exist", args[0]) 268 } 269 //Create a single profile with plugin name as notification name 270 return pluginBroker.Init(cfg.PluginConfig, []*csconfig.ProfileCfg{ 271 { 272 Notifications: []string{ 273 pcfg.Name, 274 }, 275 }, 276 }, cfg.ConfigPaths) 277 }, 278 RunE: func(_ *cobra.Command, _ []string) error { 279 pluginTomb.Go(func() error { 280 pluginBroker.Run(&pluginTomb) 281 return nil 282 }) 283 alert := &models.Alert{ 284 Capacity: ptr.Of(int32(0)), 285 Decisions: []*models.Decision{{ 286 Duration: ptr.Of("4h"), 287 Scope: ptr.Of("Ip"), 288 Value: ptr.Of("10.10.10.10"), 289 Type: ptr.Of("ban"), 290 Scenario: ptr.Of("test alert"), 291 Origin: ptr.Of(types.CscliOrigin), 292 }}, 293 Events: []*models.Event{}, 294 EventsCount: ptr.Of(int32(1)), 295 Leakspeed: ptr.Of("0"), 296 Message: ptr.Of("test alert"), 297 ScenarioHash: ptr.Of(""), 298 Scenario: ptr.Of("test alert"), 299 ScenarioVersion: ptr.Of(""), 300 Simulated: ptr.Of(false), 301 Source: &models.Source{ 302 AsName: "", 303 AsNumber: "", 304 Cn: "", 305 IP: "10.10.10.10", 306 Range: "", 307 Scope: ptr.Of("Ip"), 308 Value: ptr.Of("10.10.10.10"), 309 }, 310 StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)), 311 StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)), 312 CreatedAt: time.Now().UTC().Format(time.RFC3339), 313 } 314 if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil { 315 return fmt.Errorf("failed to unmarshal alert override: %w", err) 316 } 317 318 pluginBroker.PluginChannel <- csplugin.ProfileAlert{ 319 ProfileID: uint(0), 320 Alert: alert, 321 } 322 323 //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent 324 pluginTomb.Kill(fmt.Errorf("terminating")) 325 pluginTomb.Wait() 326 327 return nil 328 }, 329 } 330 cmd.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the generic alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)") 331 332 return cmd 333 } 334 335 func (cli *cliNotifications) NewReinjectCmd() *cobra.Command { 336 var ( 337 alertOverride string 338 alert *models.Alert 339 ) 340 341 cmd := &cobra.Command{ 342 Use: "reinject", 343 Short: "reinject an alert into profiles to trigger notifications", 344 Long: `reinject an alert into profiles to be evaluated by the filter and sent to matched notifications plugins`, 345 Example: ` 346 cscli notifications reinject <alert_id> 347 cscli notifications reinject <alert_id> -a '{"remediation": false,"scenario":"notification/test"}' 348 cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}' 349 `, 350 Args: cobra.ExactArgs(1), 351 DisableAutoGenTag: true, 352 PreRunE: func(_ *cobra.Command, args []string) error { 353 var err error 354 alert, err = cli.fetchAlertFromArgString(args[0]) 355 if err != nil { 356 return err 357 } 358 359 return nil 360 }, 361 RunE: func(_ *cobra.Command, _ []string) error { 362 var ( 363 pluginBroker csplugin.PluginBroker 364 pluginTomb tomb.Tomb 365 ) 366 367 cfg := cli.cfg() 368 369 if alertOverride != "" { 370 if err := json.Unmarshal([]byte(alertOverride), alert); err != nil { 371 return fmt.Errorf("can't unmarshal data in the alert flag: %w", err) 372 } 373 } 374 375 err := pluginBroker.Init(cfg.PluginConfig, cfg.API.Server.Profiles, cfg.ConfigPaths) 376 if err != nil { 377 return fmt.Errorf("can't initialize plugins: %w", err) 378 } 379 380 pluginTomb.Go(func() error { 381 pluginBroker.Run(&pluginTomb) 382 return nil 383 }) 384 385 profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles) 386 if err != nil { 387 return fmt.Errorf("cannot extract profiles from configuration: %w", err) 388 } 389 390 for id, profile := range profiles { 391 _, matched, err := profile.EvaluateProfile(alert) 392 if err != nil { 393 return fmt.Errorf("can't evaluate profile %s: %w", profile.Cfg.Name, err) 394 } 395 if !matched { 396 log.Infof("The profile %s didn't match", profile.Cfg.Name) 397 continue 398 } 399 log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name) 400 loop: 401 for { 402 select { 403 case pluginBroker.PluginChannel <- csplugin.ProfileAlert{ 404 ProfileID: uint(id), 405 Alert: alert, 406 }: 407 break loop 408 default: 409 time.Sleep(50 * time.Millisecond) 410 log.Info("sleeping\n") 411 } 412 } 413 414 if profile.Cfg.OnSuccess == "break" { 415 log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name) 416 break 417 } 418 } 419 //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent 420 pluginTomb.Kill(fmt.Errorf("terminating")) 421 pluginTomb.Wait() 422 423 return nil 424 }, 425 } 426 cmd.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the reinjected alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)") 427 428 return cmd 429 } 430 431 func (cli *cliNotifications) fetchAlertFromArgString(toParse string) (*models.Alert, error) { 432 cfg := cli.cfg() 433 434 id, err := strconv.Atoi(toParse) 435 if err != nil { 436 return nil, fmt.Errorf("bad alert id %s", toParse) 437 } 438 439 apiURL, err := url.Parse(cfg.API.Client.Credentials.URL) 440 if err != nil { 441 return nil, fmt.Errorf("error parsing the URL of the API: %w", err) 442 } 443 444 client, err := apiclient.NewClient(&apiclient.Config{ 445 MachineID: cfg.API.Client.Credentials.Login, 446 Password: strfmt.Password(cfg.API.Client.Credentials.Password), 447 UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), 448 URL: apiURL, 449 VersionPrefix: "v1", 450 }) 451 if err != nil { 452 return nil, fmt.Errorf("error creating the client for the API: %w", err) 453 } 454 455 alert, _, err := client.Alerts.GetByID(context.Background(), id) 456 if err != nil { 457 return nil, fmt.Errorf("can't find alert with id %d: %w", id, err) 458 } 459 460 return alert, nil 461 }