diff --git a/charts/policy-reporter/config.yaml b/charts/policy-reporter/config.yaml index 0094b7dd..14c176d8 100644 --- a/charts/policy-reporter/config.yaml +++ b/charts/policy-reporter/config.yaml @@ -177,6 +177,31 @@ telegram: {{- toYaml . | nindent 4 }} {{- end }} +googleChat: + webhook: {{ .Values.target.googleChat.webhook | quote }} + certificate: {{ .Values.target.googleChat.certificate | quote }} + skipTLS: {{ .Values.target.googleChat.skipTLS }} + secretRef: {{ .Values.target.googleChat.secretRef | quote }} + mountedSecret: {{ .Values.target.googleChat.mountedSecret | quote }} + minimumPriority: {{ .Values.target.googleChat.minimumPriority | quote }} + skipExistingOnStartup: {{ .Values.target.googleChat.skipExistingOnStartup }} + {{- with .Values.target.googleChat.sources }} + sources: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.target.googleChat.customFields }} + customFields: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.target.googleChat.filter }} + filter: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.target.googleChat.channels }} + channels: + {{- toYaml . | nindent 4 }} + {{- end }} + ui: host: {{ include "policyreporter.uihost" . }} certificate: {{ .Values.target.ui.certificate | quote }} diff --git a/charts/policy-reporter/values.yaml b/charts/policy-reporter/values.yaml index 3ff0d439..d874023c 100644 --- a/charts/policy-reporter/values.yaml +++ b/charts/policy-reporter/values.yaml @@ -514,6 +514,33 @@ target: # add additional telegram channels with different configurations and filters channels: [] + googleChat: + # GoogleChat webhook + webhook: "" + # 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: "" diff --git a/pkg/config/config.go b/pkg/config/config.go index c58a8fe7..638ac0cc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,5 +1,7 @@ package config +import "github.com/kyverno/policy-reporter/pkg/target" + type ValueFilter struct { Include []string `mapstructure:"include"` Exclude []string `mapstructure:"exclude"` @@ -37,6 +39,54 @@ type TargetBaseOptions struct { SkipExisting bool `mapstructure:"skipExistingOnStartup"` } +func (config *TargetBaseOptions) MapBaseParent(parent TargetBaseOptions) { + if config.MinimumPriority == "" { + config.MinimumPriority = parent.MinimumPriority + } + + if !config.SkipExisting { + config.SkipExisting = parent.SkipExisting + } +} + +func (config *TargetBaseOptions) ClientOptions() target.ClientOptions { + return target.ClientOptions{ + Name: config.Name, + SkipExistingOnStartup: config.SkipExisting, + ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), + ReportFilter: createReportFilter(config.Filter), + } +} + +type AWSConfig struct { + AccessKeyID string `mapstructure:"accessKeyID"` + SecretAccessKey string `mapstructure:"secretAccessKey"` + Region string `mapstructure:"region"` + Endpoint string `mapstructure:"endpoint"` +} + +func (config *AWSConfig) MapAWSParent(parent AWSConfig) { + if config.Endpoint == "" { + config.Endpoint = parent.Endpoint + } + + if config.AccessKeyID == "" { + config.AccessKeyID = parent.AccessKeyID + } + + if config.SecretAccessKey == "" { + config.SecretAccessKey = parent.SecretAccessKey + } + + if config.Region == "" { + config.Region = parent.Region + } +} + +type TargetOption interface { + BaseOptions() *TargetBaseOptions +} + // Loki configuration type Loki struct { TargetBaseOptions `mapstructure:",squash"` @@ -45,44 +95,44 @@ type Loki struct { SkipTLS bool `mapstructure:"skipTLS"` Certificate string `mapstructure:"certificate"` Path string `mapstructure:"path"` - Channels []Loki `mapstructure:"channels"` + Channels []*Loki `mapstructure:"channels"` } // Elasticsearch configuration type Elasticsearch struct { TargetBaseOptions `mapstructure:",squash"` - Host string `mapstructure:"host"` - SkipTLS bool `mapstructure:"skipTLS"` - Certificate string `mapstructure:"certificate"` - Index string `mapstructure:"index"` - Rotation string `mapstructure:"rotation"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - Channels []Elasticsearch `mapstructure:"channels"` + Host string `mapstructure:"host"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + Index string `mapstructure:"index"` + Rotation string `mapstructure:"rotation"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Channels []*Elasticsearch `mapstructure:"channels"` } // Slack configuration type Slack struct { TargetBaseOptions `mapstructure:",squash"` - Webhook string `mapstructure:"webhook"` - Channel string `mapstructure:"channel"` - Channels []Slack `mapstructure:"channels"` + Webhook string `mapstructure:"webhook"` + Channel string `mapstructure:"channel"` + Channels []*Slack `mapstructure:"channels"` } // Discord configuration type Discord struct { TargetBaseOptions `mapstructure:",squash"` - Webhook string `mapstructure:"webhook"` - Channels []Discord `mapstructure:"channels"` + Webhook string `mapstructure:"webhook"` + Channels []*Discord `mapstructure:"channels"` } // Teams configuration type Teams struct { TargetBaseOptions `mapstructure:",squash"` - Webhook string `mapstructure:"webhook"` - SkipTLS bool `mapstructure:"skipTLS"` - Certificate string `mapstructure:"certificate"` - Channels []Teams `mapstructure:"channels"` + Webhook string `mapstructure:"webhook"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + Channels []*Teams `mapstructure:"channels"` } // UI configuration @@ -100,7 +150,7 @@ type Webhook struct { SkipTLS bool `mapstructure:"skipTLS"` Certificate string `mapstructure:"certificate"` Headers map[string]string `mapstructure:"headers"` - Channels []Webhook `mapstructure:"channels"` + Channels []*Webhook `mapstructure:"channels"` } // Telegram configuration @@ -112,14 +162,17 @@ type Telegram struct { SkipTLS bool `mapstructure:"skipTLS"` Certificate string `mapstructure:"certificate"` Headers map[string]string `mapstructure:"headers"` - Channels []Telegram `mapstructure:"channels"` + Channels []*Telegram `mapstructure:"channels"` } -type AWSConfig struct { - AccessKeyID string `mapstructure:"accessKeyID"` - SecretAccessKey string `mapstructure:"secretAccessKey"` - Region string `mapstructure:"region"` - Endpoint string `mapstructure:"endpoint"` +// GoogleChat configuration +type GoogleChat struct { + TargetBaseOptions `mapstructure:",squash"` + Webhook string `mapstructure:"webhook"` + SkipTLS bool `mapstructure:"skipTLS"` + Certificate string `mapstructure:"certificate"` + Headers map[string]string `mapstructure:"headers"` + Channels []*GoogleChat `mapstructure:"channels"` } // S3 configuration @@ -132,23 +185,23 @@ type S3 struct { KmsKeyID string `mapstructure:"kmsKeyId"` ServerSideEncryption string `mapstructure:"serverSideEncryption"` PathStyle bool `mapstructure:"pathStyle"` - Channels []S3 `mapstructure:"channels"` + Channels []*S3 `mapstructure:"channels"` } // Kinesis configuration type Kinesis struct { TargetBaseOptions `mapstructure:",squash"` AWSConfig `mapstructure:",squash"` - StreamName string `mapstructure:"streamName"` - Channels []Kinesis `mapstructure:"channels"` + StreamName string `mapstructure:"streamName"` + Channels []*Kinesis `mapstructure:"channels"` } // SecurityHub configuration type SecurityHub struct { TargetBaseOptions `mapstructure:",squash"` AWSConfig `mapstructure:",squash"` - AccountID string `mapstructure:"accountId"` - Channels []SecurityHub `mapstructure:"channels"` + AccountID string `mapstructure:"accountId"` + Channels []*SecurityHub `mapstructure:"channels"` } // GCS configuration @@ -158,7 +211,7 @@ type GCS struct { Prefix string `mapstructure:"prefix"` Bucket string `mapstructure:"bucket"` Sources []string `mapstructure:"sources"` - Channels []GCS `mapstructure:"channels"` + Channels []*GCS `mapstructure:"channels"` } // SMTP configuration @@ -283,18 +336,19 @@ type Database struct { type Config struct { Version string Namespace string `mapstructure:"namespace"` - Loki Loki `mapstructure:"loki"` - Elasticsearch Elasticsearch `mapstructure:"elasticsearch"` - Slack Slack `mapstructure:"slack"` - Discord Discord `mapstructure:"discord"` - Teams Teams `mapstructure:"teams"` - S3 S3 `mapstructure:"s3"` - Kinesis Kinesis `mapstructure:"kinesis"` - SecurityHub SecurityHub `mapstructure:"securityHub"` - GCS GCS `mapstructure:"gcs"` - UI UI `mapstructure:"ui"` - Webhook Webhook `mapstructure:"webhook"` - Telegram Telegram `mapstructure:"telegram"` + Loki *Loki `mapstructure:"loki"` + Elasticsearch *Elasticsearch `mapstructure:"elasticsearch"` + Slack *Slack `mapstructure:"slack"` + Discord *Discord `mapstructure:"discord"` + Teams *Teams `mapstructure:"teams"` + S3 *S3 `mapstructure:"s3"` + Kinesis *Kinesis `mapstructure:"kinesis"` + SecurityHub *SecurityHub `mapstructure:"securityHub"` + GCS *GCS `mapstructure:"gcs"` + UI *UI `mapstructure:"ui"` + Webhook *Webhook `mapstructure:"webhook"` + Telegram *Telegram `mapstructure:"telegram"` + GoogleChat *GoogleChat `mapstructure:"googleChat"` API API `mapstructure:"api"` WorkerCount int `mapstructure:"worker"` DBFile string `mapstructure:"dbfile"` diff --git a/pkg/config/resolver.go b/pkg/config/resolver.go index 5c2226a7..954a8c86 100644 --- a/pkg/config/resolver.go +++ b/pkg/config/resolver.go @@ -280,6 +280,7 @@ func (r *Resolver) TargetClients() []target.Client { clients = append(clients, factory.WebhookClients(r.config.Webhook)...) clients = append(clients, factory.GCSClients(r.config.GCS)...) clients = append(clients, factory.TelegramClients(r.config.Telegram)...) + clients = append(clients, factory.GoogleChatClients(r.config.GoogleChat)...) if ui := factory.UIClient(r.config.UI); ui != nil { clients = append(clients, ui) diff --git a/pkg/config/resolver_test.go b/pkg/config/resolver_test.go index f37d7e0a..a8ea45dd 100644 --- a/pkg/config/resolver_test.go +++ b/pkg/config/resolver_test.go @@ -12,7 +12,7 @@ import ( ) var testConfig = &config.Config{ - Loki: config.Loki{ + Loki: &config.Loki{ Host: "http://localhost:3100", TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, @@ -20,7 +20,7 @@ var testConfig = &config.Config{ CustomFields: map[string]string{"field": "value"}, }, SkipTLS: true, - Channels: []config.Loki{ + Channels: []*config.Loki{ { TargetBaseOptions: config.TargetBaseOptions{ CustomFields: map[string]string{"label2": "value2"}, @@ -28,7 +28,7 @@ var testConfig = &config.Config{ }, }, }, - Elasticsearch: config.Elasticsearch{ + Elasticsearch: &config.Elasticsearch{ Host: "http://localhost:9200", Index: "policy-reporter", Rotation: "daily", @@ -38,33 +38,33 @@ var testConfig = &config.Config{ CustomFields: map[string]string{"field": "value"}, }, SkipTLS: true, - Channels: []config.Elasticsearch{{}}, + Channels: []*config.Elasticsearch{{}}, }, - Slack: config.Slack{ + Slack: &config.Slack{ Webhook: "http://hook.slack:80", TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", CustomFields: map[string]string{"field": "value"}, }, - Channels: []config.Slack{{ + Channels: []*config.Slack{{ Webhook: "http://localhost:9200", }, { Channel: "general", }}, }, - Discord: config.Discord{ + Discord: &config.Discord{ Webhook: "http://hook.discord:80", TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", CustomFields: map[string]string{"field": "value"}, }, - Channels: []config.Discord{{ + Channels: []*config.Discord{{ Webhook: "http://localhost:9200", }}, }, - Teams: config.Teams{ + Teams: &config.Teams{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", @@ -72,18 +72,18 @@ var testConfig = &config.Config{ }, Webhook: "http://hook.teams:80", SkipTLS: true, - Channels: []config.Teams{{ + Channels: []*config.Teams{{ Webhook: "http://localhost:9200", }}, }, - UI: config.UI{ + UI: &config.UI{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", }, Host: "http://localhost:8080", }, - Webhook: config.Webhook{ + Webhook: &config.Webhook{ Host: "http://localhost:8080", Headers: map[string]string{ "X-Custom": "Header", @@ -94,14 +94,14 @@ var testConfig = &config.Config{ CustomFields: map[string]string{"field": "value"}, }, SkipTLS: true, - Channels: []config.Webhook{{ + Channels: []*config.Webhook{{ Host: "http://localhost:8081", Headers: map[string]string{ "X-Custom-2": "Header", }, }}, }, - S3: config.S3{ + S3: &config.S3{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", @@ -119,9 +119,9 @@ var testConfig = &config.Config{ ServerSideEncryption: "", PathStyle: true, Prefix: "prefix", - Channels: []config.S3{{}}, + Channels: []*config.S3{{}}, }, - Kinesis: config.Kinesis{ + Kinesis: &config.Kinesis{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", @@ -134,9 +134,9 @@ var testConfig = &config.Config{ Region: "ru-central1", }, StreamName: "policy-reporter", - Channels: []config.Kinesis{{}}, + Channels: []*config.Kinesis{{}}, }, - SecurityHub: config.SecurityHub{ + SecurityHub: &config.SecurityHub{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", @@ -149,9 +149,9 @@ var testConfig = &config.Config{ Region: "ru-central1", }, AccountID: "AccountID", - Channels: []config.SecurityHub{{}}, + Channels: []*config.SecurityHub{{}}, }, - GCS: config.GCS{ + GCS: &config.GCS{ TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", @@ -160,7 +160,7 @@ var testConfig = &config.Config{ Credentials: `{"token": "token", "type": "authorized_user"}`, Bucket: "test", Prefix: "prefix", - Channels: []config.GCS{{}}, + Channels: []*config.GCS{{}}, }, EmailReports: config.EmailReports{ Templates: config.EmailTemplates{ @@ -175,22 +175,26 @@ var testConfig = &config.Config{ Encryption: "ssl/tls", }, }, - Telegram: config.Telegram{ + Telegram: &config.Telegram{ Token: "XXX", ChatID: "123456", - Channels: []config.Telegram{ + Channels: []*config.Telegram{ { ChatID: "1234567", }, }, }, + GoogleChat: &config.GoogleChat{ + Webhook: "http://localhost:900/webhook", + Channels: []*config.GoogleChat{{}}, + }, } func Test_ResolveTargets(t *testing.T) { resolver := config.NewResolver(testConfig, &rest.Config{}) - if count := len(resolver.TargetClients()); count != 24 { - t.Errorf("Expected 24 Clients, got %d", count) + if count := len(resolver.TargetClients()); count != 26 { + t.Errorf("Expected 26 Clients, got %d", count) } } @@ -204,14 +208,14 @@ func Test_ResolveHasTargets(t *testing.T) { func Test_ResolveSkipExistingOnStartup(t *testing.T) { testConfig := &config.Config{ - Loki: config.Loki{ + Loki: &config.Loki{ Host: "http://localhost:3100", TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, MinimumPriority: "debug", }, }, - Elasticsearch: config.Elasticsearch{ + Elasticsearch: &config.Elasticsearch{ Host: "http://localhost:9200", TargetBaseOptions: config.TargetBaseOptions{ SkipExisting: true, @@ -554,7 +558,7 @@ func Test_ResolveEnableLeaderElection(t *testing.T) { t.Run("general disabled", func(t *testing.T) { resolver := config.NewResolver(&config.Config{ LeaderElection: config.LeaderElection{Enabled: false}, - Loki: config.Loki{Host: "localhost:3100"}, + Loki: &config.Loki{Host: "localhost:3100"}, Database: config.Database{Type: database.MySQL}, }, &rest.Config{}) @@ -579,7 +583,7 @@ func Test_ResolveEnableLeaderElection(t *testing.T) { resolver := config.NewResolver(&config.Config{ LeaderElection: config.LeaderElection{Enabled: true}, Database: config.Database{Type: database.SQLite}, - Loki: config.Loki{Host: "localhost:3100"}, + Loki: &config.Loki{Host: "localhost:3100"}, DBFile: "test.db", }, &rest.Config{}) diff --git a/pkg/config/target_factory.go b/pkg/config/target_factory.go index 852c040f..00235358 100644 --- a/pkg/config/target_factory.go +++ b/pkg/config/target_factory.go @@ -17,6 +17,7 @@ import ( "github.com/kyverno/policy-reporter/pkg/target/discord" "github.com/kyverno/policy-reporter/pkg/target/elasticsearch" "github.com/kyverno/policy-reporter/pkg/target/gcs" + "github.com/kyverno/policy-reporter/pkg/target/googlechat" "github.com/kyverno/policy-reporter/pkg/target/http" "github.com/kyverno/policy-reporter/pkg/target/kinesis" "github.com/kyverno/policy-reporter/pkg/target/loki" @@ -35,22 +36,20 @@ type TargetFactory struct { } // LokiClients resolver method -func (f *TargetFactory) LokiClients(config Loki) []target.Client { +func (f *TargetFactory) LokiClients(config *Loki) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Loki" - } - if config.Path == "" { - config.Path = "/api/prom/push" + if config == nil { + return clients } - if loki := f.createLokiClient(config, Loki{}); loki != nil { + setFallback(&config.Name, "Loki") + setFallback(&config.Path, "/api/prom/push") + + if loki := f.createLokiClient(config, &Loki{}); loki != nil { clients = append(clients, loki) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Loki Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Loki Channel %d", i+1)) if loki := f.createLokiClient(channel, config); loki != nil { clients = append(clients, loki) @@ -61,19 +60,20 @@ func (f *TargetFactory) LokiClients(config Loki) []target.Client { } // ElasticsearchClients resolver method -func (f *TargetFactory) ElasticsearchClients(config Elasticsearch) []target.Client { +func (f *TargetFactory) ElasticsearchClients(config *Elasticsearch) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Elasticsearch" + if config == nil { + return clients } - if es := f.createElasticsearchClient(config, Elasticsearch{}); es != nil { + setFallback(&config.Name, "Elasticsearch") + + if es := f.createElasticsearchClient(config, &Elasticsearch{}); es != nil { clients = append(clients, es) } + for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Elasticsearch Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Elasticsearch Channel %d", i+1)) if es := f.createElasticsearchClient(channel, config); es != nil { clients = append(clients, es) @@ -84,19 +84,19 @@ func (f *TargetFactory) ElasticsearchClients(config Elasticsearch) []target.Clie } // SlackClients resolver method -func (f *TargetFactory) SlackClients(config Slack) []target.Client { +func (f *TargetFactory) SlackClients(config *Slack) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Slack" + if config == nil { + return clients } - if es := f.createSlackClient(config, Slack{}); es != nil { + setFallback(&config.Name, "Slack") + + if es := f.createSlackClient(config, &Slack{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Slack Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Slack Channel %d", i+1)) if es := f.createSlackClient(channel, config); es != nil { clients = append(clients, es) @@ -107,19 +107,19 @@ func (f *TargetFactory) SlackClients(config Slack) []target.Client { } // DiscordClients resolver method -func (f *TargetFactory) DiscordClients(config Discord) []target.Client { +func (f *TargetFactory) DiscordClients(config *Discord) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Discord" + if config == nil { + return clients } - if es := f.createDiscordClient(config, Discord{}); es != nil { + setFallback(&config.Name, "Discord") + + if es := f.createDiscordClient(config, &Discord{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Discord Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Discord Channel %d", i+1)) if es := f.createDiscordClient(channel, config); es != nil { clients = append(clients, es) @@ -130,19 +130,19 @@ func (f *TargetFactory) DiscordClients(config Discord) []target.Client { } // TeamsClients resolver method -func (f *TargetFactory) TeamsClients(config Teams) []target.Client { +func (f *TargetFactory) TeamsClients(config *Teams) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Teams" + if config == nil { + return clients } - if es := f.createTeamsClient(config, Teams{}); es != nil { + setFallback(&config.Name, "Teams") + + if es := f.createTeamsClient(config, &Teams{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Teams Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Teams Channel %d", i+1)) if es := f.createTeamsClient(channel, config); es != nil { clients = append(clients, es) @@ -153,19 +153,19 @@ func (f *TargetFactory) TeamsClients(config Teams) []target.Client { } // WebhookClients resolver method -func (f *TargetFactory) WebhookClients(config Webhook) []target.Client { +func (f *TargetFactory) WebhookClients(config *Webhook) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Webhook" + if config == nil { + return clients } - if es := f.createWebhookClient(config, Webhook{}); es != nil { + setFallback(&config.Name, "Webhook") + + if es := f.createWebhookClient(config, &Webhook{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Webhook Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Webhook Channel %d", i+1)) if es := f.createWebhookClient(channel, config); es != nil { clients = append(clients, es) @@ -176,38 +176,34 @@ func (f *TargetFactory) WebhookClients(config Webhook) []target.Client { } // UIClient resolver method -func (f *TargetFactory) UIClient(config UI) target.Client { - if config.Host == "" { +func (f *TargetFactory) UIClient(config *UI) target.Client { + if config == nil || config.Host == "" { return nil } zap.L().Info("UI configured") return ui.NewClient(ui.Options{ - ClientOptions: target.ClientOptions{ - Name: "UI", - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(TargetFilter{}, config.MinimumPriority, config.Sources), - }, - Host: config.Host, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + ClientOptions: config.ClientOptions(), + Host: config.Host, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), }) } // S3Clients resolver method -func (f *TargetFactory) S3Clients(config S3) []target.Client { +func (f *TargetFactory) S3Clients(config *S3) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "S3" + if config == nil { + return clients } - if es := f.createS3Client(config, S3{}); es != nil { + setFallback(&config.Name, "S3") + + if es := f.createS3Client(config, &S3{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("S3 Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("S3 Channel %d", i+1)) if es := f.createS3Client(channel, config); es != nil { clients = append(clients, es) @@ -218,19 +214,19 @@ func (f *TargetFactory) S3Clients(config S3) []target.Client { } // KinesisClients resolver method -func (f *TargetFactory) KinesisClients(config Kinesis) []target.Client { +func (f *TargetFactory) KinesisClients(config *Kinesis) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Kinesis" + if config == nil { + return clients } - if es := f.createKinesisClient(config, Kinesis{}); es != nil { + setFallback(&config.Name, "Kinesis") + + if es := f.createKinesisClient(config, &Kinesis{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("Kinesis Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("Kinesis Channel %d", i+1)) if es := f.createKinesisClient(channel, config); es != nil { clients = append(clients, es) @@ -241,19 +237,19 @@ func (f *TargetFactory) KinesisClients(config Kinesis) []target.Client { } // SecurityHub resolver method -func (f *TargetFactory) SecurityHubs(config SecurityHub) []target.Client { +func (f *TargetFactory) SecurityHubs(config *SecurityHub) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "SecurityHub" + if config == nil { + return clients } - if es := f.createSecurityHub(config, SecurityHub{}); es != nil { + setFallback(&config.Name, "SecurityHub") + + if es := f.createSecurityHub(config, &SecurityHub{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("SecurityHub Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("SecurityHub Channel %d", i+1)) if es := f.createSecurityHub(channel, config); es != nil { clients = append(clients, es) @@ -264,19 +260,19 @@ func (f *TargetFactory) SecurityHubs(config SecurityHub) []target.Client { } // GCSClients resolver method -func (f *TargetFactory) GCSClients(config GCS) []target.Client { +func (f *TargetFactory) GCSClients(config *GCS) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Google Cloud Storage" + if config == nil { + return clients } - if es := f.createGCSClient(config, GCS{}); es != nil { + setFallback(&config.Name, "GoogleCloudStorage") + + if es := f.createGCSClient(config, &GCS{}); es != nil { clients = append(clients, es) } for i, channel := range config.Channels { - if channel.Name == "" { - channel.Name = fmt.Sprintf("GCS Channel %d", i+1) - } + setFallback(&config.Name, fmt.Sprintf("GCS Channel %d", i+1)) if es := f.createGCSClient(channel, config); es != nil { clients = append(clients, es) @@ -287,19 +283,19 @@ func (f *TargetFactory) GCSClients(config GCS) []target.Client { } // TelegramClients resolver method -func (f *TargetFactory) TelegramClients(config Telegram) []target.Client { +func (f *TargetFactory) TelegramClients(config *Telegram) []target.Client { clients := make([]target.Client, 0) - if config.Name == "" { - config.Name = "Telegram" + if config == nil { + return clients } - if es := f.createTelegramClient(config, Telegram{}); es != nil { + setFallback(&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) - } + setFallback(&config.Name, fmt.Sprintf("Telegram Channel %d", i+1)) if es := f.createTelegramClient(channel, config); es != nil { clients = append(clients, es) @@ -309,77 +305,72 @@ func (f *TargetFactory) TelegramClients(config Telegram) []target.Client { return clients } -func (f *TargetFactory) createSlackClient(config Slack, parent Slack) target.Client { +// GoogleChatClients resolver method +func (f *TargetFactory) GoogleChatClients(config *GoogleChat) []target.Client { + clients := make([]target.Client, 0) + if config == nil { + return clients + } + + setFallback(&config.Name, "GoogleChat") + + if es := f.createGoogleChatClient(config, &GoogleChat{}); es != nil { + clients = append(clients, es) + } + for i, channel := range config.Channels { + setFallback(&config.Name, fmt.Sprintf("GoogleChat Channel %d", i+1)) + + if es := f.createGoogleChatClient(channel, config); es != nil { + clients = append(clients, es) + } + } + + return clients +} + +func (f *TargetFactory) createSlackClient(config, parent *Slack) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Webhook == "" && config.Channel == "" { return nil } - if config.Webhook == "" { - config.Webhook = parent.Webhook - } + setFallback(&config.Webhook, parent.Webhook) if config.Webhook == "" { return nil } - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapBaseParent(parent.TargetBaseOptions) zap.S().Infof("%s configured", config.Name) return slack.NewClient(slack.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Webhook: config.Webhook, - Channel: config.Channel, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient("", false), + ClientOptions: config.ClientOptions(), + Webhook: config.Webhook, + Channel: config.Channel, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient("", false), }) } -func (f *TargetFactory) createLokiClient(config Loki, parent Loki) target.Client { +func (f *TargetFactory) createLokiClient(config, parent *Loki) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Host == "" && parent.Host == "" { return nil - } else if config.Host == "" { - config.Host = parent.Host } - if config.Certificate == "" { - config.Certificate = parent.Certificate - } - - if !config.SkipTLS { - config.SkipTLS = parent.SkipTLS - } + setFallback(&config.Host, parent.Host) + setFallback(&config.Certificate, parent.Certificate) + setFallback(&config.Path, parent.Path) + setBool(&config.SkipTLS, parent.SkipTLS) - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } - - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if config.Path == "" { - config.Path = parent.Path - } + config.MapBaseParent(parent.TargetBaseOptions) zap.S().Infof("%s configured", config.Name) @@ -394,184 +385,103 @@ func (f *TargetFactory) createLokiClient(config Loki, parent Loki) target.Client } return loki.NewClient(loki.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Host: config.Host + config.Path, - CustomLabels: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + ClientOptions: config.ClientOptions(), + Host: config.Host + config.Path, + CustomLabels: config.CustomFields, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), }) } -func (f *TargetFactory) createElasticsearchClient(config Elasticsearch, parent Elasticsearch) target.Client { +func (f *TargetFactory) createElasticsearchClient(config, parent *Elasticsearch) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Host == "" && parent.Host == "" { return nil - } else if config.Host == "" { - config.Host = parent.Host } - if config.Certificate == "" { - config.Certificate = parent.Certificate - } - - if !config.SkipTLS { - config.SkipTLS = parent.SkipTLS - } - - if config.Username == "" { - config.Username = parent.Username - } + setFallback(&config.Host, parent.Host) + setFallback(&config.Certificate, parent.Certificate) + setBool(&config.SkipTLS, parent.SkipTLS) + setFallback(&config.Username, parent.Username) + setFallback(&config.Password, parent.Password) + setFallback(&config.Index, parent.Index, "policy-reporter") + setFallback(&config.Rotation, parent.Rotation, elasticsearch.Daily) - if config.Password == "" { - config.Password = parent.Password - } - - if config.Index == "" && parent.Index == "" { - config.Index = "policy-reporter" - } else if config.Index == "" { - config.Index = parent.Index - } - - if config.Rotation == "" && parent.Rotation == "" { - config.Rotation = elasticsearch.Daily - } else if config.Rotation == "" { - config.Rotation = parent.Rotation - } - - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapBaseParent(parent.TargetBaseOptions) zap.S().Infof("%s configured", config.Name) return elasticsearch.NewClient(elasticsearch.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Host: config.Host, - Username: config.Username, - Password: config.Password, - Rotation: config.Rotation, - Index: config.Index, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + ClientOptions: config.ClientOptions(), + Host: config.Host, + Username: config.Username, + Password: config.Password, + Rotation: config.Rotation, + Index: config.Index, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), }) } -func (f *TargetFactory) createDiscordClient(config Discord, parent Discord) target.Client { +func (f *TargetFactory) createDiscordClient(config, parent *Discord) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Webhook == "" { return nil } - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapBaseParent(parent.TargetBaseOptions) zap.S().Infof("%s configured", config.Name) return discord.NewClient(discord.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Webhook: config.Webhook, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient("", false), + ClientOptions: config.ClientOptions(), + Webhook: config.Webhook, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient("", false), }) } -func (f *TargetFactory) createTeamsClient(config Teams, parent Teams) target.Client { +func (f *TargetFactory) createTeamsClient(config, parent *Teams) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Webhook == "" { return nil } - if config.Certificate == "" { - config.Certificate = parent.Certificate - } - - if !config.SkipTLS { - config.SkipTLS = parent.SkipTLS - } - - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } + setFallback(&config.Certificate, parent.Certificate) + setBool(&config.SkipTLS, parent.SkipTLS) - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } - - if !config.SkipTLS { - config.SkipTLS = parent.SkipTLS - } + config.MapBaseParent(parent.TargetBaseOptions) zap.S().Infof("%s configured", config.Name) return teams.NewClient(teams.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Webhook: config.Webhook, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + ClientOptions: config.ClientOptions(), + Webhook: config.Webhook, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), }) } -func (f *TargetFactory) createWebhookClient(config Webhook, parent Webhook) target.Client { +func (f *TargetFactory) createWebhookClient(config, parent *Webhook) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } if config.Host == "" { return nil } - 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 - } + setFallback(&config.Certificate, parent.Certificate) + setBool(&config.SkipTLS, parent.SkipTLS) + config.MapBaseParent(parent.TargetBaseOptions) if len(parent.Headers) > 0 { headers := map[string]string{} @@ -588,51 +498,30 @@ func (f *TargetFactory) createWebhookClient(config Webhook, parent Webhook) targ zap.S().Infof("%s configured", config.Name) return webhook.NewClient(webhook.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Host: config.Host, - Headers: config.Headers, - CustomFields: config.CustomFields, - HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + ClientOptions: config.ClientOptions(), + Host: config.Host, + Headers: config.Headers, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), }) } -func (f *TargetFactory) createTelegramClient(config Telegram, parent Telegram) target.Client { +func (f *TargetFactory) createTelegramClient(config, parent *Telegram) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.Token == "" { - config.Token = parent.Token - } + setFallback(&config.Token, parent.Token) if config.ChatID == "" || config.Token == "" { return nil } - if config.Host == "" { - config.Host = parent.Host - } + setFallback(&config.Host, parent.Host) + setFallback(&config.Certificate, parent.Certificate) + setBool(&config.SkipTLS, parent.SkipTLS) - 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 - } + config.MapBaseParent(parent.TargetBaseOptions) if len(parent.Headers) > 0 { headers := map[string]string{} @@ -654,83 +543,83 @@ func (f *TargetFactory) createTelegramClient(config Telegram, parent Telegram) t 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), + ClientOptions: config.ClientOptions(), + 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 { +func (f *TargetFactory) createGoogleChatClient(config, parent *GoogleChat) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.Endpoint == "" && parent.Endpoint == "" { + setFallback(&config.Webhook, parent.Webhook) + + if config.Webhook == "" { return nil - } else if config.Endpoint == "" { - config.Endpoint = parent.Endpoint } - sugar := zap.S() + setFallback(&config.Certificate, parent.Certificate) + setBool(&config.SkipTLS, parent.SkipTLS) + config.MapBaseParent(parent.TargetBaseOptions) - if err := checkAWSConfig(config.Name, config.AWSConfig, parent.AWSConfig); err != nil { - sugar.Error(err) + 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 + } - return nil + config.Headers = headers } - if config.AccessKeyID == "" { - config.AccessKeyID = parent.AccessKeyID - } + zap.S().Infof("%s configured", config.Name) - if config.SecretAccessKey == "" { - config.SecretAccessKey = parent.SecretAccessKey - } + return googlechat.NewClient(googlechat.Options{ + ClientOptions: config.ClientOptions(), + Webhook: config.Webhook, + Headers: config.Headers, + CustomFields: config.CustomFields, + HTTPClient: http.NewClient(config.Certificate, config.SkipTLS), + }) +} - if config.Region == "" { - config.Region = parent.Region +func (f *TargetFactory) createS3Client(config, parent *S3) target.Client { + if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.Bucket == "" && parent.Bucket == "" { - sugar.Errorf("%s.Bucket has not been declared", config.Name) + config.MapAWSParent(parent.AWSConfig) + if config.Endpoint == "" { return nil - } else if config.Bucket == "" { - config.Bucket = parent.Bucket } - if config.Prefix == "" && parent.Prefix == "" { - config.Prefix = "policy-reporter" - } else if config.Prefix == "" { - config.Prefix = parent.Prefix - } + sugar := zap.S() - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } + if err := checkAWSConfig(config.Name, config.AWSConfig, parent.AWSConfig); err != nil { + sugar.Error(err) - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting + return nil } - if !config.BucketKeyEnabled { - config.BucketKeyEnabled = parent.BucketKeyEnabled + setFallback(&config.Bucket, parent.Bucket) + if config.Bucket == "" { + sugar.Errorf("%s.Bucket has not been declared", config.Name) + return nil } - if config.KmsKeyID == "" { - config.KmsKeyID = parent.KmsKeyID - } + setFallback(&config.Prefix, parent.Prefix, "policy-reporter") + setFallback(&config.KmsKeyID, parent.KmsKeyID) + setFallback(&config.ServerSideEncryption, parent.ServerSideEncryption) + setBool(&config.BucketKeyEnabled, parent.BucketKeyEnabled) - if config.ServerSideEncryption == "" { - config.ServerSideEncryption = parent.ServerSideEncryption - } + config.MapBaseParent(parent.TargetBaseOptions) s3Client := helper.NewS3Client( config.AccessKeyID, @@ -745,27 +634,21 @@ func (f *TargetFactory) createS3Client(config S3, parent S3) target.Client { sugar.Infof("%s configured", config.Name) return s3.NewClient(s3.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - S3: s3Client, - CustomFields: config.CustomFields, - Prefix: config.Prefix, + ClientOptions: config.ClientOptions(), + S3: s3Client, + CustomFields: config.CustomFields, + Prefix: config.Prefix, }) } -func (f *TargetFactory) createKinesisClient(config Kinesis, parent Kinesis) target.Client { +func (f *TargetFactory) createKinesisClient(config, parent *Kinesis) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.Endpoint == "" && parent.Endpoint == "" { + config.MapAWSParent(parent.AWSConfig) + if config.Endpoint == "" { return nil - } else if config.Endpoint == "" { - config.Endpoint = parent.Endpoint } sugar := zap.S() @@ -775,32 +658,13 @@ func (f *TargetFactory) createKinesisClient(config Kinesis, parent Kinesis) targ return nil } - if config.AccessKeyID == "" { - config.AccessKeyID = parent.AccessKeyID - } - - if config.SecretAccessKey == "" { - config.SecretAccessKey = parent.SecretAccessKey - } - - if config.Region == "" { - config.Region = parent.Region - } - - if config.StreamName == "" && parent.StreamName == "" { + setFallback(&config.StreamName, parent.StreamName) + if config.StreamName == "" { sugar.Errorf("%s.StreamName has not been declared", config.Name) return nil - } else if config.StreamName == "" { - config.StreamName = parent.StreamName } - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapBaseParent(parent.TargetBaseOptions) kinesisClient := helper.NewKinesisClient( config.AccessKeyID, @@ -813,26 +677,20 @@ func (f *TargetFactory) createKinesisClient(config Kinesis, parent Kinesis) targ sugar.Infof("%s configured", config.Name) return kinesis.NewClient(kinesis.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - CustomFields: config.CustomFields, - Kinesis: kinesisClient, + ClientOptions: config.ClientOptions(), + CustomFields: config.CustomFields, + Kinesis: kinesisClient, }) } -func (f *TargetFactory) createSecurityHub(config SecurityHub, parent SecurityHub) target.Client { +func (f *TargetFactory) createSecurityHub(config, parent *SecurityHub) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.AccountID == "" && parent.AccountID == "" { + setFallback(&config.AccountID, parent.AccountID) + if config.AccountID == "" { return nil - } else if config.AccountID == "" { - config.AccountID = parent.AccountID } sugar := zap.S() @@ -842,29 +700,8 @@ func (f *TargetFactory) createSecurityHub(config SecurityHub, parent SecurityHub return nil } - if config.AccessKeyID == "" { - config.AccessKeyID = parent.AccessKeyID - } - - if config.SecretAccessKey == "" { - config.SecretAccessKey = parent.SecretAccessKey - } - - if config.Region == "" { - config.Region = parent.Region - } - - if config.Endpoint == "" { - config.Endpoint = parent.Endpoint - } - - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapAWSParent(parent.AWSConfig) + config.MapBaseParent(parent.TargetBaseOptions) client := helper.NewHubClient( config.AccessKeyID, @@ -876,52 +713,35 @@ func (f *TargetFactory) createSecurityHub(config SecurityHub, parent SecurityHub sugar.Infof("%s configured", config.Name) return securityhub.NewClient(securityhub.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - CustomFields: config.CustomFields, - Client: client, - AccountID: config.AccountID, - Region: config.Region, + ClientOptions: config.ClientOptions(), + CustomFields: config.CustomFields, + Client: client, + AccountID: config.AccountID, + Region: config.Region, }) } -func (f *TargetFactory) createGCSClient(config GCS, parent GCS) target.Client { +func (f *TargetFactory) createGCSClient(config, parent *GCS) target.Client { if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" { - f.mapSecretValues(&config, config.SecretRef, config.MountedSecret) + f.mapSecretValues(config, config.SecretRef, config.MountedSecret) } - if config.Bucket == "" && parent.Bucket == "" { + setFallback(&config.Bucket, parent.Bucket) + if config.Bucket == "" { return nil - } else if config.Bucket == "" { - config.Bucket = parent.Bucket } sugar := zap.S() - if config.Credentials == "" && parent.Credentials == "" { + setFallback(&config.Credentials, parent.Credentials) + if config.Credentials == "" { sugar.Errorf("%s.Credentials has not been declared", config.Name) return nil - } else if config.Credentials == "" { - config.Credentials = parent.Credentials } - if config.Prefix == "" && parent.Prefix == "" { - config.Prefix = "policy-reporter" - } else if config.Prefix == "" { - config.Prefix = parent.Prefix - } + setFallback(&config.Prefix, parent.Prefix, "policy-reporter") - if config.MinimumPriority == "" { - config.MinimumPriority = parent.MinimumPriority - } - - if !config.SkipExisting { - config.SkipExisting = parent.SkipExisting - } + config.MapBaseParent(parent.TargetBaseOptions) gcsClient := helper.NewGCSClient( context.Background(), @@ -935,15 +755,10 @@ func (f *TargetFactory) createGCSClient(config GCS, parent GCS) target.Client { sugar.Infof("%s configured", config.Name) return gcs.NewClient(gcs.Options{ - ClientOptions: target.ClientOptions{ - Name: config.Name, - SkipExistingOnStartup: config.SkipExisting, - ResultFilter: createResultFilter(config.Filter, config.MinimumPriority, config.Sources), - ReportFilter: createReportFilter(config.Filter), - }, - Client: gcsClient, - CustomFields: config.CustomFields, - Prefix: config.Prefix, + ClientOptions: config.ClientOptions(), + Client: gcsClient, + CustomFields: config.CustomFields, + Prefix: config.Prefix, }) } @@ -1058,6 +873,10 @@ func (f *TargetFactory) mapSecretValues(config any, ref, mountedSecret string) { if values.Host != "" { c.Host = values.Host } + case *GoogleChat: + if values.Webhook != "" { + c.Webhook = values.Webhook + } } } @@ -1101,3 +920,20 @@ func checkAWSConfig(name string, config AWSConfig, parent AWSConfig) error { return nil } + +func setFallback(config *string, parents ...string) { + if *config == "" { + for _, p := range parents { + if p != "" { + *config = p + return + } + } + } +} + +func setBool(config *bool, parent bool) { + if *config == false { + *config = parent + } +} diff --git a/pkg/config/target_factory_test.go b/pkg/config/target_factory_test.go index a1efb476..2d8287c0 100644 --- a/pkg/config/target_factory_test.go +++ b/pkg/config/target_factory_test.go @@ -108,6 +108,12 @@ func Test_ResolveTarget(t *testing.T) { t.Errorf("Expected 2 Client, got %d clients", len(clients)) } }) + t.Run("GoogleChat", func(t *testing.T) { + clients := factory.GoogleChatClients(testConfig.GoogleChat) + 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 { @@ -138,137 +144,142 @@ func Test_ResolveTargetWithoutHost(t *testing.T) { factory := config.NewTargetFactory(nil) t.Run("Loki", func(t *testing.T) { - if len(factory.LokiClients(config.Loki{})) != 0 { + if len(factory.LokiClients(&config.Loki{})) != 0 { t.Error("Expected Client to be nil if no host is configured") } }) t.Run("Elasticsearch", func(t *testing.T) { - if len(factory.ElasticsearchClients(config.Elasticsearch{})) != 0 { + if len(factory.ElasticsearchClients(&config.Elasticsearch{})) != 0 { t.Error("Expected Client to be nil if no host is configured") } }) t.Run("Slack", func(t *testing.T) { - if len(factory.SlackClients(config.Slack{})) != 0 { + if len(factory.SlackClients(&config.Slack{})) != 0 { t.Error("Expected Client to be nil if no host is configured") } }) t.Run("Discord", func(t *testing.T) { - if len(factory.DiscordClients(config.Discord{})) != 0 { + if len(factory.DiscordClients(&config.Discord{})) != 0 { t.Error("Expected Client to be nil if no host is configured") } }) t.Run("Teams", func(t *testing.T) { - if len(factory.TeamsClients(config.Teams{})) != 0 { + if len(factory.TeamsClients(&config.Teams{})) != 0 { t.Error("Expected Client to be nil if no host is configured") } }) t.Run("Webhook", func(t *testing.T) { - if len(factory.WebhookClients(config.Webhook{})) != 0 { + if len(factory.WebhookClients(&config.Webhook{})) != 0 { 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 { + if len(factory.TelegramClients(&config.Telegram{})) != 0 { t.Error("Expected Client to be nil if no chatID is configured") } }) + t.Run("GoogleChat", func(t *testing.T) { + if len(factory.GoogleChatClients(&config.GoogleChat{})) != 0 { + t.Error("Expected Client to be nil if no webhook is configured") + } + }) t.Run("S3.Endoint", func(t *testing.T) { - if len(factory.S3Clients(config.S3{})) != 0 { + if len(factory.S3Clients(&config.S3{})) != 0 { t.Error("Expected Client to be nil if no endpoint is configured") } }) t.Run("S3.AccessKey", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net"}})) != 0 { t.Error("Expected Client to be nil if no accessKey is configured") } }) t.Run("S3.SecretAccessKey", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access"}})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access"}})) != 0 { t.Error("Expected Client to be nil if no secretAccessKey is configured") } }) t.Run("S3.Region", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { t.Error("Expected Client to be nil if no region is configured") } }) t.Run("S3.Bucket", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { t.Error("Expected Client to be nil if no bucket is configured") } }) t.Run("S3.SSE-S3", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "AES256"})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "AES256"})) != 0 { t.Error("Expected Client to be nil if server side encryption is not configured") } }) t.Run("S3.SSE-KMS", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms"})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms"})) != 0 { t.Error("Expected Client to be nil if server side encryption is not configured") } }) t.Run("S3.SSE-KMS-S3-KEY", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"})) != 0 { t.Error("Expected Client to be nil if server side encryption is not configured") } }) t.Run("S3.SSE-KMS-KEY-ID", func(t *testing.T) { - if len(factory.S3Clients(config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms", KmsKeyID: "kmsKeyId"})) != 0 { + if len(factory.S3Clients(&config.S3{AWSConfig: config.AWSConfig{Endpoint: "https://storage.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}, ServerSideEncryption: "aws:kms", KmsKeyID: "kmsKeyId"})) != 0 { t.Error("Expected Client to be nil if server side encryption is not configured") } }) t.Run("Kinesis.Endpoint", func(t *testing.T) { - if len(factory.KinesisClients(config.Kinesis{})) != 0 { + if len(factory.KinesisClients(&config.Kinesis{})) != 0 { t.Error("Expected Client to be nil if no endpoint is configured") } }) t.Run("Kinesis.AccessKey", func(t *testing.T) { - if len(factory.KinesisClients(config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net"}})) != 0 { + if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net"}})) != 0 { t.Error("Expected Client to be nil if no accessKey is configured") } }) t.Run("Kinesis.SecretAccessKey", func(t *testing.T) { - if len(factory.KinesisClients(config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access"}})) != 0 { + if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access"}})) != 0 { t.Error("Expected Client to be nil if no secretAccessKey is configured") } }) t.Run("Kinesis.Region", func(t *testing.T) { - if len(factory.KinesisClients(config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { + if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { t.Error("Expected Client to be nil if no region is configured") } }) t.Run("Kinesis.StreamName", func(t *testing.T) { - if len(factory.KinesisClients(config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { + if len(factory.KinesisClients(&config.Kinesis{AWSConfig: config.AWSConfig{Endpoint: "https://yds.serverless.yandexcloud.net", AccessKeyID: "access", SecretAccessKey: "secret", Region: "ru-central1"}})) != 0 { t.Error("Expected Client to be nil if no stream name is configured") } }) t.Run("SecurityHub.AccountID", func(t *testing.T) { - if len(factory.SecurityHubs(config.SecurityHub{})) != 0 { + if len(factory.SecurityHubs(&config.SecurityHub{})) != 0 { t.Error("Expected Client to be nil if no accountID is configured") } }) t.Run("SecurityHub.AccessKey", func(t *testing.T) { - if len(factory.SecurityHubs(config.SecurityHub{AccountID: "accountID"})) != 0 { + if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID"})) != 0 { t.Error("Expected Client to be nil if no accessKey is configured") } }) t.Run("SecurityHub.SecretAccessKey", func(t *testing.T) { - if len(factory.SecurityHubs(config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access"}})) != 0 { + if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access"}})) != 0 { t.Error("Expected Client to be nil if no secretAccessKey is configured") } }) t.Run("SecurityHub.Region", func(t *testing.T) { - if len(factory.SecurityHubs(config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { + if len(factory.SecurityHubs(&config.SecurityHub{AccountID: "accountID", AWSConfig: config.AWSConfig{AccessKeyID: "access", SecretAccessKey: "secret"}})) != 0 { t.Error("Expected Client to be nil if no region is configured") } }) t.Run("GCS.Bucket", func(t *testing.T) { - if len(factory.GCSClients(config.GCS{})) != 0 { + if len(factory.GCSClients(&config.GCS{})) != 0 { t.Error("Expected Client to be nil if no bucket is configured") } }) t.Run("GCS.Credentials", func(t *testing.T) { - if len(factory.GCSClients(config.GCS{Bucket: "policy-reporter"})) != 0 { + if len(factory.GCSClients(&config.GCS{Bucket: "policy-reporter"})) != 0 { t.Error("Expected Client to be nil if no accessKey is configured") } }) @@ -278,9 +289,9 @@ func Test_GetValuesFromSecret(t *testing.T) { factory := config.NewTargetFactory(secrets.NewClient(newFakeClient())) t.Run("Get Loki values from Secret", func(t *testing.T) { - clients := factory.LokiClients(config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { - t.Error("Expected one client created") + t.Fatal("Expected one client created") } fv := reflect.ValueOf(clients[0]).Elem().FieldByName("host") @@ -290,9 +301,9 @@ func Test_GetValuesFromSecret(t *testing.T) { }) t.Run("Get Elasticsearch values from Secret", func(t *testing.T) { - clients := factory.ElasticsearchClients(config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { - t.Error("Expected one client created") + t.Fatal("Expected one client created") } client := reflect.ValueOf(clients[0]).Elem() @@ -307,6 +318,16 @@ func Test_GetValuesFromSecret(t *testing.T) { t.Errorf("Expected username from secret, got %s", username) } + rotation := client.FieldByName("rotation").String() + if rotation != "daily" { + t.Errorf("Expected rotation from secret, got %s", rotation) + } + + index := client.FieldByName("index").String() + if index != "policy-reporter" { + t.Errorf("Expected rotation from secret, got %s", index) + } + password := client.FieldByName("password").String() if password != "password" { t.Errorf("Expected password from secret, got %s", password) @@ -314,7 +335,7 @@ func Test_GetValuesFromSecret(t *testing.T) { }) t.Run("Get Discord values from Secret", func(t *testing.T) { - clients := factory.DiscordClients(config.Discord{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -328,7 +349,7 @@ func Test_GetValuesFromSecret(t *testing.T) { }) t.Run("Get MS Teams values from Secret", func(t *testing.T) { - clients := factory.TeamsClients(config.Teams{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -342,7 +363,7 @@ func Test_GetValuesFromSecret(t *testing.T) { }) t.Run("Get Slack values from Secret", func(t *testing.T) { - clients := factory.SlackClients(config.Slack{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -356,7 +377,7 @@ func Test_GetValuesFromSecret(t *testing.T) { }) t.Run("Get Webhook Authentication Token from Secret", func(t *testing.T) { - clients := factory.WebhookClients(config.Webhook{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -370,7 +391,7 @@ 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"}) + clients := factory.TelegramClients(&config.Telegram{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, ChatID: "1234"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -382,44 +403,57 @@ func Test_GetValuesFromSecret(t *testing.T) { t.Errorf("Expected host with token from secret, got %s", host) } }) + t.Run("Get GoogleChat Webhook from Secret", func(t *testing.T) { + clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}}) + if len(clients) != 1 { + t.Error("Expected one client created") + } + + client := reflect.ValueOf(clients[0]).Elem() + + host := client.FieldByName("webhook").String() + if host != "http://localhost:9200/webhook" { + 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"}) + clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get S3 values from Secret with KMS", func(t *testing.T) { - clients := factory.S3Clients(config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) + clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get Kinesis values from Secret", func(t *testing.T) { - clients := factory.KinesisClients(config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) + clients := factory.KinesisClients(&config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get GCS values from Secret", func(t *testing.T) { - clients := factory.GCSClients(config.GCS{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, Bucket: "bucket"}) + clients := factory.GCSClients(&config.GCS{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}, Bucket: "bucket"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get none existing secret skips target", func(t *testing.T) { - clients := factory.LokiClients(config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: "no-exist"}}) + clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: "no-exist"}}) if len(clients) != 0 { t.Error("Expected client are skipped") } }) t.Run("Get CustomFields from Slack", func(t *testing.T) { - clients := factory.SlackClients(config.Slack{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) + clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -432,7 +466,7 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) t.Run("Get CustomFields from Discord", func(t *testing.T) { - clients := factory.DiscordClients(config.Discord{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) + clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -445,7 +479,7 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) t.Run("Get CustomFields from MS Teams", func(t *testing.T) { - clients := factory.TeamsClients(config.Teams{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) + clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http://localhost"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -458,7 +492,7 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) t.Run("Get CustomFields from Elasticsearch", func(t *testing.T) { - clients := factory.ElasticsearchClients(config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) + clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -471,7 +505,7 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) t.Run("Get CustomFields from Webhook", func(t *testing.T) { - clients := factory.WebhookClients(config.Webhook{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) + clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Host: "http://localhost"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -484,7 +518,20 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) 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"}) + 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 GoogleChat", func(t *testing.T) { + clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{CustomFields: map[string]string{"field": "value"}}, Webhook: "http;//googlechat.webhook"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -523,7 +570,7 @@ func Test_GetValuesFromSecret(t *testing.T) { } }) t.Run("Get CustomLabels from Loki", func(t *testing.T) { - clients := factory.LokiClients(config.Loki{ + clients := factory.LokiClients(&config.Loki{ CustomLabels: map[string]string{"label": "value"}, Host: "http://localhost", }) @@ -559,7 +606,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { defer os.Remove(mountedSecret) t.Run("Get Loki values from MountedSecret", func(t *testing.T) { - clients := factory.LokiClients(config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -571,7 +618,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { }) t.Run("Get Elasticsearch values from MountedSecret", func(t *testing.T) { - clients := factory.ElasticsearchClients(config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.ElasticsearchClients(&config.Elasticsearch{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -595,7 +642,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { }) t.Run("Get Discord values from MountedSecret", func(t *testing.T) { - clients := factory.DiscordClients(config.Discord{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.DiscordClients(&config.Discord{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -609,7 +656,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { }) t.Run("Get MS Teams values from MountedSecret", func(t *testing.T) { - clients := factory.TeamsClients(config.Teams{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.TeamsClients(&config.Teams{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -623,7 +670,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { }) t.Run("Get Slack values from MountedSecret", func(t *testing.T) { - clients := factory.SlackClients(config.Slack{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.SlackClients(&config.Slack{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -637,7 +684,7 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { }) t.Run("Get Webhook Authentication Token from MountedSecret", func(t *testing.T) { - clients := factory.WebhookClients(config.Webhook{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + clients := factory.WebhookClients(&config.Webhook{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -651,7 +698,7 @@ 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"}) + clients := factory.TelegramClients(&config.Telegram{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, ChatID: "123"}) if len(clients) != 1 { t.Error("Expected one client created") } @@ -664,36 +711,50 @@ func Test_GetValuesFromMountedSecret(t *testing.T) { } }) + t.Run("Get GoogleChat Webhook from MountedSecret", func(t *testing.T) { + clients := factory.GoogleChatClients(&config.GoogleChat{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}}) + if len(clients) != 1 { + t.Error("Expected one client created") + } + + client := reflect.ValueOf(clients[0]).Elem() + + token := client.FieldByName("webhook").String() + if token != "http://localhost:9200/webhook" { + 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"}) + clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get S3 values from MountedSecret with KMS", func(t *testing.T) { - clients := factory.S3Clients(config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) + clients := factory.S3Clients(&config.S3{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, Bucket: "bucket", BucketKeyEnabled: true, ServerSideEncryption: "aws:kms"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get Kinesis values from MountedSecret", func(t *testing.T) { - clients := factory.KinesisClients(config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) + clients := factory.KinesisClients(&config.Kinesis{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, AWSConfig: config.AWSConfig{Endpoint: "endpoint", Region: "region"}, StreamName: "stream"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get GCS values from MountedSecret", func(t *testing.T) { - clients := factory.GCSClients(config.GCS{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, Bucket: "bucket"}) + clients := factory.GCSClients(&config.GCS{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: mountedSecret}, Bucket: "bucket"}) if len(clients) != 1 { t.Error("Expected one client created") } }) t.Run("Get none existing mounted secret skips target", func(t *testing.T) { - clients := factory.LokiClients(config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: "no-exists"}}) + clients := factory.LokiClients(&config.Loki{TargetBaseOptions: config.TargetBaseOptions{MountedSecret: "no-exists"}}) if len(clients) != 0 { t.Error("Expected client are skipped") } diff --git a/pkg/target/googlechat/googlechat.go b/pkg/target/googlechat/googlechat.go new file mode 100644 index 00000000..683594b1 --- /dev/null +++ b/pkg/target/googlechat/googlechat.go @@ -0,0 +1,228 @@ +package googlechat + +import ( + "bytes" + "text/template" + "time" + + "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/http" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" +) + +const messageTempl string = `[{{ .Priority }}] {{ or .Result.Policy .Result.Rule }}` +const resourceTempl string = `{{ if .Namespace }}[{{ .Namespace }}] {{ end }} {{ .APIVersion }}/{{ .Kind }} {{ .Name }}` + +type values struct { + Result v1alpha2.PolicyReportResult + Priority string + Resource *corev1.ObjectReference +} + +type header struct { + Title string `json:"title"` + SubTitle string `json:"subtitle"` +} + +type decoratedText struct { + TopLabel string `json:"topLabel"` + Text string `json:"text"` +} + +type column struct { + Widgets []widget `json:"widgets"` +} + +type columns struct { + ColumnItems []column `json:"columnItems"` +} + +type textParagraph struct { + Text string `json:"text"` +} + +type widget struct { + DecoratedText *decoratedText `json:"decoratedText,omitempty"` + TextParagraph *textParagraph `json:"textParagraph,omitempty"` + Columns *columns `json:"columns,omitempty"` +} + +type section struct { + Header string `json:"header,omitempty"` + Collapsible bool `json:"collapsible,omitempty"` + Widgets []widget `json:"widgets,omitempty"` +} + +type card struct { + Header *header `json:"header,omitempty"` + Sections []section `json:"sections,omitempty"` +} + +type cardsV2 struct { + CardID string `json:"cardId,omitempty"` + Card card `json:"card,omitempty"` +} + +type Payload struct { + CardsV2 []cardsV2 `json:"cardsV2,omitempty"` +} + +// Options to configure the Discord target +type Options struct { + target.ClientOptions + Webhook string + Headers map[string]string + CustomFields map[string]string + HTTPClient http.Client +} + +type client struct { + target.BaseClient + webhook string + headers map[string]string + customFields map[string]string + client http.Client +} + +func mapPayload(result v1alpha2.PolicyReportResult) (*Payload, error) { + widgets := []widget{{TextParagraph: &textParagraph{Text: result.Message}}} + + ttmpl, err := template.New("googlechat").Parse(messageTempl) + if err != nil { + return nil, err + } + + var prio = result.Priority.String() + if prio == "" { + prio = v1alpha2.DebugPriority.String() + } + + var textBuffer bytes.Buffer + err = ttmpl.Execute(&textBuffer, values{Result: result, Priority: prio, Resource: result.GetResource()}) + if err != nil { + return nil, err + } + + subtitle := "" + + if result.HasResource() { + res := result.GetResource() + + widgets = append(widgets, widget{ + Columns: &columns{ + ColumnItems: []column{ + { + Widgets: []widget{ + {DecoratedText: &decoratedText{TopLabel: "Kind", Text: res.Kind}}, + {DecoratedText: &decoratedText{TopLabel: "Namespace", Text: res.Namespace}}, + {DecoratedText: &decoratedText{"Status", string(result.Result)}}, + }, + }, + { + Widgets: []widget{ + {DecoratedText: &decoratedText{TopLabel: "APIVersion", Text: res.APIVersion}}, + {DecoratedText: &decoratedText{TopLabel: "Name", Text: res.Name}}, + {DecoratedText: &decoratedText{"Source", result.Source}}, + }, + }, + }, + }, + }) + + stmpl, err := template.New("googlechat:resource").Parse(resourceTempl) + if err != nil { + return nil, err + } + + var subTitleBuffer bytes.Buffer + err = stmpl.Execute(&subTitleBuffer, res) + if err != nil { + return nil, err + } + + subtitle = subTitleBuffer.String() + } + + header := header{ + Title: textBuffer.String(), + SubTitle: subtitle, + } + + if result.Policy != "" { + widgets = append(widgets, widget{DecoratedText: &decoratedText{"Rule", result.Rule}}) + } + if result.Category != "" { + widgets = append(widgets, widget{DecoratedText: &decoratedText{"Category", result.Category}}) + } + + for key, value := range result.Properties { + widgets = append(widgets, widget{DecoratedText: &decoratedText{TopLabel: key, Text: value}}) + } + + widgets = append(widgets, widget{DecoratedText: &decoratedText{"time", time.Now().Format("02 Jan 06 15:04 MST")}}) + + return &Payload{ + CardsV2: []cardsV2{ + { + CardID: result.ID, + Card: card{ + Header: &header, + Sections: []section{ + { + Header: "Details", + Collapsible: true, + Widgets: widgets, + }, + }, + }, + }, + }, + }, nil +} + +func (e *client) Send(result v1alpha2.PolicyReportResult) { + if len(e.customFields) > 0 { + props := make(map[string]string, 0) + + for property, value := range e.customFields { + props[property] = value + } + + for property, value := range result.Properties { + props[property] = value + } + + result.Properties = props + } + + payload, err := mapPayload(result) + if err != nil { + zap.L().Error(e.Name()+": PUSH FAILED", zap.Error(err)) + return + } + + req, err := http.CreateJSONRequest(e.Name(), "POST", e.webhook, payload) + if err != nil { + return + } + + for header, value := range e.headers { + req.Header.Set(header, value) + } + + resp, err := e.client.Do(req) + http.ProcessHTTPResponse(e.Name(), resp, err) +} + +// NewClient creates a new loki.client to send Results to Elasticsearch +func NewClient(options Options) target.Client { + return &client{ + target.NewBaseClient(options.ClientOptions), + options.Webhook, + options.Headers, + options.CustomFields, + options.HTTPClient, + } +} diff --git a/pkg/target/googlechat/googlechat_test.go b/pkg/target/googlechat/googlechat_test.go new file mode 100644 index 00000000..25a12119 --- /dev/null +++ b/pkg/target/googlechat/googlechat_test.go @@ -0,0 +1,79 @@ +package googlechat_test + +import ( + "io" + "net/http" + "strings" + "testing" + + "github.com/kyverno/policy-reporter/pkg/fixtures" + "github.com/kyverno/policy-reporter/pkg/target" + "github.com/kyverno/policy-reporter/pkg/target/googlechat" +) + +type testClient struct { + callback func(req *http.Request) error + statusCode int +} + +func (c testClient) Do(req *http.Request) (*http.Response, error) { + err := c.callback(req) + + return &http.Response{ + StatusCode: c.statusCode, + Body: io.NopCloser(strings.NewReader("")), + }, err +} + +func Test_GoogleChatTarget(t *testing.T) { + t.Run("Send", func(t *testing.T) { + callback := func(req *http.Request) error { + if contentType := req.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" { + t.Errorf("Unexpected Content-Type: %s", contentType) + } + + if agend := req.Header.Get("User-Agent"); agend != "Policy-Reporter" { + t.Errorf("Unexpected Host: %s", agend) + } + + if url := req.URL.String(); url != "https://googlechat.webhook" { + t.Errorf("Unexpected Host: %s", url) + } + + if value := req.Header.Get("X-Code"); value != "1234" { + t.Errorf("Unexpected Header X-Code: %s", value) + } + + return nil + } + + client := googlechat.NewClient(googlechat.Options{ + ClientOptions: target.ClientOptions{ + Name: "GoogleChat", + }, + Webhook: "https://googlechat.webhook", + Headers: map[string]string{"X-Code": "1234"}, + CustomFields: map[string]string{"cluster": "name"}, + HTTPClient: testClient{callback, 200}, + }) + client.Send(fixtures.CompleteTargetSendResult) + + if len(fixtures.CompleteTargetSendResult.Properties) > 1 || fixtures.CompleteTargetSendResult.Properties["cluster"] != "" { + t.Error("expected customFields are not added to the actuel result") + } + }) + t.Run("Name", func(t *testing.T) { + client := googlechat.NewClient(googlechat.Options{ + ClientOptions: target.ClientOptions{ + Name: "GoogleChat", + }, + Webhook: "https://googlechat.webhook", + Headers: map[string]string{"X-Code": "1234"}, + HTTPClient: testClient{}, + }) + + if client.Name() != "GoogleChat" { + t.Errorf("Unexpected Name %s", client.Name()) + } + }) +}