diff --git a/Dockerfile b/Dockerfile index 2cca9e6..c939155 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ COPY go.mod go.sum ./ COPY main.go . COPY check/ check/ COPY cmd/ cmd/ +COPY internal/ internal/ ARG KUBEBENCH_VERSION ARG GOOS=linux ARG GOARCH=amd64 diff --git a/README.md b/README.md index 926ee11..24bdd30 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,10 @@ docker push .dkr.ecr..amazonaws.com/k8s/kube-bench: 8. Retrieve the value of this Pod and output the report, note the Pod name will vary: `kubectl logs kube-bench-` - You can save the report for later reference: `kubectl logs kube-bench- > kube-bench-report.txt` +#### Report kube-bench findings to AWS Security Hub + +You can configure kube-bench with the `--asff` option to send findings to AWS Security Hub for any benchmark tests that fail or that generate a warning. See [this page][kube-bench-aws-security-hub] for more information on how to enable the kube-bench integration with AWS Security Hub. + ### Running on OpenShift | OpenShift Hardening Guide | kube-bench config | @@ -243,7 +247,7 @@ kube-bench includes a set of test files for Red Hat's OpenShift hardening guide when you run the `kube-bench` command (either directly or through YAML). -There is work in progress on a [CIS Red Hat OpenShift Container Platform Benchmark](https://workbench.cisecurity.org/benchmarks/5248) which we believe should cover OCP 4.* and we intend to add support in kube-bench when it's published. +There is work in progress on a [CIS Red Hat OpenShift Container Platform Benchmark](https://workbench.cisecurity.org/benchmarks/5248) which we believe should cover OCP 4.* and we intend to add support in kube-bench when it's published. ### Running in a GKE cluster @@ -333,15 +337,15 @@ go build -o kube-bench . There are four output states: - [PASS] indicates that the test was run successfully, and passed. -- [FAIL] indicates that the test was run successfully, and failed. The remediation output describes how to correct the configuration, or includes an error message describing why the test could not be run. -- [WARN] means this test needs further attention, for example it is a test that needs to be run manually. Check the remediation output for further information. +- [FAIL] indicates that the test was run successfully, and failed. The remediation output describes how to correct the configuration, or includes an error message describing why the test could not be run. +- [WARN] means this test needs further attention, for example it is a test that needs to be run manually. Check the remediation output for further information. - [INFO] is informational output that needs no further action. Note: - If the test is Manual, this always generates WARN (because the user has to run it manually) - If the test is Scored, and kube-bench was unable to run the test, this generates FAIL (because the test has not been passed, and as a Scored test, if it doesn't pass then it must be considered a failure). - If the test is Not Scored, and kube-bench was unable to run the test, this generates WARN. -- If the test is Scored, type is empty, and there are no `test_items` present, it generates a WARN. This is to highlight tests that appear to be incompletely defined. +- If the test is Scored, type is empty, and there are no `test_items` present, it generates a WARN. This is to highlight tests that appear to be incompletely defined. ## Configuration @@ -426,3 +430,5 @@ We welcome pull requests! - Your PR is more likely to be accepted if it includes tests. (We have not historically been very strict about tests, but we would like to improve this!). - You're welcome to submit a draft PR if you would like early feedback on an idea or an approach. - Happy coding! + +[kube-bench-aws-security-hub]: ./docs/asff.md \ No newline at end of file diff --git a/cfg/eks-1.0/config.yaml b/cfg/eks-1.0/config.yaml index b783945..17301a7 100644 --- a/cfg/eks-1.0/config.yaml +++ b/cfg/eks-1.0/config.yaml @@ -1,2 +1,9 @@ --- ## Version-specific settings that override the values in cfg/config.yaml +## These settings are required if you are using the --asff option to report findings to AWS Security Hub +## AWS account number is required. +AWS_ACCOUNT: "" +## AWS region is required. +AWS_REGION: "" +## EKS Cluster ARN is required. +CLUSTER_ARN: "" diff --git a/check/controls.go b/check/controls.go index d6f6721..944e45d 100644 --- a/check/controls.go +++ b/check/controls.go @@ -19,12 +19,27 @@ import ( "encoding/json" "encoding/xml" "fmt" + "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" "github.com/golang/glog" "github.com/onsi/ginkgo/reporters" + "github.com/spf13/viper" "gopkg.in/yaml.v2" ) +const ( + // UNKNOWN is when the AWS account can't be found + UNKNOWN = "Unknown" + // ARN for the AWS Security Hub service + ARN = "arn:aws:securityhub:%s::product/aqua-security/kube-bench" + // SCHEMA for the AWS Security Hub service + SCHEMA = "2018-10-08" + // TYPE is type of Security Hub finding + TYPE = "Software and Configuration Checks/Industry and Regulatory Standards/CIS Kubernetes Benchmark" +) + // Controls holds all controls to check for master nodes. type Controls struct { ID string `yaml:"id" json:"id"` @@ -75,7 +90,7 @@ func NewControls(t NodeType, in []byte) (*Controls, error) { } // RunChecks runs the checks with the given Runner. Only checks for which the filter Predicate returns `true` will run. -func (controls *Controls) RunChecks(runner Runner, filter Predicate, skipIdMap map[string]bool) Summary { +func (controls *Controls) RunChecks(runner Runner, filter Predicate, skipIDMap map[string]bool) Summary { var g []*Group m := make(map[string]*Group) controls.Summary.Pass, controls.Summary.Fail, controls.Summary.Warn, controls.Info = 0, 0, 0, 0 @@ -87,10 +102,10 @@ func (controls *Controls) RunChecks(runner Runner, filter Predicate, skipIdMap m continue } - _, groupSkippedViaCmd := skipIdMap[group.ID] - _, checkSkippedViaCmd := skipIdMap[check.ID] + _, groupSkippedViaCmd := skipIDMap[group.ID] + _, checkSkippedViaCmd := skipIDMap[check.ID] - if group.Skip || groupSkippedViaCmd || checkSkippedViaCmd { + if group.Skip || groupSkippedViaCmd || checkSkippedViaCmd { check.Type = SKIP } @@ -185,6 +200,80 @@ func (controls *Controls) JUnit() ([]byte, error) { return b.Bytes(), nil } +// ASFF encodes the results of last run to AWS Security Finding Format(ASFF). +func (controls *Controls) ASFF() ([]*securityhub.AwsSecurityFinding, error) { + fs := []*securityhub.AwsSecurityFinding{} + a, err := getConfig("AWS_ACCOUNT") + if err != nil { + return nil, err + } + c, err := getConfig("CLUSTER_ARN") + if err != nil { + return nil, err + } + region, err := getConfig("AWS_REGION") + if err != nil { + return nil, err + } + arn := fmt.Sprintf(ARN, region) + + ti := time.Now() + tf := ti.Format(time.RFC3339) + for _, g := range controls.Groups { + for _, check := range g.Checks { + if check.State == FAIL || check.State == WARN { + // ASFF ProductFields['Actual result'] can't be longer than 1024 characters + actualValue := check.ActualValue + if len(check.ActualValue) > 1024 { + actualValue = check.ActualValue[0:1023] + } + f := securityhub.AwsSecurityFinding{ + AwsAccountId: aws.String(a), + Confidence: aws.Int64(100), + GeneratorId: aws.String(fmt.Sprintf("%s/cis-kubernetes-benchmark/%s/%s", arn, controls.Version, check.ID)), + Id: aws.String(fmt.Sprintf("%s%sEKSnodeID+%s%s", arn, a, check.ID, tf)), + CreatedAt: aws.String(tf), + Description: aws.String(check.Text), + ProductArn: aws.String(arn), + SchemaVersion: aws.String(SCHEMA), + Title: aws.String(fmt.Sprintf("%s %s", check.ID, check.Text)), + UpdatedAt: aws.String(tf), + Types: []*string{aws.String(TYPE)}, + Severity: &securityhub.Severity{ + Label: aws.String(securityhub.SeverityLabelHigh), + }, + Remediation: &securityhub.Remediation{ + Recommendation: &securityhub.Recommendation{ + Text: aws.String(check.Remediation), + }, + }, + ProductFields: map[string]*string{ + "Reason": aws.String(check.Reason), + "Actual result": aws.String(actualValue), + "Expected result": aws.String(check.ExpectedResult), + "Section": aws.String(fmt.Sprintf("%s %s", controls.ID, controls.Text)), + "Subsection": aws.String(fmt.Sprintf("%s %s", g.ID, g.Text)), + }, + Resources: []*securityhub.Resource{ + { + Id: aws.String(c), + Type: aws.String(TYPE), + }, + }, + } + fs = append(fs, &f) + } + } + } + return fs, nil +} +func getConfig(name string) (string, error) { + r := viper.GetString(name) + if len(r) == 0 { + return "", fmt.Errorf("%s not set", name) + } + return r, nil +} func summarize(controls *Controls, state State) { switch state { case PASS: diff --git a/check/controls_test.go b/check/controls_test.go index 34a76b8..de8b4ab 100644 --- a/check/controls_test.go +++ b/check/controls_test.go @@ -18,12 +18,17 @@ import ( "bytes" "encoding/json" "encoding/xml" + "fmt" "io/ioutil" "os" "path/filepath" + "reflect" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/securityhub" "github.com/onsi/ginkgo/reporters" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "gopkg.in/yaml.v2" @@ -357,3 +362,104 @@ func assertEqualGroupSummary(t *testing.T, pass, fail, info, warn int, actual *G assert.Equal(t, info, actual.Info) assert.Equal(t, warn, actual.Warn) } + +func TestControls_ASFF(t *testing.T) { + type fields struct { + ID string + Version string + Text string + Groups []*Group + Summary Summary + } + tests := []struct { + name string + fields fields + want []*securityhub.AwsSecurityFinding + wantErr bool + }{ + { + name: "Test simple conversion", + fields: fields{ + ID: "test1", + Version: "1", + Text: "test runnner", + Summary: Summary{ + Fail: 99, + Pass: 100, + Warn: 101, + Info: 102, + }, + Groups: []*Group{ + { + ID: "g1", + Text: "Group text", + Checks: []*Check{ + {ID: "check1id", + Text: "check1text", + State: FAIL, + Remediation: "fix me", + Reason: "failed", + ExpectedResult: "failed", + ActualValue: "failed", + }, + }, + }, + }}, + want: []*securityhub.AwsSecurityFinding{ + { + AwsAccountId: aws.String("foo account"), + Confidence: aws.Int64(100), + GeneratorId: aws.String(fmt.Sprintf("%s/cis-kubernetes-benchmark/%s/%s", fmt.Sprintf(ARN, "somewhere"), "1", "check1id")), + Description: aws.String("check1text"), + ProductArn: aws.String(fmt.Sprintf(ARN, "somewhere")), + SchemaVersion: aws.String(SCHEMA), + Title: aws.String(fmt.Sprintf("%s %s", "check1id", "check1text")), + Types: []*string{aws.String(TYPE)}, + Severity: &securityhub.Severity{ + Label: aws.String(securityhub.SeverityLabelHigh), + }, + Remediation: &securityhub.Remediation{ + Recommendation: &securityhub.Recommendation{ + Text: aws.String("fix me"), + }, + }, + ProductFields: map[string]*string{ + "Reason": aws.String("failed"), + "Actual result": aws.String("failed"), + "Expected result": aws.String("failed"), + "Section": aws.String(fmt.Sprintf("%s %s", "test1", "test runnner")), + "Subsection": aws.String(fmt.Sprintf("%s %s", "g1", "Group text")), + }, + Resources: []*securityhub.Resource{ + { + Id: aws.String("foo Cluster"), + Type: aws.String(TYPE), + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Set("AWS_ACCOUNT", "foo account") + viper.Set("CLUSTER_ARN", "foo Cluster") + viper.Set("AWS_REGION", "somewhere") + controls := &Controls{ + ID: tt.fields.ID, + Version: tt.fields.Version, + Text: tt.fields.Text, + Groups: tt.fields.Groups, + Summary: tt.fields.Summary, + } + got, _ := controls.ASFF() + tt.want[0].CreatedAt = got[0].CreatedAt + tt.want[0].UpdatedAt = got[0].UpdatedAt + tt.want[0].Id = got[0].Id + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Controls.ASFF() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/common.go b/cmd/common.go index 55b5c35..9f17ff9 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -373,6 +373,10 @@ func writeOutput(controlsCollection []*check.Controls) { writePgsqlOutput(controlsCollection) return } + if aSFF { + writeASFFOutput(controlsCollection) + return + } writeStdoutOutput(controlsCollection) } @@ -404,6 +408,18 @@ func writePgsqlOutput(controlsCollection []*check.Controls) { } } +func writeASFFOutput(controlsCollection []*check.Controls) { + for _, controls := range controlsCollection { + out, err := controls.ASFF() + if err != nil { + exitWithError(fmt.Errorf("failed to format findings as ASFF: %v", err)) + } + if err := writeFinding(out); err != nil { + exitWithError(fmt.Errorf("failed to output to ASFF: %v", err)) + } + } +} + func writeStdoutOutput(controlsCollection []*check.Controls) { for _, controls := range controlsCollection { summary := controls.Summary diff --git a/cmd/root.go b/cmd/root.go index 327f2e7..4d72cd9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,6 +42,7 @@ var ( jsonFmt bool junitFmt bool pgSQL bool + aSFF bool masterFile = "master.yaml" nodeFile = "node.yaml" etcdFile = "etcd.yaml" @@ -156,6 +157,7 @@ func init() { RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON") RootCmd.PersistentFlags().BoolVar(&junitFmt, "junit", false, "Prints the results as JUnit") RootCmd.PersistentFlags().BoolVar(&pgSQL, "pgsql", false, "Save the results to PostgreSQL") + RootCmd.PersistentFlags().BoolVar(&aSFF, "asff", false, "Send the results to AWS Security Hub") RootCmd.PersistentFlags().BoolVar(&filterOpts.Scored, "scored", true, "Run the scored CIS checks") RootCmd.PersistentFlags().BoolVar(&filterOpts.Unscored, "unscored", true, "Run the unscored CIS checks") RootCmd.PersistentFlags().StringVar(&skipIds, "skip", "", "List of comma separated values of checks to be skipped") diff --git a/cmd/securityHub.go b/cmd/securityHub.go new file mode 100644 index 0000000..ae6fd81 --- /dev/null +++ b/cmd/securityHub.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/aquasecurity/kube-bench/internal/findings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/spf13/viper" +) + +//REGION ... +const REGION = "AWS_REGION" + +func writeFinding(in []*securityhub.AwsSecurityFinding) error { + r := viper.GetString(REGION) + if len(r) == 0 { + return fmt.Errorf("%s not set", REGION) + } + sess, err := session.NewSession(&aws.Config{ + Region: aws.String(r)}, + ) + if err != nil { + return err + } + svc := securityhub.New(sess) + p := findings.New(svc) + out, perr := p.PublishFinding(in) + print(out) + return perr +} + +func print(out *findings.PublisherOutput) { + if out.SuccessCount > 0 { + log.Printf("Number of findings that were successfully imported:%v\n", out.SuccessCount) + } + if out.FailedCount > 0 { + log.Printf("Number of findings that failed to import:%v\n", out.FailedCount) + for _, f := range out.FailedFindings { + log.Printf("ID:%s", *f.Id) + log.Printf("Message:%s", *f.ErrorMessage) + log.Printf("Error Code:%s", *f.ErrorCode) + } + } +} diff --git a/docs/asff.md b/docs/asff.md new file mode 100644 index 0000000..5d56e1f --- /dev/null +++ b/docs/asff.md @@ -0,0 +1,39 @@ +# Integrating kube-bench with AWS Security Hub + +You can configure kube-bench with the `--asff` to send findings to AWS Security Hub. There are some additional steps required so that kube-bench has information and permissions to send these findings. + +## Enable the AWS Security Hub integration + +* You will need AWS Security Hub to be enabled in your account +* In the Security Hub console, under Integrations, search for kube-bench + +

+ +

+ +* Click on `Accept findings`. This gives information about the IAM permissions required to send findings to your Security Hub account. kube-bench runs within a pod on your EKS cluster, and will need to be associated with a Role that has these permissions. + +## Configure permissions in an IAM Role + +* Grant these permissions to the IAM Role that the kube-bench pod will be associated with. There are two potions: + * You can run the kube-bench pod under a specific [service account associated with an IAM role](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) that has these permissions to write Security Hub findings. + * Alternatively the pod can be granted permissions specified by the Role that your [EKS node group uses](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html). + +## Configure and rebuild kube-bench + +You will need to download, build and push the kube-bench container image to your ECR repo as described in Step 3 of the [EKS instructions][eks-instructions], except that before you build the container image, you need to edit `cfg/eks-1.0/config.yaml` to specify the AWS account, AWS region, and the EKS Cluster ARN. + +## Modify the job configuration + +* Modify `job-eks.yaml` to specify the `--asff` flag, so that kube-bench writes output in ASFF format to Security Hub +* Make sure that `job-eks.yaml` specifies the container image you just pushed to your ECR registry. + +You can now run kube-bench as a pod in your cluster: `kubectl apply -f job-eks.yaml` + +Findings will be generated for any kube-bench test that generates a `[FAIL]` or `[WARN]` output. If all tests pass, no findings will be generated. However, it's recommended that you consult the pod log output to check whether any findings were generated but could not be written to Security Hub. + +

+ +

+ +[eks-instructions]: ../README.md#running-in-an-EKS-cluster \ No newline at end of file diff --git a/go.mod b/go.mod index 181954e..0ff6fbb 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/aquasecurity/kube-bench go 1.13 require ( + github.com/aws/aws-sdk-go v1.35.28 github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect github.com/fatih/color v1.5.0 github.com/go-sql-driver/mysql v1.4.1 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b + github.com/google/go-cmp v0.3.1 // indirect github.com/imdario/mergo v0.3.5 // indirect github.com/jinzhu/gorm v0.0.0-20160404144928-5174cc5c242a github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d // indirect @@ -17,13 +19,13 @@ require ( github.com/mattn/go-isatty v0.0.0-20170307163044-57fdcb988a5c // indirect github.com/mattn/go-sqlite3 v1.10.0 // indirect github.com/onsi/ginkgo v1.10.1 - github.com/pkg/errors v0.8.1 + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.4.0 - github.com/stretchr/testify v1.3.0 - golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a // indirect + github.com/stretchr/testify v1.4.0 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect google.golang.org/appengine v1.5.0 // indirect - gopkg.in/yaml.v2 v2.2.4 + gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d k8s.io/client-go v11.0.0+incompatible diff --git a/go.sum b/go.sum index 18c8660..9321e52 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.35.28 h1:S2LuRnfC8X05zgZLC8gy/Sb82TGv2Cpytzbzz7tkeHc= +github.com/aws/aws-sdk-go v1.35.28/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -87,6 +89,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -118,6 +122,10 @@ github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVt github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= @@ -177,6 +185,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -223,6 +233,8 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -257,10 +269,12 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -320,8 +334,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/images/asff-example-finding.png b/images/asff-example-finding.png new file mode 100644 index 0000000..eb545ac Binary files /dev/null and b/images/asff-example-finding.png differ diff --git a/images/kube-bench-security-hub.png b/images/kube-bench-security-hub.png new file mode 100644 index 0000000..18cf056 Binary files /dev/null and b/images/kube-bench-security-hub.png differ diff --git a/internal/findings/doc.go b/internal/findings/doc.go new file mode 100644 index 0000000..5a9bcf5 --- /dev/null +++ b/internal/findings/doc.go @@ -0,0 +1,4 @@ +/* +Package findings handles sending findings to Security Hub. +*/ +package findings diff --git a/internal/findings/publisher.go b/internal/findings/publisher.go new file mode 100644 index 0000000..4661cc2 --- /dev/null +++ b/internal/findings/publisher.go @@ -0,0 +1,69 @@ +package findings + +import ( + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/aws/aws-sdk-go/service/securityhub/securityhubiface" + "github.com/pkg/errors" +) + +// A Publisher represents an object that publishes finds to AWS Security Hub. +type Publisher struct { + client securityhubiface.SecurityHubAPI // AWS Security Hub Service Client +} + +// A PublisherOutput represents an object that contains information about the service call. +type PublisherOutput struct { + // The number of findings that failed to import. + // + // FailedCount is a required field + FailedCount int64 + + // The list of findings that failed to import. + FailedFindings []*securityhub.ImportFindingsError + + // The number of findings that were successfully imported. + // + // SuccessCount is a required field + SuccessCount int64 +} + +// New creates a new Publisher. +func New(client securityhubiface.SecurityHubAPI) *Publisher { + return &Publisher{ + client: client, + } +} + +// PublishFinding publishes findings to AWS Security Hub Service +func (p *Publisher) PublishFinding(finding []*securityhub.AwsSecurityFinding) (*PublisherOutput, error) { + o := PublisherOutput{} + i := securityhub.BatchImportFindingsInput{} + i.Findings = finding + var errs error + + // Split the slice into batches of 100 finding. + batch := 100 + + for i := 0; i < len(finding); i += batch { + j := i + batch + if j > len(finding) { + j = len(finding) + } + i := securityhub.BatchImportFindingsInput{} + i.Findings = finding + r, err := p.client.BatchImportFindings(&i) // Process the batch. + if err != nil { + errs = errors.Wrap(err, "finding publish failed") + } + if r.FailedCount != nil { + o.FailedCount += *r.FailedCount + } + if r.SuccessCount != nil { + o.SuccessCount += *r.SuccessCount + } + for _, ff := range r.FailedFindings { + o.FailedFindings = append(o.FailedFindings, ff) + } + } + return &o, errs +} diff --git a/internal/findings/publisher_test.go b/internal/findings/publisher_test.go new file mode 100644 index 0000000..2a5381c --- /dev/null +++ b/internal/findings/publisher_test.go @@ -0,0 +1,68 @@ +package findings + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/securityhub" + "github.com/aws/aws-sdk-go/service/securityhub/securityhubiface" +) + +// Define a mock struct to be used in your unit tests of myFunc. +type MockSHClient struct { + securityhubiface.SecurityHubAPI + Batches int + NumberOfFinding int +} + +func NewMockSHClient() *MockSHClient { + return &MockSHClient{} +} + +func (m *MockSHClient) BatchImportFindings(input *securityhub.BatchImportFindingsInput) (*securityhub.BatchImportFindingsOutput, error) { + o := securityhub.BatchImportFindingsOutput{} + m.Batches++ + m.NumberOfFinding = len(input.Findings) + return &o, nil +} + +func TestPublisher_publishFinding(t *testing.T) { + type fields struct { + client *MockSHClient + } + type args struct { + finding []*securityhub.AwsSecurityFinding + } + tests := []struct { + name string + fields fields + args args + wantBatchCount int + wantFindingCount int + }{ + {"Test single finding", fields{NewMockSHClient()}, args{makeFindings(1)}, 1, 1}, + {"Test 150 finding should return 2 batches", fields{NewMockSHClient()}, args{makeFindings(150)}, 2, 150}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New(tt.fields.client) + p.PublishFinding(tt.args.finding) + if tt.fields.client.NumberOfFinding != tt.wantFindingCount { + t.Errorf("Publisher.publishFinding() want = %v, got %v", tt.wantFindingCount, tt.fields.client.NumberOfFinding) + } + if tt.fields.client.Batches != tt.wantBatchCount { + t.Errorf("Publisher.publishFinding() want = %v, got %v", tt.wantBatchCount, tt.fields.client.Batches) + } + }) + } +} + +func makeFindings(count int) []*securityhub.AwsSecurityFinding { + var findings []*securityhub.AwsSecurityFinding + + for i := 0; i < count; i++ { + t := securityhub.AwsSecurityFinding{} + findings = append(findings, &t) + + } + return findings +} diff --git a/job-eks.yaml b/job-eks.yaml index 0e4b325..7a51e75 100644 --- a/job-eks.yaml +++ b/job-eks.yaml @@ -11,6 +11,10 @@ spec: - name: kube-bench # Push the image to your ECR and then refer to it here image: + # Use the --asff flag if you would like to send findings to AWS Security Hub + # Note that this requires you to rebuild a version of the kube-bench image + # after editing the cfg/eks-1.0/config.yaml with your account information + # command: ["kube-bench", "node", "--benchmark", "eks-1.0", "--asff"] command: ["kube-bench", "node", "--benchmark", "eks-1.0"] volumeMounts: - name: var-lib-kubelet