From f50700429c8e0eee6ad6ef62c83d270becaefbb4 Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 7 Apr 2024 12:37:19 +0100 Subject: [PATCH] wip: basic impl of file notification no log rotate but might now do it :shrug: --- cmd/notification-file/Makefile | 17 ++++ cmd/notification-file/file.yaml | 25 +++++ cmd/notification-file/main.go | 160 ++++++++++++++++++++++++++++++++ wizard.sh | 4 + 4 files changed, 206 insertions(+) create mode 100644 cmd/notification-file/Makefile create mode 100644 cmd/notification-file/file.yaml create mode 100644 cmd/notification-file/main.go diff --git a/cmd/notification-file/Makefile b/cmd/notification-file/Makefile new file mode 100644 index 000000000..4504328c4 --- /dev/null +++ b/cmd/notification-file/Makefile @@ -0,0 +1,17 @@ +ifeq ($(OS), Windows_NT) + SHELL := pwsh.exe + .SHELLFLAGS := -NoProfile -Command + EXT = .exe +endif + +GO = go +GOBUILD = $(GO) build + +BINARY_NAME = notification-file$(EXT) + +build: clean + $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) + +.PHONY: clean +clean: + @$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR) diff --git a/cmd/notification-file/file.yaml b/cmd/notification-file/file.yaml new file mode 100644 index 000000000..e1d94c748 --- /dev/null +++ b/cmd/notification-file/file.yaml @@ -0,0 +1,25 @@ +# Don't change this +type: file + +name: file_default # this must match with the registered plugin in the profile +log_level: info # Options include: trace, debug, info, warn, error, off + +# This template render all events as ndjson +format: | + {{range . -}} + { "time": "{{.StopAt}}", "program": "crowdsec", "alert": {{. | toJson }} } + {{ end -}} + +# group_wait: # duration to wait collecting alerts before sending to this plugin, eg "30s" +# group_threshold: # if alerts exceed this, then the plugin will be sent the message. eg "10" + +#Use full path EG /tmp/crowdsec_alerts.json or %TEMP%\crowdsec_alerts.json +log_path: "/tmp/crowdsec_alerts.json" +rotate: + enabled: true # Change to false if you want to handle log rotate on system basis + max_size: 500 + max_files: 5 + max_age: 5 + compress: true +log_format: + custom_format: "%msg%" # https://github.com/t-tomalak/logrus-easy-formatter diff --git a/cmd/notification-file/main.go b/cmd/notification-file/main.go new file mode 100644 index 000000000..7d568f67a --- /dev/null +++ b/cmd/notification-file/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/protobufs" + "github.com/hashicorp/go-hclog" + plugin "github.com/hashicorp/go-plugin" + "gopkg.in/yaml.v2" +) + +type PluginConfig struct { + Name string `yaml:"name"` + LogLevel string `yaml:"log_level"` + LogPath string `yaml:"log_path"` + WriteChan chan string `yaml:"-"` + FileWriter *os.File `yaml:"-"` + LogRotate LogRotate `yaml:"rotate"` +} + +type LogRotate struct { + MaxSize int `yaml:"max_size"` + MaxAge int `yaml:"max_age"` + MaxFiles int `yaml:"max_files"` + Enabled bool `yaml:"enabled"` + Compress bool `yaml:"compress"` +} + +type FilePlugin struct { + PluginConfigByName map[string]PluginConfig +} + +var logger hclog.Logger = hclog.New(&hclog.LoggerOptions{ + Name: "file-plugin", + Level: hclog.LevelFromString("INFO"), + Output: os.Stderr, + JSONFormat: true, +}) + +func Monit(cfg PluginConfig) { + logger.Debug("Starting monit") + queue := make([]string, 0) + queueMutex := &sync.Mutex{} + ticker := time.NewTicker(5 * time.Second) + if cfg.FileWriter == nil { + cfg.FileWriter, _ = os.OpenFile(cfg.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + } + for { + select { + case <-ticker.C: + logger.Debug("Checking queue") + if len(queue) == 0 { + logger.Debug("Queue is empty") + continue + } + logger.Debug(fmt.Sprintf("Writing %d logs to file", len(queue))) + newQueue := make([]string, 0, len(queue)) + originalFileInfo, err := cfg.FileWriter.Stat() + if err != nil { + logger.Error("Failed to get file info", "error", err) + } + for _, log := range queue { + var err error + currentFileInfo, _ := os.Stat(cfg.LogPath) + // Check if the file writer is still pointing to the same file + if !os.SameFile(originalFileInfo, currentFileInfo) { + // The file has been rotated + logger.Info("Log file has been rotated or missing attempting to reopen it") + cfg.FileWriter.Close() + cfg.FileWriter, err = os.OpenFile(cfg.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + logger.Error("Failed to reopen log file", "error", err) + newQueue = append(newQueue, log) + continue + } + logger.Info("Log file has been reopened successfully") + originalFileInfo, _ = cfg.FileWriter.Stat() + } + _, err = cfg.FileWriter.WriteString(log + "\n") + if err != nil { + logger.Error("Failed to write log", "error", err) + newQueue = append(newQueue, log) + } + } + cfg.FileWriter.Sync() + queueMutex.Lock() + queue = newQueue + queueMutex.Unlock() + // TODO! Implement log rotation + // if cfg.LogRotate.Enabled { + // // check if file size is greater than max size + // fileInfo, _ := cfg.FileWriter.Stat() + // if fileInfo.Size() > int64(cfg.LogRotate.MaxSize) { + // // close file + // cfg.FileWriter.Close() + // // rename file + // os.Rename(cfg.LogPath, cfg.LogPath+".1") + // // open file + // cfg.FileWriter, _ = os.OpenFile(cfg.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + // // compress file + // if cfg.LogRotate.Compress { + // // compress file + // } + // } + // } + case log := <-cfg.WriteChan: + logger.Trace("Received log", log) + queueMutex.Lock() + queue = append(queue, log) + queueMutex.Unlock() + } + } +} + +func (s *FilePlugin) Notify(ctx context.Context, notification *protobufs.Notification) (*protobufs.Empty, error) { + if _, ok := s.PluginConfigByName[notification.Name]; !ok { + return nil, fmt.Errorf("invalid plugin config name %s", notification.Name) + } + cfg := s.PluginConfigByName[notification.Name] + + go func() { + cfg.WriteChan <- notification.Text + }() + + return &protobufs.Empty{}, nil +} + +func (s *FilePlugin) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) { + d := PluginConfig{} + err := yaml.Unmarshal(config.Config, &d) + d.WriteChan = make(chan string) + s.PluginConfigByName[d.Name] = d + logger.SetLevel(hclog.LevelFromString(d.LogLevel)) + go Monit(d) + return &protobufs.Empty{}, err +} + +func main() { + var handshake = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "CROWDSEC_PLUGIN_KEY", + MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), + } + + sp := &FilePlugin{PluginConfigByName: make(map[string]PluginConfig)} + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: handshake, + Plugins: map[string]plugin.Plugin{ + "file": &protobufs.NotifierPlugin{ + Impl: sp, + }, + }, + GRPCServer: plugin.DefaultGRPCServer, + Logger: logger, + }) +} diff --git a/wizard.sh b/wizard.sh index 598f0c765..7142dae55 100755 --- a/wizard.sh +++ b/wizard.sh @@ -82,12 +82,14 @@ SLACK_PLUGIN_BINARY="./cmd/notification-slack/notification-slack" SPLUNK_PLUGIN_BINARY="./cmd/notification-splunk/notification-splunk" EMAIL_PLUGIN_BINARY="./cmd/notification-email/notification-email" SENTINEL_PLUGIN_BINARY="./cmd/notification-sentinel/notification-sentinel" +FILE_PLUGIN_BINARY="./cmd/notification-file/notification-file" HTTP_PLUGIN_CONFIG="./cmd/notification-http/http.yaml" SLACK_PLUGIN_CONFIG="./cmd/notification-slack/slack.yaml" SPLUNK_PLUGIN_CONFIG="./cmd/notification-splunk/splunk.yaml" EMAIL_PLUGIN_CONFIG="./cmd/notification-email/email.yaml" SENTINEL_PLUGIN_CONFIG="./cmd/notification-sentinel/sentinel.yaml" +FILE_PLUGIN_CONFIG="./cmd/notification-file/file.yaml" BACKUP_DIR=$(mktemp -d) @@ -523,6 +525,7 @@ install_plugins(){ cp ${HTTP_PLUGIN_BINARY} ${CROWDSEC_PLUGIN_DIR} cp ${EMAIL_PLUGIN_BINARY} ${CROWDSEC_PLUGIN_DIR} cp ${SENTINEL_PLUGIN_BINARY} ${CROWDSEC_PLUGIN_DIR} + cp ${FILE_PLUGIN_BINARY} ${CROWDSEC_PLUGIN_DIR} if [[ ${DOCKER_MODE} == "false" ]]; then cp -n ${SLACK_PLUGIN_CONFIG} /etc/crowdsec/notifications/ @@ -530,6 +533,7 @@ install_plugins(){ cp -n ${HTTP_PLUGIN_CONFIG} /etc/crowdsec/notifications/ cp -n ${EMAIL_PLUGIN_CONFIG} /etc/crowdsec/notifications/ cp -n ${SENTINEL_PLUGIN_CONFIG} /etc/crowdsec/notifications/ + cp -n ${FILE_PLUGIN_CONFIG} /etc/crowdsec/notifications/ fi }