diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index 87b806348..9a2025cc9 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -86,7 +86,7 @@ func initConfig() { var validArgs = []string{ "scenarios", "parsers", "collections", "capi", "lapi", "postoverflows", "machines", "metrics", "bouncers", "alerts", "decisions", "simulation", "hub", "dashboard", - "config", "completion", "version", "console", + "config", "completion", "version", "console", "notifications", } func prepender(filename string) string { @@ -181,6 +181,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall rootCmd.AddCommand(NewConsoleCmd()) rootCmd.AddCommand(NewExplainCmd()) rootCmd.AddCommand(NewHubTestCmd()) + rootCmd.AddCommand(NewNotificationsCmd()) if err := rootCmd.Execute(); err != nil { log.Fatalf("While executing root command : %s", err) } diff --git a/cmd/crowdsec-cli/notifications.go b/cmd/crowdsec-cli/notifications.go new file mode 100644 index 000000000..487cc96fd --- /dev/null +++ b/cmd/crowdsec-cli/notifications.go @@ -0,0 +1,191 @@ +package main + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/csplugin" + "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +type NotificationsCfg struct { + Config csplugin.PluginConfig `json:"plugin_config"` + Profiles []*csconfig.ProfileCfg `json:"associated_profiles"` +} + +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()) + } + }, + } + + 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, + Run: func(cmd *cobra.Command, arg []string) { + ncfgs := getNotificationsConfiguration() + 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 { + log.Fatalf("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 { + log.Fatalf("failed to write raw header: %s", err) + } + 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 { + log.Fatalf("failed to write raw content: %s", err) + } + } + csvwriter.Flush() + } + }, + } + 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, + Run: func(cmd *cobra.Command, arg []string) { + var ( + cfg NotificationsCfg + ok bool + ) + + pluginName := arg[0] + + if pluginName == "" { + log.Fatalf("Please provide a plugin name to inspect") + } + ncfgs := getNotificationsConfiguration() + if cfg, ok = ncfgs[pluginName]; !ok { + log.Fatalf("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 { + log.Fatalf("failed to marshal notification configuration") + } + fmt.Printf("%s", string(x)) + } + }, + } + cmdNotifications.AddCommand(cmdNotificationsInspect) + return cmdNotifications +} + +func getNotificationsConfiguration() map[string]NotificationsCfg { + pcfgs := map[string]csplugin.PluginConfig{} + wf := func(path string, info fs.FileInfo, err error) error { + 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 { + log.Fatalf("Loading notifification plugin configuration: %s", err) + } + + // A bit of a tricky stuf now: reconcile profiles and notification plugins + ncfgs := map[string]NotificationsCfg{} + for _, profile := range csConfig.API.Server.Profiles { + loop: + for _, notif := range profile.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}, + } + continue loop + } + tmp := ncfgs[pc.Name] + for _, pr := range tmp.Profiles { + var profiles []*csconfig.ProfileCfg + if pr.Name == profile.Name { + continue + } + profiles = append(tmp.Profiles, profile) + ncfgs[pc.Name] = NotificationsCfg{ + Config: tmp.Config, + Profiles: profiles, + } + } + } + } + } + } + return ncfgs +} diff --git a/pkg/csplugin/broker.go b/pkg/csplugin/broker.go index 2c9091be5..6e2536cde 100644 --- a/pkg/csplugin/broker.go +++ b/pkg/csplugin/broker.go @@ -156,7 +156,7 @@ func (pb *PluginBroker) loadConfig(path string) error { continue } - pluginConfigs, err := parsePluginConfigFile(configFilePath) + pluginConfigs, err := ParsePluginConfigFile(configFilePath) if err != nil { return err } @@ -319,7 +319,7 @@ func (pb *PluginBroker) pushNotificationsToPlugin(pluginName string, alerts []*m return err } -func parsePluginConfigFile(path string) ([]PluginConfig, error) { +func ParsePluginConfigFile(path string) ([]PluginConfig, error) { parsedConfigs := make([]PluginConfig, 0) yamlFile, err := os.Open(path) if err != nil {