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 }