diff --git a/.gitignore b/.gitignore index 3e9c4a329..04221a070 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ plugins/notifications/http/notification-http plugins/notifications/slack/notification-slack plugins/notifications/splunk/notification-splunk plugins/notifications/email/notification-email +plugins/notifications/dummy/notification-dummy diff --git a/Makefile b/Makefile index 04568e7ea..46f63ae8e 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,13 @@ HTTP_PLUGIN_FOLDER = "./plugins/notifications/http" SLACK_PLUGIN_FOLDER = "./plugins/notifications/slack" SPLUNK_PLUGIN_FOLDER = "./plugins/notifications/splunk" EMAIL_PLUGIN_FOLDER = "./plugins/notifications/email" +DUMMY_PLUGIN_FOLDER = "./plugins/notifications/dummy" HTTP_PLUGIN_BIN = "notification-http" SLACK_PLUGIN_BIN = "notification-slack" SPLUNK_PLUGIN_BIN = "notification-splunk" EMAIL_PLUGIN_BIN = "notification-email" +DUMMY_PLUGIN_BIN= "notification-dummy" HTTP_PLUGIN_CONFIG = "http.yaml" SLACK_PLUGIN_CONFIG = "slack.yaml" @@ -73,9 +75,9 @@ all: clean test build static: crowdsec_static cscli_static plugins_static .PHONY: plugins -plugins: http-plugin slack-plugin splunk-plugin email-plugin +plugins: http-plugin slack-plugin splunk-plugin email-plugin dummy-plugin -plugins_static: http-plugin_static slack-plugin_static splunk-plugin_static email-plugin_static +plugins_static: http-plugin_static slack-plugin_static splunk-plugin_static email-plugin_static dummy-plugin_static goversion: @if [ $(GO_MAJOR_VERSION) -gt $(MINIMUM_SUPPORTED_GO_MAJOR_VERSION) ]; then \ @@ -101,6 +103,7 @@ clean: testclean @$(RM) $(SLACK_PLUGIN_FOLDER)/$(SLACK_PLUGIN_BIN) @$(RM) $(SPLUNK_PLUGIN_FOLDER)/$(SPLUNK_PLUGIN_BIN) @$(RM) $(EMAIL_PLUGIN_FOLDER)/$(EMAIL_PLUGIN_BIN) + @$(RM) $(DUMMY_PLUGIN_FOLDER)/$(DUMMY_PLUGIN_BIN) cscli: goversion @@ -121,6 +124,9 @@ splunk-plugin: goversion email-plugin: goversion @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(EMAIL_PLUGIN_FOLDER) build --no-print-directory +dummy-plugin: goversion + @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) build --no-print-directory + cscli_static: goversion @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(CSCLI_FOLDER) static --no-print-directory @@ -139,6 +145,9 @@ splunk-plugin_static:goversion email-plugin_static:goversion @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(EMAIL_PLUGIN_FOLDER) static --no-print-directory +dummy-plugin_static:goversion + @GOARCH=$(GOARCH) GOOS=$(GOOS) $(MAKE) -C $(DUMMY_PLUGIN_FOLDER) static --no-print-directory + .PHONY: testclean testclean: bats-clean @$(RM) pkg/apiserver/ent diff --git a/config/profiles.yaml b/config/profiles.yaml index ebbf44db7..f4945b7a0 100644 --- a/config/profiles.yaml +++ b/config/profiles.yaml @@ -7,7 +7,7 @@ decisions: duration: 4h # notifications: # - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this. -# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this. -# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this. -# - email_default # Set the required http parameters in /etc/crowdsec/notifications/email.yaml before enabling this. +# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this. +# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this. +# - email_default # Set the required email parameters in /etc/crowdsec/notifications/email.yaml before enabling this. on_success: break diff --git a/plugins/notifications/dummy/LICENSE b/plugins/notifications/dummy/LICENSE new file mode 100644 index 000000000..912563863 --- /dev/null +++ b/plugins/notifications/dummy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Crowdsec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/notifications/dummy/Makefile b/plugins/notifications/dummy/Makefile new file mode 100644 index 000000000..f45e4115e --- /dev/null +++ b/plugins/notifications/dummy/Makefile @@ -0,0 +1,16 @@ +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +BINARY_NAME=notification-dummy + +clean: + @$(RM) "$(BINARY_NAME)" + +build: clean + @$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v + +static: clean + $(GOBUILD) $(LD_OPTS_STATIC) -o $(BINARY_NAME) -v -a -tags netgo diff --git a/plugins/notifications/dummy/dummy.yaml b/plugins/notifications/dummy/dummy.yaml new file mode 100644 index 000000000..8288a9c8a --- /dev/null +++ b/plugins/notifications/dummy/dummy.yaml @@ -0,0 +1,22 @@ +type: dummy # Don't change +name: dummy_default # Must match the registered plugin in the profile + +# One of "trace", "debug", "info", "warn", "error", "off" +log_level: info + +# group_wait: # Time to wait collecting alerts before relaying a message to this plugin, eg "30s" +# group_threshold: # Amount of alerts that triggers a message before has expired, eg "10" +# max_retry: # Number of attempts to relay messages to plugins in case of error +# timeout: # Time to wait for response from the plugin before considering the attempt a failure, eg "10s" + +#------------------------- +# plugin-specific options + +# The following template receives a list of models.Alert objects +# The output goes in the logs and to a text file, if defined +format: | + {{.|toJson}} + +# +# output_file: # notifications will be appended here. optional + diff --git a/plugins/notifications/dummy/main.go b/plugins/notifications/dummy/main.go new file mode 100644 index 000000000..7ac35c0a7 --- /dev/null +++ b/plugins/notifications/dummy/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "context" + "fmt" + "os" + + "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"` + OutputFile *string `yaml:"output_file"` +} + +type DummyPlugin struct { + PluginConfigByName map[string]PluginConfig +} + +var logger hclog.Logger = hclog.New(&hclog.LoggerOptions{ + Name: "dummy-plugin", + Level: hclog.LevelFromString("INFO"), + Output: os.Stderr, + JSONFormat: true, +}) + +func (s *DummyPlugin) 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] + + if cfg.LogLevel != nil && *cfg.LogLevel != "" { + logger.SetLevel(hclog.LevelFromString(*cfg.LogLevel)) + } + + logger.Info(fmt.Sprintf("received signal for %s config", notification.Name)) + logger.Debug(notification.Text) + + if cfg.OutputFile != nil && *cfg.OutputFile != "" { + f, err := os.OpenFile(*cfg.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + logger.Error(fmt.Sprintf("Cannot open notification file: %s", err)) + } + if _, err := f.WriteString(notification.Text); err != nil { + f.Close() + logger.Error(fmt.Sprintf("Cannot write notification to file: %s", err)) + } + err = f.Close() + if err != nil { + logger.Error(fmt.Sprintf("Cannot close notification file: %s", err)) + } + } + fmt.Print(notification.Text) + + return &protobufs.Empty{}, nil +} + +func (s *DummyPlugin) Configure(ctx context.Context, config *protobufs.Config) (*protobufs.Empty, error) { + d := PluginConfig{} + err := yaml.Unmarshal(config.Config, &d) + s.PluginConfigByName[d.Name] = d + return &protobufs.Empty{}, err +} + +func main() { + var handshake = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "CROWDSEC_PLUGIN_KEY", + MagicCookieValue: os.Getenv("CROWDSEC_PLUGIN_KEY"), + } + + sp := &DummyPlugin{PluginConfigByName: make(map[string]PluginConfig)} + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: handshake, + Plugins: map[string]plugin.Plugin{ + "dummy": &protobufs.NotifierPlugin{ + Impl: sp, + }, + }, + GRPCServer: plugin.DefaultGRPCServer, + Logger: logger, + }) +} diff --git a/scripts/test_env.sh b/scripts/test_env.sh index 5d545322c..b203e7f3e 100755 --- a/scripts/test_env.sh +++ b/scripts/test_env.sh @@ -18,7 +18,7 @@ do BASE=${2} shift #past argument shift - ;; + ;; -h|--help) usage exit 0 @@ -47,7 +47,7 @@ PARSER_S02="$PARSER_DIR/s02-enrich" SCENARIOS_DIR="$CONFIG_DIR/scenarios" POSTOVERFLOWS_DIR="$CONFIG_DIR/postoverflows" HUB_DIR="$CONFIG_DIR/hub" -PLUGINS="http slack splunk" +PLUGINS="http slack splunk email" PLUGINS_DIR="plugins" NOTIF_DIR="notifications" @@ -70,13 +70,13 @@ create_arbo() { mkdir -p "$POSTOVERFLOWS_DIR" mkdir -p "$CSCLI_DIR" mkdir -p "$HUB_DIR" - mkdir -p $CONFIG_DIR/$NOTIF_DIR/$plugin - mkdir -p $BASE/$PLUGINS_DIR + mkdir -p "$CONFIG_DIR/$NOTIF_DIR/$plugin" + mkdir -p "$BASE/$PLUGINS_DIR" } copy_files() { cp "./config/profiles.yaml" "$CONFIG_DIR" - cp "./config/simulation.yaml" "$CONFIG_DIR" + cp "./config/simulation.yaml" "$CONFIG_DIR" cp "./cmd/crowdsec/crowdsec" "$BASE" cp "./cmd/crowdsec-cli/cscli" "$BASE" cp -r "./config/patterns" "$CONFIG_DIR" diff --git a/tests/bats.mk b/tests/bats.mk index 50afa98aa..b2383cdf2 100644 --- a/tests/bats.mk +++ b/tests/bats.mk @@ -41,6 +41,7 @@ bats-build: bats-environment bats-check-requirements @install -m 0755 plugins/notifications/http/notification-http $(PLUGIN_DIR)/ @install -m 0755 plugins/notifications/slack/notification-slack $(PLUGIN_DIR)/ @install -m 0755 plugins/notifications/splunk/notification-splunk $(PLUGIN_DIR)/ + @install -m 0755 plugins/notifications/dummy/notification-dummy $(PLUGIN_DIR)/ # Create a reusable package with initial configuration + data @$(TEST_DIR)/instance-data make # Generate dynamic tests diff --git a/tests/bats/70_plugins.bats b/tests/bats/70_http_plugin.bats similarity index 100% rename from tests/bats/70_plugins.bats rename to tests/bats/70_http_plugin.bats diff --git a/tests/bats/71_dummy_plugin.bats b/tests/bats/71_dummy_plugin.bats new file mode 100644 index 000000000..b25592f44 --- /dev/null +++ b/tests/bats/71_dummy_plugin.bats @@ -0,0 +1,55 @@ +#!/usr/bin/env bats +# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si: + +set -u + +setup_file() { + load "../lib/setup_file.sh" >&3 2>&1 + ./instance-data load + + tempfile=$(TMPDIR="${BATS_FILE_TMPDIR}" mktemp) + export tempfile + + yq " + .group_wait=\"5s\" | + .group_threshold=2 | + .output_file=\"${tempfile}\" + " -i "${CONFIG_DIR}/notifications/dummy.yaml" + + yq ' + .notifications=["dummy_default"] | + .filters=["Alert.GetScope() == \"Ip\""] + ' -i "${CONFIG_DIR}/profiles.yaml" + + yq ' + .plugin_config.user="" | + .plugin_config.group="" + ' -i "${CONFIG_DIR}/config.yaml" + + ./instance-crowdsec start +} + +teardown_file() { + load "../lib/teardown_file.sh" >&3 2>&1 +} + +setup() { + load "../lib/setup.sh" +} + +#---------- + +@test "$FILE add two bans" { + run -0 cscli decisions add --ip 1.2.3.4 --duration 30s + assert_output --partial 'Decision successfully added' + + run -0 cscli decisions add --ip 1.2.3.5 --duration 30s + assert_output --partial 'Decision successfully added' + sleep 2 +} + +@test "$FILE expected 1 notification" { + run -0 cat "${tempfile}" + assert_output --partial 1.2.3.4 + assert_output --partial 1.2.3.5 +} diff --git a/tests/config-templates/notifications/dummy.yaml b/tests/config-templates/notifications/dummy.yaml new file mode 100644 index 000000000..8288a9c8a --- /dev/null +++ b/tests/config-templates/notifications/dummy.yaml @@ -0,0 +1,22 @@ +type: dummy # Don't change +name: dummy_default # Must match the registered plugin in the profile + +# One of "trace", "debug", "info", "warn", "error", "off" +log_level: info + +# group_wait: # Time to wait collecting alerts before relaying a message to this plugin, eg "30s" +# group_threshold: # Amount of alerts that triggers a message before has expired, eg "10" +# max_retry: # Number of attempts to relay messages to plugins in case of error +# timeout: # Time to wait for response from the plugin before considering the attempt a failure, eg "10s" + +#------------------------- +# plugin-specific options + +# The following template receives a list of models.Alert objects +# The output goes in the logs and to a text file, if defined +format: | + {{.|toJson}} + +# +# output_file: # notifications will be appended here. optional + diff --git a/tests/config-templates/profiles.yaml b/tests/config-templates/profiles.yaml index ebbf44db7..f4945b7a0 100644 --- a/tests/config-templates/profiles.yaml +++ b/tests/config-templates/profiles.yaml @@ -7,7 +7,7 @@ decisions: duration: 4h # notifications: # - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this. -# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this. -# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this. -# - email_default # Set the required http parameters in /etc/crowdsec/notifications/email.yaml before enabling this. +# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this. +# - http_default # Set the required http parameters in /etc/crowdsec/notifications/http.yaml before enabling this. +# - email_default # Set the required email parameters in /etc/crowdsec/notifications/email.yaml before enabling this. on_success: break diff --git a/tests/instance-data b/tests/instance-data index 0b2b74cb3..91449db84 100755 --- a/tests/instance-data +++ b/tests/instance-data @@ -57,6 +57,7 @@ make_init_data() { envsubst < "./config-templates/notifications/email.yaml" > "${CONFIG_DIR}/notifications/email.yaml" envsubst < "./config-templates/notifications/slack.yaml" > "${CONFIG_DIR}/notifications/slack.yaml" envsubst < "./config-templates/notifications/splunk.yaml" > "${CONFIG_DIR}/notifications/splunk.yaml" + envsubst < "./config-templates/notifications/dummy.yaml" > "${CONFIG_DIR}/notifications/dummy.yaml" mkdir -p "${CONFIG_DIR}/hub" "${BIN_DIR}/cscli" machines add githubciXXXXXXXXXXXXXXXXXXXXXXXX --auto diff --git a/wizard.sh b/wizard.sh index 7e2238009..baf4d3ef8 100755 --- a/wizard.sh +++ b/wizard.sh @@ -77,7 +77,7 @@ SPLUNK_PLUGIN_CONFIG="./plugins/notifications/splunk/splunk.yaml" EMAIL_PLUGIN_CONFIG="./plugins/notifications/email/email.yaml" BACKUP_DIR=$(mktemp -d) -rm -rf $BACKUP_DIR +rm -rf -- "$BACKUP_DIR" log_info() { msg=$1 @@ -502,7 +502,7 @@ install_plugins(){ cp -n ${SLACK_PLUGIN_CONFIG} /etc/crowdsec/notifications/ cp -n ${SPLUNK_PLUGIN_CONFIG} /etc/crowdsec/notifications/ cp -n ${HTTP_PLUGIN_CONFIG} /etc/crowdsec/notifications/ - cp -n ${EMAIL_PLUGIN_CONFIG} /etc/crowdsec/notifications + cp -n ${EMAIL_PLUGIN_CONFIG} /etc/crowdsec/notifications/ fi }