Skip to content
This repository has been archived by the owner on Jul 19, 2022. It is now read-only.

Commit

Permalink
adding more integrations and features
Browse files Browse the repository at this point in the history
  • Loading branch information
betorvs committed Jun 8, 2021
1 parent 1f8b4bd commit 8d5ff02
Show file tree
Hide file tree
Showing 40 changed files with 2,906 additions and 532 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- github actions
- change from nlopes/slack to slack-go/slack
- change tests to assert lib
- change telegram webhook struct to receive more data
- fix execute and silence verb
- in gateway directory split between integrations (monitoring softwares) and chat

### Added
- google chat integration
- alert manager integration
- user and password authentication option for sensu api
- test directory with mock interfaces
- new config variables for security options:
- SENSUBOT_BLOCKED_VERBS: blocked list of verbs (get, execute, silence, delete, resolve)
- SENSUBOT_BLOCKED_RESOURCES: blocked list of resources from sensu api
- SENSUBOT_SLACK_ADMIN_ID_LIST, SENSUBOT_TELEGRAM_ADMIN_ID_LIST, SENSUBOT_GCHAT_ADMIN_LIST: User ID (from slack, google chat and telegram) allowed to run anything

## [0.0.1] - 2020-02-03
### Added
Expand Down
75 changes: 66 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,58 @@ SensuBot
![Go Test](https://github.com/betorvs/sensubot/workflows/Go%20Test/badge.svg)
[![Coverage Status](https://coveralls.io/repos/github/betorvs/sensubot/badge.svg?branch=main)](https://coveralls.io/github/betorvs/sensubot?branch=main)

SensuBot can receive messages from Slack and/or Telegram. It can answer simple commands like get, execute and silence.
SensuBot was initially design to work with chats (Slack, Telegram) to receive requests from users and send it to Sensu API. It should be stateless and never keep any data. We expand this concept to use multiple integrations and the default integration still be Sensu API.

It can list almost all resources available in Sensu: assets, checks, entities, events, namespaces, mutators, filters, handlers, hooks and health.
Simple Diagram:

```
┌────────────┐ ┌──────────────┐ ┌────────────────┐
│ Chats ├─────────────►│ SensuBot ├───────────────►│ Integrations │
│ │ │ │ │ │
└────────────┘ ◄────────────┴──────────────┘ ◄──────────────┴────────────────┘
```
Created using [asciiflow][1].

It can list these resources in Sensu: assets, checks, entities, events, namespaces, mutators, filters, handlers, hooks and health.

# Environment Variables

## Basic setup

* **SENSUBOT_PORT**: default "9090";
* **SENSUBOT_TIMEOUT**: default "15" seconds;
* **SENSUBOT_DEBUG_SENSU_REQUESTS**: default "false";
* **LOG_LEVEL**: default "INFO";

## Integrations

* **SENSUBOT_DEFAULT_INTEGRATION_NAME**: Default integration to connect. Default "sensu".
* **SENSUBOT_API_SCHEME**: If your Sensu Backend are using https, change here. Default: "https";
* **SENSUBOT_API_TOKEN**: Sensu Backend API token;
* **SENSUBOT_API_URL**: Sensu Backend API URL (like "sensu-api.sensu.svc.cluster.local:8080")
* **SENSUBOT_ALERTMANAGER_ENDPOINTS**: Alert Manager integration

## Chat configurations

* **SENSUBOT_SLASH_COMMAND**: default "/sensubot";
* **SENSUBOT_SLACK_TOKEN**: Please create one in api.slack and configure it (starts with "xoxb-")
* **SENSUBOT_SLACK_SIGNING_SECRET**: Please get from api.slack these secret;
* **SENSUBOT_SLACK_CHANNEL**: For slack, sensuBot needs to have on channel to listen (looks like "MM34AASDD");
* **SENSUBOT_CA_CERTIFICATE**: If you are using private certificates in Sensu Backend, please share CA public certificate here (like /etc/sensu/ca.pem)
* **SENSUBOT_TELEGRAM_TOKEN**: Please, configure your token here;
* **SENSUBOT_TELEGRAM_URL**: If you want to change Telegram API URL, set this. Default: "https://api.telegram.org/bot";
* **SENSUBOT_API_SCHEME**: If your Sensu Backend are using https, change here. Default: "https";
* **SENSUBOT_API_TOKEN**: Sensu Backend API token;
* **SENSUBOT_API_URL**: Sensu Backend API URL (like "sensu-api.sensu.svc.cluster.local:8080")
* **SENSUBOT_TELEGRAM_NAME**: Telegram bot name. SensuBot use it to remove sensuBot Name from requests when it is used inside a group chat message.
* **SENSUBOT_GCHAT_PROJECTID**: Google Cloud Project ID (numbers)
* **SENSUBOT_GCHAT_BOT_NAME**: Google Chat Bot name
* **SENSUBOT_GCHAT_SA_PATH**: Path for service account json file

## Chat security options

* **SENSUBOT_BLOCKED_VERBS** : blocked list of verbs (get, execute, silence, delete, resolve)
* **SENSUBOT_BLOCKED_RESOURCES** : blocked list of resources from sensu api
* **SENSUBOT_SLACK_ADMIN_ID_LIST** : User ID from slack, google chat and telegram) allowed to run anything
* **SENSUBOT_TELEGRAM_ADMIN_ID_LIST** : User ID from telegram allowed to run anything
* **SENSUBOT_GCHAT_ADMIN_LIST** : User ID from google chat allowed to run anything


# Get Configurations

Expand Down Expand Up @@ -67,6 +100,26 @@ TIP: If you need to share these SensuBot in a Group, ask to make this public in
* Past your bot name;
* Choose "DISABLE".

### Curl tips

```bash
curl https://api.telegram.org/bot${my_bot_token}/getWebhookInfo
```

```bash
curl -X POST https://api.telegram.org/bot${my_bot_token}/setWebhook?url=https://YOUR-URL/sensubot/v1/telegram
```

[Source](https://xabaras.medium.com/setting-your-telegram-bot-webhook-the-easy-way-c7577b2d6f72)

## Create Bot In Google Chat (Hangouts Chat)

* This is a HTTPS Bot
* Create Google Chat Bot [here](https://developers.google.com/chat/how-tos/bots-publish)
- Avatar URl: `https://docs.sensu.io/images/lizy-logo-a.png`
- Connection settings: Check `Bot URL` and in `Bot URL` add your external endpoint ending in `https://your-domain.com/sensubot/v1/gchat`
* Create and Download service account JSON [here](https://developers.google.com/chat/how-tos/service-accounts)

# Deploy sensubot in Kubernetes

## Create sensuBot secrets
Expand Down Expand Up @@ -107,7 +160,7 @@ If your sensu backend api use https, don't forgot to add CA certificate into sec

in Slack:
```
/sensubot get all checks
/sensubot get checks
/sensubot get health
```
Expand All @@ -118,7 +171,7 @@ in Telegram:
```
or directly messages:
```
get all checks
get checks
```

# Build
Expand Down Expand Up @@ -151,4 +204,8 @@ The project was initialized using [Golang Spell](https://github.com/golangspell/

## Architectural Model
The Architectural Model adopted to structure the application is based on The Clean Architecture.
Further details can be found here: [The Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) and in the Clean Architecture Book.
Further details can be found here: [The Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) and in the Clean Architecture Book.


[1]: https://asciiflow.com/#/
[2]: https://petstore.swagger.io/?url=https://raw.githubusercontent.com/prometheus/alertmanager/master/api/v2/openapi.yaml#/
10 changes: 6 additions & 4 deletions appcontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import "sync"

//List of consts containing the names of the available components in the Application Context - appcontext.Current (Add your component names here as constants)
const (
Logger = "Logger"
SensuRepository = "SensuRepository"
SlackRepository = "SlackRepository"
TelegramRepository = "TelegramRepository"
Logger = "Logger"
SensuRepository = "SensuRepository"
SlackRepository = "SlackRepository"
TelegramRepository = "TelegramRepository"
GoogleChatRepository = "GoogleChatRepository"
AlertManagerRepository = "AlertManagerRepository"
)

//Component is the Base interface for all Components
Expand Down
60 changes: 54 additions & 6 deletions config/environment.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package config

import (
"time"

"github.com/spf13/viper"
)

Expand All @@ -21,6 +19,8 @@ type Config struct {
TestRun bool
//UsePrometheus to enable prometheus metrics endpoint
UsePrometheus bool
// DefaultIntegrationName string
DefaultIntegrationName string
// SlackToken string
SlackToken string
// SlackSigningSecret string
Expand All @@ -35,16 +35,38 @@ type Config struct {
TelegramToken string
// TelegramURL string
TelegramURL string
// TelegramBotName string
TelegramBotName string
// SensuAPIToken string
SensuAPIToken string
// SensuUser string
SensuUser string
// SensuPassword string
SensuPassword string
// SensuAPI string
SensuAPI string
// SensuAPIScheme string
SensuAPIScheme string
// DebugSensuRequests string
DebugSensuRequests string
// BotTimeout time.Duration
BotTimeout time.Duration
// BotTimeout int
BotTimeout int
// GoogleChatProjectID string
GoogleChatProjectID string
// GoogleChatSAPath string
GoogleChatSAPath string
// BlockedVerbs []string splited by comma
BlockedVerbs []string
// BlockedResources []string splited by comma
BlockedResources []string
// SlackAdminIDList []string splited by comma
SlackAdminIDList []string
// TelegramAdminIDList []string splited by comma
TelegramAdminIDList []string
// GoogleChatAdminList []string splited by comma
GoogleChatAdminList []string
// DeleteAllowedResources []string splited by comma
DeleteAllowedResources []string
// AlertManagerEndpoints []string
AlertManagerEndpoints []string
}

func init() {
Expand All @@ -60,6 +82,8 @@ func init() {
viper.SetDefault("LogLevel", "INFO")
_ = viper.BindEnv("BotTimeout", "SENSUBOT_TIMEOUT")
viper.SetDefault("BotTimeout", 15)
_ = viper.BindEnv("DefaultIntegrationName", "SENSUBOT_DEFAULT_INTEGRATION_NAME")
viper.SetDefault("DefaultIntegrationName", "sensu")
_ = viper.BindEnv("SlackSlashCommand", "SENSUBOT_SLASH_COMMAND")
viper.SetDefault("SlackSlashCommand", "/sensubot")
_ = viper.BindEnv("SlackToken", "SENSUBOT_SLACK_TOKEN")
Expand All @@ -74,12 +98,36 @@ func init() {
viper.SetDefault("TelegramToken", "disabled")
_ = viper.BindEnv("TelegramURL", "SENSUBOT_TELEGRAM_URL")
viper.SetDefault("TelegramURL", "https://api.telegram.org/bot")
_ = viper.BindEnv("TelegramBotName", "SENSUBOT_TELEGRAM_NAME")
viper.SetDefault("TelegramBotName", "@sensubot")
_ = viper.BindEnv("SensuAPIScheme", "SENSUBOT_API_SCHEME")
viper.SetDefault("SensuAPIScheme", "http")
_ = viper.BindEnv("SensuAPIToken", "SENSUBOT_API_TOKEN")
viper.SetDefault("SensuAPIToken", "Absent")
_ = viper.BindEnv("SensuUser", "SENSUBOT_USER")
viper.SetDefault("SensuUser", "Absent")
_ = viper.BindEnv("SensuPassword", "SENSUBOT_PASSWORD")
viper.SetDefault("SensuPassword", "Absent")
_ = viper.BindEnv("SensuAPI", "SENSUBOT_API_URL")
viper.SetDefault("SensuAPI", "Absent")
_ = viper.BindEnv("GoogleChatProjectID", "SENSUBOT_GCHAT_PROJECTID")
viper.SetDefault("GoogleChatProjectID", "Absent")
_ = viper.BindEnv("GoogleChatSAPath", "SENSUBOT_GCHAT_SA_PATH")
viper.SetDefault("GoogleChatSAPath", "Absent")
_ = viper.BindEnv("BlockedVerbs", "SENSUBOT_BLOCKED_VERBS")
viper.SetDefault("BlockedVerbs", []string{})
_ = viper.BindEnv("BlockedResources", "SENSUBOT_BLOCKED_RESOURCES")
viper.SetDefault("BlockedResources", []string{})
_ = viper.BindEnv("SlackAdminListID", "SENSUBOT_SLACK_ADMIN_ID_LIST")
viper.SetDefault("SlackAdminListID", []string{})
_ = viper.BindEnv("TelegramAdminIDList", "SENSUBOT_TELEGRAM_ADMIN_ID_LIST")
viper.SetDefault("TelegramAdminIDList", []string{})
_ = viper.BindEnv("GoogleChatAdminList", "SENSUBOT_GCHAT_ADMIN_LIST")
viper.SetDefault("GoogleChatAdminList", []string{})
_ = viper.BindEnv("DeleteAllowedResources", "SENSUBOT_DELETE_ALLOWED_RESOURCES")
viper.SetDefault("DeleteAllowedResources", []string{"events", "checks"})
_ = viper.BindEnv("AlertManagerEndpoints", "SENSUBOT_ALERTMANAGER_ENDPOINTS")
viper.SetDefault("AlertManagerEndpoints", []string{})

_ = viper.Unmarshal(&Values)
}
77 changes: 77 additions & 0 deletions controller/googlechat_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package controller

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/betorvs/sensubot/config"
"github.com/betorvs/sensubot/usecase"
oidc "github.com/coreos/go-oidc"
"github.com/labstack/echo/v4"
"google.golang.org/api/chat/v1"
)

const (
jwtURL string = "https://www.googleapis.com/service_accounts/v1/jwk/"
chatIssuer string = "chat@system.gserviceaccount.com"
)

// GChatEvents func receive an post from GChat slash integration
func GChatEvents(c echo.Context) error {
// "Authorization"
bearerToken := c.Request().Header.Get("Authorization")
// fmt.Println(bearerToken)
token := strings.Split(bearerToken, " ")
if len(token) != 2 || !verifyJWT(config.Values.GoogleChatProjectID, token[1]) {
// fmt.Println(len(token), verifyJWT(config.Values.GoogleChatProjectID, token[1]))
return c.JSON(http.StatusForbidden, nil)
}
event := new(chat.DeprecatedEvent)
if err := c.Bind(event); err != nil {
return c.JSON(http.StatusBadRequest, err)
}
switch event.Type {
case "ADDED_TO_SPACE":
if event.Space.Type != "ROOM" {
break
}
text := new(chat.Message)
text.Text = "thanks for adding me."
return c.JSON(http.StatusAccepted, text)
case "MESSAGE":
text := new(chat.Message)

res, err := usecase.ParseGoogleChatCommand(event)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
text.Text = fmt.Sprintf("you requested %s %s", event.Message.ArgumentText, res)
return c.JSON(http.StatusAccepted, text)
}
return nil
}

func verifyJWT(audience, token string) bool {
logLocal := config.GetLogger()
logLocal.Debug("starting verifying")
ctx := context.Background()
keySet := oidc.NewRemoteKeySet(ctx, jwtURL+chatIssuer)

configLocal := &oidc.Config{
SkipClientIDCheck: false,
ClientID: audience,
}
newVerifier := oidc.NewVerifier(chatIssuer, keySet, configLocal)
test, err := newVerifier.Verify(ctx, token)
if err != nil {
logLocal.Debug("Audience dont match")
return false
}
if len(test.Audience) == 1 {
logLocal.Debug("Audience matches")
}

return true
}
24 changes: 24 additions & 0 deletions controller/googlechat_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package controller

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

func TestPostGChatEvents(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/sensubot/v1/gchat")

// Assertions
if assert.NoError(t, GChatEvents(c)) {
assert.Equal(t, http.StatusForbidden, rec.Code)
}
}
1 change: 1 addition & 0 deletions controller/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ func MapRoutes(e *echo.Echo) {
g.GET("/info", GetInfo)
g.POST("/slack", SlackEvents)
g.POST("/telegram", TelegramEvents)
g.POST("/gchat", GChatEvents)
}
2 changes: 1 addition & 1 deletion controller/telegram_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TelegramEvents(c echo.Context) error {
return c.JSON(http.StatusBadRequest, err)
}
if strings.Contains(event.Message.Text, "/start") || strings.Contains(event.Message.Text, "/help") {
err := usecase.SendHelpMessage(event.Message.Chat.ID)
err := usecase.SendHelpMessage(int64(event.Message.Chat.ID))
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
Expand Down
15 changes: 15 additions & 0 deletions domain/alertmanager_domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package domain

import "github.com/betorvs/sensubot/appcontext"

// AlertManagerRepository interface
type AlertManagerRepository interface {
appcontext.Component
// GetAlertManager receives an string URL and return []byte and error
GetAlertManager(amURL string) (result []byte, err error)
}

// GetAlertManagerRepository func return AlertManagerRepository interface
func GetAlertManagerRepository() AlertManagerRepository {
return appcontext.Current.Get(appcontext.AlertManagerRepository).(AlertManagerRepository)
}
Loading

0 comments on commit 8d5ff02

Please sign in to comment.