Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introducing telegram output #431

Merged
merged 2 commits into from
Mar 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 :
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1380,4 +1391,4 @@ make test-coverage

## Author

Thomas Labarussias (https://github.com/Issif)
Thomas Labarussias (https://github.com/Issif)
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# 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)
4 changes: 4 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
17 changes: 17 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var (
spyderbatClient *outputs.Client
timescaleDBClient *outputs.Client
redisClient *outputs.Client
telegramClient *outputs.Client

statsdClient, dogstatsdClient *statsd.Client
config *types.Configuration
Expand Down Expand Up @@ -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)

Expand Down
89 changes: 89 additions & 0 deletions outputs/telegram.go
Original file line number Diff line number Diff line change
@@ -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 }}
Issif marked this conversation as resolved.
Show resolved Hide resolved
• *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()
}
30 changes: 30 additions & 0 deletions outputs/telegram_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type Configuration struct {
Spyderbat SpyderbatConfig
TimescaleDB TimescaleDBConfig
Redis RedisConfig
Telegram TelegramConfig
}

// SlackOutputConfig represents parameters for Slack
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down