package main import ( "context" "encoding/csv" "encoding/json" "fmt" "io/fs" "net/url" "os" "path/filepath" "strconv" "strings" "time" "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csprofiles" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/go-openapi/strfmt" "github.com/olekukonko/tablewriter" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/tomb.v2" ) type NotificationsCfg struct { Config csplugin.PluginConfig `json:"plugin_config"` Profiles []*csconfig.ProfileCfg `json:"associated_profiles"` ids []uint } func NewNotificationsCmd() *cobra.Command { var cmdNotifications = &cobra.Command{ Use: "notifications [action]", Short: "Helper for notification plugin configuration", Long: "To list/inspect/test notification template", Args: cobra.MinimumNArgs(1), Aliases: []string{"notifications", "notification"}, DisableAutoGenTag: true, PersistentPreRun: func(cmd *cobra.Command, args []string) { var ( err error ) if err = csConfig.API.Server.LoadProfiles(); err != nil { log.Fatalf(err.Error()) } if csConfig.ConfigPaths.NotificationDir == "" { log.Fatalf("config_paths.notification_dir is not set in crowdsec config") } }, } var cmdNotificationsList = &cobra.Command{ Use: "list", Short: "List active notifications plugins", Long: `List active notifications plugins`, Example: `cscli notifications list`, Args: cobra.ExactArgs(0), DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, arg []string) error { ncfgs, err := getNotificationsConfiguration() if err != nil { return errors.Wrap(err, "Can't build profiles configuration") } if csConfig.Cscli.Output == "human" { table := tablewriter.NewWriter(os.Stdout) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetHeader([]string{"Name", "Type", "Profile name"}) for _, b := range ncfgs { profilesList := []string{} for _, p := range b.Profiles { profilesList = append(profilesList, p.Name) } table.Append([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")}) } table.Render() } else if csConfig.Cscli.Output == "json" { x, err := json.MarshalIndent(ncfgs, "", " ") if err != nil { return errors.New("failed to marshal notification configuration") } fmt.Printf("%s", string(x)) } else if csConfig.Cscli.Output == "raw" { csvwriter := csv.NewWriter(os.Stdout) err := csvwriter.Write([]string{"Name", "Type", "Profile name"}) if err != nil { return errors.Wrap(err, "failed to write raw header") } for _, b := range ncfgs { profilesList := []string{} for _, p := range b.Profiles { profilesList = append(profilesList, p.Name) } err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")}) if err != nil { return errors.Wrap(err, "failed to write raw content") } } csvwriter.Flush() } return nil }, } cmdNotifications.AddCommand(cmdNotificationsList) var cmdNotificationsInspect = &cobra.Command{ Use: "inspect", Short: "Inspect active notifications plugin configuration", Long: `Inspect active notifications plugin and show configuration`, Example: `cscli notifications inspect `, Args: cobra.ExactArgs(1), DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, arg []string) error { var ( cfg NotificationsCfg ok bool ) pluginName := arg[0] if pluginName == "" { errors.New("Please provide a plugin name to inspect") } ncfgs, err := getNotificationsConfiguration() if err != nil { return errors.Wrap(err, "Can't build profiles configuration") } if cfg, ok = ncfgs[pluginName]; !ok { return errors.New("The provided plugin name doesn't exist or isn't active") } if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" { fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type) fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name) fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut) fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format) for k, v := range cfg.Config.Config { fmt.Printf(" - %15s: %15v\n", k, v) } } else if csConfig.Cscli.Output == "json" { x, err := json.MarshalIndent(cfg, "", " ") if err != nil { return errors.New("failed to marshal notification configuration") } fmt.Printf("%s", string(x)) } return nil }, } cmdNotifications.AddCommand(cmdNotificationsInspect) var remediation bool var alertOverride string var cmdNotificationsReinject = &cobra.Command{ Use: "reinject", Short: "reinject alert into notifications system", Long: `Reinject alert into notifications system`, Example: ` cscli notifications reinject cscli notifications reinject --remediation cscli notifications reinject -a '{"remediation": true,"scenario":"notification/test"}' `, Args: cobra.ExactArgs(1), DisableAutoGenTag: true, RunE: func(cmd *cobra.Command, args []string) error { var ( pluginBroker csplugin.PluginBroker pluginTomb tomb.Tomb ) if len(args) != 1 { printHelp(cmd) return errors.New("Wrong number of argument: there should be one argument") } //first: get the alert id, err := strconv.Atoi(args[0]) if err != nil { return errors.New(fmt.Sprintf("bad alert id %s", args[0])) } if err := csConfig.LoadAPIClient(); err != nil { return errors.Wrapf(err, "loading api client") } if csConfig.API.Client == nil { return errors.New("There is no configuration on 'api_client:'") } if csConfig.API.Client.Credentials == nil { return errors.New(fmt.Sprintf("Please provide credentials for the API in '%s'", csConfig.API.Client.CredentialsFilePath)) } apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL) if err != nil { return errors.Wrapf(err, "error parsing the URL of the API") } client, err := apiclient.NewClient(&apiclient.Config{ MachineID: csConfig.API.Client.Credentials.Login, Password: strfmt.Password(csConfig.API.Client.Credentials.Password), UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()), URL: apiURL, VersionPrefix: "v1", }) if err != nil { return errors.Wrapf(err, "error creating the client for the API") } alert, _, err := client.Alerts.GetByID(context.Background(), id) if err != nil { return errors.Wrapf(err, fmt.Sprintf("can't find alert with id %s", args[0])) } if alertOverride != "" { if err = json.Unmarshal([]byte(alertOverride), alert); err != nil { return errors.Wrapf(err, "Can't unmarshal the data given in the alert flag") } } if !remediation { alert.Remediation = true } // second we start plugins err = pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths) if err != nil { return errors.Wrapf(err, "Can't initialize plugins") } pluginTomb.Go(func() error { pluginBroker.Run(&pluginTomb) return nil }) //third: get the profile(s), and process the whole stuff profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) if err != nil { return errors.Wrap(err, "Cannot extract profiles from configuration") } for id, profile := range profiles { _, matched, err := profile.EvaluateProfile(alert) if err != nil { return errors.Wrapf(err, "can't evaluate profile %s", profile.Cfg.Name) } if !matched { log.Infof("The profile %s didn't match", profile.Cfg.Name) continue } log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name) loop: for { select { case pluginBroker.PluginChannel <- csplugin.ProfileAlert{ ProfileID: uint(id), Alert: alert, }: break loop default: time.Sleep(50 * time.Millisecond) log.Info("sleeping\n") } } if profile.Cfg.OnSuccess == "break" { log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name) break } } // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent pluginTomb.Kill(errors.New("terminating")) pluginTomb.Wait() return nil }, } cmdNotificationsReinject.Flags().BoolVarP(&remediation, "remediation", "r", false, "Set Alert.Remediation to false in the reinjected alert (see your profile filter configuration)") cmdNotificationsReinject.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)") cmdNotifications.AddCommand(cmdNotificationsReinject) return cmdNotifications } func getNotificationsConfiguration() (map[string]NotificationsCfg, error) { pcfgs := map[string]csplugin.PluginConfig{} wf := func(path string, info fs.FileInfo, err error) error { if info == nil { return errors.Wrapf(err, "error while traversing directory %s", path) } name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) { ts, err := csplugin.ParsePluginConfigFile(name) if err != nil { return errors.Wrapf(err, "Loading notifification plugin configuration with %s", name) } for _, t := range ts { pcfgs[t.Name] = t } } return nil } if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil { return nil, errors.Wrap(err, "Loading notifification plugin configuration") } // A bit of a tricky stuf now: reconcile profiles and notification plugins ncfgs := map[string]NotificationsCfg{} profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) if err != nil { return nil, errors.Wrap(err, "Cannot extract profiles from configuration") } for profileID, profile := range profiles { loop: for _, notif := range profile.Cfg.Notifications { for name, pc := range pcfgs { if notif == name { if _, ok := ncfgs[pc.Name]; !ok { ncfgs[pc.Name] = NotificationsCfg{ Config: pc, Profiles: []*csconfig.ProfileCfg{profile.Cfg}, ids: []uint{uint(profileID)}, } continue loop } tmp := ncfgs[pc.Name] for _, pr := range tmp.Profiles { var profiles []*csconfig.ProfileCfg if pr.Name == profile.Cfg.Name { continue } profiles = append(tmp.Profiles, profile.Cfg) ids := append(tmp.ids, uint(profileID)) ncfgs[pc.Name] = NotificationsCfg{ Config: tmp.Config, Profiles: profiles, ids: ids, } } } } } } return ncfgs, nil }