github.com/crowdsecurity/crowdsec@v1.6.1/pkg/csplugin/broker.go (about)

     1  package csplugin
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"reflect"
    10  	"strings"
    11  	"sync"
    12  	"text/template"
    13  	"time"
    14  
    15  	"github.com/Masterminds/sprig/v3"
    16  	"github.com/google/uuid"
    17  	plugin "github.com/hashicorp/go-plugin"
    18  	log "github.com/sirupsen/logrus"
    19  	"gopkg.in/tomb.v2"
    20  	"gopkg.in/yaml.v2"
    21  
    22  	"github.com/crowdsecurity/go-cs-lib/csstring"
    23  	"github.com/crowdsecurity/go-cs-lib/slicetools"
    24  
    25  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    26  	"github.com/crowdsecurity/crowdsec/pkg/models"
    27  	"github.com/crowdsecurity/crowdsec/pkg/protobufs"
    28  	"github.com/crowdsecurity/crowdsec/pkg/types"
    29  )
    30  
    31  var pluginMutex sync.Mutex
    32  
    33  const (
    34  	PluginProtocolVersion uint   = 1
    35  	CrowdsecPluginKey     string = "CROWDSEC_PLUGIN_KEY"
    36  )
    37  
    38  // The broker is responsible for running the plugins and dispatching events
    39  // It receives all the events from the main process and stacks them up
    40  // It is as well notified by the watcher when it needs to deliver events to plugins (based on time or count threshold)
    41  type PluginBroker struct {
    42  	PluginChannel                   chan ProfileAlert
    43  	alertsByPluginName              map[string][]*models.Alert
    44  	profileConfigs                  []*csconfig.ProfileCfg
    45  	pluginConfigByName              map[string]PluginConfig
    46  	pluginMap                       map[string]plugin.Plugin
    47  	notificationConfigsByPluginType map[string][][]byte // "slack" -> []{config1, config2}
    48  	notificationPluginByName        map[string]Notifier
    49  	watcher                         PluginWatcher
    50  	pluginKillMethods               []func()
    51  	pluginProcConfig                *csconfig.PluginCfg
    52  	pluginsTypesToDispatch          map[string]struct{}
    53  }
    54  
    55  // holder to determine where to dispatch config and how to format messages
    56  type PluginConfig struct {
    57  	Type           string        `yaml:"type"`
    58  	Name           string        `yaml:"name"`
    59  	GroupWait      time.Duration `yaml:"group_wait,omitempty"`
    60  	GroupThreshold int           `yaml:"group_threshold,omitempty"`
    61  	MaxRetry       int           `yaml:"max_retry,omitempty"`
    62  	TimeOut        time.Duration `yaml:"timeout,omitempty"`
    63  
    64  	Format string `yaml:"format,omitempty"` // specific to notification plugins
    65  
    66  	Config map[string]interface{} `yaml:",inline"` //to keep the plugin-specific config
    67  
    68  }
    69  
    70  type ProfileAlert struct {
    71  	ProfileID uint
    72  	Alert     *models.Alert
    73  }
    74  
    75  func (pb *PluginBroker) Init(pluginCfg *csconfig.PluginCfg, profileConfigs []*csconfig.ProfileCfg, configPaths *csconfig.ConfigurationPaths) error {
    76  	pb.PluginChannel = make(chan ProfileAlert)
    77  	pb.notificationConfigsByPluginType = make(map[string][][]byte)
    78  	pb.notificationPluginByName = make(map[string]Notifier)
    79  	pb.pluginMap = make(map[string]plugin.Plugin)
    80  	pb.pluginConfigByName = make(map[string]PluginConfig)
    81  	pb.alertsByPluginName = make(map[string][]*models.Alert)
    82  	pb.profileConfigs = profileConfigs
    83  	pb.pluginProcConfig = pluginCfg
    84  	pb.pluginsTypesToDispatch = make(map[string]struct{})
    85  	if err := pb.loadConfig(configPaths.NotificationDir); err != nil {
    86  		return fmt.Errorf("while loading plugin config: %w", err)
    87  	}
    88  	if err := pb.loadPlugins(configPaths.PluginDir); err != nil {
    89  		return fmt.Errorf("while loading plugin: %w", err)
    90  	}
    91  	pb.watcher = PluginWatcher{}
    92  	pb.watcher.Init(pb.pluginConfigByName, pb.alertsByPluginName)
    93  	return nil
    94  
    95  }
    96  
    97  func (pb *PluginBroker) Kill() {
    98  	for _, kill := range pb.pluginKillMethods {
    99  		kill()
   100  	}
   101  }
   102  
   103  func (pb *PluginBroker) Run(pluginTomb *tomb.Tomb) {
   104  	//we get signaled via the channel when notifications need to be delivered to plugin (via the watcher)
   105  	pb.watcher.Start(&tomb.Tomb{})
   106  loop:
   107  	for {
   108  		select {
   109  		case profileAlert := <-pb.PluginChannel:
   110  			pb.addProfileAlert(profileAlert)
   111  
   112  		case pluginName := <-pb.watcher.PluginEvents:
   113  			// this can be run in goroutine, but then locks will be needed
   114  			pluginMutex.Lock()
   115  			log.Tracef("going to deliver %d alerts to plugin %s", len(pb.alertsByPluginName[pluginName]), pluginName)
   116  			tmpAlerts := pb.alertsByPluginName[pluginName]
   117  			pb.alertsByPluginName[pluginName] = make([]*models.Alert, 0)
   118  			pluginMutex.Unlock()
   119  			go func() {
   120  				//Chunk alerts to respect group_threshold
   121  				threshold := pb.pluginConfigByName[pluginName].GroupThreshold
   122  				if threshold == 0 {
   123  					threshold = 1
   124  				}
   125  				for _, chunk := range slicetools.Chunks(tmpAlerts, threshold) {
   126  					if err := pb.pushNotificationsToPlugin(pluginName, chunk); err != nil {
   127  						log.WithField("plugin:", pluginName).Error(err)
   128  					}
   129  				}
   130  			}()
   131  
   132  		case <-pluginTomb.Dying():
   133  			log.Infof("pluginTomb dying")
   134  			pb.watcher.tomb.Kill(errors.New("Terminating"))
   135  			for {
   136  				select {
   137  				case <-pb.watcher.tomb.Dead():
   138  					log.Info("killing all plugins")
   139  					pb.Kill()
   140  					break loop
   141  				case pluginName := <-pb.watcher.PluginEvents:
   142  					// this can be run in goroutine, but then locks will be needed
   143  					pluginMutex.Lock()
   144  					log.Tracef("going to deliver %d alerts to plugin %s", len(pb.alertsByPluginName[pluginName]), pluginName)
   145  					tmpAlerts := pb.alertsByPluginName[pluginName]
   146  					pb.alertsByPluginName[pluginName] = make([]*models.Alert, 0)
   147  					pluginMutex.Unlock()
   148  
   149  					if err := pb.pushNotificationsToPlugin(pluginName, tmpAlerts); err != nil {
   150  						log.WithField("plugin:", pluginName).Error(err)
   151  					}
   152  				}
   153  			}
   154  		}
   155  	}
   156  }
   157  
   158  func (pb *PluginBroker) addProfileAlert(profileAlert ProfileAlert) {
   159  	for _, pluginName := range pb.profileConfigs[profileAlert.ProfileID].Notifications {
   160  		if _, ok := pb.pluginConfigByName[pluginName]; !ok {
   161  			log.Errorf("plugin %s is not configured properly.", pluginName)
   162  			continue
   163  		}
   164  		pluginMutex.Lock()
   165  		pb.alertsByPluginName[pluginName] = append(pb.alertsByPluginName[pluginName], profileAlert.Alert)
   166  		pluginMutex.Unlock()
   167  		pb.watcher.Inserts <- pluginName
   168  	}
   169  }
   170  func (pb *PluginBroker) profilesContainPlugin(pluginName string) bool {
   171  	for _, profileCfg := range pb.profileConfigs {
   172  		for _, name := range profileCfg.Notifications {
   173  			if pluginName == name {
   174  				return true
   175  			}
   176  		}
   177  	}
   178  	return false
   179  }
   180  func (pb *PluginBroker) loadConfig(path string) error {
   181  	files, err := listFilesAtPath(path)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	for _, configFilePath := range files {
   186  		if !strings.HasSuffix(configFilePath, ".yaml") && !strings.HasSuffix(configFilePath, ".yml") {
   187  			continue
   188  		}
   189  
   190  		pluginConfigs, err := ParsePluginConfigFile(configFilePath)
   191  		if err != nil {
   192  			return err
   193  		}
   194  		for _, pluginConfig := range pluginConfigs {
   195  			SetRequiredFields(&pluginConfig)
   196  			if _, ok := pb.pluginConfigByName[pluginConfig.Name]; ok {
   197  				log.Warningf("notification '%s' is defined multiple times", pluginConfig.Name)
   198  			}
   199  			pb.pluginConfigByName[pluginConfig.Name] = pluginConfig
   200  			if !pb.profilesContainPlugin(pluginConfig.Name) {
   201  				continue
   202  			}
   203  		}
   204  	}
   205  	err = pb.verifyPluginConfigsWithProfile()
   206  	return err
   207  }
   208  
   209  // checks whether every notification in profile has its own config file
   210  func (pb *PluginBroker) verifyPluginConfigsWithProfile() error {
   211  	for _, profileCfg := range pb.profileConfigs {
   212  		for _, pluginName := range profileCfg.Notifications {
   213  			if _, ok := pb.pluginConfigByName[pluginName]; !ok {
   214  				return fmt.Errorf("config file for plugin %s not found", pluginName)
   215  			}
   216  			pb.pluginsTypesToDispatch[pb.pluginConfigByName[pluginName].Type] = struct{}{}
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  // check whether each plugin in profile has its own binary
   223  func (pb *PluginBroker) verifyPluginBinaryWithProfile() error {
   224  	for _, profileCfg := range pb.profileConfigs {
   225  		for _, pluginName := range profileCfg.Notifications {
   226  			if _, ok := pb.notificationPluginByName[pluginName]; !ok {
   227  				return fmt.Errorf("binary for plugin %s not found", pluginName)
   228  			}
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  func (pb *PluginBroker) loadPlugins(path string) error {
   235  	binaryPaths, err := listFilesAtPath(path)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	for _, binaryPath := range binaryPaths {
   240  		if err := pluginIsValid(binaryPath); err != nil {
   241  			return err
   242  		}
   243  		pType, pSubtype, err := getPluginTypeAndSubtypeFromPath(binaryPath) // eg pType="notification" , pSubtype="slack"
   244  		if err != nil {
   245  			return err
   246  		}
   247  		if pType != "notification" {
   248  			continue
   249  		}
   250  
   251  		if _, ok := pb.pluginsTypesToDispatch[pSubtype]; !ok {
   252  			continue
   253  		}
   254  
   255  		pluginClient, err := pb.loadNotificationPlugin(pSubtype, binaryPath)
   256  		if err != nil {
   257  			return err
   258  		}
   259  		for _, pc := range pb.pluginConfigByName {
   260  			if pc.Type != pSubtype {
   261  				continue
   262  			}
   263  
   264  			data, err := yaml.Marshal(pc)
   265  			if err != nil {
   266  				return err
   267  			}
   268  			data = []byte(csstring.StrictExpand(string(data), os.LookupEnv))
   269  			_, err = pluginClient.Configure(context.Background(), &protobufs.Config{Config: data})
   270  			if err != nil {
   271  				return fmt.Errorf("while configuring %s: %w", pc.Name, err)
   272  			}
   273  			log.Infof("registered plugin %s", pc.Name)
   274  			pb.notificationPluginByName[pc.Name] = pluginClient
   275  		}
   276  	}
   277  	return pb.verifyPluginBinaryWithProfile()
   278  }
   279  
   280  func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (Notifier, error) {
   281  
   282  	handshake, err := getHandshake()
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	log.Debugf("Executing plugin %s", binaryPath)
   287  	cmd, err := pb.CreateCmd(binaryPath)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	pb.pluginMap[name] = &NotifierPlugin{}
   292  	l := log.New()
   293  	err = types.ConfigureLogger(l)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	// We set the highest level to permit plugins to set their own log level
   298  	// without that, crowdsec log level is controlling plugins level
   299  	l.SetLevel(log.TraceLevel)
   300  	logger := NewHCLogAdapter(l, "")
   301  	c := plugin.NewClient(&plugin.ClientConfig{
   302  		HandshakeConfig:  handshake,
   303  		Plugins:          pb.pluginMap,
   304  		Cmd:              cmd,
   305  		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   306  		Logger:           logger,
   307  	})
   308  	client, err := c.Client()
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	raw, err := client.Dispense(name)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	pb.pluginKillMethods = append(pb.pluginKillMethods, c.Kill)
   317  	return raw.(Notifier), nil
   318  }
   319  
   320  func (pb *PluginBroker) pushNotificationsToPlugin(pluginName string, alerts []*models.Alert) error {
   321  	log.WithField("plugin", pluginName).Debugf("pushing %d alerts to plugin", len(alerts))
   322  	if len(alerts) == 0 {
   323  		return nil
   324  	}
   325  
   326  	message, err := FormatAlerts(pb.pluginConfigByName[pluginName].Format, alerts)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	plugin := pb.notificationPluginByName[pluginName]
   331  	backoffDuration := time.Second
   332  	for i := 1; i <= pb.pluginConfigByName[pluginName].MaxRetry; i++ {
   333  		ctx, cancel := context.WithTimeout(context.Background(), pb.pluginConfigByName[pluginName].TimeOut)
   334  		defer cancel()
   335  		_, err = plugin.Notify(
   336  			ctx,
   337  			&protobufs.Notification{
   338  				Text: message,
   339  				Name: pluginName,
   340  			},
   341  		)
   342  		if err == nil {
   343  			return nil
   344  		}
   345  		log.WithField("plugin", pluginName).Errorf("%s error, retry num %d", err, i)
   346  		time.Sleep(backoffDuration)
   347  		backoffDuration *= 2
   348  	}
   349  
   350  	return err
   351  }
   352  
   353  func ParsePluginConfigFile(path string) ([]PluginConfig, error) {
   354  	parsedConfigs := make([]PluginConfig, 0)
   355  	yamlFile, err := os.Open(path)
   356  	if err != nil {
   357  		return nil, fmt.Errorf("while opening %s: %w", path, err)
   358  	}
   359  	dec := yaml.NewDecoder(yamlFile)
   360  	dec.SetStrict(true)
   361  	for {
   362  		pc := PluginConfig{}
   363  		err = dec.Decode(&pc)
   364  		if err != nil {
   365  			if errors.Is(err, io.EOF) {
   366  				break
   367  			}
   368  			return nil, fmt.Errorf("while decoding %s got error %s", path, err)
   369  		}
   370  		// if the yaml document is empty, skip
   371  		if reflect.DeepEqual(pc, PluginConfig{}) {
   372  			continue
   373  		}
   374  		parsedConfigs = append(parsedConfigs, pc)
   375  	}
   376  	return parsedConfigs, nil
   377  }
   378  
   379  func SetRequiredFields(pluginCfg *PluginConfig) {
   380  	if pluginCfg.MaxRetry == 0 {
   381  		pluginCfg.MaxRetry++
   382  	}
   383  
   384  	if pluginCfg.TimeOut == time.Second*0 {
   385  		pluginCfg.TimeOut = time.Second * 5
   386  	}
   387  }
   388  
   389  func getUUID() (string, error) {
   390  	uuidv4, err := uuid.NewRandom()
   391  	if err != nil {
   392  		return "", err
   393  	}
   394  	return uuidv4.String(), nil
   395  }
   396  
   397  func getHandshake() (plugin.HandshakeConfig, error) {
   398  	uuid, err := getUUID()
   399  	if err != nil {
   400  		return plugin.HandshakeConfig{}, err
   401  	}
   402  	handshake := plugin.HandshakeConfig{
   403  		ProtocolVersion:  PluginProtocolVersion,
   404  		MagicCookieKey:   CrowdsecPluginKey,
   405  		MagicCookieValue: uuid,
   406  	}
   407  	return handshake, nil
   408  }
   409  
   410  func FormatAlerts(format string, alerts []*models.Alert) (string, error) {
   411  	template, err := template.New("").Funcs(sprig.TxtFuncMap()).Funcs(funcMap()).Parse(format)
   412  	if err != nil {
   413  		return "", err
   414  	}
   415  	b := new(strings.Builder)
   416  	err = template.Execute(b, alerts)
   417  	if err != nil {
   418  		return "", err
   419  	}
   420  	return b.String(), nil
   421  }