Skip to content

Commit

Permalink
Merge pull request #341 from kyverno/telegram-target
Browse files Browse the repository at this point in the history
Telegram push target support
  • Loading branch information
fjogeleit authored Sep 4, 2023
2 parents da46f7d + a6aecd2 commit 83366ac
Show file tree
Hide file tree
Showing 9 changed files with 478 additions and 2 deletions.
27 changes: 27 additions & 0 deletions charts/policy-reporter/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,33 @@ webhook:
{{- toYaml . | nindent 4 }}
{{- end }}

telegram:
token: {{ .Values.target.telegram.token | quote }}
chatID: {{ .Values.target.telegram.chatID | quote }}
host: {{ .Values.target.telegram.host | quote }}
certificate: {{ .Values.target.telegram.certificate | quote }}
skipTLS: {{ .Values.target.telegram.skipTLS }}
secretRef: {{ .Values.target.telegram.secretRef | quote }}
mountedSecret: {{ .Values.target.telegram.mountedSecret | quote }}
minimumPriority: {{ .Values.target.telegram.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.telegram.skipExistingOnStartup }}
{{- with .Values.target.telegram.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.telegram.customFields }}
customFields:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.telegram.filter }}
filter:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.telegram.channels }}
channels:
{{- toYaml . | nindent 4 }}
{{- end }}

ui:
host: {{ include "policyreporter.uihost" . }}
certificate: {{ .Values.target.ui.certificate | quote }}
Expand Down
31 changes: 31 additions & 0 deletions charts/policy-reporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,37 @@ target:
# add additional webhook channels with different configurations and filters
channels: []

telegram:
# telegram bot token
token: ""
# telegram chat id
chatID: ""
# optional telegram proxy host
host: ""
# path to your custom certificate
# can be added under extraVolumes
certificate: ""
# skip TLS verification if necessary
skipTLS: false
# receive the host and/or token from an existing secret, the token is added as Authorization header
secretRef: ""
# Mounted secret path by Secrets Controller, secret should be in json format
mountedSecret: ""
# additional http headers
headers: {}
# minimum priority "" < info < warning < critical < error
minimumPriority: ""
# list of sources which should send to telegram
sources: []
# Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true
# Added as additional properties to each notification
customFields: {}
# filter results send by namespaces, policies and priorities
filter: {}
# add additional telegram channels with different configurations and filters
channels: []

s3:
# S3 access key
accessKeyID: ""
Expand Down
13 changes: 13 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ type Webhook struct {
Channels []Webhook `mapstructure:"channels"`
}

// Telegram configuration
type Telegram struct {
TargetBaseOptions `mapstructure:",squash"`
Host string `mapstructure:"host"`
Token string `mapstructure:"token"`
ChatID string `mapstructure:"chatID"`
SkipTLS bool `mapstructure:"skipTLS"`
Certificate string `mapstructure:"certificate"`
Headers map[string]string `mapstructure:"headers"`
Channels []Telegram `mapstructure:"channels"`
}

type AWSConfig struct {
AccessKeyID string `mapstructure:"accessKeyID"`
SecretAccessKey string `mapstructure:"secretAccessKey"`
Expand Down Expand Up @@ -282,6 +294,7 @@ type Config struct {
GCS GCS `mapstructure:"gcs"`
UI UI `mapstructure:"ui"`
Webhook Webhook `mapstructure:"webhook"`
Telegram Telegram `mapstructure:"telegram"`
API API `mapstructure:"api"`
WorkerCount int `mapstructure:"worker"`
DBFile string `mapstructure:"dbfile"`
Expand Down
1 change: 1 addition & 0 deletions pkg/config/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ func (r *Resolver) TargetClients() []target.Client {
clients = append(clients, factory.SecurityHubs(r.config.SecurityHub)...)
clients = append(clients, factory.WebhookClients(r.config.Webhook)...)
clients = append(clients, factory.GCSClients(r.config.GCS)...)
clients = append(clients, factory.TelegramClients(r.config.Telegram)...)

if ui := factory.UIClient(r.config.UI); ui != nil {
clients = append(clients, ui)
Expand Down
13 changes: 11 additions & 2 deletions pkg/config/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,22 @@ var testConfig = &config.Config{
Encryption: "ssl/tls",
},
},
Telegram: config.Telegram{
Token: "XXX",
ChatID: "123456",
Channels: []config.Telegram{
{
ChatID: "1234567",
},
},
},
}

func Test_ResolveTargets(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})

if count := len(resolver.TargetClients()); count != 22 {
t.Errorf("Expected 22 Clients, got %d", count)
if count := len(resolver.TargetClients()); count != 24 {
t.Errorf("Expected 24 Clients, got %d", count)
}
}

Expand Down
99 changes: 99 additions & 0 deletions pkg/config/target_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"

_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/target/securityhub"
"github.com/kyverno/policy-reporter/pkg/target/slack"
"github.com/kyverno/policy-reporter/pkg/target/teams"
"github.com/kyverno/policy-reporter/pkg/target/telegram"
"github.com/kyverno/policy-reporter/pkg/target/ui"
"github.com/kyverno/policy-reporter/pkg/target/webhook"
)
Expand Down Expand Up @@ -284,6 +286,29 @@ func (f *TargetFactory) GCSClients(config GCS) []target.Client {
return clients
}

// TelegramClients resolver method
func (f *TargetFactory) TelegramClients(config Telegram) []target.Client {
clients := make([]target.Client, 0)
if config.Name == "" {
config.Name = "Telegram"
}

if es := f.createTelegramClient(config, Telegram{}); es != nil {
clients = append(clients, es)
}
for i, channel := range config.Channels {
if channel.Name == "" {
channel.Name = fmt.Sprintf("Webhook Channel %d", i+1)
}

if es := f.createTelegramClient(channel, config); es != nil {
clients = append(clients, es)
}
}

return clients
}

func (f *TargetFactory) createSlackClient(config Slack, parent Slack) target.Client {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
Expand Down Expand Up @@ -576,6 +601,73 @@ func (f *TargetFactory) createWebhookClient(config Webhook, parent Webhook) targ
})
}

func (f *TargetFactory) createTelegramClient(config Telegram, parent Telegram) target.Client {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
}

if config.Token == "" {
config.Token = parent.Token
}

if config.ChatID == "" || config.Token == "" {
return nil
}

if config.Host == "" {
config.Host = parent.Host
}

if config.Certificate == "" {
config.Certificate = parent.Certificate
}

if !config.SkipTLS {
config.SkipTLS = parent.SkipTLS
}

if config.MinimumPriority == "" {
config.MinimumPriority = parent.MinimumPriority
}

if !config.SkipExisting {
config.SkipExisting = parent.SkipExisting
}

if len(parent.Headers) > 0 {
headers := map[string]string{}
for header, value := range parent.Headers {
headers[header] = value
}
for header, value := range config.Headers {
headers[header] = value
}

config.Headers = headers
}

host := "https://api.telegram.org"
if config.Host != "" {
host = strings.TrimSuffix(config.Host, "/")
}

zap.S().Infof("%s configured", config.Name)

return telegram.NewClient(telegram.Options{
ClientOptions: target.ClientOptions{
Name: config.Name,
SkipExistingOnStartup: config.SkipExisting,
ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources),
ReportFilter: createReportFilter(config.Filter),
},
Host: fmt.Sprintf("%s/bot%s/sendMessage", host, config.Token),
ChatID: config.ChatID,
Headers: config.Headers,
CustomFields: config.CustomFields,
HTTPClient: http.NewClient(config.Certificate, config.SkipTLS),
})
}

func (f *TargetFactory) createS3Client(config S3, parent S3) target.Client {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
Expand Down Expand Up @@ -959,6 +1051,13 @@ func (f *TargetFactory) mapSecretValues(config any, ref, mountedSecret string) {

c.Headers["Authorization"] = values.Token
}
case *Telegram:
if values.Token != "" {
c.Token = values.Token
}
if values.Host != "" {
c.Host = values.Host
}
}
}

Expand Down
52 changes: 52 additions & 0 deletions pkg/config/target_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ func Test_ResolveTarget(t *testing.T) {
t.Errorf("Expected 2 Client, got %d clients", len(clients))
}
})
t.Run("Telegram", func(t *testing.T) {
clients := factory.TelegramClients(testConfig.Telegram)
if len(clients) != 2 {
t.Errorf("Expected 2 Client, got %d clients", len(clients))
}
})
t.Run("S3", func(t *testing.T) {
clients := factory.S3Clients(testConfig.S3)
if len(clients) != 2 {
Expand Down Expand Up @@ -161,6 +167,11 @@ func Test_ResolveTargetWithoutHost(t *testing.T) {
t.Error("Expected Client to be nil if no host is configured")
}
})
t.Run("Telegram", func(t *testing.T) {
if len(factory.TelegramClients(config.Telegram{})) != 0 {
t.Error("Expected Client to be nil if no chatID is configured")
}
})
t.Run("S3.Endoint", func(t *testing.T) {
if len(factory.S3Clients(config.S3{})) != 0 {
t.Error("Expected Client to be nil if no endpoint is configured")
Expand Down Expand Up @@ -358,6 +369,20 @@ func Test_GetValuesFromSecret(t *testing.T) {
}
})

t.Run("Get Telegram Token from Secret", func(t *testing.T) {
clients := factory.TelegramClients(config.Telegram{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, ChatID: "1234"})
if len(clients) != 1 {
t.Error("Expected one client created")
}

client := reflect.ValueOf(clients[0]).Elem()

host := client.FieldByName("host").String()
if host != "http://localhost:9200/bottoken/sendMessage" {
t.Errorf("Expected host with token from secret, got %s", host)
}
})

t.Run("Get S3 values from Secret", func(t *testing.T) {
clients := factory.S3Clients(config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket"})
if len(clients) != 1 {
Expand Down Expand Up @@ -458,6 +483,19 @@ func Test_GetValuesFromSecret(t *testing.T) {
t.Errorf("Expected customFields are added")
}
})
t.Run("Get CustomFields from Telegram", func(t *testing.T) {
clients := factory.TelegramClients(config.Telegram{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Token: "XXX", ChatID: "1234"})
if len(clients) != 1 {
t.Error("Expected one client created")
}

client := reflect.ValueOf(clients[0]).Elem()

customFields := client.FieldByName("customFields").MapKeys()
if customFields[0].String() != "field" {
t.Errorf("Expected customFields are added")
}
})
t.Run("Get CustomFields from Kinesis", func(t *testing.T) {
clients := factory.KinesisClients(testConfig.Kinesis)
if len(clients) < 1 {
Expand Down Expand Up @@ -612,6 +650,20 @@ func Test_GetValuesFromMountedSecret(t *testing.T) {
}
})

t.Run("Get Telegram Token from MountedSecret", func(t *testing.T) {
clients := factory.TelegramClients(config.Telegram{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, ChatID: "123"})
if len(clients) != 1 {
t.Error("Expected one client created")
}

client := reflect.ValueOf(clients[0]).Elem()

token := client.FieldByName("host").String()
if token != "http://localhost:9200/bottoken/sendMessage" {
t.Errorf("Expected token from mounted secret, got %s", token)
}
})

t.Run("Get S3 values from MountedSecret", func(t *testing.T) {
clients := factory.S3Clients(config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket"})
if len(clients) != 1 {
Expand Down
Loading

0 comments on commit 83366ac

Please sign in to comment.