Skip to content

Commit

Permalink
init securityhub integration (kyverno#298)
Browse files Browse the repository at this point in the history
Signed-off-by: Frank Jogeleit <[email protected]>
  • Loading branch information
fjogeleit authored Apr 11, 2023
1 parent 8227879 commit cd56188
Show file tree
Hide file tree
Showing 12 changed files with 446 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 @@ -213,6 +213,33 @@ kinesis:
{{- toYaml . | nindent 4 }}
{{- end }}

securityHub:
accountID: {{ .Values.target.securityHub.accountID }}
accessKeyID: {{ .Values.target.securityHub.accessKeyID }}
secretAccessKey: {{ .Values.target.securityHub.secretAccessKey }}
secretRef: {{ .Values.target.securityHub.secretRef | quote }}
region: {{ .Values.target.securityHub.region }}
endpoint: {{ .Values.target.securityHub.endpoint }}
streamName: {{ .Values.target.securityHub.streamName }}
minimumPriority: {{ .Values.target.securityHub.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.securityHub.skipExistingOnStartup }}
{{- with .Values.target.securityHub.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.securityHub.customFields }}
customFields:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.securityHub.filter }}
filter:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.target.securityHub.channels }}
channels:
{{- toYaml . | nindent 4 }}
{{- end }}

gcs:
credentials: {{ .Values.target.gcs.credentials }}
secretRef: {{ .Values.target.gcs.secretRef | quote }}
Expand Down
28 changes: 28 additions & 0 deletions charts/policy-reporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,34 @@ target:
# add additional s3 channels with different configurations and filters
channels: []

securityHub:
# AWS access key
accessKeyID: ""
# AWS secret access key
secretAccessKey: ""
# receive the accessKeyID and/or secretAccessKey from an existing secret instead
secretRef: ""
# Mounted secret path by Secrets Controller, secret should be in json format
mountedSecret: ""
# AWS region
region: ""
# AWS SecurityHub endpoint (optional)
endpoint: ""
# AWS accountID
accountID: ""
# minimum priority "" < info < warning < critical < error
minimumPriority: ""
# list of sources which should send to S3
sources: []
# Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true
# Added as additional properties to each securityHub event
customFields: {}
# filter results send by namespaces, policies and priorities
filter: {}
# add additional s3 channels with different configurations and filters
channels: []

gcs:
# GCS (Google Cloud Storage) Service Accout Credentials
credentials: ""
Expand Down
12 changes: 12 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ type Kinesis struct {
Channels []Kinesis `mapstructure:"channels"`
}

// SecurityHub configuration
type SecurityHub struct {
TargetBaseOptions `mapstructure:",squash"`
AccountID string `mapstructure:"accountId"`
AccessKeyID string `mapstructure:"accessKeyID"`
SecretAccessKey string `mapstructure:"secretAccessKey"`
Region string `mapstructure:"region"`
Endpoint string `mapstructure:"endpoint"`
Channels []SecurityHub `mapstructure:"channels"`
}

// GCS configuration
type GCS struct {
TargetBaseOptions `mapstructure:",squash"`
Expand Down Expand Up @@ -255,6 +266,7 @@ type Config struct {
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"`
Expand Down
1 change: 1 addition & 0 deletions pkg/config/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ func (r *Resolver) TargetClients() []target.Client {
clients = append(clients, factory.TeamsClients(r.config.Teams)...)
clients = append(clients, factory.S3Clients(r.config.S3)...)
clients = append(clients, factory.KinesisClients(r.config.Kinesis)...)
clients = append(clients, factory.SecurityHubs(r.config.SecurityHub)...)
clients = append(clients, factory.WebhookClients(r.config.Webhook)...)
clients = append(clients, factory.GCSClients(r.config.GCS)...)

Expand Down
17 changes: 15 additions & 2 deletions pkg/config/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ var testConfig = &config.Config{
Region: "ru-central1",
Channels: []config.Kinesis{{}},
},
SecurityHub: config.SecurityHub{
TargetBaseOptions: config.TargetBaseOptions{
SkipExisting: true,
MinimumPriority: "debug",
CustomFields: map[string]string{"field": "value"},
},
AccessKeyID: "AccessKey",
SecretAccessKey: "SecretAccessKey",
AccountID: "AccountID",
Endpoint: "https://yds.serverless.yandexcloud.net",
Region: "ru-central1",
Channels: []config.SecurityHub{{}},
},
GCS: config.GCS{
TargetBaseOptions: config.TargetBaseOptions{
SkipExisting: true,
Expand Down Expand Up @@ -159,8 +172,8 @@ var testConfig = &config.Config{
func Test_ResolveTargets(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})

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

Expand Down
104 changes: 104 additions & 0 deletions pkg/config/target_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/target/kinesis"
"github.com/kyverno/policy-reporter/pkg/target/loki"
"github.com/kyverno/policy-reporter/pkg/target/s3"
"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/ui"
Expand Down Expand Up @@ -238,6 +239,29 @@ func (f *TargetFactory) KinesisClients(config Kinesis) []target.Client {
return clients
}

// SecurityHub resolver method
func (f *TargetFactory) SecurityHubs(config SecurityHub) []target.Client {
clients := make([]target.Client, 0)
if config.Name == "" {
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)
}

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

return clients
}

// GCSClients resolver method
func (f *TargetFactory) GCSClients(config GCS) []target.Client {
clients := make([]target.Client, 0)
Expand Down Expand Up @@ -716,6 +740,75 @@ func (f *TargetFactory) createKinesisClient(config Kinesis, parent Kinesis) targ
})
}

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

if config.AccountID == "" && parent.AccountID == "" {
return nil
} else if config.AccountID == "" {
config.AccountID = parent.AccountID
}

sugar := zap.S()

if config.AccessKeyID == "" && parent.AccessKeyID == "" {
sugar.Errorf("%s.AccessKeyID has not been declared", config.Name)
return nil
} else if config.AccessKeyID == "" {
config.AccessKeyID = parent.AccessKeyID
}

if config.SecretAccessKey == "" && parent.SecretAccessKey == "" {
sugar.Errorf("%s.SecretAccessKey has not been declared", config.Name)
return nil
} else if config.SecretAccessKey == "" {
config.SecretAccessKey = parent.SecretAccessKey
}

if config.Region == "" && parent.Region == "" {
sugar.Errorf("%s.Region has not been declared", config.Name)
return nil
} else 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
}

client := helper.NewHubClient(
config.AccessKeyID,
config.SecretAccessKey,
config.Region,
config.Endpoint,
)

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,
})
}

func (f *TargetFactory) createGCSClient(config GCS, parent GCS) target.Client {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
Expand Down Expand Up @@ -851,6 +944,17 @@ func (f *TargetFactory) mapSecretValues(config any, ref, mountedSecret string) {
c.SecretAccessKey = values.SecretAccessKey
}

case *SecurityHub:
if values.AccessKeyID != "" {
c.AccessKeyID = values.AccessKeyID
}
if values.SecretAccessKey != "" {
c.SecretAccessKey = values.SecretAccessKey
}
if values.AccountID != "" {
c.AccountID = values.AccessKeyID
}

case *GCS:
if values.Credentials != "" {
c.Credentials = values.Credentials
Expand Down
26 changes: 26 additions & 0 deletions pkg/config/target_factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ func Test_ResolveTarget(t *testing.T) {
t.Errorf("Expected 2 Client, got %d clients", len(clients))
}
})
t.Run("SecurityHub", func(t *testing.T) {
clients := factory.SecurityHubs(testConfig.SecurityHub)
if len(clients) != 2 {
t.Errorf("Expected 2 Client, got %d clients", len(clients))
}
})
}

func Test_ResolveTargetWithoutHost(t *testing.T) {
Expand Down Expand Up @@ -221,6 +227,26 @@ func Test_ResolveTargetWithoutHost(t *testing.T) {
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 {
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 {
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", 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", 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 {
t.Error("Expected Client to be nil if no bucket is configured")
Expand Down
29 changes: 29 additions & 0 deletions pkg/crd/api/policyreport/v1alpha2/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package v1alpha2
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/segmentio/fasthash/fnv1a"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -278,6 +280,33 @@ func (r *PolicyReportResult) GetID() string {
return r.ID
}

func (r *PolicyReportResult) ResourceString() string {
if !r.HasResource() {
return ""
}

res := r.GetResource()
var resource string

if res.Namespace != "" {
resource = res.Namespace
}

if res.Kind != "" && resource != "" {
resource = fmt.Sprintf("%s/%s", resource, strings.ToLower(res.Kind))
} else if res.Kind != "" {
resource = strings.ToLower(res.Kind)
}

if res.Name != "" && resource != "" {
resource = fmt.Sprintf("%s/%s", resource, res.Name)
} else if res.Name != "" {
resource = res.Name
}

return resource
}

type ReportInterface interface {
metav1.Object
GetID() string
Expand Down
22 changes: 22 additions & 0 deletions pkg/helper/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kinesis"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/aws/aws-sdk-go/service/securityhub"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -117,3 +118,24 @@ func NewKinesisClient(accessKeyID, secretAccessKey, region, endpoint, streamName
kinesis.New(sess),
}
}

// NewHubClient creates a new SecurityHub client to send finding events
func NewHubClient(accessKeyID, secretAccessKey, region, endpoint string) *securityhub.SecurityHub {
config := &aws.Config{
Region: aws.String(region),
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, ""),
}

sess, err := session.NewSession(config)
if err != nil {
zap.L().Error("error while creating S3 session")
return nil
}

optional := make([]*aws.Config, 0)
if endpoint != "" {
optional = append(optional, aws.NewConfig().WithEndpoint(endpoint))
}

return securityhub.New(sess, optional...)
}
Loading

0 comments on commit cd56188

Please sign in to comment.