diff --git a/README.md b/README.md index 37e719e43..29c068c3b 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ It works as a single endpoint for as many as you want `Falco` instances : - [**Discord**](https://www.discord.com/) - [**Google Chat**](https://workspace.google.com/products/chat/) - [**Zoho Cliq**](https://www.zoho.com/cliq/) +- [**Telegram**](https://telegram.org) ### Metrics / Observability @@ -578,6 +579,12 @@ redis: # storagetype: "" # Redis storage type: hashmap or list (default: list) # key: "" # Redis storage key name for hashmap, list(default: "falco") # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) + +telegram: + # token: "" # telegram bot authentication token + # chatid: "" # telegram Identifier of the shared chat + # checkcert: true # check if ssl certificate of the output is valid (default: true) + # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) ``` Usage : @@ -1065,6 +1072,10 @@ order is - **REDIS_STORAGE**: Redis storage type: hashmap or list (default: "list") - **REDIS_Key**: Redis storage key name for hashmap, list(default: "falco") - **REDIS_MINIMUMPRIORITY**: minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) +- **TELEGRAM_TOKEN**: telegram bot authentication token +- **TELEGRAM_CHATID**: telegram Identifier of the shared chat +- **TELEGRAM_CHECKCERT**: check if ssl certificate of the output is valid (default: true) +- **TELEGRAM_MINIMUMPRIORITY**: minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) #### Slack/Rocketchat/Mattermost/Googlechat Message Formatting @@ -1380,4 +1391,4 @@ make test-coverage ## Author -Thomas Labarussias (https://github.com/Issif) \ No newline at end of file +Thomas Labarussias (https://github.com/Issif) diff --git a/config.go b/config.go index fc70eca4c..5ee98bf1d 100644 --- a/config.go +++ b/config.go @@ -416,6 +416,11 @@ func getConfig() *types.Configuration { v.SetDefault("Redis.MutualTls", false) v.SetDefault("Redis.CheckCert", true) + v.SetDefault("Slack.Token", "") + v.SetDefault("Slack.ChatID", "") + v.SetDefault("Slack.MinimumPriority", "") + v.SetDefault("Slack.CheckCert", true) + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() if *configFile != "" { @@ -601,6 +606,7 @@ func getConfig() *types.Configuration { c.Gotify.MinimumPriority = checkPriority(c.Gotify.MinimumPriority) c.TimescaleDB.MinimumPriority = checkPriority(c.TimescaleDB.MinimumPriority) c.Redis.MinimumPriority = checkPriority(c.Redis.MinimumPriority) + c.Telegram.MinimumPriority = checkPriority(c.Telegram.MinimumPriority) c.Slack.MessageFormatTemplate = getMessageFormatTemplate("Slack", c.Slack.MessageFormat) c.Rocketchat.MessageFormatTemplate = getMessageFormatTemplate("Rocketchat", c.Rocketchat.MessageFormat) diff --git a/config_example.yaml b/config_example.yaml index 2a0ec589b..67ecae785 100644 --- a/config_example.yaml +++ b/config_example.yaml @@ -415,4 +415,10 @@ redis: # database: "" # Redis database number (default: 0) # storagetype: "" # Redis storage type: hashmap or list (default: "list") # key: "" # Redis storage key name for hashmap, list(default: "falco") - # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) \ No newline at end of file + # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) + +telegram: + # token: "" # telegram bot authentication token + # chatid: "" # telegram Identifier of the shared chat + # checkcert: true # check if ssl certificate of the output is valid (default: true) + # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) diff --git a/handlers.go b/handlers.go index ca274cb6f..3582c13a7 100644 --- a/handlers.go +++ b/handlers.go @@ -382,4 +382,8 @@ func forwardEvent(falcopayload types.FalcoPayload) { if config.Redis.Address != "" && (falcopayload.Priority >= types.Priority(config.Redis.MinimumPriority) || falcopayload.Rule == testRule) { go redisClient.RedisPost(falcopayload) } + + if config.Telegram.ChatID != "" && config.Telegram.Token != "" && (falcopayload.Priority >= types.Priority(config.Telegram.MinimumPriority) || falcopayload.Rule == testRule) { + go telegramClient.TelegramPost(falcopayload) + } } diff --git a/main.go b/main.go index d05bf70ee..16ffe5078 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ var ( spyderbatClient *outputs.Client timescaleDBClient *outputs.Client redisClient *outputs.Client + telegramClient *outputs.Client statsdClient, dogstatsdClient *statsd.Client config *types.Configuration @@ -663,6 +664,22 @@ func init() { } } + if config.Telegram.ChatID != "" && config.Telegram.Token != "" { + var err error + var urlFormat = "https://api.telegram.org/bot%s/sendMessage" + + telegramClient, err = outputs.NewClient("Telegram", fmt.Sprintf(urlFormat, config.Telegram.Token), false, config.Telegram.CheckCert, config, stats, promStats, statsdClient, dogstatsdClient) + + if err != nil { + config.Telegram.ChatID = "" + config.Telegram.Token = "" + + log.Printf("[ERROR] : Telegram - %v\n", err) + } else { + outputs.EnabledOutputs = append(outputs.EnabledOutputs, "Telegram") + } + } + log.Printf("[INFO] : Falco Sidekick version: %s\n", GetVersionInfo().GitVersion) log.Printf("[INFO] : Enabled Outputs : %s\n", outputs.EnabledOutputs) diff --git a/outputs/telegram.go b/outputs/telegram.go new file mode 100644 index 000000000..cbe51189a --- /dev/null +++ b/outputs/telegram.go @@ -0,0 +1,89 @@ +package outputs + +import ( + "bytes" + "fmt" + "log" + "strings" + textTemplate "text/template" + + "github.com/falcosecurity/falcosidekick/types" +) + +func markdownV2EscapeText(text interface{}) string { + + replacer := strings.NewReplacer( + "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", + "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", + "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", + "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!", + ) + + return replacer.Replace(fmt.Sprintf("%v", text)) +} + +var ( + telegramMarkdownV2Tmpl = `*\[Falco\] \[{{markdownV2EscapeText .Priority }}\] {{markdownV2EscapeText .Rule }}* + +• *Time*: {{markdownV2EscapeText .Time }} +• *Source*: {{markdownV2EscapeText .Source }} +• *Hostname*: {{markdownV2EscapeText .Hostname }} +• *Tags*: {{ range .Tags }}{{markdownV2EscapeText . }} {{ end }} +• *Fields*: +{{ range $key, $value := .OutputFields }} • *{{markdownV2EscapeText $key }}*: {{markdownV2EscapeText $value }} +{{ end }} + +**Output**: {{markdownV2EscapeText .Output }} +` +) + +// Payload +type telegramPayload struct { + Text string `json:"text,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` + ChatID string `json:"chat_id,omitempty"` +} + +func newTelegramPayload(falcopayload types.FalcoPayload, config *types.Configuration) telegramPayload { + payload := telegramPayload{ + + ParseMode: "MarkdownV2", + DisableWebPagePreview: true, + ChatID: config.Telegram.ChatID, + } + + // template engine + var textBuffer bytes.Buffer + funcs := textTemplate.FuncMap{ + "markdownV2EscapeText": markdownV2EscapeText, + } + ttmpl, _ := textTemplate.New("telegram").Funcs(funcs).Parse(telegramMarkdownV2Tmpl) + err := ttmpl.Execute(&textBuffer, falcopayload) + if err != nil { + log.Printf("[ERROR] : Telegram - %v\n", err) + return payload + } + payload.Text = textBuffer.String() + + return payload +} + +// TelegramPost posts event to Telegram +func (c *Client) TelegramPost(falcopayload types.FalcoPayload) { + c.Stats.Telegram.Add(Total, 1) + + err := c.Post(newTelegramPayload(falcopayload, c.Config)) + if err != nil { + go c.CountMetric(Outputs, 1, []string{"output:telegram", "status:error"}) + c.Stats.Telegram.Add(Error, 1) + c.PromStats.Outputs.With(map[string]string{"destination": "telegram", "status": Error}).Inc() + log.Printf("[ERROR] : Telegram - %v\n", err) + return + } + + // Setting the success status + go c.CountMetric(Outputs, 1, []string{"output:telegram", "status:ok"}) + c.Stats.Telegram.Add(OK, 1) + c.PromStats.Outputs.With(map[string]string{"destination": "telegram", "status": OK}).Inc() +} diff --git a/outputs/telegram_test.go b/outputs/telegram_test.go new file mode 100644 index 000000000..3200cc9e5 --- /dev/null +++ b/outputs/telegram_test.go @@ -0,0 +1,30 @@ +package outputs + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/falcosecurity/falcosidekick/types" +) + +func TestNewTelegramPayload(t *testing.T) { + expectedOutput := telegramPayload{ + Text: "*\\[Falco\\] \\[Debug\\] Test rule*\n\n• *Time*: 2001\\-01\\-01 01:10:00 \\+0000 UTC\n• *Source*: syscalls\n• *Hostname*: test\\-host\n• *Tags*: test example \n• *Fields*:\n\t • *proc\\.name*: falcosidekick\n\t • *proc\\.tty*: 1234\n\n\n**Output**: This is a test from falcosidekick\n", + ParseMode: "MarkdownV2", + DisableWebPagePreview: true, + ChatID: "-987654321", + } + + var f types.FalcoPayload + require.Nil(t, json.Unmarshal([]byte(falcoTestInput), &f)) + config := &types.Configuration{ + Telegram: types.TelegramConfig{ + ChatID: "-987654321", + }, + } + + output := newTelegramPayload(f, config) + require.Equal(t, expectedOutput, output) +} diff --git a/stats.go b/stats.go index e7f272f58..98fa85821 100644 --- a/stats.go +++ b/stats.go @@ -76,6 +76,7 @@ func getInitStats() *types.Statistics { Gotify: getOutputNewMap("gotify"), TimescaleDB: getOutputNewMap("timescaledb"), Redis: getOutputNewMap("redis"), + Telegram: getOutputNewMap("telegram"), } stats.Falco.Add(outputs.Emergency, 0) stats.Falco.Add(outputs.Alert, 0) diff --git a/types/types.go b/types/types.go index 0a381d35c..ace26ec11 100644 --- a/types/types.go +++ b/types/types.go @@ -99,6 +99,7 @@ type Configuration struct { Spyderbat SpyderbatConfig TimescaleDB TimescaleDBConfig Redis RedisConfig + Telegram TelegramConfig } // SlackOutputConfig represents parameters for Slack @@ -640,6 +641,14 @@ type RedisConfig struct { MutualTLS bool } +// TelegramConfig represents parameters for Telegram +type TelegramConfig struct { + Token string + ChatID string + MinimumPriority string + CheckCert bool +} + // Statistics is a struct to store stastics type Statistics struct { Requests *expvar.Map @@ -700,6 +709,7 @@ type Statistics struct { Spyderbat *expvar.Map TimescaleDB *expvar.Map Redis *expvar.Map + Telegram *expvar.Map } // PromStatistics is a struct to store prometheus metrics