From 8b87bc84e9cf75d34259d999c324162d2532b0b9 Mon Sep 17 00:00:00 2001 From: Genevieve Luyt <11131143+genevieveluyt@users.noreply.github.com> Date: Thu, 27 Aug 2020 10:36:09 -0400 Subject: [PATCH] Pretty Print (#287) --- README.md | 78 ++++++-- ...rvice-account-token-redundant-override.yml | 2 +- .../service-account-token-true-allowed.yml | 2 +- ...me-allowed-multi-containers-mix-labels.yml | 4 +- ...e-allowed-multi-containers-some-labels.yml | 2 +- .../fixtures/capabilities-some-allowed.yml | 2 +- auditors/hostns/hostns.go | 24 ++- auditors/limits/limits.go | 2 +- cmd/commands/root.go | 30 +-- docs/all.md | 128 +++++++++---- docs/auditors/apparmor.md | 36 +++- docs/auditors/asat.md | 16 +- docs/auditors/capabilities.md | 60 ++++-- docs/auditors/hostns.md | 24 ++- docs/auditors/image.md | 51 ++++- docs/auditors/limits.md | 82 +++++++- docs/auditors/mountds.md | 18 +- docs/auditors/netpols.md | 52 ++++- docs/auditors/nonroot.md | 18 +- docs/auditors/privesc.md | 18 +- docs/auditors/privileged.md | 18 +- docs/auditors/rootfs.md | 18 +- docs/auditors/seccomp.md | 18 +- docs/autofix.md | 8 +- example_custom_test.go | 3 +- example_test.go | 39 +++- internal/color/color.go | 63 ++++++ internal/override/override.go | 10 +- kubeaudit.go | 45 +++-- printer.go | 181 ++++++++++++++++++ result.go | 31 ++- util.go | 25 --- util_test.go | 69 ++++--- 33 files changed, 943 insertions(+), 234 deletions(-) create mode 100644 internal/color/color.go create mode 100644 printer.go diff --git a/README.md b/README.md index 8a5376d0..d3166891 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The rest of this README will focus on how to use kubeaudit as a command line too * [Installation](#installation) * [Quick Start](#quick-start) +* [Audit Results](#audit-results) * [Commands](#commands) * [Configuration File](#configuration-file) * [Override Errors](#override-errors) @@ -41,7 +42,7 @@ Kubeaudit has official releases that are blessed and stable: ### DIY build -Master will have newer features than the stable releases. If you need a newer +Master may have newer features than the stable releases. If you need a newer feature not yet included in a release you can do the following to get kubeaudit: @@ -91,18 +92,32 @@ kubeaudit all -f "/path/to/manifest.yml" Example output: ``` -$ kubeaudit all -f auditors/all/fixtures/audit_all_v1.yml -ERRO[0000] AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/fakeContainerSC' should be added. AuditResultName=AppArmorAnnotationMissing Container=fakeContainerSC MissingAnnotation=container.apparmor.security.beta.kubernetes.io/fakeContainerSC -ERRO[0000] Default serviceAccount with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. AuditResultName=AutomountServiceAccountTokenTrueAndDefaultSA -WARN[0000] Image tag is missing. AuditResultName=ImageTagMissing Container=fakeContainerSC -WARN[0000] Resource limits not set. AuditResultName=LimitsNotSet Container=fakeContainerSC -ERRO[0000] runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. AuditResultName=RunAsNonRootPSCNilCSCNil Container=fakeContainerSC -ERRO[0000] allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. AuditResultName=AllowPrivilegeEscalationNil Container=fakeContainerSC -WARN[0000] privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'. AuditResultName=PrivilegedNil Container=fakeContainerSC -ERRO[0000] readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. AuditResultName=ReadOnlyRootFilesystemNil Container=fakeContainerSC -ERRO[0000] Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. AuditResultName=SeccompAnnotationMissing MissingAnnotation=seccomp.security.alpha.kubernetes.io/pod -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=AUDIT_WRITE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=CHOWN Container=fakeContainerSC +$ kubeaudit all -f "internal/test/fixtures/all_resources/deployment-apps-v1.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + namespace: deployment-apps-v1 + +-------------------------------------------- + +-- [error] AppArmorAnnotationMissing + Message: AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/container' should be added. + Metadata: + Container: container + MissingAnnotation: container.apparmor.security.beta.kubernetes.io/container + +-- [error] AutomountServiceAccountTokenTrueAndDefaultSA + Message: Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. + +-- [error] CapabilityNotDropped + Message: Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. + Metadata: + Container: container + Capability: AUDIT_WRITE ... ``` @@ -137,6 +152,16 @@ Kubeaudit can detect if it is running within a container in a cluster. If so, it kubeaudit all ``` +## Audit Results + +Kubeaudit produces results with three levels of severity: + +`Error`: A security issue or invalid kubernetes configuration +`Warning`: A best practice recommendation +`Info`: Informational, no action required. This includes results that are [overridden](#override-errors) + +The minimum severity level can be set using the `--minSeverity/-m` flag. See [Global Flags](#global-flags) for a more detailed description. + ## Commands | Command | Description | Documentation | @@ -168,11 +193,11 @@ Auditors can also be run individually. | Short | Long | Description | | :------ | :------------- | :-------------------------------------------------------------------------------------------------- | -| -j | --json | Output audit results in JSON | +| | --format | The output format to use (one of "pretty", "logrus", "json") (default is "pretty") | | -c | --kubeconfig | Path to local Kubernetes config file. Only used in local mode (default is `$HOME/.kube/config`) | | -f | --manifest | Path to the yaml configuration to audit. Only used in manifest mode. | -| -n | --namespace | Only audit resources in the specified namespace. Only used in cluster mode. | -| -m | --minseverity | Set the lowest severity level to report (one of "ERROR", "WARN", "INFO") (default "INFO") | +| -n | --namespace | Only audit resources in the specified namespace. Not currently supported in manifest mode. | +| -m | --minseverity | Set the lowest severity level to report (one of "error", "warning", "info") (default "info") | ## Configuration File @@ -180,7 +205,7 @@ Kubeaudit can be used with a configuration file instead of flags. See the [all c ## Override Errors -Security issues can be ignored for specific containers or pods by adding override labels. This means the auditor will produce `WARN` results instead of `ERROR` results. The labels are documented in each auditor's documentation, but the general format for auditors that support overrides is as follows: +Security issues can be ignored for specific containers or pods by adding override labels. This means the auditor will produce `info` results instead of `error` results and the audit result name will have `Allowed` appended to it. The labels are documented in each auditor's documentation, but the general format for auditors that support overrides is as follows: An override label consists of a `key` and a `value`. @@ -195,9 +220,24 @@ container.audit.kubernetes.io/[container name].[override identifier] audit.kubernetes.io/pod.[override identifier] ``` -If the `value` is set to a non-empty string, it will be displayed in the `WARN` result as the `OverrideReason`: +If the `value` is set to a non-empty string, it will be displayed in the `info` result as the `OverrideReason`: ``` -WARN[0000] ... AuditResultName=DockerSocketMounted OverrideReason=AppNeedsAccessToDocker +$ kubeaudit asat -f "auditors/asat/fixtures/service-account-token-true-allowed.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: ReplicationController + metadata: + name: replicationcontroller + namespace: service-account-token-true-allowed + +-------------------------------------------- + +-- [info] AutomountServiceAccountTokenTrueAndDefaultSAAllowed + Message: Audit result overridden: Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. + Metadata: + OverrideReason: SomeReason ``` As per Kubernetes spec, `value` must be 63 characters or less and must be empty or begin and end with an alphanumeric character (`[a-z0-9A-Z]`) with dashes (`-`), underscores (`_`), dots (`.`), and alphanumerics between. diff --git a/auditors/asat/fixtures/service-account-token-redundant-override.yml b/auditors/asat/fixtures/service-account-token-redundant-override.yml index 2aa92ab0..2d885276 100644 --- a/auditors/asat/fixtures/service-account-token-redundant-override.yml +++ b/auditors/asat/fixtures/service-account-token-redundant-override.yml @@ -8,7 +8,7 @@ spec: metadata: labels: name: replicationcontroller - audit.kubernetes.io/pod.allow-automount-service-account-token: "True" + audit.kubernetes.io/pod.allow-automount-service-account-token: "SomeReason" spec: automountServiceAccountToken: false containers: diff --git a/auditors/asat/fixtures/service-account-token-true-allowed.yml b/auditors/asat/fixtures/service-account-token-true-allowed.yml index cd7810a2..919f5ae4 100644 --- a/auditors/asat/fixtures/service-account-token-true-allowed.yml +++ b/auditors/asat/fixtures/service-account-token-true-allowed.yml @@ -8,7 +8,7 @@ spec: metadata: labels: name: replicationcontroller - audit.kubernetes.io/pod.allow-automount-service-account-token: "True" + audit.kubernetes.io/pod.allow-automount-service-account-token: "SomeReason" spec: automountServiceAccountToken: true containers: diff --git a/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-mix-labels.yml b/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-mix-labels.yml index 4fd61974..87694ff6 100644 --- a/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-mix-labels.yml +++ b/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-mix-labels.yml @@ -11,8 +11,8 @@ spec: metadata: labels: name: deployment - audit.kubernetes.io/pod.allow-capability-chown: "True" - container.audit.kubernetes.io/container1.allow-capability-chown: "True" + audit.kubernetes.io/pod.allow-capability-chown: "SomeReason" + container.audit.kubernetes.io/container1.allow-capability-chown: "SomeReason" container.audit.kubernetes.io/container1.allow-capability-sys-time: "SomeReason" container.audit.kubernetes.io/container2.allow-capability-sys-time: "SomeReason" spec: diff --git a/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-some-labels.yml b/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-some-labels.yml index f5805829..8e5dab92 100644 --- a/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-some-labels.yml +++ b/auditors/capabilities/fixtures/capabilities-some-allowed-multi-containers-some-labels.yml @@ -11,7 +11,7 @@ spec: metadata: labels: name: deployment - container.audit.kubernetes.io/container1.allow-capability-chown: "True" + container.audit.kubernetes.io/container1.allow-capability-chown: "SomeReason" container.audit.kubernetes.io/container1.allow-capability-sys-time: "SomeReason" spec: containers: diff --git a/auditors/capabilities/fixtures/capabilities-some-allowed.yml b/auditors/capabilities/fixtures/capabilities-some-allowed.yml index ad5e860a..997a0387 100644 --- a/auditors/capabilities/fixtures/capabilities-some-allowed.yml +++ b/auditors/capabilities/fixtures/capabilities-some-allowed.yml @@ -11,7 +11,7 @@ spec: metadata: labels: name: deployment - audit.kubernetes.io/pod.allow-capability-chown: "True" + audit.kubernetes.io/pod.allow-capability-chown: "SomeReason" audit.kubernetes.io/pod.allow-capability-sys-time: "SomeReason" spec: containers: diff --git a/auditors/hostns/hostns.go b/auditors/hostns/hostns.go index ade44900..0c7a196e 100644 --- a/auditors/hostns/hostns.go +++ b/auditors/hostns/hostns.go @@ -58,6 +58,10 @@ func (a *HostNamespaces) Audit(resource k8stypes.Resource, _ []k8stypes.Resource func auditHostNetwork(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { if podSpec.HostNetwork { + metadata := kubeaudit.Metadata{} + if podSpec.Hostname != "" { + metadata["PodHost"] = podSpec.Hostname + } return &kubeaudit.AuditResult{ Name: NamespaceHostNetworkTrue, Severity: kubeaudit.Error, @@ -65,9 +69,7 @@ func auditHostNetwork(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { PendingFix: &fixHostNetworkTrue{ podSpec: podSpec, }, - Metadata: kubeaudit.Metadata{ - "PodHost": podSpec.Hostname, - }, + Metadata: metadata, } } @@ -76,6 +78,10 @@ func auditHostNetwork(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { func auditHostIPC(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { if podSpec.HostIPC { + metadata := kubeaudit.Metadata{} + if podSpec.Hostname != "" { + metadata["PodHost"] = podSpec.Hostname + } return &kubeaudit.AuditResult{ Name: NamespaceHostIPCTrue, Severity: kubeaudit.Error, @@ -83,9 +89,7 @@ func auditHostIPC(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { PendingFix: &fixHostIPCTrue{ podSpec: podSpec, }, - Metadata: kubeaudit.Metadata{ - "PodHost": podSpec.Hostname, - }, + Metadata: metadata, } } @@ -94,6 +98,10 @@ func auditHostIPC(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { func auditHostPID(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { if podSpec.HostPID { + metadata := kubeaudit.Metadata{} + if podSpec.Hostname != "" { + metadata["PodHost"] = podSpec.Hostname + } return &kubeaudit.AuditResult{ Name: NamespaceHostPIDTrue, Severity: kubeaudit.Error, @@ -101,9 +109,7 @@ func auditHostPID(podSpec *k8stypes.PodSpecV1) *kubeaudit.AuditResult { PendingFix: &fixHostPIDTrue{ podSpec: podSpec, }, - Metadata: kubeaudit.Metadata{ - "PodHost": podSpec.Hostname, - }, + Metadata: metadata, } } diff --git a/auditors/limits/limits.go b/auditors/limits/limits.go index 7de218d9..19f4430b 100644 --- a/auditors/limits/limits.go +++ b/auditors/limits/limits.go @@ -99,7 +99,7 @@ func (limits *Limits) auditContainer(container *k8stypes.ContainerV1) (auditResu Metadata: kubeaudit.Metadata{ "Container": container.Name, "ContainerCpuLimit": cpu, - "maxCPU": maxCPU, + "MaxCPU": maxCPU, }, } auditResults = append(auditResults, auditResult) diff --git a/cmd/commands/root.go b/cmd/commands/root.go index a72d1ca4..5c059c67 100644 --- a/cmd/commands/root.go +++ b/cmd/commands/root.go @@ -2,6 +2,7 @@ package commands import ( "os" + "strings" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -16,7 +17,7 @@ import ( var rootConfig rootFlags type rootFlags struct { - json bool + format string kubeConfig string manifest string namespace string @@ -48,31 +49,36 @@ func Execute() { func init() { RootCmd.PersistentFlags().StringVarP(&rootConfig.kubeConfig, "kubeconfig", "c", "", "Path to local Kubernetes config file. Only used in local mode (default is $HOME/.kube/config)") - RootCmd.PersistentFlags().StringVarP(&rootConfig.minSeverity, "minseverity", "m", "INFO", "Set the lowest severity level to report (one of \"ERROR\", \"WARN\", \"INFO\")") - RootCmd.PersistentFlags().BoolVarP(&rootConfig.json, "json", "j", false, "Output audit results in JSON") + RootCmd.PersistentFlags().StringVarP(&rootConfig.minSeverity, "minseverity", "m", "info", "Set the lowest severity level to report (one of \"error\", \"warning\", \"info\")") + RootCmd.PersistentFlags().StringVarP(&rootConfig.format, "format", "p", "pretty", "The output format to use (one of \"pretty\", \"logrus\", \"json\")") RootCmd.PersistentFlags().StringVarP(&rootConfig.namespace, "namespace", "n", apiv1.NamespaceAll, "Only audit resources in the specified namespace. Not currently supported in manifest mode.") RootCmd.PersistentFlags().StringVarP(&rootConfig.manifest, "manifest", "f", "", "Path to the yaml configuration to audit. Only used in manifest mode.") } // KubeauditLogLevels represents an enum for the supported log levels. -var KubeauditLogLevels = map[string]int{ - "ERROR": kubeaudit.Error, - "WARN": kubeaudit.Warn, - "INFO": kubeaudit.Info, +var KubeauditLogLevels = map[string]kubeaudit.SeverityLevel{ + "error": kubeaudit.Error, + "warn": kubeaudit.Warn, + "warning": kubeaudit.Warn, + "info": kubeaudit.Info, } func runAudit(auditable ...kubeaudit.Auditable) func(cmd *cobra.Command, args []string) { return func(cmd *cobra.Command, args []string) { report := getReport(auditable...) - minSeverity := KubeauditLogLevels[rootConfig.minSeverity] + printOptions := []kubeaudit.PrintOption{ + kubeaudit.WithMinSeverity(KubeauditLogLevels[strings.ToLower(rootConfig.minSeverity)]), + } - var formatter log.Formatter - if rootConfig.json { - formatter = &log.JSONFormatter{} + switch rootConfig.format { + case "json": + printOptions = append(printOptions, kubeaudit.WithFormatter(&log.JSONFormatter{})) + case "logrus": + printOptions = append(printOptions, kubeaudit.WithFormatter(&log.TextFormatter{})) } - report.PrintResults(os.Stdout, minSeverity, formatter) + report.PrintResults(printOptions...) if report.HasErrors() { os.Exit(2) diff --git a/docs/all.md b/docs/all.md index 32ce1ccb..82b61393 100644 --- a/docs/all.md +++ b/docs/all.md @@ -65,33 +65,97 @@ For more details about each auditor, including a description of the auditor-spec ## Examples ``` -$ kubeaudit all -f "auditors/all/fixtures/audit_all_v1.yml" -ERRO[0000] AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/fakeContainerSC' should be added. AuditResultName=AppArmorAnnotationMissing Container=fakeContainerSC MissingAnnotation=container.apparmor.security.beta.kubernetes.io/fakeContainerSC -ERRO[0000] Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. AuditResultName=AutomountServiceAccountTokenTrueAndDefaultSA -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=AUDIT_WRITE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=CHOWN Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=DAC_OVERRIDE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=FOWNER Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=FSETID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=KILL Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=MKNOD Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=NET_BIND_SERVICE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=NET_RAW Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETFCAP Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETGID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETPCAP Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETUID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SYS_CHROOT Container=fakeContainerSC -ERRO[0000] hostNetwork is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostNetworkTrue PodHost= -ERRO[0000] hostIPC is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostIPCTrue PodHost= -ERRO[0000] hostPID is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostPIDTrue PodHost= -WARN[0000] Image tag is missing. AuditResultName=ImageTagMissing Container=fakeContainerSC -WARN[0000] Resource limits not set. AuditResultName=LimitsNotSet Container=fakeContainerSC -ERRO[0000] runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. AuditResultName=RunAsNonRootPSCNilCSCNil Container=fakeContainerSC -ERRO[0000] allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. AuditResultName=AllowPrivilegeEscalationNil Container=fakeContainerSC -WARN[0000] privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'. AuditResultName=PrivilegedNil Container=fakeContainerSC -ERRO[0000] readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. AuditResultName=ReadOnlyRootFilesystemNil Container=fakeContainerSC -ERRO[0000] Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. AuditResultName=SeccompAnnotationMissing MissingAnnotation=seccomp.security.alpha.kubernetes.io/pod +$ kubeaudit all -f "internal/test/fixtures/all_resources/deployment-apps-v1.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Namespace + metadata: + name: deployment-apps-v1 + +-------------------------------------------- + +-- [error] MissingDefaultDenyIngressAndEgressNetworkPolicy + Message: Namespace is missing a default deny ingress and egress NetworkPolicy. + Metadata: + Namespace: deployment-apps-v1 + + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + namespace: deployment-apps-v1 + +-------------------------------------------- + +-- [error] AppArmorAnnotationMissing + Message: AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/container' should be added. + Metadata: + Container: container + MissingAnnotation: container.apparmor.security.beta.kubernetes.io/container + +-- [error] AutomountServiceAccountTokenTrueAndDefaultSA + Message: Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. + +-- [error] CapabilityNotDropped + Message: Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. + Metadata: + Container: container + Capability: AUDIT_WRITE + +-- [error] NamespaceHostNetworkTrue + Message: hostNetwork is set to 'true' in PodSpec. It should be set to 'false'. + Metadata: + PodHost: + +-- [error] NamespaceHostIPCTrue + Message: hostIPC is set to 'true' in PodSpec. It should be set to 'false'. + Metadata: + PodHost: + +-- [error] NamespaceHostPIDTrue + Message: hostPID is set to 'true' in PodSpec. It should be set to 'false'. + Metadata: + PodHost: + +-- [warning] ImageTagMissing + Message: Image tag is missing. + Metadata: + Container: container + +-- [warning] LimitsNotSet + Message: Resource limits not set. + Metadata: + Container: container + +-- [error] RunAsNonRootPSCNilCSCNil + Message: runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. + Metadata: + Container: container + +-- [error] AllowPrivilegeEscalationNil + Message: allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. + Metadata: + Container: container + +-- [warning] PrivilegedNil + Message: privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'. + Metadata: + Container: container + +-- [error] ReadOnlyRootFilesystemNil + Message: readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. + Metadata: + Container: container + +-- [error] SeccompAnnotationMissing + Message: Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. + Metadata: + MissingAnnotation: seccomp.security.alpha.kubernetes.io/pod ``` ### Example with Kubeaudit Config @@ -111,16 +175,6 @@ auditors: The config can be passed to the `all` command using the `-k/--kconfig` flag: ``` $ kubeaudit all -k "config.yaml" -f "auditors/all/fixtures/audit_all_v1.yml" -ERRO[0000] allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. AuditResultName=AllowPrivilegeEscalationNil Container=fakeContainerSC -WARN[0000] privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'. AuditResultName=PrivilegedNil Container=fakeContainerSC -ERRO[0000] Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. AuditResultName=AutomountServiceAccountTokenTrueAndDefaultSA -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=AUDIT_WRITE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=CHOWN Container=fakeContainerSC -ERRO[0000] runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. AuditResultName=RunAsNonRootPSCNilCSCNil Container=fakeContainerSC -ERRO[0000] AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/fakeContainerSC' should be added. AuditResultName=AppArmorAnnotationMissing Container=fakeContainerSC MissingAnnotation=container.apparmor.security.beta.kubernetes.io/fakeContainerSC -ERRO[0000] readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. AuditResultName=ReadOnlyRootFilesystemNil Container=fakeContainerSC -ERRO[0000] Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. AuditResultName=SeccompAnnotationMissing MissingAnnotation=seccomp.security.alpha.kubernetes.io/pod -ERRO[0000] Namespace is missing a default deny ingress and egress NetworkPolicy. AuditResultName=MissingDefaultDenyIngressAndEgressNetworkPolicy Namespace=fakeDeploymentSC ``` ### Example with Flags diff --git a/docs/auditors/apparmor.md b/docs/auditors/apparmor.md index 67e1f5ce..12ac116f 100644 --- a/docs/auditors/apparmor.md +++ b/docs/auditors/apparmor.md @@ -13,15 +13,45 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit apparmor -f "auditors/apparmor/fixtures/apparmor_annotation_missing_v1.yml" -ERRO[0000] AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/AAcontainer' should be added. AuditResultName=AppArmorAnnotationMissing Container=AAcontainer MissingAnnotation=container.apparmor.security.beta.kubernetes.io/AAcontainer +$ kubeaudit apparmor -f "auditors/apparmor/fixtures/apparmor-annotation-missing.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + namespace: apparmor-annotation-missing + +-------------------------------------------- + +-- [error] AppArmorAnnotationMissing + Message: AppArmor annotation missing. The annotation 'container.apparmor.security.beta.kubernetes.io/container' should be added. + Metadata: + MissingAnnotation: container.apparmor.security.beta.kubernetes.io/container + Container: container ``` If an apparmor annotation refers to a container which doesn't exist, `kubectl apply` will fail. Kubeaudit produces an error for this case: ``` $ kubeaudit apparmor -f "auditors/apparmor/fixtures/apparmor-invalid-annotation.yml" -ERRO[0000] AppArmor annotation key refers to a container that doesn't exist. Remove the annotation 'container.apparmor.security.beta.kubernetes.io/container2: runtime/default'. Annotation="container.apparmor.security.beta.kubernetes.io/container2: runtime/default" AuditResultName=AppArmorInvalidAnnotation Container=container + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + namespace: apparmor-enabled + +-------------------------------------------- + +-- [error] AppArmorInvalidAnnotation + Message: AppArmor annotation key refers to a container that doesn't exist. Remove the annotation 'container.apparmor.security.beta.kubernetes.io/container2: runtime/default'. + Metadata: + Container: container2 + Annotation: container.apparmor.security.beta.kubernetes.io/container2: runtime/default ``` ## Explanation diff --git a/docs/auditors/asat.md b/docs/auditors/asat.md index fd8fa71f..395857d5 100644 --- a/docs/auditors/asat.md +++ b/docs/auditors/asat.md @@ -14,8 +14,20 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -kubeaudit asat -f "auditors/asat/fixtures/service_account_token_true_and_no_name_v1.yml" -ERRO[0000] Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. AuditResultName=AutomountServiceAccountTokenTrueAndDefaultSA +kubeaudit asat -f "auditors/asat/fixtures/service-account-token-true-and-no-name.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: ReplicationController + metadata: + name: replicationcontroller + namespace: service-account-token-true-and-no-name + +-------------------------------------------- + +-- [error] AutomountServiceAccountTokenTrueAndDefaultSA + Message: Default service account with token mounted. automountServiceAccountToken should be set to 'false' or a non-default service account should be used. ``` ## Explanation diff --git a/docs/auditors/capabilities.md b/docs/auditors/capabilities.md index 21288efa..50e8572d 100644 --- a/docs/auditors/capabilities.md +++ b/docs/auditors/capabilities.md @@ -37,21 +37,31 @@ Also see [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit capabilities -f "auditors/capabilities/fixtures/capabilities_nil_v1beta2.yml" -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=AUDIT_WRITE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=CHOWN Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=DAC_OVERRIDE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=FOWNER Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=FSETID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=KILL Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=MKNOD Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=NET_BIND_SERVICE Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=NET_RAW Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETFCAP Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETGID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETPCAP Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SETUID Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=SYS_CHROOT Container=fakeContainerSC +$ kubeaudit capabilities -f "auditors/capabilities/fixtures/capabilities-nil.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1beta2 + kind: Deployment + metadata: + name: deployment + namespace: capabilities-nil + +-------------------------------------------- + +-- [error] CapabilityNotDropped + Message: Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. + Metadata: + Container: container + Capability: AUDIT_WRITE + +-- [error] CapabilityNotDropped + Message: Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. + Metadata: + Container: container + Capability: CHOWN + +... ``` ### Example with Custom Drop List @@ -59,9 +69,23 @@ ERRO[0000] Capability not dropped. Ideally, the capability drop list should incl A custom drop list can be provided as a space-separated list of capabilities using the `-d/--drop` flag: ``` -$ kubeaudit capabilities --drop "MAC_ADMIN AUDIT_WRITE" -f "auditors/capabilities/fixtures/capabilities_nil_v1beta2.yml" -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=MAC_ADMIN Container=fakeContainerSC -ERRO[0000] Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. AuditResultName=CapabilityNotDropped Capability=AUDIT_WRITE Container=fakeContainerSC +$ kubeaudit capabilities --drop "MAC_ADMIN AUDIT_WRITE" -f "auditors/capabilities/fixtures/capabilities-nil.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1beta2 + kind: Deployment + metadata: + name: deployment + namespace: capabilities-nil + +-------------------------------------------- + +-- [error] CapabilityNotDropped + Message: Capability not dropped. Ideally, the capability drop list should include the single capability 'ALL' which drops all capabilities. + Metadata: + Container: container + Capability: MAC_ADMIN AUDIT_WRITE ``` **Note**: if using http://man7.org/linux/man-pages/man7/capabilities.7.html as a reference for capability names, drop the `CAP_` prefix. diff --git a/docs/auditors/hostns.md b/docs/auditors/hostns.md index 46bb78ee..c2863020 100644 --- a/docs/auditors/hostns.md +++ b/docs/auditors/hostns.md @@ -13,10 +13,26 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit hostns -f "auditors/hostns/fixtures/namespaces_all_true_v1.yml" -ERRO[0000] hostNetwork is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostNetworkTrue -ERRO[0000] hostIPC is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostIPCTrue -ERRO[0000] hostPID is set to 'true' in PodSpec. It should be set to 'false'. AuditResultName=NamespaceHostPIDTrue +$ kubeaudit hostns -f "auditors/hostns/fixtures/namespaces-all-true.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + namespace: namespaces-all-true + +-------------------------------------------- + +-- [error] NamespaceHostNetworkTrue + Message: hostNetwork is set to 'true' in PodSpec. It should be set to 'false'. + +-- [error] NamespaceHostIPCTrue + Message: hostIPC is set to 'true' in PodSpec. It should be set to 'false'. + +-- [error] NamespaceHostPIDTrue + Message: hostPID is set to 'true' in PodSpec. It should be set to 'false'. ``` ## Explanation diff --git a/docs/auditors/image.md b/docs/auditors/image.md index 675aa00b..73114c8e 100644 --- a/docs/auditors/image.md +++ b/docs/auditors/image.md @@ -20,20 +20,59 @@ Also see [Global Flags](/README.md#global-flags) The image and tag to look for are specified using the `-i/--image image:tag` flag. For example, `-i gcr.io/google_containers/echoserver:1.7` will look for containers using the `gcr.io/google_containers/echoserver` image which have a tag other than `1.7`. ``` -$ kubeaudit image -i "fakeContainerImg:1.6" -f "auditors/image/fixtures/image_tag_present_v1.yml" -ERRO[0000] Container tag is incorrect. It should be set to '1.6'. AuditResultName=ImageTagIncorrect Container=fakeContainerImg +$ kubeaudit image -i "scratch:1.6" -f "auditors/image/fixtures/image-tag-present.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + +-------------------------------------------- + +-- [error] ImageTagIncorrect + Message: Container tag is incorrect. It should be set to '1.6'. + Metadata: + Container: deployment ``` If the container image matches the provided image but the container image has no tag, a warning is produced: ``` -$ kubeaudit image -i "fakeContainerImg:1.6" -f "auditors/image/fixtures/image_tag_missing_v1.yml" -WARN[0000] Image tag is missing. AuditResultName=ImageTagMissing Container=fakeContainerImg +$ kubeaudit image -i "scratch:1.6" -f "auditors/image/fixtures/image-tag-missing.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + +-------------------------------------------- + +-- [warning] ImageTagMissing + Message: Image tag is missing. + Metadata: + Container: container ``` The `image` auditor can be used to find all containers that use an image without a tag by omitting the `-i/--image` flag: ``` -$ kubeaudit image -f "auditors/image/fixtures/image_tag_missing_v1.yml" -WARN[0000] Image tag is missing. AuditResultName=ImageTagMissing Container=fakeContainerImg +$ kubeaudit image -f "auditors/image/fixtures/image-tag-missing.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + +-------------------------------------------- + +-- [warning] ImageTagMissing + Message: Image tag is missing. + Metadata: + Container: container ``` ## Override Errors diff --git a/docs/auditors/limits.md b/docs/auditors/limits.md index c04cdf2e..4c9ef7d6 100644 --- a/docs/auditors/limits.md +++ b/docs/auditors/limits.md @@ -20,27 +20,91 @@ Also see [Global Flags](/README.md#global-flags) The max CPU is specified using the `--cpu` flag: ``` -$ kubeaudit limits --cpu 600m -f "auditors/limits/fixtures/resources_limit_v1beta1.yml" -WARN[0000] CPU limit exceeded. It is set to '750m' which exceeds the max CPU limit of '600m'. AuditResultName=LimitsCPUExceeded Container=fakeContainerLimitOk ContainerCpuLimit=750m maxCPU=600m +$ kubeaudit limits --cpu 600m -f "auditors/limits/fixtures/resources-limit.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + +-------------------------------------------- + +-- [warning] LimitsCPUExceeded + Message: CPU limit exceeded. It is set to '750m' which exceeds the max CPU limit of '600m'. + Metadata: + Container: container + ContainerCpuLimit: 750m + MaxCPU: 600m ``` The max memory is specified using the `--memory` flag: ``` -$ kubeaudit limits --memory 384 -f "auditors/limits/fixtures/resources_limit_v1beta1.yml" -WARN[0000] Memory limit exceeded. It is set to '512Mi' which exceeds the max Memory limit of '384'. AuditResultName=LimitsMemoryExceeded Container=fakeContainerLimitOk ContainerMemoryLimit=512Mi MaxMemory=384 +$ kubeaudit limits --memory 384 -f "auditors/limits/fixtures/resources-limit.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + +-------------------------------------------- + +-- [warning] LimitsMemoryExceeded + Message: Memory limit exceeded. It is set to '512Mi' which exceeds the max Memory limit of '384'. + Metadata: + MaxMemory: 384 + Container: container + ContainerMemoryLimit: 512Mi ``` The CPU and memory can be audited at the same time by including both the `--cpu` and `--memory` flags: ``` -$ kubeaudit limits --cpu 600m --memory 384 -f "auditors/limits/fixtures/resources_limit_v1beta1.yml" -WARN[0000] CPU limit exceeded. It is set to '750m' which exceeds the max CPU limit of '600m'. AuditResultName=LimitsCPUExceeded Container=fakeContainerLimitOk ContainerCpuLimit=750m maxCPU=600m -WARN[0000] Memory limit exceeded. It is set to '512Mi' which exceeds the max Memory limit of '384'. AuditResultName=LimitsMemoryExceeded Container=fakeContainerLimitOk ContainerMemoryLimit=512Mi MaxMemory=384 +$ kubeaudit limits --cpu 600m --memory 384 -f "auditors/limits/fixtures/resources-limit.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + +-------------------------------------------- + +-- [warning] LimitsCPUExceeded + Message: CPU limit exceeded. It is set to '750m' which exceeds the max CPU limit of '600m'. + Metadata: + Container: container + ContainerCpuLimit: 750m + MaxCPU: 600m + +-- [warning] LimitsMemoryExceeded + Message: Memory limit exceeded. It is set to '512Mi' which exceeds the max Memory limit of '384'. + Metadata: + Container: container + ContainerMemoryLimit: 512Mi + MaxMemory: 384 ``` The `limits` auditor can be used to find all containers which do not specify a max CPU or memory by omitting the `--cpu` and `--memory` flags: ``` -$ kubeaudit limits -f "auditors/limits/fixtures/resources_limit_nil_v1beta1.yml" -WARN[0000] Resource limits not set. AuditResultName=LimitsNotSet Container=fakeContainerNoLimit +$ kubeaudit limits -f "auditors/limits/fixtures/resources-limit-nil.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + +-------------------------------------------- + +-- [warning] LimitsNotSet + Message: Resource limits not set. + Metadata: + Container: container ``` ## Override Errors diff --git a/docs/auditors/mountds.md b/docs/auditors/mountds.md index 53714c31..13754fdf 100644 --- a/docs/auditors/mountds.md +++ b/docs/auditors/mountds.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit mountds -f "auditors/mountds/fixtures/docker_sock_mounted.yml" -WARN[0000] Docker socket is mounted. '/var/run/docker.sock' should be removed from the container's volume mount list. AuditResultName=DockerSocketMounted Container=container +$ kubeaudit mountds -f "auditors/mountds/fixtures/docker-sock-mounted.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + namespace: docker-sock-mounted + +-------------------------------------------- + +-- [warning] DockerSocketMounted + Message: Docker socket is mounted. '/var/run/docker.sock' should be removed from the container's volume mount list. + Metadata: + Container: container ``` ## Explanation diff --git a/docs/auditors/netpols.md b/docs/auditors/netpols.md index 41f583aa..78ed1d6f 100644 --- a/docs/auditors/netpols.md +++ b/docs/auditors/netpols.md @@ -13,8 +13,21 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit netpols -f "auditors/netpols/fixtures/namespace_missing_default_deny_netpol.yml" -ERRO[0000] Namespace is missing a default deny ingress and egress NetworkPolicy. AuditResultName=MissingDefaultDenyIngressAndEgressNetworkPolicy Namespace=default +$ kubeaudit netpols -f "auditors/netpols/fixtures/namespace-missing-default-deny-netpol.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Namespace + metadata: + name: namespace-missing-default-deny-netpol + +-------------------------------------------- + +-- [error] MissingDefaultDenyIngressAndEgressNetworkPolicy + Message: Namespace is missing a default deny ingress and egress NetworkPolicy. + Metadata: + Namespace: namespace-missing-default-deny-netpol ``` ## Explanation @@ -79,11 +92,30 @@ spec: podSelector: {} policyTypes: - Egress + +--- + +apiVersion: v1 +kind: Namespace +metadata: + name: my-namespace ``` The `netpols` auditor will produce an error because there is no `deny-all` Network Policy for ingress traffic: ``` -ERRO[0000] All ingress traffic should be blocked by default for namespace my-namespace. AuditResultName=MissingDefaultDenyIngressNetworkPolicy Namespace=my-namespace +---------------- Results for --------------- + + apiVersion: v1 + kind: Namespace + metadata: + name: my-namespace + +-------------------------------------------- + +-- [error] MissingDefaultDenyIngressNetworkPolicy + Message: All ingress traffic should be blocked by default for namespace my-namespace. + Metadata: + Namespace: my-namespace ``` This error can be overridden by adding the `audit.kubernetes.io/namespace.allow-non-default-deny-ingress-network-policy: ""` label to the corresponding Namespace resource: @@ -98,5 +130,17 @@ metadata: The auditor will now produce a warning instead of an error: ``` -WARN[0000] All ingress traffic should be blocked by default for namespace my-namespace. AuditResultName=MissingDefaultDenyIngressNetworkPolicyAllowed Namespace=my-namespace +---------------- Results for --------------- + + apiVersion: v1 + kind: Namespace + metadata: + name: my-namespace + +-------------------------------------------- + +-- [warning] MissingDefaultDenyIngressNetworkPolicyAllowed + Message: All ingress traffic should be blocked by default for namespace my-namespace. + Metadata: + Namespace: my-namespace ``` diff --git a/docs/auditors/nonroot.md b/docs/auditors/nonroot.md index e3355794..770a2de2 100644 --- a/docs/auditors/nonroot.md +++ b/docs/auditors/nonroot.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit nonroot -f "auditors/nonroot/fixtures/run_as_non_root_nil_v1.yml" -ERRO[0000] runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. AuditResultName=RunAsNonRootPSCNilCSCNil Container=fakeContainerRANR +$ kubeaudit nonroot -f "auditors/nonroot/fixtures/run-as-non-root-nil.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment + namespace: run-as-non-root-nil + +-------------------------------------------- + +-- [error] RunAsNonRootPSCNilCSCNil + Message: runAsNonRoot is not set in container SecurityContext nor the PodSecurityContext. It should be set to 'true' in at least one of the two. + Metadata: + Container: container ``` ## Explanation diff --git a/docs/auditors/privesc.md b/docs/auditors/privesc.md index 83258c23..9477e433 100644 --- a/docs/auditors/privesc.md +++ b/docs/auditors/privesc.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit privesc -f "auditors/privesc/fixtures/allow_privilege_escalation_nil_v1.yml" -ERRO[0000] allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. AuditResultName=AllowPrivilegeEscalationNil Container=fakeContainerAPE +$ kubeaudit privesc -f "auditors/privesc/fixtures/allow-privilege-escalation-nil.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: statefulset + namespace: allow-privilege-escalation-nil + +-------------------------------------------- + +-- [error] AllowPrivilegeEscalationNil + Message: allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'. + Metadata: + Container: container ``` ## Explanation diff --git a/docs/auditors/privileged.md b/docs/auditors/privileged.md index 93b98518..d59b3a82 100644 --- a/docs/auditors/privileged.md +++ b/docs/auditors/privileged.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit privileged -f "auditors/privileged/fixtures/privileged_true_v1.yml" -ERRO[0000] privileged is set to 'true' in container SecurityContext. It should be set to 'false'. AuditResultName=PrivilegedTrue Container=fakeContainerPrivileged +$ kubeaudit privileged -f "auditors/privileged/fixtures/privileged-true.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: daemonset + namespace: privileged-true + +-------------------------------------------- + +-- [error] PrivilegedTrue + Message: privileged is set to 'true' in container SecurityContext. It should be set to 'false'. + Metadata: + Container: container ``` ## Explanation diff --git a/docs/auditors/rootfs.md b/docs/auditors/rootfs.md index e944bd21..0de04397 100644 --- a/docs/auditors/rootfs.md +++ b/docs/auditors/rootfs.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit rootfs -f "auditors/rootfs/fixtures/read_only_root_filesystem_nil_v1.yml" -ERRO[0000] readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. AuditResultName=ReadOnlyRootFilesystemNil Container=fakeContainerRORF +$ kubeaudit rootfs -f "auditors/rootfs/fixtures/read-only-root-filesystem-nil.yml" + +---------------- Results for --------------- + + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: statefulset + namespace: read-only-root-filesystem-nil + +-------------------------------------------- + +-- [error] ReadOnlyRootFilesystemNil + Message: readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'. + Metadata: + Container: container ``` ## Explanation diff --git a/docs/auditors/seccomp.md b/docs/auditors/seccomp.md index d1f1b7aa..9de35f91 100644 --- a/docs/auditors/seccomp.md +++ b/docs/auditors/seccomp.md @@ -13,8 +13,22 @@ See [Global Flags](/README.md#global-flags) ## Examples ``` -$ kubeaudit seccomp -f "auditors/seccomp/fixtures/seccomp_annotation_missing_v1.yml" -ERRO[0000] Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. AuditResultName=SeccompAnnotationMissing MissingAnnotation=seccomp.security.alpha.kubernetes.io/pod +$ kubeaudit seccomp -f "auditors/seccomp/fixtures/seccomp-annotation-missing.yml" + +---------------- Results for --------------- + + apiVersion: v1 + kind: Pod + metadata: + name: pod + namespace: seccomp-annotation-missing + +-------------------------------------------- + +-- [error] SeccompAnnotationMissing + Message: Seccomp annotation is missing. The annotation seccomp.security.alpha.kubernetes.io/pod: runtime/default should be added. + Metadata: + MissingAnnotation: seccomp.security.alpha.kubernetes.io/pod ``` ## Explanation diff --git a/docs/autofix.md b/docs/autofix.md index db7e5491..6387d1ca 100644 --- a/docs/autofix.md +++ b/docs/autofix.md @@ -216,11 +216,11 @@ Fixed manifest: apiVersion: apps/v1 kind: Deployment -# DeploymentSpec +# PodSpec spec: - # PodTemplateSpec + # PodTemplate template: - # PodSpec + # ContainerSpec spec: containers: - name: myContainer # this is a sample container @@ -253,6 +253,8 @@ spec: seccomp.security.alpha.kubernetes.io/pod: runtime/default selector: null strategy: {} +metadata: + ``` ### Example with Custom Output File diff --git a/example_custom_test.go b/example_custom_test.go index 4a358cfe..68ee4fd9 100644 --- a/example_custom_test.go +++ b/example_custom_test.go @@ -2,7 +2,6 @@ package kubeaudit_test import ( "fmt" - "os" "strings" "github.com/Shopify/kubeaudit" @@ -105,5 +104,5 @@ func Example_customAuditor() { } // Print the results to screen - report.PrintResults(os.Stdout, kubeaudit.Info, nil) + report.PrintResults() } diff --git a/example_test.go b/example_test.go index f8b80ea1..4ddfdfd2 100644 --- a/example_test.go +++ b/example_test.go @@ -12,6 +12,7 @@ import ( "github.com/Shopify/kubeaudit/config" "github.com/Shopify/kubeaudit/internal/k8s" + "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" ) @@ -49,7 +50,7 @@ metadata: } // Print the audit results to screen - report.PrintResults(os.Stdout, kubeaudit.Error, nil) + report.PrintResults() // Print the plan to screen. These are the steps that will be taken by calling "report.Fix()". fmt.Println("\nPlan:") @@ -84,7 +85,7 @@ func Example_auditLocal() { } // Print the audit results to screen - report.PrintResults(os.Stdout, kubeaudit.Info, nil) + report.PrintResults() } // ExampleAuditCluster shows how to run kubeaudit in cluster mode (only works if kubeaudit is being run from a container insdie of a cluster) @@ -108,7 +109,7 @@ func Example_auditCluster() { } // Print the audit results to screen - report.PrintResults(os.Stdout, kubeaudit.Info, nil) + report.PrintResults() } // ExampleAuditorSubset shows how to run kubeaudit with a subset of auditors @@ -129,7 +130,7 @@ func Example_auditorSubset() { } // Print the audit results to screen - report.PrintResults(os.Stdout, kubeaudit.Info, &log.JSONFormatter{}) + report.PrintResults() } // ExampleConfig shows how to use a kubeaudit with a config file. @@ -169,5 +170,33 @@ func Example_config() { } // Print the audit results to screen - report.PrintResults(os.Stdout, kubeaudit.Error, nil) + report.PrintResults() +} + +// ExamplePrintOptions shows how to use different print options for printing audit results. +func Example_printOptions() { + auditor, err := kubeaudit.New([]kubeaudit.Auditable{apparmor.New()}) + if err != nil { + log.Fatal(err) + } + + report, err := auditor.AuditLocal("", k8s.ClientOptions{}) + if err != nil { + log.Fatal(err) + } + + // Print the audit results to a file + f, err := os.Create("output.txt") + if err != nil { + log.Fatal(err) + } + defer f.Close() + defer os.Remove("output.txt") + report.PrintResults(kubeaudit.WithWriter(f)) + + // Only print audit results with severity of Error (ignore info and warning) + report.PrintResults(kubeaudit.WithMinSeverity(kubeaudit.Error)) + + // Print results as JSON + report.PrintResults(kubeaudit.WithFormatter(&logrus.JSONFormatter{})) } diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 00000000..b8fc1a1c --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,63 @@ +package color + +import "runtime" + +var Reset = "\033[0m" +var RedColor = "\033[31m" +var GreenColor = "\033[32m" +var YellowColor = "\033[33m" +var BlueColor = "\033[34m" +var PurpleColor = "\033[35m" +var CyanColor = "\033[36m" +var GrayColor = "\033[37m" +var WhiteColor = "\033[97m" + +func Red(s string) string { + return Colored(RedColor, s) +} + +func Green(s string) string { + return Colored(GreenColor, s) +} + +func Yellow(s string) string { + return Colored(YellowColor, s) +} + +func Blue(s string) string { + return Colored(BlueColor, s) +} + +func Purple(s string) string { + return Colored(PurpleColor, s) +} + +func Cyan(s string) string { + return Colored(CyanColor, s) +} + +func Gray(s string) string { + return Colored(GrayColor, s) +} + +func White(s string) string { + return Colored(WhiteColor, s) +} + +func Colored(color, s string) string { + return color + s + Reset +} + +func init() { + if runtime.GOOS == "windows" { + Reset = "" + RedColor = "" + GreenColor = "" + YellowColor = "" + BlueColor = "" + PurpleColor = "" + CyanColor = "" + GrayColor = "" + WhiteColor = "" + } +} diff --git a/internal/override/override.go b/internal/override/override.go index 0d84eb7b..679f3849 100644 --- a/internal/override/override.go +++ b/internal/override/override.go @@ -1,6 +1,8 @@ package override import ( + "strings" + "github.com/Shopify/kubeaudit" "github.com/Shopify/kubeaudit/internal/k8s" "github.com/Shopify/kubeaudit/k8stypes" @@ -50,12 +52,10 @@ func ApplyOverride(auditResult *kubeaudit.AuditResult, containerName string, res auditResult.Name = GetOverriddenResultName(auditResult.Name) auditResult.PendingFix = nil + auditResult.Severity = kubeaudit.Info + auditResult.Message = "Audit result overridden: " + auditResult.Message - if auditResult.Severity == kubeaudit.Error { - auditResult.Severity = kubeaudit.Warn - } - - if overrideReason != "" && overrideReason != "true" { + if overrideReason != "" && strings.ToLower(overrideReason) != "true" { if auditResult.Metadata == nil { auditResult.Metadata = make(kubeaudit.Metadata) } diff --git a/kubeaudit.go b/kubeaudit.go index e460de70..3e60b2f7 100644 --- a/kubeaudit.go +++ b/kubeaudit.go @@ -105,7 +105,6 @@ import ( "github.com/Shopify/kubeaudit/internal/k8s" "github.com/Shopify/kubeaudit/k8stypes" - log "github.com/sirupsen/logrus" ) // Kubeaudit provides functions to audit and fix Kubernetes manifests @@ -216,6 +215,26 @@ func (r *Report) Results() []Result { return results } +// ResultsWithMinSeverity returns the audit results for each Kubernetes resource with a minimum severity +func (r *Report) ResultsWithMinSeverity(minSeverity SeverityLevel) []Result { + var results []Result + for _, result := range r.results { + var filteredAuditResults []*AuditResult + for _, auditResult := range result.GetAuditResults() { + if auditResult.Severity >= minSeverity { + filteredAuditResults = append(filteredAuditResults, auditResult) + } + } + if len(filteredAuditResults) > 0 { + results = append(results, &workloadResult{ + Resource: result.GetResource(), + AuditResults: filteredAuditResults, + }) + } + } + return results +} + // HasErrors returns true if any findings have the level of Error func (r *Report) HasErrors() (errorsFound bool) { for _, workloadResult := range r.Results() { @@ -228,26 +247,10 @@ func (r *Report) HasErrors() (errorsFound bool) { return false } -// PrintResults writes the audit results with a severity greater than or matching minSeverity in a human-readable -// way to the provided writer -func (r *Report) PrintResults(writer io.Writer, minSeverity int, formatter log.Formatter) { - resultLogger := log.New() - - resultLogger.SetOutput(writer) - if formatter != nil { - resultLogger.SetFormatter(formatter) - } - - // We manually manage what severity levels to log, lorgus should let everything through - resultLogger.SetLevel(log.DebugLevel) - - for _, workloadResult := range r.Results() { - for _, auditResult := range workloadResult.GetAuditResults() { - if auditResult.Severity >= minSeverity { - logAuditResult(auditResult, resultLogger) - } - } - } +// PrintResults writes the audit results to the specified writer. Defaults to printing results to stdout +func (r *Report) PrintResults(printOptions ...PrintOption) { + printer := NewPrinter(printOptions...) + printer.PrintReport(r) } // Fix tries to automatically patch any security concerns and writes the resulting manifest to the provided writer. diff --git a/printer.go b/printer.go new file mode 100644 index 00000000..cbf98df7 --- /dev/null +++ b/printer.go @@ -0,0 +1,181 @@ +package kubeaudit + +import ( + "fmt" + "io" + "os" + + "github.com/Shopify/kubeaudit/internal/color" + "github.com/Shopify/kubeaudit/internal/k8s" + "github.com/Shopify/kubeaudit/k8stypes" + log "github.com/sirupsen/logrus" +) + +type Printer struct { + writer io.Writer + minSeverity SeverityLevel + formatter log.Formatter + color bool +} + +type PrintOption func(p *Printer) + +func WithMinSeverity(minSeverity SeverityLevel) PrintOption { + return func(p *Printer) { + p.minSeverity = minSeverity + } +} + +func WithWriter(writer io.Writer) PrintOption { + return func(p *Printer) { + p.writer = writer + } +} + +func WithFormatter(formatter log.Formatter) PrintOption { + return func(p *Printer) { + p.formatter = formatter + } +} + +func (p *Printer) parseOptions(opts ...PrintOption) { + for _, opt := range opts { + opt(p) + } +} + +func NewPrinter(opts ...PrintOption) Printer { + p := Printer{ + writer: os.Stdout, + minSeverity: Info, + } + p.parseOptions(opts...) + if p.writer == os.Stdout { + p.color = true + } + return p +} + +func (p *Printer) PrintReport(report *Report) { + if p.formatter == nil { + p.prettyPrintReport(report) + } else { + p.logReport(report) + } +} + +func (p *Printer) prettyPrintReport(report *Report) { + for _, workloadResult := range report.ResultsWithMinSeverity(p.minSeverity) { + resource := workloadResult.GetResource().Object() + groupVersionKind := resource.GetObjectKind().GroupVersionKind() + resourceName := k8s.GetObjectMeta(resource).GetName() + resourceNamespace := k8s.GetObjectMeta(resource).GetNamespace() + + p.printColor(color.CyanColor, "\n---------------- Results for ---------------\n\n") + p.printColor(color.CyanColor, " apiVersion: ") + if groupVersionKind.Group != "" { + p.printColor(color.CyanColor, groupVersionKind.Group+"/") + } + p.printColor(color.CyanColor, groupVersionKind.Version+"\n") + p.printColor(color.CyanColor, (" kind: " + groupVersionKind.Kind + "\n")) + if resourceName != "" || resourceNamespace != "" { + p.printColor(color.CyanColor, " metadata:\n") + if resourceName != "" { + p.printColor(color.CyanColor, " name: "+resourceName+"\n") + } + if resourceNamespace != "" { + p.printColor(color.CyanColor, " namespace: "+resourceNamespace+"\n") + } + } + p.printColor(color.CyanColor, "\n--------------------------------------------\n\n") + + for _, auditResult := range workloadResult.GetAuditResults() { + severityColor := color.YellowColor + switch auditResult.Severity { + case Info: + severityColor = color.CyanColor + case Warn: + severityColor = color.YellowColor + case Error: + severityColor = color.RedColor + } + p.print("-- ") + p.printColor(severityColor, "["+auditResult.Severity.String()+"] ") + p.print(auditResult.Name + "\n") + p.print(" Message: " + auditResult.Message + "\n") + if len(auditResult.Metadata) > 0 { + p.print(" Metadata:\n") + } + for k, v := range auditResult.Metadata { + p.print(fmt.Sprintf(" %s: %s\n", k, v)) + } + p.print("\n") + } + } +} + +func (p *Printer) print(s string) { + fmt.Fprint(p.writer, s) +} + +func (p *Printer) printColor(c string, s string) { + if p.color { + fmt.Fprint(p.writer, color.Colored(c, s)) + } else { + p.print(s) + } +} + +func (p *Printer) logReport(report *Report) { + resultLogger := log.New() + resultLogger.SetOutput(p.writer) + resultLogger.SetFormatter(p.formatter) + + // We manually manage what severity levels to log, lorgus should let everything through + resultLogger.SetLevel(log.DebugLevel) + + for _, workloadResult := range report.ResultsWithMinSeverity(p.minSeverity) { + for _, auditResult := range workloadResult.GetAuditResults() { + p.logAuditResult(workloadResult.GetResource().Object(), auditResult, resultLogger) + } + } +} + +func (p *Printer) logAuditResult(resource k8stypes.Resource, result *AuditResult, baseLogger *log.Logger) { + logger := baseLogger.WithFields(p.getLogFieldsForResult(resource, result)) + switch result.Severity { + case Info: + logger.Info(result.Message) + case Warn: + logger.Warn(result.Message) + case Error: + logger.Error(result.Message) + } +} + +func (p *Printer) getLogFieldsForResult(resource k8stypes.Resource, result *AuditResult) log.Fields { + groupVersionKind := resource.GetObjectKind().GroupVersionKind() + resourceMetadata := k8s.GetObjectMeta(resource) + + fields := log.Fields{ + "AuditResultName": result.Name, + "ResourceVersion": groupVersionKind.Version, + "ResourceKind": groupVersionKind.Kind, + "ResourceGroup": groupVersionKind.Group, + "ResourceNamespace": resourceMetadata.GetNamespace(), + } + + if fields["ResourceGroup"] == "" { + fields["ResourceGroup"] = "core" + } + + if resourceMetadata.GetName() != "" { + fields["ResourceName"] = resourceMetadata.GetName() + } + + for k, v := range result.Metadata { + fields[k] = v + } + + return fields +} diff --git a/result.go b/result.go index 62605346..76dc9814 100644 --- a/result.go +++ b/result.go @@ -5,13 +5,13 @@ import "github.com/Shopify/kubeaudit/k8stypes" // AuditResult severity levels. They also correspond to log levels const ( // Info is used for informational audit results where no action is required - Info = iota + Info SeverityLevel = 0 // Warn is used for audit results where there may be security concerns. If an auditor is disabled for a resource // using an override label, the audit results will be warnings instead of errors. Kubeaudit will NOT attempt to // fix these - Warn + Warn SeverityLevel = 1 // Error is used for audit results where action is required. Kubeaudit will attempt to fix these - Error + Error SeverityLevel = 2 ) // Result contains the audit results for a single Kubernetes resource @@ -20,13 +20,28 @@ type Result interface { GetAuditResults() []*AuditResult } +type SeverityLevel int + +func (s SeverityLevel) String() string { + switch s { + case Info: + return "info" + case Warn: + return "warning" + case Error: + return "error" + default: + return "unknown" + } +} + // AuditResult represents a potential security issue. There may be multiple AuditResults per resource and audit type AuditResult struct { - Name string // Name uniquely identifies a type of audit result - Severity int // Severity is one of Error, Warn, or Info - Message string // Message is a human-readable description of the audit result - PendingFix PendingFix // PendingFix is the fix that will be applied to automatically fix the security issue - Metadata Metadata // Metadata includes additional context for an audit result + Name string // Name uniquely identifies a type of audit result + Severity SeverityLevel // Severity is one of Error, Warn, or Info + Message string // Message is a human-readable description of the audit result + PendingFix PendingFix // PendingFix is the fix that will be applied to automatically fix the security issue + Metadata Metadata // Metadata includes additional context for an audit result } func (result *AuditResult) Fix(resource k8stypes.Resource) (newResources []k8stypes.Resource) { diff --git a/util.go b/util.go index 1cc6bbc4..261a56d3 100644 --- a/util.go +++ b/util.go @@ -6,7 +6,6 @@ import ( "github.com/Shopify/kubeaudit/internal/k8s" "github.com/Shopify/kubeaudit/k8stypes" - log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "k8s.io/client-go/kubernetes" ) @@ -101,27 +100,3 @@ func unwrapResources(resources []KubeResource) []k8stypes.Resource { } return unwrappedResources } - -func logAuditResult(result *AuditResult, baseLogger *log.Logger) { - logger := baseLogger.WithFields(getLogFieldsForResult(result)) - switch result.Severity { - case Info: - logger.Info(result.Message) - case Warn: - logger.Warn(result.Message) - case Error: - logger.Error(result.Message) - } -} - -func getLogFieldsForResult(result *AuditResult) log.Fields { - fields := log.Fields{ - "AuditResultName": result.Name, - } - - for k, v := range result.Metadata { - fields[k] = v - } - - return fields -} diff --git a/util_test.go b/util_test.go index 825e96b4..3d023c55 100644 --- a/util_test.go +++ b/util_test.go @@ -14,15 +14,14 @@ import ( ) type logEntry struct { - AuditResultName string - Foo string - Level string `json:"level"` -} - -var levelString = map[int]string{ - Error: "error", - Warn: "warning", - Info: "info", + AuditResultName string + Foo string + Level string `json:"level"` + ResourceKind string + ResourceGroup string + ResourceVersion string + ResourceName string + ResourceNamespace string } func TestGetResourcesFromClientset(t *testing.T) { @@ -46,27 +45,32 @@ func TestPrintResults(t *testing.T) { newTestAuditResult(Warn), newTestAuditResult(Info), }, + Resource: &kubeResource{ + object: k8stypes.NewPod(), + }, }, }, } out := bytes.NewBuffer(nil) + writerOption := WithWriter(out) + formatterOption := WithFormatter(&log.JSONFormatter{}) // Error results only - report.PrintResults(out, Error, &log.JSONFormatter{}) + report.PrintResults(writerOption, WithMinSeverity(Error), formatterOption) assert.Equal(t, 1, bytes.Count(out.Bytes(), []byte{'\n'})) out.Reset() // Error and warn results - report.PrintResults(out, Warn, &log.JSONFormatter{}) + report.PrintResults(writerOption, WithMinSeverity(Warn), formatterOption) assert.Equal(t, 2, bytes.Count(out.Bytes(), []byte{'\n'})) out.Reset() // Error, warn, and info results - report.PrintResults(out, Info, &log.JSONFormatter{}) + report.PrintResults(writerOption, WithMinSeverity(Info), formatterOption) assert.Equal(t, 3, bytes.Count(out.Bytes(), []byte{'\n'})) } -func newTestAuditResult(severity int) *AuditResult { +func newTestAuditResult(severity SeverityLevel) *AuditResult { return &AuditResult{ Name: "MyAuditResult", Severity: severity, @@ -75,22 +79,41 @@ func newTestAuditResult(severity int) *AuditResult { } func TestLogAuditResult(t *testing.T) { - // Send all log output as JSON to this byte buffer - out := bytes.NewBuffer(nil) - logger := log.New() - logger.SetFormatter(&log.JSONFormatter{}) - logger.SetOutput(out) + for _, severity := range []SeverityLevel{Error, Warn, Info} { + // Send all log output as JSON to this byte buffer + out := bytes.NewBuffer(nil) + + resource := k8stypes.NewDeployment() + resource.Name = "mydeployment" + resource.Namespace = "mynamespace" - for _, severity := range []int{Error, Warn, Info} { auditResult := newTestAuditResult(severity) + report := &Report{ + results: []Result{ + &workloadResult{ + AuditResults: []*AuditResult{ + auditResult, + }, + Resource: &kubeResource{ + object: resource, + }, + }, + }, + } expected := logEntry{ - AuditResultName: "MyAuditResult", - Level: levelString[severity], - Foo: auditResult.Metadata["Foo"], + AuditResultName: "MyAuditResult", + Level: severity.String(), + Foo: auditResult.Metadata["Foo"], + ResourceKind: resource.Kind, + ResourceVersion: resource.GroupVersionKind().Version, + ResourceGroup: resource.GroupVersionKind().Group, + ResourceName: resource.GetName(), + ResourceNamespace: resource.GetNamespace(), } // This writes the log to the variable out, parses the JSON into the logEntry struct, and checks the struct - logAuditResult(auditResult, logger) + printer := NewPrinter(WithWriter(out), WithFormatter(&log.JSONFormatter{})) + printer.PrintReport(report) got := logEntry{} err := json.Unmarshal(out.Bytes(), &got) assert.NoError(t, err)