diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 019d7de46e..d6660a3058 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -131,6 +131,16 @@ jobs: tag_with_ref: true tag_with_sha: true build_args: baseImageTag=ci-local + - uses: docker/build-push-action@v1 + name: "Build & Push kubeaudit Parser Image" + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: securecodebox/parser-kubeaudit + path: ./scanners/kubeaudit/parser/ + tag_with_ref: true + tag_with_sha: true + build_args: baseImageTag=ci-local - uses: docker/build-push-action@v1 name: "Build & Push kube-hunter Parser Image" with: @@ -336,6 +346,14 @@ jobs: path: ./scanners/kube-hunter/scanner/ # Note: not prefixed with a "v" as this matches the aquasec/kube-hunter tags tags: "0.3.0,latest" + - uses: docker/build-push-action@v1 + name: "Build & Push kubeaudit Scanner Image" + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: securecodebox/scanner-kubeaudit + path: ./scanners/kubeaudit/scanner/ + tags: "v0.11.5,latest" - uses: docker/build-push-action@v1 name: "Build & Push test-scan Scanner Image" with: @@ -453,6 +471,17 @@ jobs: --set="image.tag=0.3.0" cd tests/integration/ npx jest --ci --color kube-hunter + - name: "kubeaudit Integration Tests" + run: | + kubectl create namespace kubeaudit-tests + helm -n kubeaudit-tests install juice-shop ./demo-apps/juice-shop/ --wait + helm -n integration-tests install kubeaudit ./scanners/kubeaudit/ \ + --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" \ + --set="image.tag=0.11.5" \ + --set="kubeauditScope=cluster" + cd tests/integration/ + npx jest --ci --color kubeaudit + kubectl delete namespace kubeaudit-tests - name: "ssh-scan Integration Tests" run: | helm -n integration-tests install ssh-scan ./scanners/ssh_scan/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" diff --git a/operator/controllers/execution/scans/scan_reconciler.go b/operator/controllers/execution/scans/scan_reconciler.go index bee387a177..ba864172df 100644 --- a/operator/controllers/execution/scans/scan_reconciler.go +++ b/operator/controllers/execution/scans/scan_reconciler.go @@ -176,7 +176,9 @@ func (r *ScanReconciler) constructJobForScan(scan *executionv1.Scan, scanType *e podAnnotations["sidecar.istio.io/inject"] = "false" job.Spec.Template.Annotations = podAnnotations - job.Spec.Template.Spec.ServiceAccountName = "lurcher" + if job.Spec.Template.Spec.ServiceAccountName == "" { + job.Spec.Template.Spec.ServiceAccountName = "lurcher" + } // merging volume definition from ScanType (if existing) with standard results volume if job.Spec.Template.Spec.Containers[0].VolumeMounts == nil || len(job.Spec.Template.Spec.Containers[0].VolumeMounts) == 0 { diff --git a/scanners/gitleaks/README.md b/scanners/gitleaks/README.md index f898acc300..4015bf2a52 100644 --- a/scanners/gitleaks/README.md +++ b/scanners/gitleaks/README.md @@ -17,12 +17,15 @@ with all commits up to the initial one. To learn more about gitleaks visit ## Deployment + The gitleaks scanner can be deployed with helm: + ```bash helm upgrade --install gitleaks secureCodeBox/gitleaks ``` ## Scanner configuration + For a complete overview of the configuration options checkout the [Gitleaks documentation](https://github.com/zricethezav/gitleaks/wiki/Options). @@ -35,6 +38,7 @@ The only mandatory parameters are: **Do not** override the option `--report-format` or `--report`. It is already configured for automatic findings parsing. #### Ruleset + At this point we provide three rulesets which you can pass to the `--config` oprtion: - `/home/config_all.toml`: Includes every rule. @@ -43,11 +47,13 @@ At this point we provide three rulesets which you can pass to the `--config` opr find something like **password = Ej2ifDk2jfeo2** but it will reduce resulting false positives. #### Other useful options are: + - `--commit-since`: Scan commits more recent than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format. - `--commit-until`: Scan commits older than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format. - `--repo-config`: Load config from target repo. Config file must be ".gitleaks.toml" or "gitleaks.toml". #### Finding format + It is not an easy task to classify the severity of the scans because we can't tell for sure if the finding is e.g. a real or a testing password. Another issue is that the rate of false positives for generic rules can be very high. Therefore, we tried to classify the severity of the finding by looking at the accuracy of the rule which detected it. Rules for AWS diff --git a/scanners/gitleaks/README.md.gotmpl b/scanners/gitleaks/README.md.gotmpl index 0c6dac85d2..457606df76 100644 --- a/scanners/gitleaks/README.md.gotmpl +++ b/scanners/gitleaks/README.md.gotmpl @@ -17,12 +17,15 @@ with all commits up to the initial one. To learn more about gitleaks visit ## Deployment + The gitleaks scanner can be deployed with helm: + ```bash helm upgrade --install gitleaks secureCodeBox/gitleaks ``` ## Scanner configuration + For a complete overview of the configuration options checkout the [Gitleaks documentation](https://github.com/zricethezav/gitleaks/wiki/Options). @@ -35,6 +38,7 @@ The only mandatory parameters are: **Do not** override the option `--report-format` or `--report`. It is already configured for automatic findings parsing. #### Ruleset + At this point we provide three rulesets which you can pass to the `--config` oprtion: - `/home/config_all.toml`: Includes every rule. @@ -43,11 +47,13 @@ At this point we provide three rulesets which you can pass to the `--config` opr find something like **password = Ej2ifDk2jfeo2** but it will reduce resulting false positives. #### Other useful options are: + - `--commit-since`: Scan commits more recent than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format. - `--commit-until`: Scan commits older than a specific date. Ex: '2006-01-02' or '2006-01-02T15:04:05-0700' format. - `--repo-config`: Load config from target repo. Config file must be ".gitleaks.toml" or "gitleaks.toml". #### Finding format + It is not an easy task to classify the severity of the scans because we can't tell for sure if the finding is e.g. a real or a testing password. Another issue is that the rate of false positives for generic rules can be very high. Therefore, we tried to classify the severity of the finding by looking at the accuracy of the rule which detected it. Rules for AWS diff --git a/scanners/kubeaudit/.helmignore b/scanners/kubeaudit/.helmignore new file mode 100644 index 0000000000..84e9115fbc --- /dev/null +++ b/scanners/kubeaudit/.helmignore @@ -0,0 +1,5 @@ +.DS_Store + +parser/ +scanner/ +examples/ diff --git a/scanners/kubeaudit/Chart.yaml b/scanners/kubeaudit/Chart.yaml new file mode 100644 index 0000000000..46538abc7a --- /dev/null +++ b/scanners/kubeaudit/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: kubeaudit +description: A Helm chart for the kubeaudit security scanner that integrates with the secureCodeBox. + +type: application +version: latest +appVersion: "v0.11.5" + +keywords: + - security + - kubeaudit + - scanner + - secureCodeBox +home: https://www.securecodebox.io/scanners/kubeaudit +icon: https://www.securecodebox.io/scannerIcons/kubeaudit.svg +sources: + - https://github.com/secureCodeBox/secureCodeBox +maintainers: + - name: iteratec GmbH + email: secureCodeBox@iteratec.com diff --git a/scanners/kubeaudit/README.md b/scanners/kubeaudit/README.md index 4c030fb255..ed91a7c5b0 100644 --- a/scanners/kubeaudit/README.md +++ b/scanners/kubeaudit/README.md @@ -2,18 +2,46 @@ title: "kubeaudit" category: "scanner" type: "Kubernetes" -state: "roadmap" -appVersion: "0.9.0" -usecase: "Audit your Kubernetes clusters" +state: "released" +appVersion: "0.15.1" +usecase: "Kubernetes Configuration Scanner" --- -kubeaudit helps you audit your Kubernetes clusters against common security controls. +Kubeaudit finds security misconfigurations in you Kubernetes Resources and gives tips on how to resolve these. -To learn more about the kubeaudit scanner itself visit [kubeaudit GitHub]. +Kubeaudit comes with a large lists of "auditors" which test various aspects, like the SecurityContext of pods. +You can find the complete list of [auditors here](https://github.com/Shopify/kubeaudit/tree/master/docs/auditors). + +To learn more about the kubeaudit itself visit [kubeaudit GitHub]. -> 🔧 The secureCodeBox core team is working on an integration of kubeaudit. We will keep you informed. +## Deployment + +The kube-hunter ScanType can be deployed via helm: + +```bash +helm upgrade --install kubeaudit secureCodeBox/kubeaudit +``` + +## Chart Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| kubeauditScope | string | `"namespace"` | Automatically sets up rbac roles for kubeaudit to access the ressources it scans. Can be either "cluster" (ClusterRole) or "namespace" (Role) | +| parserImage.repository | string | `"docker.io/securecodebox/parser-kubeaudit"` | Parser image repository | +| parserImage.tag | string | defaults to the charts version | Parser image tag | +| scannerJob.env | list | `[]` | Optional environment variables mapped into each scanJob (see: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) | +| scannerJob.extraContainers | list | `[]` | Optional additional Containers started with each scanJob (see: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) | +| scannerJob.extraVolumeMounts | list | `[]` | Optional VolumeMounts mapped into each scanJob (see: https://kubernetes.io/docs/concepts/storage/volumes/) | +| scannerJob.extraVolumes | list | `[]` | Optional Volumes mapped into each scanJob (see: https://kubernetes.io/docs/concepts/storage/volumes/) | +| scannerJob.resources | object | `{}` | CPU/memory resource requests/limits (see: https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/, https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/) | +| scannerJob.securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["all"]},"privileged":false,"readOnlyRootFilesystem":true,"runAsNonRoot":true}` | Optional securityContext set on scanner container (see: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) | +| scannerJob.securityContext.allowPrivilegeEscalation | bool | `false` | Ensure that users privileges cannot be escalated | +| scannerJob.securityContext.capabilities.drop[0] | string | `"all"` | This drops all linux privileges from the container. | +| scannerJob.securityContext.privileged | bool | `false` | Ensures that the scanner container is not run in privileged mode | +| scannerJob.securityContext.readOnlyRootFilesystem | bool | `true` | Prevents write access to the containers file system | +| scannerJob.securityContext.runAsNonRoot | bool | `true` | Enforces that the scanner image is run as a non root user | +| scannerJob.ttlSecondsAfterFinished | string | `nil` | Defines how long the scanner job after finishing will be available (see: https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/) | -[kubeaudit GitHub]: https://github.com/Shopify/kubeaudit -[kubeaudit Documentation]: https://github.com/Shopify/kubeaudit#quick-start +[kubeaudit GitHub]: https://github.com/Shopify/kubeaudit/ diff --git a/scanners/kubeaudit/README.md.gotmpl b/scanners/kubeaudit/README.md.gotmpl new file mode 100644 index 0000000000..28e259bdfa --- /dev/null +++ b/scanners/kubeaudit/README.md.gotmpl @@ -0,0 +1,31 @@ +--- +title: "kubeaudit" +category: "scanner" +type: "Kubernetes" +state: "released" +appVersion: "0.15.1" +usecase: "Kubernetes Configuration Scanner" +--- + +Kubeaudit finds security misconfigurations in you Kubernetes Resources and gives tips on how to resolve these. + +Kubeaudit comes with a large lists of "auditors" which test various aspects, like the SecurityContext of pods. +You can find the complete list of [auditors here](https://github.com/Shopify/kubeaudit/tree/master/docs/auditors). + +To learn more about the kubeaudit itself visit [kubeaudit GitHub]. + + + +## Deployment + +The kube-hunter ScanType can be deployed via helm: + +```bash +helm upgrade --install kubeaudit secureCodeBox/kubeaudit +``` + +## Chart Configuration + +{{ template "chart.valuesTable" . }} + +[kubeaudit GitHub]: https://github.com/Shopify/kubeaudit/ diff --git a/scanners/kubeaudit/examples/juice-shop/scan.yaml b/scanners/kubeaudit/examples/juice-shop/scan.yaml new file mode 100644 index 0000000000..fdfe3e0571 --- /dev/null +++ b/scanners/kubeaudit/examples/juice-shop/scan.yaml @@ -0,0 +1,9 @@ +apiVersion: "execution.securecodebox.io/v1" +kind: Scan +metadata: + name: "kubeaudit-juiceshop" +spec: + scanType: "kubeaudit" + parameters: + - "-n" + - "juice-shop" diff --git a/scanners/kubeaudit/parser/Dockerfile b/scanners/kubeaudit/parser/Dockerfile new file mode 100644 index 0000000000..5925068437 --- /dev/null +++ b/scanners/kubeaudit/parser/Dockerfile @@ -0,0 +1,4 @@ +ARG baseImageTag +FROM securecodebox/parser-sdk-nodejs:${baseImageTag:-latest} +WORKDIR /home/app/parser-wrapper/parser/ +COPY --chown=app:app ./parser.js ./parser.js diff --git a/scanners/kubeaudit/parser/__snapshots__/parser.test.js.snap b/scanners/kubeaudit/parser/__snapshots__/parser.test.js.snap new file mode 100644 index 0000000000..ab3b0171b7 --- /dev/null +++ b/scanners/kubeaudit/parser/__snapshots__/parser.test.js.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`example parser parses empty json to zero findings 1`] = ` +Array [ + Object { + "attributes": Object {}, + "category": "Automounted ServiceAccount Token", + "description": "Default service account with token mounted. automountServiceAccountToken should be set to 'false' on either the ServiceAccount or on the PodSpec or a non-default service account should be used.", + "location": null, + "name": "Default ServiceAccount uses Automounted Service Account Token", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "AUDIT_WRITE", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'AUDIT_WRITE' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "CHOWN", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'CHOWN' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "DAC_OVERRIDE", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'DAC_OVERRIDE' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "FOWNER", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'FOWNER' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "FSETID", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'FSETID' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "KILL", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'KILL' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "MKNOD", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'MKNOD' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "NET_BIND_SERVICE", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'NET_BIND_SERVICE' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "NET_RAW", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'NET_RAW' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "SETFCAP", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'SETFCAP' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "SETGID", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'SETGID' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "SETPCAP", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'SETPCAP' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "SETUID", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'SETUID' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "capability": "SYS_CHROOT", + "container": "juice-shop", + }, + "category": "Capability Not Dropped", + "description": "Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.", + "location": "container://juice-shop", + "name": "Capability 'SYS_CHROOT' Not Dropped", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "container": "juice-shop", + }, + "category": "Non Root User Not Enforced", + "description": "runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two.", + "location": "container://juice-shop", + "name": "NonRoot User not enforced for Container", + "osi_layer": "NOT_APPLICABLE", + "severity": "MEDIUM", + }, + Object { + "attributes": Object { + "container": "juice-shop", + }, + "category": "Non ReadOnly Root Filesystem", + "description": "readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'.", + "location": "container://juice-shop", + "name": "Container Uses a non ReadOnly Root Filesystem", + "osi_layer": "NOT_APPLICABLE", + "severity": "LOW", + }, + Object { + "attributes": Object { + "Namespace": "default", + }, + "category": "No Default Deny NetworkPolicy", + "description": "Namespace is missing a default deny ingress and egress NetworkPolicy.", + "location": "namespace://default", + "name": "Namespace \\"default\\" is missing a Default Deny NetworkPolicy", + "osi_layer": "NOT_APPLICABLE", + "severity": "MEDIUM", + }, +] +`; diff --git a/scanners/kubeaudit/parser/__testFiles__/juice-shop.jsonl b/scanners/kubeaudit/parser/__testFiles__/juice-shop.jsonl new file mode 100644 index 0000000000..ef5cb75252 --- /dev/null +++ b/scanners/kubeaudit/parser/__testFiles__/juice-shop.jsonl @@ -0,0 +1,23 @@ +{"AuditResultName":"AppArmorAnnotationMissing","Container":"juice-shop","MissingAnnotation":"container.apparmor.security.beta.kubernetes.io/juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/juice-shop' should be added.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"AutomountServiceAccountTokenTrueAndDefaultSA","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Default service account with token mounted. automountServiceAccountToken should be set to 'false' on either the ServiceAccount or on the PodSpec or a non-default service account should be used.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"AUDIT_WRITE","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"CHOWN","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"DAC_OVERRIDE","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"FOWNER","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"FSETID","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"KILL","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"MKNOD","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"NET_BIND_SERVICE","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"NET_RAW","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"SETFCAP","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"SETGID","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"SETPCAP","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"SETUID","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"CapabilityNotDropped","Capability":"SYS_CHROOT","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"LimitsNotSet","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"warning","msg":"Resource limits not set.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"RunAsNonRootPSCNilCSCNil","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"AllowPrivilegeEscalationNil","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"PrivilegedNil","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"warning","msg":"privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"ReadOnlyRootFilesystemNil","Container":"juice-shop","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"SeccompAnnotationMissing","MissingAnnotation":"seccomp.security.alpha.kubernetes.io/pod","ResourceApiVersion":"apps/v1","ResourceKind":"Deployment","ResourceName":"juice-shop","ResourceNamespace":"default","level":"error","msg":"Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added.","time":"2020-10-09T08:32:57Z"} +{"AuditResultName":"MissingDefaultDenyIngressAndEgressNetworkPolicy","Namespace":"default","ResourceApiVersion":"v1","ResourceKind":"Namespace","ResourceName":"default","level":"error","msg":"Namespace is missing a default deny ingress and egress NetworkPolicy.","time":"2020-10-09T08:32:57Z"} diff --git a/scanners/kubeaudit/parser/parser.js b/scanners/kubeaudit/parser/parser.js new file mode 100644 index 0000000000..a2af7251f3 --- /dev/null +++ b/scanners/kubeaudit/parser/parser.js @@ -0,0 +1,131 @@ +function createDropCapabilityFinding({ Capability, Container, msg }) { + return { + name: `Capability '${Capability}' Not Dropped`, + description: msg, + category: "Capability Not Dropped", + location: `container://${Container}`, + osi_layer: "NOT_APPLICABLE", + severity: "LOW", + attributes: { + capability: Capability, + container: Container, + }, + }; +} + +function createNonReadOnlyRootFsFinding({ Container, msg }) { + return { + name: `Container Uses a non ReadOnly Root Filesystem`, + description: msg, + category: "Non ReadOnly Root Filesystem", + location: `container://${Container}`, + osi_layer: "NOT_APPLICABLE", + severity: "LOW", + attributes: { + container: Container, + }, + }; +} + +function createPrivilegedContainerFinding({ Container, msg }) { + return { + name: `Container using Privileged Flag`, + description: msg, + category: "Privileged Container", + location: `container://${Container}`, + osi_layer: "NOT_APPLICABLE", + severity: "HIGH", + attributes: { + container: Container, + }, + }; +} + +function createAutomountedServiceAccountTokenFinding({ msg }) { + return { + name: `Default ServiceAccount uses Automounted Service Account Token`, + description: msg, + category: "Automounted ServiceAccount Token", + location: null, + osi_layer: "NOT_APPLICABLE", + severity: "LOW", + attributes: {}, + }; +} + +function createNonRootUserNotEnforcedFinding({ msg, Container }) { + return { + name: `NonRoot User not enforced for Container`, + description: msg, + category: "Non Root User Not Enforced", + location: `container://${Container}`, + osi_layer: "NOT_APPLICABLE", + severity: "MEDIUM", + attributes: { + container: Container, + }, + }; +} + +function createMissingNetworkPolicyFinding({ msg, Namespace }) { + return { + name: `Namespace "${Namespace}" is missing a Default Deny NetworkPolicy`, + description: msg, + category: "No Default Deny NetworkPolicy", + location: `namespace://${Namespace}`, + osi_layer: "NOT_APPLICABLE", + severity: "MEDIUM", + attributes: { + Namespace: Namespace, + }, + }; +} + +async function parse(fileContent) { + return fileContent + .split("\n") + .filter(Boolean) + .filter((line) => line && line.startsWith("{") && line.endsWith("}")) + .map(JSON.parse) + .map((finding) => { + if (!finding || !finding.AuditResultName) { + return null; + } + + if (finding.AuditResultName === "CapabilityNotDropped") { + return createDropCapabilityFinding(finding); + } + if ( + finding.AuditResultName === "ReadOnlyRootFilesystemFalse" || + finding.AuditResultName === "ReadOnlyRootFilesystemNil" + ) { + return createNonReadOnlyRootFsFinding(finding); + } + if (finding.AuditResultName === "PrivilegedTrue") { + return createPrivilegedContainerFinding(finding); + } + if ( + finding.AuditResultName === + "AutomountServiceAccountTokenTrueAndDefaultSA" + ) { + return createAutomountedServiceAccountTokenFinding(finding); + } + if ( + finding.AuditResultName === "RunAsNonRootCSCFalse" || + finding.AuditResultName === "RunAsNonRootPSCNilCSCNil" || + finding.AuditResultName === "RunAsNonRootPSCFalseCSCNil" + ) { + return createNonRootUserNotEnforcedFinding(finding); + } + if ( + finding.AuditResultName === "MissingDefaultDenyIngressAndEgressNetworkPolicy" + ) { + return createMissingNetworkPolicyFinding(finding); + } + + return null; + }) + .filter(Boolean); +} + +module.exports.parse = parse; diff --git a/scanners/kubeaudit/parser/parser.test.js b/scanners/kubeaudit/parser/parser.test.js new file mode 100644 index 0000000000..a6aadf6072 --- /dev/null +++ b/scanners/kubeaudit/parser/parser.test.js @@ -0,0 +1,18 @@ +const fs = require("fs"); +const util = require("util"); + +// eslint-disable-next-line security/detect-non-literal-fs-filename +const readFile = util.promisify(fs.readFile); + +const { parse } = require("./parser"); + +test("example parser parses empty json to zero findings", async () => { + const fileContent = await readFile( + __dirname + "/__testFiles__/juice-shop.jsonl", + { + encoding: "utf8", + } + ); + + expect(await parse(fileContent)).toMatchSnapshot(); +}); diff --git a/scanners/kubeaudit/scanner/Dockerfile b/scanners/kubeaudit/scanner/Dockerfile new file mode 100644 index 0000000000..dc5d5c51b1 --- /dev/null +++ b/scanners/kubeaudit/scanner/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.15.1 AS builder + +# no need to include cgo bindings +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 + +# this is where we build our app +WORKDIR /go/src/app/ + +RUN git clone https://github.com/Shopify/kubeaudit.git /go/src/app/ +RUN go mod download + +RUN go build -a -ldflags '-w -s -extldflags "-static"' -o /go/bin/kubeaudit ./cmd/ \ + && chmod +x /go/bin/kubeaudit + +FROM alpine:3.12 +COPY --from=builder /go/bin/kubeaudit /kubeaudit +COPY wrapper.sh /wrapper.sh +RUN addgroup --system --gid 1001 kubeaudit && adduser kubeaudit --system --uid 1001 --ingroup kubeaudit +USER 1001 +ENTRYPOINT ["/kubeaudit"] +CMD ["all"] diff --git a/scanners/kubeaudit/scanner/wrapper.sh b/scanners/kubeaudit/scanner/wrapper.sh new file mode 100644 index 0000000000..67a782f987 --- /dev/null +++ b/scanners/kubeaudit/scanner/wrapper.sh @@ -0,0 +1 @@ +/kubeaudit $@ >/home/securecodebox/kubeaudit.jsonl diff --git a/scanners/kubeaudit/templates/kubeaudit-parse-definition.yaml b/scanners/kubeaudit/templates/kubeaudit-parse-definition.yaml new file mode 100644 index 0000000000..808fd2d3c8 --- /dev/null +++ b/scanners/kubeaudit/templates/kubeaudit-parse-definition.yaml @@ -0,0 +1,7 @@ +apiVersion: "execution.securecodebox.io/v1" +kind: ParseDefinition +metadata: + name: "kubeaudit-jsonl" +spec: + handlesResultsType: kubeaudit-jsonl + image: "{{ .Values.parserImage.repository }}:{{ .Values.parserImage.tag | default .Chart.Version }}" diff --git a/scanners/kubeaudit/templates/kubeaudit-rbac.yaml b/scanners/kubeaudit/templates/kubeaudit-rbac.yaml new file mode 100644 index 0000000000..69d7971e43 --- /dev/null +++ b/scanners/kubeaudit/templates/kubeaudit-rbac.yaml @@ -0,0 +1,105 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kubeaudit + namespace: {{ .Release.Namespace}} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kubeaudit-lurcher + namespace: {{ .Release.Namespace}} +subjects: + - kind: ServiceAccount + name: kubeaudit + namespace: {{ .Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: lurcher +--- +{{- if eq .Values.kubeauditScope "namespace" }} +kind: Role +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kubeaudit + namespace: {{ .Release.Namespace}} +rules: + - apiGroups: [""] + resources: + - pods + - podtemplates + - replicationcontrollers + - namespaces + verbs: ["get", "list"] + - apiGroups: ["apps"] + resources: + - daemonsets + - statefulsets + - deployments + verbs: ["get", "list"] + - apiGroups: ["batch"] + resources: + - cronjobs + verbs: ["get", "list"] + - apiGroups: ["networking"] + resources: + - networkpolicies + verbs: ["get", "list"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kubeaudit + namespace: {{ .Release.Namespace}} +subjects: + - kind: ServiceAccount + name: kubeaudit + namespace: {{ .Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kubeaudit +{{- end }} +{{- if eq .Values.kubeauditScope "cluster" }} +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kubeaudit +rules: + - apiGroups: [""] + resources: + - pods + - podtemplates + - replicationcontrollers + - namespaces + verbs: ["get", "list"] + - apiGroups: ["apps"] + resources: + - daemonsets + - statefulsets + - deployments + verbs: ["get", "list"] + - apiGroups: ["batch"] + resources: + - cronjobs + verbs: ["get", "list"] + - apiGroups: ["networking"] + resources: + - networkpolicies + verbs: ["get", "list"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: kubeaudit +subjects: + - kind: ServiceAccount + name: kubeaudit + namespace: {{ .Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kubeaudit +{{- end }} diff --git a/scanners/kubeaudit/templates/kubeaudit-scan-type.yaml b/scanners/kubeaudit/templates/kubeaudit-scan-type.yaml new file mode 100644 index 0000000000..49d0122b2a --- /dev/null +++ b/scanners/kubeaudit/templates/kubeaudit-scan-type.yaml @@ -0,0 +1,39 @@ +apiVersion: "execution.securecodebox.io/v1" +kind: ScanType +metadata: + name: "kubeaudit" +spec: + extractResults: + type: kubeaudit-jsonl + location: "/home/securecodebox/kubeaudit.jsonl" + jobTemplate: + spec: + ttlSecondsAfterFinished: 10 + template: + spec: + restartPolicy: OnFailure + containers: + - name: kubeaudit + image: "securecodebox/scanner-kubeaudit:{{ .Chart.AppVersion }}" + command: + - "sh" + - "/wrapper.sh" + - "all" + - "--exitcode" + - "0" + - "--format" + - "json" + resources: + {{- toYaml .Values.scannerJob.resources | nindent 16 }} + securityContext: + {{- toYaml .Values.scannerJob.securityContext | nindent 16 }} + env: + {{- toYaml .Values.scannerJob.env | nindent 16 }} + volumeMounts: + {{- toYaml .Values.scannerJob.extraVolumeMounts | nindent 16 }} + {{- if .Values.scannerJob.extraContainers }} + {{- toYaml .Values.scannerJob.extraContainers | nindent 12 }} + {{- end }} + volumes: + {{- toYaml .Values.scannerJob.extraVolumeMounts | nindent 12 }} + serviceAccountName: kubeaudit diff --git a/scanners/kubeaudit/values.yaml b/scanners/kubeaudit/values.yaml new file mode 100644 index 0000000000..9f185a3831 --- /dev/null +++ b/scanners/kubeaudit/values.yaml @@ -0,0 +1,51 @@ +parserImage: + # parserImage.tag - defaults to the charts version + # parserImage.repository -- Parser image repository + repository: docker.io/securecodebox/parser-kubeaudit + # parserImage.tag -- Parser image tag + # @default -- defaults to the charts version + tag: null + +scannerJob: + # scannerJob.ttlSecondsAfterFinished -- Defines how long the scanner job after finishing will be available (see: https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/) + ttlSecondsAfterFinished: null + + # scannerJob.resources -- CPU/memory resource requests/limits (see: https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/, https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/) + resources: {} + # resources: + # requests: + # memory: "256Mi" + # cpu: "250m" + # limits: + # memory: "512Mi" + # cpu: "500m" + + # scannerJob.env -- Optional environment variables mapped into each scanJob (see: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) + env: [] + + # scannerJob.extraVolumes -- Optional Volumes mapped into each scanJob (see: https://kubernetes.io/docs/concepts/storage/volumes/) + extraVolumes: [] + + # scannerJob.extraVolumeMounts -- Optional VolumeMounts mapped into each scanJob (see: https://kubernetes.io/docs/concepts/storage/volumes/) + extraVolumeMounts: [] + + # scannerJob.extraContainers -- Optional additional Containers started with each scanJob (see: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) + extraContainers: [] + + # scannerJob.securityContext -- Optional securityContext set on scanner container (see: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) + securityContext: + # scannerJob.securityContext.runAsNonRoot -- Enforces that the scanner image is run as a non root user + runAsNonRoot: true + # scannerJob.securityContext.readOnlyRootFilesystem -- Prevents write access to the containers file system + readOnlyRootFilesystem: true + # scannerJob.securityContext.allowPrivilegeEscalation -- Ensure that users privileges cannot be escalated + allowPrivilegeEscalation: false + # scannerJob.securityContext.privileged -- Ensures that the scanner container is not run in privileged mode + privileged: false + capabilities: + drop: + # scannerJob.securityContext.capabilities.drop[0] -- This drops all linux privileges from the container. + - all + +# kubeauditScope -- Automatically sets up rbac roles for kubeaudit to access the ressources it scans. Can be either "cluster" (ClusterRole) or "namespace" (Role) +kubeauditScope: "namespace" diff --git a/tests/integration/helpers.js b/tests/integration/helpers.js index 4c9e7663db..575489c85c 100644 --- a/tests/integration/helpers.js +++ b/tests/integration/helpers.js @@ -102,7 +102,7 @@ async function disasterRecovery(scanName) { /** * - * @param {string} name name of the scan. Actual name will be sufixed with a random number to avoid conflicts + * @param {string} name name of the scan. Actual name will be suffixed with a random number to avoid conflicts * @param {string} scanType type of the scan. Must match the name of a ScanType CRD * @param {string[]} parameters cli argument to be passed to the scanner * @param {number} timeout in seconds @@ -114,7 +114,7 @@ async function scan(name, scanType, parameters = [], timeout = 180) { apiVersion: "execution.securecodebox.io/v1", kind: "Scan", metadata: { - // Use `generateName` instead of name to generate a random sufix and avoid name clashes + // Use `generateName` instead of name to generate a random suffix and avoid name clashes generateName: `${name}-`, }, spec: { @@ -163,6 +163,8 @@ async function scan(name, scanType, parameters = [], timeout = 180) { * @param {string} name name of the scan. Actual name will be sufixed with a random number to avoid conflicts * @param {string} scanType type of the scan. Must match the name of a ScanType CRD * @param {string[]} parameters cli argument to be passed to the scanner + * @param {string} nameCascade name of cascading scan + * @param {object} matchLabels set invasive and intensive of cascading scan * @param {number} timeout in seconds * @returns {scan.findings} returns findings { categories, severities, count } */ @@ -173,7 +175,7 @@ async function cascadingScan(name, scanType, parameters = [], { nameCascade, mat apiVersion: "execution.securecodebox.io/v1", kind: "Scan", metadata: { - // Use `generateName` instead of name to generate a random sufix and avoid name clashes + // Use `generateName` instead of name to generate a random suffix and avoid name clashes generateName: `${name}-`, }, spec: { diff --git a/tests/integration/scanner/kubeaudit.test.js b/tests/integration/scanner/kubeaudit.test.js new file mode 100644 index 0000000000..a84e860e7c --- /dev/null +++ b/tests/integration/scanner/kubeaudit.test.js @@ -0,0 +1,30 @@ +const { scan } = require("../helpers"); + +test( + "kubeaudit should run and check the jshop in kubeaudit-tests namespace", + async () => { + const { categories, severities } = await scan( + "kubeaudit-tests", + "kubeaudit", + ["-n", "kubeaudit-tests"], + 90 + ); + + expect(categories).toMatchInlineSnapshot(` + Object { + "Automounted ServiceAccount Token": 1, + "Capability Not Dropped": 14, + "No Default Deny NetworkPolicy": 1, + "Non ReadOnly Root Filesystem": 1, + "Non Root User Not Enforced": 1, + } + `); + expect(severities).toMatchInlineSnapshot(` + Object { + "low": 16, + "medium": 2, + } + `); + }, + 5 * 60 * 1000 +);