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

Add Google Chat support #107

Merged
merged 7 commits into from
Nov 19, 2020
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Currently available outputs are :
* [**Azure Event Hubs**](https://azure.microsoft.com/en-in/services/event-hubs/)
* [**Prometheus**](https://prometheus.io/) (for both events and monitoring of `falcosidekick`)
* [**GCP PubSub**](https://cloud.google.com/pubsub)
* [**Google Chat**](https://workspace.google.com/products/chat/)

## Usage

Expand Down Expand Up @@ -237,6 +238,12 @@ gcp:
projectid: "" # The GCP Project ID containing the Pub/Sub Topic
topic: "" # The name of the Pub/Sub topic
# minimumpriority: "debug" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default)

googlechat:
webhookurl: "" # Google Chat WebhookURL (ex: https://chat.googleapis.com/v1/spaces/XXXXXX/YYYYYY), if not empty, Google Chat output is enabled
# outputformat: "" # all (default), text
# minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default)
messageformat: "Alert : rule *{{ .Rule }}* triggered by user *{{ index .OutputFields \"user.name\" }}*" # a Go template to format Google Chat Text above Attachment, displayed in addition to the output from `GOOGLECHAT_OUTPUTFORMAT`, see [Slack Message Formatting](#slack-message-formatting) in the README for details. If empty, no Text is displayed before Attachment.
```

Usage :
Expand Down Expand Up @@ -340,8 +347,12 @@ The *env vars* "match" field names in *yaml file with this structure (**take car
* **GCP_PUBSUB_PROJECTID**: The GCP Project ID containing the Pub/Sub Topic
* **GCP_PUBSUB_TOPIC**: The name of the Pub/Sub topic
* **GCP_PUBSUB_MINIMUMPRIORITY**: minimum priority of event for using this output, order is `emergency|alert|critical|error|warning|notice|informational|debug or "" (default)`
* **GOOGLECHAT_WEBHOOKURL** : Google Chat URL (ex: https://chat.googleapis.com/v1/spaces/XXXXXX/YYYYYY), if not `empty`, Google Chat output is *enabled*
* **GOOGLECHAT_OUTPUTFORMAT** : `all` (default), `text` (only text is displayed in Google Chat)
* **GOOGLECHAT_MINIMUMPRIORITY** : minimum priority of event for using this output, order is `emergency|alert|critical|error|warning|notice|informational|debug or "" (default)`
* **GOOGLECHAT_MESSAGEFORMAT** : a Go template to format Google Chat Text above Attachment, displayed in addition to the output from `GOOGLECHAT_OUTPUTFORMAT`, see [Slack Message Formatting](#slack-message-formatting) in the README for details. If empty, no Text is displayed before sections.

#### Slack/Rocketchat/Mattermost Message Formatting
#### Slack/Rocketchat/Mattermost/Googlechat Message Formatting

The `SLACK_MESSAGEFORMAT` environment variable and `slack.messageformat` YAML value accept a [Go template](https://golang.org/pkg/text/template/) which can be used to format the text of a slack alert. These templates are evaluated on the JSON data from each Falco event - the following fields are available:

Expand Down Expand Up @@ -486,6 +497,16 @@ time akey bkey ckey priority rule value

![discord example](https://github.com/falcosecurity/falcosidekick/raw/master/imgs/discord_example.png)

### Google Chat

(GOOGLECHAT_OUTPUTFORMAT="**all**")

![google chat example](https://github.com/falcosecurity/falcosidekick/raw/master/imgs/google_chat_example.png)

(GOOGLECHAT_OUTPUTFORMAT="**text**")

![google chat no fields example](https://github.com/falcosecurity/falcosidekick/raw/master/imgs/google_chat_no_fields.png)

## Development

### Build
Expand Down
7 changes: 6 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func getConfig() *types.Configuration {
v.SetDefault("GCP.PubSub.ProjectID", "")
v.SetDefault("GCP.PubSub.Topic", "")
v.SetDefault("GCP.PubSub.MinimumPriority", "")
v.SetDefault("Googlechat.WebhookURL", "")
v.SetDefault("Googlechat.OutputFormat", "all")
v.SetDefault("Googlechat.MessageFormat", "")
v.SetDefault("Googlechat.MinimumPriority", "")

v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
Expand Down Expand Up @@ -171,11 +175,12 @@ func getConfig() *types.Configuration {
c.Webhook.MinimumPriority = checkPriority(c.Webhook.MinimumPriority)
c.Azure.EventHub.MinimumPriority = checkPriority(c.Azure.EventHub.MinimumPriority)
c.GCP.PubSub.MinimumPriority = checkPriority(c.GCP.PubSub.MinimumPriority)
c.Googlechat.MinimumPriority = checkPriority(c.Googlechat.MinimumPriority)

c.Slack.MessageFormatTemplate = getMessageFormatTemplate("Slack", c.Slack.MessageFormat)
c.Rocketchat.MessageFormatTemplate = getMessageFormatTemplate("Rocketchat", c.Rocketchat.MessageFormat)
c.Mattermost.MessageFormatTemplate = getMessageFormatTemplate("Mattermost", c.Mattermost.MessageFormat)

c.Googlechat.MessageFormatTemplate = getMessageFormatTemplate("Googlechat", c.Googlechat.MessageFormat)
return c
}

Expand Down
6 changes: 6 additions & 0 deletions config_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ gcp:
projectid: "" # The GCP Project ID containing the Pub/Sub Topic
topic: "" # The name of the Pub/Sub topic
# minimumpriority: "debug" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default)

googlechat:
webhookurl: "" # Google Chat WebhookURL (ex: https://chat.googleapis.com/v1/spaces/XXXXXX/YYYYYY), if not empty, Google Chat output is enabled
# outputformat: "" # all (default), text
# minimumpriority: "" # minimum priority of event for using this output, order is emergency|alert|critical|error|warning|notice|informational|debug or "" (default)
messageformat: 'Alert : rule *{{ .Rule }}* triggered by user *{{ index .OutputFields "user.name" }}*' # a Go template to format Slack Text above Attachment, displayed in addition to the output from `SLACK_OUTPUTFORMAT`, see [Slack Message Formatting](#slack-message-formatting) in the README for details. If empty, no Text is displayed before Attachment.
4 changes: 4 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,8 @@ func forwardEvent(falcopayload types.FalcoPayload) {
if config.GCP.PubSub.Topic != "" && (priorityMap[strings.ToLower(falcopayload.Priority)] >= priorityMap[strings.ToLower(config.GCP.PubSub.MinimumPriority)] || falcopayload.Rule == TestRule) {
go gcpClient.GCPPublishTopic(falcopayload)
}

if config.Googlechat.WebhookURL != "" && (priorityMap[strings.ToLower(falcopayload.Priority)] >= priorityMap[strings.ToLower(config.Googlechat.MinimumPriority)] || falcopayload.Rule == TestRule) {
go googleChatClient.GooglechatPost(falcopayload)
}
}
Binary file added imgs/google_chat_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added imgs/google_chat_no_fields.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
webhookClient *outputs.Client
azureClient *outputs.Client
gcpClient *outputs.Client
googleChatClient *outputs.Client
)
var statsdClient, dogstatsdClient *statsd.Client
var config *types.Configuration
Expand Down Expand Up @@ -272,6 +273,16 @@ func init() {
}
}

if config.Googlechat.WebhookURL != "" {
var err error
googleChatClient, err = outputs.NewClient("Googlechat", config.Googlechat.WebhookURL, config, stats, promStats, statsdClient, dogstatsdClient)
if err != nil {
config.Googlechat.WebhookURL = ""
} else {
enabledOutputsText += "Google Chat "
}
}

log.Printf("%v\n", enabledOutputsText)
}

Expand Down
102 changes: 102 additions & 0 deletions outputs/googlechat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package outputs

import (
"bytes"
"log"

"github.com/falcosecurity/falcosidekick/types"
)

type header struct {
Title string `json:"title"`
SubTitle string `json:"subtitle"`
}

type keyValue struct {
TopLabel string `json:"topLabel"`
Content string `json:"content"`
}

type widget struct {
KeyValue keyValue `json:"keyValue,omitempty"`
}

type section struct {
Widgets []widget `json:"widgets"`
}

type card struct {
Header header `json:"header,omitempty"`
Sections []section `json:"sections,omitempty"`
}

type googlechatPayload struct {
Text string `json:"text,omitempty"`
Cards []card `json:"cards,omitempty"`
}

func newGooglechatPayload(falcopayload types.FalcoPayload, config *types.Configuration) googlechatPayload {
var messageText string
widgets := []widget{}

if config.Googlechat.MessageFormatTemplate != nil {
buf := &bytes.Buffer{}
if err := config.Googlechat.MessageFormatTemplate.Execute(buf, falcopayload); err != nil {
log.Printf("[ERROR] : Error expanding Google Chat message %v", err)
} else {
messageText = buf.String()
}
}

if config.Googlechat.OutputFormat == Text {
return googlechatPayload{
Text: messageText,
}
}

for i, j := range falcopayload.OutputFields {
var w widget
switch v := j.(type) {
case string:
w = widget{
KeyValue: keyValue{
TopLabel: i,
Content: v,
},
}
default:
continue
}

widgets = append(widgets, w)
}

widgets = append(widgets, widget{KeyValue: keyValue{"rule", falcopayload.Rule}})
widgets = append(widgets, widget{KeyValue: keyValue{"priority", falcopayload.Priority}})
widgets = append(widgets, widget{KeyValue: keyValue{"time", falcopayload.Time.String()}})

return googlechatPayload{
Text: messageText,
Cards: []card{
{
Sections: []section{
{Widgets: widgets},
},
},
},
}
}

// GooglechatPost posts event to Google Chat
func (c *Client) GooglechatPost(falcopayload types.FalcoPayload) {
err := c.Post(newGooglechatPayload(falcopayload, c.Config))
if err != nil {
c.Stats.GoogleChat.Add(Error, 1)
c.PromStats.Outputs.With(map[string]string{"destination": "googlechat", "status": Error}).Inc()
} else {
c.Stats.GoogleChat.Add(OK, 1)
c.PromStats.Outputs.With(map[string]string{"destination": "googlechat", "status": OK}).Inc()
}

c.Stats.GoogleChat.Add(Total, 1)
}
63 changes: 63 additions & 0 deletions outputs/googlechat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package outputs

import (
"encoding/json"
"reflect"
"testing"
"text/template"

"github.com/falcosecurity/falcosidekick/types"
)

func TestNewGoogleChatPayload(t *testing.T) {
expectedOutput := googlechatPayload{
Text: "Rule: Test rule Priority: Debug",
Cards: []card{
{
Sections: []section{
{
Widgets: []widget{
{
keyValue{
TopLabel: "proc.name",
Content: "falcosidekick",
},
},
{
keyValue{
TopLabel: "rule",
Content: "Test rule",
},
},
{
keyValue{
TopLabel: "priority",
Content: "Debug",
},
},
{
keyValue{
TopLabel: "time",
Content: "2001-01-01 01:10:00 +0000 UTC",
},
},
},
},
},
},
},
}

var f types.FalcoPayload
json.Unmarshal([]byte(falcoTestInput), &f)
config := &types.Configuration{
Googlechat: types.GooglechatConfig{},
}

config.Googlechat.MessageFormatTemplate, _ = template.New("").Parse("Rule: {{ .Rule }} Priority: {{ .Priority }}")
output := newGooglechatPayload(f, config)

if !reflect.DeepEqual(output, expectedOutput) {
t.Fatalf("\nexpected payload: \n%#v\ngot: \n%#v\n", expectedOutput, output)
}
}
1 change: 1 addition & 0 deletions stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func getInitStats() *types.Statistics {
Webhook: getOutputNewMap("webhook"),
AzureEventHub: getOutputNewMap("azureeventhub"),
GCPPubSub: getOutputNewMap("gcppubsub"),
GoogleChat: getOutputNewMap("googlechat"),
}
stats.Falco.Add("emergency", 0)
stats.Falco.Add("alert", 0)
Expand Down
11 changes: 11 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Configuration struct {
Webhook WebhookOutputConfig
Azure azureConfig
GCP gcpOutputConfig
Googlechat GooglechatConfig
}

// SlackOutputConfig represents parameters for Slack
Expand Down Expand Up @@ -206,6 +207,15 @@ type gcpPubSub struct {
MinimumPriority string
}

// GooglechatConfig represents parameters for Google chat
type GooglechatConfig struct {
WebhookURL string
OutputFormat string
MinimumPriority string
MessageFormat string
MessageFormatTemplate *template.Template
}

// Statistics is a struct to store stastics
type Statistics struct {
Requests *expvar.Map
Expand Down Expand Up @@ -233,6 +243,7 @@ type Statistics struct {
Webhook *expvar.Map
AzureEventHub *expvar.Map
GCPPubSub *expvar.Map
GoogleChat *expvar.Map
}

// PromStatistics is a struct to store prometheus metrics
Expand Down