diff --git a/cmd/crowdsec-cli/notifications.go b/cmd/crowdsec-cli/notifications.go index a00d0d617..9f0700bd1 100644 --- a/cmd/crowdsec-cli/notifications.go +++ b/cmd/crowdsec-cli/notifications.go @@ -18,15 +18,19 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/tomb.v2" + "gopkg.in/yaml.v3" + "github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/version" "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/types" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require" + "github.com/crowdsecurity/crowdsec/pkg/models" ) type NotificationsCfg struct { @@ -61,11 +65,12 @@ func NewNotificationsCmd() *cobra.Command { cmdNotifications.AddCommand(NewNotificationsListCmd()) cmdNotifications.AddCommand(NewNotificationsInspectCmd()) cmdNotifications.AddCommand(NewNotificationsReinjectCmd()) + cmdNotifications.AddCommand(NewNotificationsTestCmd()) return cmdNotifications } -func getNotificationsConfiguration() (map[string]NotificationsCfg, error) { +func getPluginConfigs() (map[string]csplugin.PluginConfig, error) { pcfgs := map[string]csplugin.PluginConfig{} wf := func(path string, info fs.FileInfo, err error) error { if info == nil { @@ -78,6 +83,7 @@ func getNotificationsConfiguration() (map[string]NotificationsCfg, error) { return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err) } for _, t := range ts { + csplugin.SetRequiredFields(&t) pcfgs[t.Name] = t } } @@ -87,8 +93,15 @@ func getNotificationsConfiguration() (map[string]NotificationsCfg, error) { if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil { return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err) } + return pcfgs, nil +} +func getProfilesConfigs() (map[string]NotificationsCfg, error) { // A bit of a tricky stuf now: reconcile profiles and notification plugins + pcfgs, err := getPluginConfigs() + if err != nil { + return nil, err + } ncfgs := map[string]NotificationsCfg{} profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) if err != nil { @@ -131,13 +144,13 @@ func getNotificationsConfiguration() (map[string]NotificationsCfg, error) { func NewNotificationsListCmd() *cobra.Command { var cmdNotificationsList = &cobra.Command{ Use: "list", - Short: "List active notifications plugins", - Long: `List active notifications plugins`, + 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() + ncfgs, err := getProfilesConfigs() if err != nil { return fmt.Errorf("can't build profiles configuration: %w", err) } @@ -183,25 +196,21 @@ func NewNotificationsInspectCmd() *cobra.Command { 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 == "" { + PreRunE: func(cmd *cobra.Command, args []string) error { + if args[0] == "" { return fmt.Errorf("please provide a plugin name to inspect") } - ncfgs, err := getNotificationsConfiguration() + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + ncfgs, err := getProfilesConfigs() if err != nil { return fmt.Errorf("can't build profiles configuration: %w", err) } - if cfg, ok = ncfgs[pluginName]; !ok { - return fmt.Errorf("plugin '%s' does not exist or is not active", pluginName) + cfg, ok := ncfgs[args[0]] + if !ok { + return fmt.Errorf("plugin '%s' does not exist or is not active", args[0]) } - 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) @@ -224,75 +233,125 @@ func NewNotificationsInspectCmd() *cobra.Command { return cmdNotificationsInspect } +func NewNotificationsTestCmd() *cobra.Command { + var ( + pluginBroker csplugin.PluginBroker + pluginTomb tomb.Tomb + alertOverride string + ) + var cmdNotificationsTest = &cobra.Command{ + Use: "test [plugin name]", + Short: "send a generic test alert to notification plugin", + Long: `send a generic test alert to a notification plugin to test configuration even if is not active`, + Example: `cscli notifications test [plugin_name]`, + Args: cobra.ExactArgs(1), + DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, args []string) error { + pconfigs, err := getPluginConfigs() + if err != nil { + return fmt.Errorf("can't build profiles configuration: %w", err) + } + cfg, ok := pconfigs[args[0]] + if !ok { + return fmt.Errorf("plugin name: '%s' does not exist", args[0]) + } + //Create a single profile with plugin name as notification name + return pluginBroker.Init(csConfig.PluginConfig, []*csconfig.ProfileCfg{ + { + Notifications: []string{ + cfg.Name, + }, + }, + }, csConfig.ConfigPaths) + }, + RunE: func(cmd *cobra.Command, args []string) error { + pluginTomb.Go(func() error { + pluginBroker.Run(&pluginTomb) + return nil + }) + alert := &models.Alert{ + Capacity: ptr.Of(int32(0)), + Decisions: []*models.Decision{{ + Duration: ptr.Of("4h"), + Scope: ptr.Of("Ip"), + Value: ptr.Of("10.10.10.10"), + Type: ptr.Of("ban"), + Scenario: ptr.Of("test alert"), + Origin: ptr.Of(types.CscliOrigin), + }}, + Events: []*models.Event{}, + EventsCount: ptr.Of(int32(1)), + Leakspeed: ptr.Of("0"), + Message: ptr.Of("test alert"), + ScenarioHash: ptr.Of(""), + Scenario: ptr.Of("test alert"), + ScenarioVersion: ptr.Of(""), + Simulated: ptr.Of(false), + Source: &models.Source{ + AsName: "", + AsNumber: "", + Cn: "", + IP: "10.10.10.10", + Range: "", + Scope: ptr.Of("Ip"), + Value: ptr.Of("10.10.10.10"), + }, + StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)), + StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)), + CreatedAt: time.Now().UTC().Format(time.RFC3339), + } + if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil { + return fmt.Errorf("failed to unmarshal alert override: %w", err) + } + pluginBroker.PluginChannel <- csplugin.ProfileAlert{ + ProfileID: uint(0), + Alert: alert, + } + //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent + pluginTomb.Kill(fmt.Errorf("terminating")) + pluginTomb.Wait() + return nil + }, + } + cmdNotificationsTest.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)") + + return cmdNotificationsTest +} + func NewNotificationsReinjectCmd() *cobra.Command { - var remediation bool var alertOverride string + var alert *models.Alert var cmdNotificationsReinject = &cobra.Command{ Use: "reinject", - Short: "reinject alert into notifications system", - Long: `Reinject alert into notifications system`, + Short: "reinject an alert into profiles to trigger notifications", + Long: `reinject an alert into profiles to be evaluated by the filter and sent to matched notifications plugins`, Example: ` cscli notifications reinject -cscli notifications reinject --remediation +cscli notifications reinject -a '{"remediation": false,"scenario":"notification/test"}' cscli notifications reinject -a '{"remediation": true,"scenario":"notification/test"}' `, Args: cobra.ExactArgs(1), DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, args []string) error { + var err error + alert, err = FetchAlertFromArgString(args[0]) + if err != nil { + return err + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { var ( pluginBroker csplugin.PluginBroker pluginTomb tomb.Tomb ) - if len(args) != 1 { - printHelp(cmd) - return fmt.Errorf("wrong number of argument: there should be one argument") - } - - //first: get the alert - id, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("bad alert id %s", args[0]) - } - if err := csConfig.LoadAPIClient(); err != nil { - return fmt.Errorf("loading api client: %w", err) - } - if csConfig.API.Client == nil { - return fmt.Errorf("missing configuration on 'api_client:'") - } - if csConfig.API.Client.Credentials == nil { - return fmt.Errorf("missing API credentials in '%s'", csConfig.API.Client.CredentialsFilePath) - } - apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL) - if err != nil { - return fmt.Errorf("error parsing the URL of the API: %w", err) - } - 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", version.String()), - URL: apiURL, - VersionPrefix: "v1", - }) - if err != nil { - return fmt.Errorf("error creating the client for the API: %w", err) - } - alert, _, err := client.Alerts.GetByID(context.Background(), id) - if err != nil { - return fmt.Errorf("can't find alert with id %s: %w", args[0], err) - } - if alertOverride != "" { - if err = json.Unmarshal([]byte(alertOverride), alert); err != nil { + if err := json.Unmarshal([]byte(alertOverride), alert); err != nil { return fmt.Errorf("can't unmarshal data in the alert flag: %w", err) } } - if !remediation { - alert.Remediation = true - } - - // second we start plugins - err = pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths) + err := pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths) if err != nil { return fmt.Errorf("can't initialize plugins: %w", err) } @@ -302,8 +361,6 @@ cscli notifications reinject -a '{"remediation": true,"scenario":"not return nil }) - //third: get the profile(s), and process the whole stuff - profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) if err != nil { return fmt.Errorf("cannot extract profiles from configuration: %w", err) @@ -338,15 +395,39 @@ cscli notifications reinject -a '{"remediation": true,"scenario":"not break } } - - // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent + //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent pluginTomb.Kill(fmt.Errorf("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)") return cmdNotificationsReinject } + +func FetchAlertFromArgString(toParse string) (*models.Alert, error) { + id, err := strconv.Atoi(toParse) + if err != nil { + return nil, fmt.Errorf("bad alert id %s", toParse) + } + apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL) + if err != nil { + return nil, fmt.Errorf("error parsing the URL of the API: %w", err) + } + 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", version.String()), + URL: apiURL, + VersionPrefix: "v1", + }) + if err != nil { + return nil, fmt.Errorf("error creating the client for the API: %w", err) + } + alert, _, err := client.Alerts.GetByID(context.Background(), id) + if err != nil { + return nil, fmt.Errorf("can't find alert with id %d: %w", id, err) + } + return alert, nil +} diff --git a/pkg/csplugin/broker.go b/pkg/csplugin/broker.go index b1c30ad77..b5c86f224 100644 --- a/pkg/csplugin/broker.go +++ b/pkg/csplugin/broker.go @@ -192,7 +192,7 @@ func (pb *PluginBroker) loadConfig(path string) error { return err } for _, pluginConfig := range pluginConfigs { - setRequiredFields(&pluginConfig) + SetRequiredFields(&pluginConfig) if _, ok := pb.pluginConfigByName[pluginConfig.Name]; ok { log.Warningf("notification '%s' is defined multiple times", pluginConfig.Name) } @@ -376,7 +376,7 @@ func ParsePluginConfigFile(path string) ([]PluginConfig, error) { return parsedConfigs, nil } -func setRequiredFields(pluginCfg *PluginConfig) { +func SetRequiredFields(pluginCfg *PluginConfig) { if pluginCfg.MaxRetry == 0 { pluginCfg.MaxRetry++ }