diff --git a/.gitignore b/.gitignore index cc1298984..96aa59040 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ config.yaml .env .vscode .idea +*.swp diff --git a/README.md b/README.md index e1a002739..b05d9d5e1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Currently available outputs are : * [**Mattermost**](https://mattermost.com/) * [**Teams**](https://products.office.com/en-us/microsoft-teams/group-chat-software) * [**Datadog**](https://www.datadoghq.com/) +* [**Discord**](https://www.discord.com/) * [**AlertManager**](https://prometheus.io/docs/alerting/alertmanager/) * [**Elasticsearch**](https://www.elastic.co/) * [**Loki**](https://grafana.com/oss/loki) @@ -194,6 +195,11 @@ azure: # namespace: "" # The name of the space the Hub is part of # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) +discord: + webhookurl: "" # discord WebhookURL (ex: https://discord.com/api/webhooks/xxxxxxxxxx...), if not empty, Discord output is enabled + #icon: "" # Discord icon (avatar) + minimumpriority: "debug" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) + ``` Usage : @@ -239,6 +245,9 @@ The *env vars* "match" field names in *yaml file with this structure (**take car * **DATADOG_APIKEY** : Datadog API Key, if not `empty`, Datadog output is *enabled* * **DATADOG_HOST** : Datadog host. Override if you are on the Datadog EU site. Defaults to american site with "https://api.datadoghq.com" * **DATADOG_MINIMUMPRIORITY** : minimum priority of event for using this output, order is `emergency|alert|critical|error|warning|notice|informational|debug or "" (default)` +* **DISCORD_WEBHOOKURL** : Discord WebhookURL (ex: https://discord.com/api/webhooks/xxxxxxxxxx...), if not empty, Discord output is enabled +* **DISCORD_ICON** : Discord icon (avatar) +* **DISCORD_MINIMUMPRIORITY** : minimum priority of event for using use this output, order is `emergency|alert|critical|error|warning|notice|informational|debug or "" (default)` * **ALERTMANAGER_HOSTPORT** : AlertManager http://host:port, if not `empty`, AlertManager is *enabled* * **ALERTMANAGER_MINIMUMPRIORITY** : minimum priority of event for using this output, order is `emergency|alert|critical|error|warning|notice|informational|debug or "" (default)` * **ELASTICSEARCH_HOSTPORT** : Elasticsearch http://host:port, if not `empty`, Elasticsearch is *enabled* @@ -407,6 +416,10 @@ time akey bkey ckey priority rule value ![opsgenie example](https://github.com/falcosecurity/falcosidekick/raw/master/imgs/opsgenie.png) +### Discord + +![discord example](https://github.com/falcosecurity/falcosidekick/raw/master/imgs/discord_example.png) + ## Development ### Build @@ -415,6 +428,14 @@ time akey bkey ckey priority rule value go build ``` +### Quicktest + +Create a debug event + +```bash +curl -H "Content-Type: application/json" -H "Accept: application/json" localhost:2801/test +``` + ### Test & Coverage ```bash diff --git a/config.go b/config.go index b5ce256f7..f2f8a7c0b 100644 --- a/config.go +++ b/config.go @@ -49,6 +49,9 @@ func getConfig() *types.Configuration { v.SetDefault("Datadog.APIKey", "") v.SetDefault("Datadog.Host", "https://api.datadoghq.com") v.SetDefault("Datadog.MinimumPriority", "") + v.SetDefault("Discord.WebhookURL", "") + v.SetDefault("Discord.MinimumPriority", "") + v.SetDefault("Discord.Icon", "https://raw.githubusercontent.com/falcosecurity/falcosidekick/master/imgs/falcosidekick_color.png") v.SetDefault("Alertmanager.HostPort", "") v.SetDefault("Alertmanager.MinimumPriority", "") v.SetDefault("Elasticsearch.HostPort", "") diff --git a/config_example.yaml b/config_example.yaml index 4dff79fe5..399c3b496 100644 --- a/config_example.yaml +++ b/config_example.yaml @@ -109,3 +109,8 @@ azure: name: "" # Name of the Hub, if not empty, EventHub is enabled namespace: "" # Name of the space the Hub is in # minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default) + # +discord: + webhookurl: "" # discord WebhookURL (ex: https://discord.com/api/webhooks/xxxxxxxxxx...), if not empty, Discord output is enabled + #icon: "" # Discord icon (avatar) + minimumpriority: "debug" # 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 82ddbf7f1..49a1a0ef9 100644 --- a/handlers.go +++ b/handlers.go @@ -125,6 +125,9 @@ func forwardEvent(falcopayload types.FalcoPayload) { if config.Datadog.APIKey != "" && (priorityMap[strings.ToLower(falcopayload.Priority)] >= priorityMap[strings.ToLower(config.Datadog.MinimumPriority)] || falcopayload.Rule == "Test rule") { go datadogClient.DatadogPost(falcopayload) } + if config.Discord.WebhookURL != "" && (priorityMap[strings.ToLower(falcopayload.Priority)] >= priorityMap[strings.ToLower(config.Discord.MinimumPriority)] || falcopayload.Rule == "Test rule") { + go discordClient.DiscordPost(falcopayload) + } if config.Alertmanager.HostPort != "" && (priorityMap[strings.ToLower(falcopayload.Priority)] >= priorityMap[strings.ToLower(config.Alertmanager.MinimumPriority)] || falcopayload.Rule == "Test rule") { go alertmanagerClient.AlertmanagerPost(falcopayload) } diff --git a/imgs/discord.png b/imgs/discord.png new file mode 100644 index 000000000..dbc713b34 Binary files /dev/null and b/imgs/discord.png differ diff --git a/main.go b/main.go index 81ac4ebd2..1f352c830 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ var ( mattermostClient *outputs.Client teamsClient *outputs.Client datadogClient *outputs.Client + discordClient *outputs.Client alertmanagerClient *outputs.Client elasticsearchClient *outputs.Client influxdbClient *outputs.Client @@ -30,7 +31,6 @@ var ( webhookClient *outputs.Client azureClient *outputs.Client ) - var statsdClient, dogstatsdClient *statsd.Client var config *types.Configuration var stats *types.Statistics @@ -116,6 +116,15 @@ func init() { enabledOutputsText += "Datadog " } } + if config.Discord.WebhookURL != "" { + var err error + discordClient, err = outputs.NewClient("Discord", config.Discord.WebhookURL, config, stats, statsdClient, dogstatsdClient) + if err != nil { + config.Discord.WebhookURL = "" + } else { + enabledOutputsText += "Discord " + } + } if config.Alertmanager.HostPort != "" { var err error alertmanagerClient, err = outputs.NewClient("AlertManager", config.Alertmanager.HostPort+outputs.AlertmanagerURI, config, stats, statsdClient, dogstatsdClient) diff --git a/outputs/discord.go b/outputs/discord.go new file mode 100644 index 000000000..576c486a3 --- /dev/null +++ b/outputs/discord.go @@ -0,0 +1,110 @@ +package outputs + +import ( + "github.com/falcosecurity/falcosidekick/types" + "strings" +) + +type discordPayload struct { + Content string `json:"content"` + Avatar_url string `json:"avatar_url,omitempty"` + Embeds []discordEmbedPayload `json:"embeds"` +} + +type discordEmbedPayload struct { + Title string `json:"title"` + Url string `json:"url"` + Description string `json:"description"` + Color string `json:"color"` + Fields []discordEmbedFieldPayload `json:"fields"` +} + +type discordEmbedFieldPayload struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` +} + +func newDiscordPayload(falcopayload types.FalcoPayload, config *types.Configuration) discordPayload { + var iconURL string + if config.Discord.Icon != "" { + iconURL = config.Discord.Icon + } else { + iconURL = "https://raw.githubusercontent.com/falcosecurity/falcosidekick/master/imgs/falcosidekick.png" + } + + var color string + switch strings.ToLower(falcopayload.Priority) { + case "emergency": + color = "15158332" // red + case "alert": + color = "11027200" // dark orange + case "critical": + color = "15105570" // orange + case "error": + color = "15844367" // gold + case "warning": + color = "12745742" // dark gold + case "notice": + color = "3066993" // teal + case "informational": + color = "3447003" // blue + case "debug": + color = "12370112" // light grey + } + + embeds := make([]discordEmbedPayload, 0) + + embedFields := make([]discordEmbedFieldPayload, 0) + var embedField discordEmbedFieldPayload + + for i, j := range falcopayload.OutputFields { + switch j.(type) { + case string: + embedField.Name = i + embedField.Inline = true + embedField.Value = "```" + j.(string) + "```" + default: + continue + } + embedFields = append(embedFields, embedField) + } + embedField.Name = "rule" + embedField.Value = falcopayload.Rule + embedField.Inline = true + embedFields = append(embedFields, embedField) + embedField.Name = "priority" + embedField.Value = falcopayload.Priority + embedField.Inline = true + embedFields = append(embedFields, embedField) + embedField.Name = "time" + embedField.Value = falcopayload.Time.String() + embedField.Inline = true + embedFields = append(embedFields, embedField) + + embed := discordEmbedPayload{ + Title: "", + Description: falcopayload.Output, + Color: color, + Fields: embedFields, + } + embeds = append(embeds, embed) + + ds := discordPayload{ + Content: "", + Avatar_url: iconURL, + Embeds: embeds, + } + return ds +} + +// DiscordPost posts events to discord +func (c *Client) DiscordPost(falcopayload types.FalcoPayload) { + err := c.Post(newDiscordPayload(falcopayload, c.Config)) + if err != nil { + c.Stats.Discord.Add("error", 1) + } else { + c.Stats.Discord.Add("ok", 1) + } + c.Stats.Discord.Add("total", 1) +} diff --git a/stats.go b/stats.go index a686461fe..44f5d6345 100644 --- a/stats.go +++ b/stats.go @@ -26,6 +26,7 @@ func getInitStats() *types.Statistics { Mattermost: expvar.NewMap("outputs.mattermost"), Teams: expvar.NewMap("outputs.teams"), Datadog: expvar.NewMap("outputs.datadog"), + Discord: expvar.NewMap("outputs.discord"), Alertmanager: expvar.NewMap("outputs.alertmanager"), Elasticsearch: expvar.NewMap("outputs.elasticsearch"), Loki: expvar.NewMap("outputs.loki"), @@ -72,6 +73,9 @@ func getInitStats() *types.Statistics { stats.Datadog.Add("total", 0) stats.Datadog.Add("error", 0) stats.Datadog.Add("ok", 0) + stats.Discord.Add("total", 0) + stats.Discord.Add("error", 0) + stats.Discord.Add("ok", 0) stats.Alertmanager.Add("total", 0) stats.Alertmanager.Add("error", 0) stats.Alertmanager.Add("ok", 0) diff --git a/types/types.go b/types/types.go index c109d4576..b980c7f32 100644 --- a/types/types.go +++ b/types/types.go @@ -24,6 +24,7 @@ type Configuration struct { Rocketchat rocketchatOutputConfig Teams teamsOutputConfig Datadog datadogOutputConfig + Discord discordOutputConfig Alertmanager alertmanagerOutputConfig Elasticsearch elasticsearchOutputConfig Influxdb influxdbOutputConfig @@ -82,6 +83,12 @@ type datadogOutputConfig struct { MinimumPriority string } +type discordOutputConfig struct { + WebhookURL string + MinimumPriority string + Icon string +} + type alertmanagerOutputConfig struct { HostPort string MinimumPriority string @@ -181,6 +188,7 @@ type Statistics struct { Rocketchat *expvar.Map Teams *expvar.Map Datadog *expvar.Map + Discord *expvar.Map Alertmanager *expvar.Map Elasticsearch *expvar.Map Loki *expvar.Map