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  }