diff --git a/.gitignore b/.gitignore index d886a4db..26bb0065 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage.txt /vendor /.vscode .go-version +profile.out diff --git a/README.md b/README.md index f5ee9fd4..83c27466 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,11 @@ The minimum severity level can be set using the `--minSeverity/-m` flag. By default kubeaudit will output results in a human-readable way. If the output is intended to be further processed, it can be set to output JSON using the `--format json` flag. To output results as logs (the previous default) use `--format logrus`. Some output formats include colors to make results easier to read in a terminal. To disable colors (for example, if you are sending output to a text file), you can use the `--no-color` flag. +You can generate a kubeaudit report in [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html) using the `--format sarif` flag. To write the SARIF results to a file, you can redirect the output with `>`. For example: +``` +kubeaudit all -f path-to-my-file.yaml --format="sarif" > example.sarif +``` + If there are results of severity level `error`, kubeaudit will exit with exit code 2. This can be changed using the `--exitcode/-e` flag. For all the ways kubeaudit can be customized, see [Global Flags](#global-flags). @@ -212,7 +217,7 @@ Auditors can also be run individually. | Short | Long | Description | | :---- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | -| | --format | The output format to use (one of "pretty", "logrus", "json") (default is "pretty") | +| | --format | The output format to use (one of "sarif", "pretty", "logrus", "json") (default is "pretty") | | | --kubeconfig | Path to local Kubernetes config file. Only used in local mode (default is `$HOME/.kube/config`) | | -c | --context | The name of the kubeconfig context to use | | -f | --manifest | Path to the yaml configuration to audit. Only used in manifest mode. You may use `-` to read from stdin. | diff --git a/auditors/apparmor/apparmor.go b/auditors/apparmor/apparmor.go index c11c1379..24e820d8 100644 --- a/auditors/apparmor/apparmor.go +++ b/auditors/apparmor/apparmor.go @@ -64,7 +64,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isAppArmorAnnotationMissing(containerAnnotation, annotations) { return &kubeaudit.AuditResult{ - Name: AppArmorAnnotationMissing, + Auditor: Name, + Rule: AppArmorAnnotationMissing, Severity: kubeaudit.Error, Message: fmt.Sprintf("AppArmor annotation missing. The annotation '%s' should be added.", containerAnnotation), Metadata: kubeaudit.Metadata{ @@ -80,7 +81,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isAppArmorDisabled(containerAnnotation, annotations) { return &kubeaudit.AuditResult{ - Name: AppArmorDisabled, + Auditor: Name, + Rule: AppArmorDisabled, Message: fmt.Sprintf("AppArmor is disabled. The apparmor annotation should be set to '%s' or start with '%s'.", ProfileRuntimeDefault, ProfileNamePrefix), Severity: kubeaudit.Error, Metadata: kubeaudit.Metadata{ @@ -107,7 +109,8 @@ func auditPodAnnotations(resource k8s.Resource, containerNames []string) []*kube containerName := strings.Split(annotationKey, "/")[1] if !contains(containerNames, containerName) { auditResults = append(auditResults, &kubeaudit.AuditResult{ - Name: AppArmorInvalidAnnotation, + Auditor: Name, + Rule: AppArmorInvalidAnnotation, Severity: kubeaudit.Error, Message: fmt.Sprintf("AppArmor annotation key refers to a container that doesn't exist. Remove the annotation '%s: %s'.", annotationKey, annotationValue), Metadata: kubeaudit.Metadata{ diff --git a/auditors/asat/asat.go b/auditors/asat/asat.go index c5fa3034..c9ef1e3f 100644 --- a/auditors/asat/asat.go +++ b/auditors/asat/asat.go @@ -29,7 +29,7 @@ func New() *AutomountServiceAccountToken { // being automatically mounted func (a *AutomountServiceAccountToken) Audit(resource k8s.Resource, resources []k8s.Resource) ([]*kubeaudit.AuditResult, error) { auditResult := auditResource(resource, resources) - auditResult = override.ApplyOverride(auditResult, "", resource, OverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, OverrideLabel) if auditResult != nil { return []*kubeaudit.AuditResult{auditResult}, nil } @@ -45,7 +45,8 @@ func auditResource(resource k8s.Resource, resources []k8s.Resource) *kubeaudit.A if isDeprecatedServiceAccountName(podSpec) && !hasServiceAccountName(podSpec) { return &kubeaudit.AuditResult{ - Name: AutomountServiceAccountTokenDeprecated, + Auditor: Name, + Rule: AutomountServiceAccountTokenDeprecated, Severity: kubeaudit.Warn, Message: "serviceAccount is a deprecated alias for serviceAccountName. serviceAccountName should be used instead.", PendingFix: &fixDeprecatedServiceAccountName{ @@ -60,7 +61,8 @@ func auditResource(resource k8s.Resource, resources []k8s.Resource) *kubeaudit.A defaultServiceAccount := getDefaultServiceAccount(resources) if usesDefaultServiceAccount(podSpec) && isAutomountTokenTrue(podSpec, defaultServiceAccount) { return &kubeaudit.AuditResult{ - Name: AutomountServiceAccountTokenTrueAndDefaultSA, + Auditor: Name, + Rule: AutomountServiceAccountTokenTrueAndDefaultSA, Severity: kubeaudit.Error, Message: "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.", PendingFix: &fixDefaultServiceAccountWithAutomountToken{ diff --git a/auditors/capabilities/capabilities.go b/auditors/capabilities/capabilities.go index 693dd69e..f433c432 100644 --- a/auditors/capabilities/capabilities.go +++ b/auditors/capabilities/capabilities.go @@ -49,7 +49,7 @@ func (a *Capabilities) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeau for _, capability := range uniqueCapabilities(container) { for _, auditResult := range auditContainer(container, capability, a.allowAddList) { - auditResult = override.ApplyOverride(auditResult, container.Name, resource, getOverrideLabel(capability)) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, getOverrideLabel(capability)) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -75,7 +75,8 @@ func auditContainer(container *k8s.ContainerV1, capability string, allowAddList if IsCapabilityInAddList(container, capability) { message := fmt.Sprintf("Capability \"%s\" added. It should be removed from the capability add list. If you need this capability, add an override label such as '%s: SomeReason'.", capability, override.GetContainerOverrideLabel(container.Name, getOverrideLabel(capability))) auditResult := &kubeaudit.AuditResult{ - Name: CapabilityAdded, + Auditor: Name, + Rule: CapabilityAdded, Severity: kubeaudit.Error, Message: message, PendingFix: &fixCapabilityAdded{ @@ -103,7 +104,8 @@ func auditContainerForDropAll(container *k8s.ContainerV1) *kubeaudit.AuditResult if !SecurityContextOrCapabilities(container) { message := "Security Context not set. The Security Context should be specified and all Capabilities should be dropped by setting the Drop list to ALL." return &kubeaudit.AuditResult{ - Name: CapabilityOrSecurityContextMissing, + Auditor: Name, + Rule: CapabilityOrSecurityContextMissing, Severity: kubeaudit.Error, Message: message, PendingFix: &fixMissingSecurityContextOrCapability{ @@ -118,7 +120,8 @@ func auditContainerForDropAll(container *k8s.ContainerV1) *kubeaudit.AuditResult if !IsDropAll(container) { message := "Capability Drop list should be set to ALL. Add the specific ones you need to the Add list and set an override label." return &kubeaudit.AuditResult{ - Name: CapabilityShouldDropAll, + Auditor: Name, + Rule: CapabilityShouldDropAll, Severity: kubeaudit.Error, Message: message, PendingFix: &fixCapabilityNotDroppedAll{ diff --git a/auditors/deprecatedapis/depreceatedapis.go b/auditors/deprecatedapis/depreceatedapis.go index eaad682c..60e67347 100644 --- a/auditors/deprecatedapis/depreceatedapis.go +++ b/auditors/deprecatedapis/depreceatedapis.go @@ -119,7 +119,8 @@ func (deprecatedAPIs *DeprecatedAPIs) Audit(resource k8s.Resource, _ []k8s.Resou } } auditResult := &kubeaudit.AuditResult{ - Name: DeprecatedAPIUsed, + Auditor: Name, + Rule: DeprecatedAPIUsed, Severity: severity, Message: deprecationMessage, Metadata: metadata, diff --git a/auditors/deprecatedapis/depreceatedapis_test.go b/auditors/deprecatedapis/depreceatedapis_test.go index 749617f6..e6bd2cc0 100644 --- a/auditors/deprecatedapis/depreceatedapis_test.go +++ b/auditors/deprecatedapis/depreceatedapis_test.go @@ -53,8 +53,12 @@ func TestAuditDeprecatedAPIs(t *testing.T) { require.Nil(t, err) report := test.AuditManifest(t, fixtureDir, tc.file, auditor, []string{DeprecatedAPIUsed}) assertReport(t, report, tc.expectedSeverity, message, metadata) + report = test.AuditLocal(t, fixtureDir, tc.file, auditor, fmt.Sprintf("%s-%d", strings.Split(tc.file, ".")[0], i), []string{DeprecatedAPIUsed}) - assertReport(t, report, tc.expectedSeverity, message, metadata) + + if report != nil { + assertReport(t, report, tc.expectedSeverity, message, metadata) + } }) } } diff --git a/auditors/hostns/hostns.go b/auditors/hostns/hostns.go index 8c897ae3..232a43e4 100644 --- a/auditors/hostns/hostns.go +++ b/auditors/hostns/hostns.go @@ -46,7 +46,7 @@ func (a *HostNamespaces) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kube {auditHostPID, HostPIDOverrideLabel}, } { auditResult := check.auditFunc(podSpec) - auditResult = override.ApplyOverride(auditResult, "", resource, check.overrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, check.overrideLabel) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -62,7 +62,8 @@ func auditHostNetwork(podSpec *k8s.PodSpecV1) *kubeaudit.AuditResult { metadata["PodHost"] = podSpec.Hostname } return &kubeaudit.AuditResult{ - Name: NamespaceHostNetworkTrue, + Auditor: Name, + Rule: NamespaceHostNetworkTrue, Severity: kubeaudit.Error, Message: "hostNetwork is set to 'true' in PodSpec. It should be set to 'false'.", PendingFix: &fixHostNetworkTrue{ @@ -82,7 +83,8 @@ func auditHostIPC(podSpec *k8s.PodSpecV1) *kubeaudit.AuditResult { metadata["PodHost"] = podSpec.Hostname } return &kubeaudit.AuditResult{ - Name: NamespaceHostIPCTrue, + Auditor: Name, + Rule: NamespaceHostIPCTrue, Severity: kubeaudit.Error, Message: "hostIPC is set to 'true' in PodSpec. It should be set to 'false'.", PendingFix: &fixHostIPCTrue{ @@ -102,7 +104,8 @@ func auditHostPID(podSpec *k8s.PodSpecV1) *kubeaudit.AuditResult { metadata["PodHost"] = podSpec.Hostname } return &kubeaudit.AuditResult{ - Name: NamespaceHostPIDTrue, + Auditor: Name, + Rule: NamespaceHostPIDTrue, Severity: kubeaudit.Error, Message: "hostPID is set to 'true' in PodSpec. It should be set to 'false'.", PendingFix: &fixHostPIDTrue{ diff --git a/auditors/image/image.go b/auditors/image/image.go index 9be19f3a..5059fdc3 100644 --- a/auditors/image/image.go +++ b/auditors/image/image.go @@ -50,7 +50,8 @@ func auditContainer(container *k8s.ContainerV1, image string) *kubeaudit.AuditRe if isImageTagMissing(containerTag) { return &kubeaudit.AuditResult{ - Name: ImageTagMissing, + Auditor: Name, + Rule: ImageTagMissing, Severity: kubeaudit.Warn, Message: "Image tag is missing.", Metadata: kubeaudit.Metadata{ @@ -61,7 +62,8 @@ func auditContainer(container *k8s.ContainerV1, image string) *kubeaudit.AuditRe if isImageTagIncorrect(name, tag, containerName, containerTag) { return &kubeaudit.AuditResult{ - Name: ImageTagIncorrect, + Auditor: Name, + Rule: ImageTagIncorrect, Severity: kubeaudit.Error, Message: fmt.Sprintf("Container tag is incorrect. It should be set to '%s'.", tag), Metadata: kubeaudit.Metadata{ @@ -72,7 +74,8 @@ func auditContainer(container *k8s.ContainerV1, image string) *kubeaudit.AuditRe if isImageCorrect(name, tag, containerName, containerTag) { return &kubeaudit.AuditResult{ - Name: ImageCorrect, + Auditor: Name, + Rule: ImageCorrect, Severity: kubeaudit.Info, Message: "Image tag is correct", Metadata: kubeaudit.Metadata{ diff --git a/auditors/limits/limits.go b/auditors/limits/limits.go index 833457a0..7ad46ff8 100644 --- a/auditors/limits/limits.go +++ b/auditors/limits/limits.go @@ -65,7 +65,8 @@ func (limits *Limits) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaud func (limits *Limits) auditContainer(container *k8s.ContainerV1) (auditResults []*kubeaudit.AuditResult) { if isLimitsNil(container) { auditResult := &kubeaudit.AuditResult{ - Name: LimitsNotSet, + Auditor: Name, + Rule: LimitsNotSet, Severity: kubeaudit.Warn, Message: "Resource limits not set.", Metadata: kubeaudit.Metadata{ @@ -81,7 +82,8 @@ func (limits *Limits) auditContainer(container *k8s.ContainerV1) (auditResults [ if isCPULimitUnset(container) { auditResult := &kubeaudit.AuditResult{ - Name: LimitsCPUNotSet, + Auditor: Name, + Rule: LimitsCPUNotSet, Severity: kubeaudit.Warn, Message: "Resource CPU limit not set.", Metadata: kubeaudit.Metadata{ @@ -92,7 +94,8 @@ func (limits *Limits) auditContainer(container *k8s.ContainerV1) (auditResults [ } else if exceedsCPULimit(container, limits) { maxCPU := limits.maxCPU.String() auditResult := &kubeaudit.AuditResult{ - Name: LimitsCPUExceeded, + Auditor: Name, + Rule: LimitsCPUExceeded, Severity: kubeaudit.Warn, Message: fmt.Sprintf("CPU limit exceeded. It is set to '%s' which exceeds the max CPU limit of '%s'.", cpu, maxCPU), Metadata: kubeaudit.Metadata{ @@ -106,7 +109,8 @@ func (limits *Limits) auditContainer(container *k8s.ContainerV1) (auditResults [ if isMemoryLimitUnset(container) { auditResult := &kubeaudit.AuditResult{ - Name: LimitsMemoryNotSet, + Auditor: Name, + Rule: LimitsMemoryNotSet, Severity: kubeaudit.Warn, Message: "Resource Memory limit not set.", Metadata: kubeaudit.Metadata{ @@ -117,7 +121,8 @@ func (limits *Limits) auditContainer(container *k8s.ContainerV1) (auditResults [ } else if exceedsMemoryLimit(container, limits) { maxMemory := limits.maxMemory.String() auditResult := &kubeaudit.AuditResult{ - Name: LimitsMemoryExceeded, + Auditor: Name, + Rule: LimitsMemoryExceeded, Severity: kubeaudit.Warn, Message: fmt.Sprintf("Memory limit exceeded. It is set to '%s' which exceeds the max Memory limit of '%s'.", memory, maxMemory), Metadata: kubeaudit.Metadata{ diff --git a/auditors/mounts/mounts.go b/auditors/mounts/mounts.go index c94141ac..81b7d38d 100644 --- a/auditors/mounts/mounts.go +++ b/auditors/mounts/mounts.go @@ -61,7 +61,7 @@ func (sensitive *SensitivePathMounts) Audit(resource k8s.Resource, _ []k8s.Resou for _, container := range k8s.GetContainers(resource) { for _, auditResult := range auditContainer(container, sensitiveVolumes) { - auditResult = override.ApplyOverride(auditResult, container.Name, resource, getOverrideLabel(auditResult.Metadata[MountNameMetadataKey])) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, getOverrideLabel(auditResult.Metadata[MountNameMetadataKey])) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -100,7 +100,8 @@ func auditContainer(container *k8s.ContainerV1, sensitiveVolumes map[string]v1.V for _, mount := range container.VolumeMounts { if volume, ok := sensitiveVolumes[mount.Name]; ok { auditResults = append(auditResults, &kubeaudit.AuditResult{ - Name: SensitivePathsMounted, + Auditor: Name, + Rule: SensitivePathsMounted, Severity: kubeaudit.Error, Message: fmt.Sprintf("Sensitive path mounted as volume: %s (hostPath: %s). It should be removed from the container's mounts list.", mount.Name, volume.HostPath.Path), Metadata: kubeaudit.Metadata{ diff --git a/auditors/netpols/netpols.go b/auditors/netpols/netpols.go index e2c1f88a..6938eccd 100644 --- a/auditors/netpols/netpols.go +++ b/auditors/netpols/netpols.go @@ -72,7 +72,8 @@ func auditNetworkPolicy(networkPolicy *k8s.NetworkPolicyV1) []*kubeaudit.AuditRe if allIngressTrafficAllowed(networkPolicy) { auditResult := &kubeaudit.AuditResult{ - Name: AllowAllIngressNetworkPolicyExists, + Auditor: Name, + Rule: AllowAllIngressNetworkPolicyExists, Severity: kubeaudit.Warn, Message: "Found allow all ingress traffic NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -84,7 +85,8 @@ func auditNetworkPolicy(networkPolicy *k8s.NetworkPolicyV1) []*kubeaudit.AuditRe if allEgressTrafficAllowed(networkPolicy) { auditResult := &kubeaudit.AuditResult{ - Name: AllowAllEgressNetworkPolicyExists, + Auditor: Name, + Rule: AllowAllEgressNetworkPolicyExists, Severity: kubeaudit.Warn, Message: "Found allow all egress traffic NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -108,7 +110,8 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou if hasCatchAllNetPol { if !hasDefaultDenyIngress { auditResult := &kubeaudit.AuditResult{ - Name: MissingDefaultDenyIngressNetworkPolicy, + Auditor: Name, + Rule: MissingDefaultDenyIngressNetworkPolicy, Severity: kubeaudit.Error, Message: fmt.Sprintf("All ingress traffic should be blocked by default for namespace %s.", namespace), Metadata: kubeaudit.Metadata{ @@ -119,13 +122,14 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou policyType: Ingress, }, } - auditResult = override.ApplyOverride(auditResult, "", resource, IngressOverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, IngressOverrideLabel) auditResults = append(auditResults, auditResult) } if !hasDefaultDenyEgress { auditResult := &kubeaudit.AuditResult{ - Name: MissingDefaultDenyEgressNetworkPolicy, + Auditor: Name, + Rule: MissingDefaultDenyEgressNetworkPolicy, Severity: kubeaudit.Error, Message: fmt.Sprintf("All egress traffic should be blocked by default for namespace %s.", namespace), Metadata: kubeaudit.Metadata{ @@ -136,7 +140,7 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou policyType: Egress, }, } - auditResult = override.ApplyOverride(auditResult, "", resource, EgressOverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, EgressOverrideLabel) auditResults = append(auditResults, auditResult) } @@ -149,7 +153,8 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou if !hasIngressOverride && !hasEgressOverride { auditResult := &kubeaudit.AuditResult{ - Name: MissingDefaultDenyIngressAndEgressNetworkPolicy, + Auditor: Name, + Rule: MissingDefaultDenyIngressAndEgressNetworkPolicy, Severity: kubeaudit.Error, Message: "Namespace is missing a default deny ingress and egress NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -165,7 +170,8 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou if hasIngressOverride && hasEgressOverride { auditResult := &kubeaudit.AuditResult{ - Name: override.GetOverriddenResultName(MissingDefaultDenyIngressAndEgressNetworkPolicy), + Auditor: Name, + Rule: override.GetOverriddenResultName(MissingDefaultDenyIngressAndEgressNetworkPolicy), Severity: kubeaudit.Warn, Message: "Namespace is missing a default deny ingress and egress NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -179,7 +185,8 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou // At this point there is exactly one override label for either ingress or egress which means one needs to be // fixed and the other is overridden auditResult := &kubeaudit.AuditResult{ - Name: MissingDefaultDenyIngressNetworkPolicy, + Auditor: Name, + Rule: MissingDefaultDenyIngressNetworkPolicy, Severity: kubeaudit.Error, Message: "Namespace is missing a default deny ingress NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -190,11 +197,12 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou namespace: namespace, }, } - auditResult = override.ApplyOverride(auditResult, "", resource, IngressOverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, IngressOverrideLabel) auditResults = append(auditResults, auditResult) auditResult = &kubeaudit.AuditResult{ - Name: MissingDefaultDenyEgressNetworkPolicy, + Auditor: Name, + Rule: MissingDefaultDenyEgressNetworkPolicy, Severity: kubeaudit.Error, Message: "Namespace is missing a default deny egress NetworkPolicy.", Metadata: kubeaudit.Metadata{ @@ -205,7 +213,7 @@ func auditNetworkPoliciesForDenyAll(resource k8s.Resource, resources []k8s.Resou namespace: namespace, }, } - auditResult = override.ApplyOverride(auditResult, "", resource, EgressOverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, "", resource, EgressOverrideLabel) auditResults = append(auditResults, auditResult) return auditResults diff --git a/auditors/nonroot/nonroot.go b/auditors/nonroot/nonroot.go index 8a09ca2d..5452c1cc 100644 --- a/auditors/nonroot/nonroot.go +++ b/auditors/nonroot/nonroot.go @@ -38,7 +38,7 @@ func (a *RunAsNonRoot) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeau for _, container := range k8s.GetContainers(resource) { auditResult := auditContainer(container, resource) - auditResult = override.ApplyOverride(auditResult, container.Name, resource, OverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, OverrideLabel) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -56,7 +56,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if !isContainerRunAsUserNil(container) { if *container.SecurityContext.RunAsUser == 0 { return &kubeaudit.AuditResult{ - Name: RunAsUserCSCRoot, + Auditor: Name, + Rule: RunAsUserCSCRoot, Severity: kubeaudit.Error, Message: "runAsUser is set to UID 0 (root user) in the container SecurityContext. Either set it to a value > 0 or remove it and set runAsNonRoot to true.", PendingFix: &fixRunAsNonRoot{ @@ -71,7 +72,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if !isPodRunAsUserNil(podSpec) { if *podSpec.SecurityContext.RunAsUser == 0 { return &kubeaudit.AuditResult{ - Name: RunAsUserPSCRoot, + Auditor: Name, + Rule: RunAsUserPSCRoot, Severity: kubeaudit.Warn, Message: "runAsUser is set to UID 0 (root user) in the PodSecurityContext. Either set it to a value > 0 or remove it and set runAsNonRoot to true.", Metadata: kubeaudit.Metadata{ @@ -87,7 +89,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if !isPodRunAsUserNil(podSpec) { if *podSpec.SecurityContext.RunAsUser == 0 { return &kubeaudit.AuditResult{ - Name: RunAsUserPSCRoot, + Auditor: Name, + Rule: RunAsUserPSCRoot, Severity: kubeaudit.Error, Message: "runAsUser is set to UID 0 (root user) in the PodSecurityContext. Either set it to a value > 0 or remove it and set runAsNonRoot to true.", PendingFix: &fixRunAsNonRoot{ @@ -104,7 +107,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isContainerRunAsNonRootCSCFalse(container) { return &kubeaudit.AuditResult{ - Name: RunAsNonRootCSCFalse, + Auditor: Name, + Rule: RunAsNonRootCSCFalse, Severity: kubeaudit.Error, Message: "runAsNonRoot is set to false in the container SecurityContext. Either set it to true or set runAsUser to a value > 0.", PendingFix: &fixRunAsNonRoot{ @@ -119,7 +123,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isContainerRunAsNonRootNil(container) { if isPodRunAsNonRootNil(podSpec) { return &kubeaudit.AuditResult{ - Name: RunAsNonRootPSCNilCSCNil, + Auditor: Name, + Rule: RunAsNonRootPSCNilCSCNil, Severity: kubeaudit.Error, Message: "runAsNonRoot should be set to true or runAsUser should be set to a value > 0 either in the container SecurityContext or PodSecurityContext.", PendingFix: &fixRunAsNonRoot{ @@ -133,7 +138,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isPodRunAsNonRootFalse(podSpec) { return &kubeaudit.AuditResult{ - Name: RunAsNonRootPSCFalseCSCNil, + Auditor: Name, + Rule: RunAsNonRootPSCFalseCSCNil, Severity: kubeaudit.Error, Message: "runAsNonRoot is set to false in the PodSecurityContext. Either set it to true or set runAsUser to a value > 0.", PendingFix: &fixRunAsNonRoot{ diff --git a/auditors/privesc/privesc.go b/auditors/privesc/privesc.go index 924e8d59..95a86929 100644 --- a/auditors/privesc/privesc.go +++ b/auditors/privesc/privesc.go @@ -32,7 +32,7 @@ func (a *AllowPrivilegeEscalation) Audit(resource k8s.Resource, _ []k8s.Resource for _, container := range k8s.GetContainers(resource) { auditResult := auditContainer(container) - auditResult = override.ApplyOverride(auditResult, container.Name, resource, OverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, OverrideLabel) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -44,7 +44,8 @@ func (a *AllowPrivilegeEscalation) Audit(resource k8s.Resource, _ []k8s.Resource func auditContainer(container *k8s.ContainerV1) *kubeaudit.AuditResult { if isAllowPrivilegeEscalationNil(container) { return &kubeaudit.AuditResult{ - Name: AllowPrivilegeEscalationNil, + Auditor: Name, + Rule: AllowPrivilegeEscalationNil, Severity: kubeaudit.Error, Message: "allowPrivilegeEscalation not set which allows privilege escalation. It should be set to 'false'.", PendingFix: &fixBySettingAllowPrivilegeEscalationFalse{ @@ -58,7 +59,8 @@ func auditContainer(container *k8s.ContainerV1) *kubeaudit.AuditResult { if isAllowPrivilegeEscalationTrue(container) { return &kubeaudit.AuditResult{ - Name: AllowPrivilegeEscalationTrue, + Auditor: Name, + Rule: AllowPrivilegeEscalationTrue, Severity: kubeaudit.Error, Message: "allowPrivilegeEscalation set to 'true'. It should be set to 'false'.", PendingFix: &fixBySettingAllowPrivilegeEscalationFalse{ diff --git a/auditors/privileged/privileged.go b/auditors/privileged/privileged.go index 0fa9ee50..1d276834 100644 --- a/auditors/privileged/privileged.go +++ b/auditors/privileged/privileged.go @@ -31,7 +31,7 @@ func (a *Privileged) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudi for _, container := range k8s.GetContainers(resource) { auditResult := auditContainer(container, resource) - auditResult = override.ApplyOverride(auditResult, container.Name, resource, OverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, OverrideLabel) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -43,7 +43,8 @@ func (a *Privileged) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudi func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudit.AuditResult { if isPrivilegedNil(container) { return &kubeaudit.AuditResult{ - Name: PrivilegedNil, + Auditor: Name, + Rule: PrivilegedNil, Severity: kubeaudit.Warn, Message: "privileged is not set in container SecurityContext. Privileged defaults to 'false' but it should be explicitly set to 'false'.", PendingFix: &fixPrivileged{ @@ -57,7 +58,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isPrivilegedTrue(container) { return &kubeaudit.AuditResult{ - Name: PrivilegedTrue, + Auditor: Name, + Rule: PrivilegedTrue, Severity: kubeaudit.Error, Message: "privileged is set to 'true' in container SecurityContext. It should be set to 'false'.", PendingFix: &fixPrivileged{ diff --git a/auditors/rootfs/rootfs.go b/auditors/rootfs/rootfs.go index 044590a4..a9e44e8f 100644 --- a/auditors/rootfs/rootfs.go +++ b/auditors/rootfs/rootfs.go @@ -31,7 +31,7 @@ func (a *ReadOnlyRootFilesystem) Audit(resource k8s.Resource, _ []k8s.Resource) for _, container := range k8s.GetContainers(resource) { auditResult := auditContainer(container, resource) - auditResult = override.ApplyOverride(auditResult, container.Name, resource, OverrideLabel) + auditResult = override.ApplyOverride(auditResult, Name, container.Name, resource, OverrideLabel) if auditResult != nil { auditResults = append(auditResults, auditResult) } @@ -43,7 +43,8 @@ func (a *ReadOnlyRootFilesystem) Audit(resource k8s.Resource, _ []k8s.Resource) func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudit.AuditResult { if isReadOnlyRootFilesystemNil(container) { return &kubeaudit.AuditResult{ - Name: ReadOnlyRootFilesystemNil, + Auditor: Name, + Rule: ReadOnlyRootFilesystemNil, Severity: kubeaudit.Error, Message: "readOnlyRootFilesystem is not set in container SecurityContext. It should be set to 'true'.", PendingFix: &fixReadOnlyRootFilesystem{ @@ -57,7 +58,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isReadOnlyRootFilesystemFalse(container) { return &kubeaudit.AuditResult{ - Name: ReadOnlyRootFilesystemFalse, + Auditor: Name, + Rule: ReadOnlyRootFilesystemFalse, Severity: kubeaudit.Error, Message: "readOnlyRootFilesystem is set to 'false' in container SecurityContext. It should be set to 'true'.", PendingFix: &fixReadOnlyRootFilesystem{ diff --git a/auditors/seccomp/seccomp.go b/auditors/seccomp/seccomp.go index 0f612207..3f01d319 100644 --- a/auditors/seccomp/seccomp.go +++ b/auditors/seccomp/seccomp.go @@ -76,7 +76,8 @@ func auditPod(resource k8s.Resource) *kubeaudit.AuditResult { } return &kubeaudit.AuditResult{ - Name: SeccompAnnotationMissing, + Auditor: Name, + Rule: SeccompAnnotationMissing, Severity: kubeaudit.Error, Message: fmt.Sprintf("Seccomp annotation is missing. The annotation %s: %s should be added.", PodAnnotationKey, ProfileRuntimeDefault), PendingFix: &fix.ByAddingPodAnnotation{ @@ -93,7 +94,8 @@ func auditPod(resource k8s.Resource) *kubeaudit.AuditResult { if isSeccompProfileDeprecated(podSeccompProfile) { return &kubeaudit.AuditResult{ - Name: SeccompDeprecatedPod, + Auditor: Name, + Rule: SeccompDeprecatedPod, Severity: kubeaudit.Error, Message: fmt.Sprintf("Seccomp pod annotation is set to deprecated value %s. It should be set to %s instead.", podSeccompProfile, ProfileRuntimeDefault), PendingFix: &fix.BySettingPodAnnotation{ @@ -109,7 +111,8 @@ func auditPod(resource k8s.Resource) *kubeaudit.AuditResult { if !isSeccompEnabled(podSeccompProfile) { return &kubeaudit.AuditResult{ - Name: SeccompDisabledPod, + Auditor: Name, + Rule: SeccompDisabledPod, Severity: kubeaudit.Error, Message: fmt.Sprintf("Seccomp pod annotation is set to %s which disables Seccomp. It should be set to the default profile %s or should start with %s.", podSeccompProfile, ProfileRuntimeDefault, ProfileNamePrefix), PendingFix: &fix.BySettingPodAnnotation{ @@ -154,7 +157,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if isSeccompProfileDeprecated(containerSeccompProfile) { return &kubeaudit.AuditResult{ - Name: SeccompDeprecatedContainer, + Auditor: Name, + Rule: SeccompDeprecatedContainer, Severity: kubeaudit.Error, Message: fmt.Sprintf("Seccomp container annotation is set to deprecated value %s. It should be set to %s instead.", containerSeccompProfile, ProfileRuntimeDefault), PendingFix: pendingFix, @@ -167,7 +171,8 @@ func auditContainer(container *k8s.ContainerV1, resource k8s.Resource) *kubeaudi if !isSeccompEnabled(containerSeccompProfile) { return &kubeaudit.AuditResult{ - Name: SeccompDisabledContainer, + Auditor: Name, + Rule: SeccompDisabledContainer, Severity: kubeaudit.Error, Message: fmt.Sprintf("Seccomp container annotation is set to %s which disables Seccomp. It should be set to the default profile %s or should start with %s.", containerSeccompProfile, ProfileRuntimeDefault, ProfileNamePrefix), PendingFix: pendingFix, diff --git a/cmd/commands/root.go b/cmd/commands/root.go index 631f00d9..b9f05d7c 100644 --- a/cmd/commands/root.go +++ b/cmd/commands/root.go @@ -12,6 +12,7 @@ import ( "github.com/Shopify/kubeaudit/auditors/all" "github.com/Shopify/kubeaudit/config" "github.com/Shopify/kubeaudit/internal/k8sinternal" + "github.com/Shopify/kubeaudit/internal/sarif" ) var rootConfig rootFlags @@ -52,7 +53,7 @@ func init() { RootCmd.PersistentFlags().StringVarP(&rootConfig.kubeConfig, "kubeconfig", "", "", "Path to local Kubernetes config file. Only used in local mode (default is $HOME/.kube/config)") RootCmd.PersistentFlags().StringVarP(&rootConfig.context, "context", "c", "", "The name of the kubeconfig context to use") 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.format, "format", "p", "pretty", "The output format to use (one of \"sarif\",\"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().BoolVarP(&rootConfig.includeGenerated, "includegenerated", "g", false, "Include generated resources in scan (eg. pods generated by deployments).") RootCmd.PersistentFlags().BoolVar(&rootConfig.noColor, "no-color", false, "Don't produce colored output.") @@ -78,6 +79,13 @@ func runAudit(auditable ...kubeaudit.Auditable) func(cmd *cobra.Command, args [] } switch rootConfig.format { + case "sarif": + sarifReport, err := sarif.Create(report) + if err != nil { + log.WithError(err).Fatal("Error generating the SARIF output") + } + sarifReport.PrettyWrite(os.Stdout) + return case "json": printOptions = append(printOptions, kubeaudit.WithFormatter(&log.JSONFormatter{})) case "logrus": @@ -99,6 +107,7 @@ func getReport(auditors ...kubeaudit.Auditable) *kubeaudit.Report { var f *os.File if rootConfig.manifest == "-" { f = os.Stdin + rootConfig.manifest = "" } else { manifest, err := os.Open(rootConfig.manifest) if err != nil { @@ -108,7 +117,7 @@ func getReport(auditors ...kubeaudit.Auditable) *kubeaudit.Report { f = manifest } - report, err := auditor.AuditManifest(f) + report, err := auditor.AuditManifest(rootConfig.manifest, f) if err != nil { log.WithError(err).Fatal("Error auditing manifest") } diff --git a/example_custom_test.go b/example_custom_test.go index b4d50deb..9f15d4f7 100644 --- a/example_custom_test.go +++ b/example_custom_test.go @@ -29,7 +29,8 @@ type myAuditor struct{} func (a *myAuditor) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudit.AuditResult, error) { return []*kubeaudit.AuditResult{ { - Name: "MyAudit", + Auditor: "Awesome", + Rule: "MyAudit", Severity: kubeaudit.Error, Message: "My custom error", PendingFix: &myAuditorFix{ @@ -98,7 +99,7 @@ func Example_customAuditor() { } // Run the audit in the mode of your choosing. Here we use manifest mode. - report, err := auditor.AuditManifest(strings.NewReader(manifest)) + report, err := auditor.AuditManifest("", strings.NewReader(manifest)) if err != nil { log.Fatal(err) } diff --git a/example_test.go b/example_test.go index 41c51e04..79cfbf59 100644 --- a/example_test.go +++ b/example_test.go @@ -43,7 +43,7 @@ metadata: } // Run the audit in manifest mode - report, err := auditor.AuditManifest(strings.NewReader(manifest)) + report, err := auditor.AuditManifest("", strings.NewReader(manifest)) if err != nil { log.Fatal(err) } @@ -136,7 +136,7 @@ metadata: } // Run the audit in the mode of your choosing. Here we use manifest mode. - report, err := auditor.AuditManifest(strings.NewReader(manifest)) + report, err := auditor.AuditManifest("", strings.NewReader(manifest)) if err != nil { log.Fatal(err) } @@ -188,7 +188,7 @@ metadata: } // Run the audit in the mode of your choosing. Here we use manifest mode. - report, err := auditor.AuditManifest(strings.NewReader(manifest)) + report, err := auditor.AuditManifest("", strings.NewReader(manifest)) if err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index be981126..8fae50a1 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,11 @@ module github.com/Shopify/kubeaudit require ( github.com/jetstack/cert-manager v1.6.1 + github.com/owenrumney/go-sarif/v2 v2.1.2 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.0 + github.com/xeipuuv/gojsonschema v1.2.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.24.3 k8s.io/apiextensions-apiserver v0.23.5 @@ -47,6 +49,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.4.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect diff --git a/go.sum b/go.sum index a4619bf7..5b63da88 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,7 @@ github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:C github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -889,6 +890,10 @@ github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3 github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE= +github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= +github.com/owenrumney/go-sarif/v2 v2.1.2 h1:PMDK7tXShJ9zsB7bfvlpADH5NEw1dfA9xwU8Xtdj73U= +github.com/owenrumney/go-sarif/v2 v2.1.2/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -978,7 +983,6 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -1059,11 +1063,16 @@ github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:tw github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= @@ -1077,6 +1086,7 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -1407,7 +1417,6 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/sarif/fixtures/apparmor-invalid.yaml b/internal/sarif/fixtures/apparmor-invalid.yaml new file mode 100644 index 00000000..a67bf336 --- /dev/null +++ b/internal/sarif/fixtures/apparmor-invalid.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod + namespace: apparmor-disabled + annotations: + container.apparmor.security.beta.kubernetes.io/container: badval diff --git a/internal/sarif/fixtures/apparmor-valid.yaml b/internal/sarif/fixtures/apparmor-valid.yaml new file mode 100644 index 00000000..7833c2e3 --- /dev/null +++ b/internal/sarif/fixtures/apparmor-valid.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod + namespace: apparmor-enabled + annotations: + container.apparmor.security.beta.kubernetes.io/container: localhost/something +spec: + containers: + - name: container + image: scratch diff --git a/internal/sarif/fixtures/capabilities-added.yaml b/internal/sarif/fixtures/capabilities-added.yaml new file mode 100644 index 00000000..a504c250 --- /dev/null +++ b/internal/sarif/fixtures/capabilities-added.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +spec: + selector: + matchLabels: + name: deployment + template: + metadata: + labels: + name: deployment + spec: + containers: + - name: container + image: scratch + securityContext: + capabilities: + add: + - NET_ADMIN + - CHOWN + drop: + - ALL diff --git a/internal/sarif/fixtures/image-tag-present.yaml b/internal/sarif/fixtures/image-tag-present.yaml new file mode 100644 index 00000000..a24e6ac5 --- /dev/null +++ b/internal/sarif/fixtures/image-tag-present.yaml @@ -0,0 +1,16 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment +spec: + selector: + matchLabels: + name: deployment + template: + metadata: + labels: + name: deployment + spec: + containers: + - name: deployment + image: scratch:1.5 diff --git a/internal/sarif/fixtures/limits-nil.yaml b/internal/sarif/fixtures/limits-nil.yaml new file mode 100644 index 00000000..e26bd505 --- /dev/null +++ b/internal/sarif/fixtures/limits-nil.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - name: container + image: scratch diff --git a/internal/sarif/rules.go b/internal/sarif/rules.go new file mode 100644 index 00000000..9944acda --- /dev/null +++ b/internal/sarif/rules.go @@ -0,0 +1,35 @@ +package sarif + +import ( + "github.com/Shopify/kubeaudit/auditors/apparmor" + "github.com/Shopify/kubeaudit/auditors/asat" + "github.com/Shopify/kubeaudit/auditors/capabilities" + "github.com/Shopify/kubeaudit/auditors/deprecatedapis" + "github.com/Shopify/kubeaudit/auditors/hostns" + "github.com/Shopify/kubeaudit/auditors/image" + "github.com/Shopify/kubeaudit/auditors/limits" + "github.com/Shopify/kubeaudit/auditors/mounts" + "github.com/Shopify/kubeaudit/auditors/netpols" + "github.com/Shopify/kubeaudit/auditors/nonroot" + "github.com/Shopify/kubeaudit/auditors/privesc" + "github.com/Shopify/kubeaudit/auditors/privileged" + "github.com/Shopify/kubeaudit/auditors/rootfs" + "github.com/Shopify/kubeaudit/auditors/seccomp" +) + +var allAuditors = map[string]string{ + apparmor.Name: "Finds containers that do not have AppArmor enabled", + asat.Name: "Finds containers where the deprecated SA field is used or with a mounted default SA", + capabilities.Name: "Finds containers that do not drop the recommended capabilities or add new ones", + deprecatedapis.Name: "Finds any resource defined with a deprecated API version", + hostns.Name: "Finds containers that have HostPID, HostIPC or HostNetwork enabled", + image.Name: "Finds containers which do not use the desired version of an image (via the tag) or use an image without a tag", + limits.Name: "Finds containers which exceed the specified CPU and memory limits or do not specify any", + mounts.Name: "Finds containers that have sensitive host paths mounted", + netpols.Name: "Finds namespaces that do not have a default-deny network policy", + nonroot.Name: "Finds containers allowed to run as root", + privesc.Name: "Finds containers that allow privilege escalation", + privileged.Name: "Finds containers running as privileged", + rootfs.Name: "Finds containers which do not have a read-only filesystem", + seccomp.Name: "Finds containers running without seccomp", +} diff --git a/internal/sarif/rules_test.go b/internal/sarif/rules_test.go new file mode 100644 index 00000000..1737392a --- /dev/null +++ b/internal/sarif/rules_test.go @@ -0,0 +1,17 @@ +package sarif + +import ( + "testing" + + "github.com/Shopify/kubeaudit/auditors/all" + "github.com/stretchr/testify/assert" +) + +func TestAuditorsLengthAndDescription(t *testing.T) { + // if new auditors are created + // make sure they're added with a matching description + for _, auditorName := range all.AuditorNames { + description, ok := allAuditors[auditorName] + assert.Truef(t, ok && description != "", "missing description for auditor %s", auditorName) + } +} diff --git a/internal/sarif/sarif.go b/internal/sarif/sarif.go new file mode 100644 index 00000000..07c77174 --- /dev/null +++ b/internal/sarif/sarif.go @@ -0,0 +1,87 @@ +package sarif + +import ( + "bytes" + "fmt" + "strings" + + "github.com/Shopify/kubeaudit" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +const repoURL = "https://github.com/Shopify/kubeaudit" + +// Create generates new sarif Report or returns an error +func Create(kubeauditReport *kubeaudit.Report) (*sarif.Report, error) { + // create a new report object + report, err := sarif.New(sarif.Version210) + if err != nil { + return nil, err + } + + // create a run for kubeaudit + run := sarif.NewRunWithInformationURI("kubeaudit", repoURL) + + report.AddRun(run) + + var results []*kubeaudit.AuditResult + + for _, reportResult := range kubeauditReport.Results() { + r := reportResult.GetAuditResults() + results = append(results, r...) + } + + for _, result := range results { + severityLevel := result.Severity.String() + + auditor := strings.ToLower(result.Auditor) + + docsURL := "https://github.com/Shopify/kubeaudit/blob/main/docs/auditors/" + auditor + ".md" + + helpText := fmt.Sprintf("Type: kubernetes\nAuditor Docs: To find out more about the issue and how to fix it, follow [this link](%s)\nDescription: %s\n\n Note: These audit results are generated with `kubeaudit`, a command line tool and a Go package that checks for potential security concerns in kubernetes manifest specs. You can read more about it at https://github.com/Shopify/kubeaudit ", docsURL, allAuditors[auditor]) + + helpMarkdown := fmt.Sprintf("**Type**: kubernetes\n**Auditor Docs**: To find out more about the issue and how to fix it, follow [this link](%s)\n**Description:** %s\n\n *Note*: These audit results are generated with `kubeaudit`, a command line tool and a Go package that checks for potential security concerns in kubernetes manifest specs. You can read more about it at https://github.com/Shopify/kubeaudit ", + docsURL, allAuditors[auditor]) + + // we only add rules to the report based on the result findings + run.AddRule(result.Rule). + WithName(result.Auditor). + WithHelp(&sarif.MultiformatMessageString{Text: &helpText, Markdown: &helpMarkdown}). + WithShortDescription(&sarif.MultiformatMessageString{Text: &result.Rule}). + WithProperties(sarif.Properties{ + "tags": []string{ + "security", + "kubernetes", + "infrastructure", + }, + }) + + // SARIF specifies the following severity levels: warning, error, note and none + // https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html + // so we're converting info to note here so we get valid SARIF output + if result.Severity.String() == kubeaudit.Info.String() { + severityLevel = "note" + } + + details := fmt.Sprintf("Details: %s\n Auditor: %s\nDescription: %s\nAuditor docs: %s ", + result.Message, result.Auditor, allAuditors[auditor], docsURL) + + location := sarif.NewPhysicalLocation(). + WithArtifactLocation(sarif.NewSimpleArtifactLocation(result.FilePath).WithUriBaseId("ROOTPATH")). + WithRegion(sarif.NewRegion().WithStartLine(1)) + result := sarif.NewRuleResult(result.Rule). + WithMessage(sarif.NewTextMessage(details)). + WithLevel(severityLevel). + WithLocations([]*sarif.Location{sarif.NewLocation().WithPhysicalLocation(location)}) + run.AddResult(result) + } + + var reportBytes bytes.Buffer + + err = report.Write(&reportBytes) + if err != nil { + return nil, nil + } + + return report, nil +} diff --git a/internal/sarif/sarif_test.go b/internal/sarif/sarif_test.go new file mode 100644 index 00000000..8dd45cb4 --- /dev/null +++ b/internal/sarif/sarif_test.go @@ -0,0 +1,137 @@ +package sarif + +import ( + "os" + "path/filepath" + "testing" + + "github.com/Shopify/kubeaudit" + "github.com/Shopify/kubeaudit/auditors/apparmor" + "github.com/Shopify/kubeaudit/auditors/capabilities" + "github.com/Shopify/kubeaudit/auditors/image" + "github.com/Shopify/kubeaudit/auditors/limits" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateWithResults(t *testing.T) { + capabilitiesAuditable := capabilities.New(capabilities.Config{}) + apparmorAuditable := apparmor.New() + imageAuditable := image.New(image.Config{Image: "scratch:1.5"}) + limitsAuditable, _ := limits.New(limits.Config{}) + + cases := []struct { + file string + auditors []kubeaudit.Auditable + expectedRule string + expectedErrorLevel string + expectedMessage string + expectedURI string + }{ + { + "apparmor-invalid.yaml", + []kubeaudit.Auditable{apparmorAuditable}, + apparmor.AppArmorInvalidAnnotation, + "error", + "AppArmor annotation key refers to a container that doesn't exist", + "https://github.com/Shopify/kubeaudit/blob/main/docs/auditors/apparmor.md", + }, + { + "capabilities-added.yaml", + []kubeaudit.Auditable{capabilitiesAuditable}, + capabilities.CapabilityAdded, + "error", + "It should be removed from the capability add list", + "https://github.com/Shopify/kubeaudit/blob/main/docs/auditors/capabilities.md", + }, + { + "image-tag-present.yaml", + []kubeaudit.Auditable{imageAuditable}, + image.ImageCorrect, + "note", + "Image tag is correct", + "https://github.com/Shopify/kubeaudit/blob/main/docs/auditors/image.md", + }, + { + "limits-nil.yaml", + []kubeaudit.Auditable{limitsAuditable}, + limits.LimitsNotSet, + "warning", + "Resource limits not set", + "https://github.com/Shopify/kubeaudit/blob/main/docs/auditors/limits.md", + }, + } + + for _, tc := range cases { + t.Run(tc.file, func(t *testing.T) { + fixture := filepath.Join("fixtures", tc.file) + auditor, err := kubeaudit.New(tc.auditors) + require.NoError(t, err) + + manifest, openErr := os.Open(fixture) + require.NoError(t, openErr) + + defer manifest.Close() + + kubeAuditReport, err := auditor.AuditManifest(fixture, manifest) + require.NoError(t, err) + + sarifReport, err := Create(kubeAuditReport) + require.NoError(t, err) + + assert.Equal(t, repoURL, + *sarifReport.Runs[0].Tool.Driver.InformationURI) + + // verify that the rules have been added as per report findings + assert.Equal(t, tc.expectedRule, sarifReport.Runs[0].Tool.Driver.Rules[0].ID) + + var ruleNames []string + + // check for rules occurrences + for _, sarifRule := range sarifReport.Runs[0].Tool.Driver.Rules { + assert.Equal(t, []string{ + "security", + "kubernetes", + "infrastructure", + }, + sarifRule.Properties["tags"], + ) + + ruleNames = append(ruleNames, sarifRule.ID) + + assert.Contains(t, *sarifRule.Help.Text, tc.expectedURI) + } + + for _, sarifResult := range sarifReport.Runs[0].Results { + assert.Contains(t, ruleNames, *sarifResult.RuleID) + assert.Equal(t, tc.expectedErrorLevel, *sarifResult.Level) + assert.Contains(t, *sarifResult.Message.Text, tc.expectedMessage) + assert.Contains(t, "sarif/fixtures/"+tc.file, *sarifResult.Locations[0].PhysicalLocation.ArtifactLocation.URI) + } + }) + } +} + +func TestCreateWithNoResults(t *testing.T) { + apparmorAuditable := apparmor.New() + + fixture := filepath.Join("fixtures", "apparmor-valid.yaml") + auditor, err := kubeaudit.New([]kubeaudit.Auditable{apparmorAuditable}) + require.NoError(t, err) + + manifest, openErr := os.Open(fixture) + require.NoError(t, openErr) + + defer manifest.Close() + + kubeAuditReport, err := auditor.AuditManifest(fixture, manifest) + require.NoError(t, err) + + sarifReport, err := Create(kubeAuditReport) + require.NoError(t, err) + + require.NotEmpty(t, *sarifReport.Runs[0]) + + // verify that the rules are only added as per report findings + assert.Len(t, sarifReport.Runs[0].Tool.Driver.Rules, 0) +} diff --git a/internal/test/test.go b/internal/test/test.go index ead07904..b7f4e599 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -46,7 +46,7 @@ func AuditMultiple(t *testing.T, fixtureDir, fixture string, auditables []kubeau errors := make(map[string]bool) for _, result := range report.Results() { for _, auditResult := range result.GetAuditResults() { - errors[auditResult.Name] = true + errors[auditResult.Rule] = true } } @@ -79,7 +79,7 @@ func FixSetupMultiple(t *testing.T, fixtureDir, fixture string, auditables []kub auditor, err := kubeaudit.New(auditables) require.Nil(err) - report, err = auditor.AuditManifest(fixedManifest) + report, err = auditor.AuditManifest("", fixedManifest) require.Nil(err) results := report.RawResults() @@ -107,7 +107,8 @@ func GetReport(t *testing.T, fixtureDir, fixture string, auditables []kubeaudit. case MANIFEST_MODE: manifest, openErr := os.Open(fixture) require.NoError(openErr) - report, err = auditor.AuditManifest(manifest) + defer manifest.Close() + report, err = auditor.AuditManifest("", manifest) case LOCAL_MODE: defer DeleteNamespace(t, namespace) CreateNamespace(t, namespace) diff --git a/kubeaudit.go b/kubeaudit.go index aea3852d..bc2a57e6 100644 --- a/kubeaudit.go +++ b/kubeaudit.go @@ -108,6 +108,8 @@ import ( "fmt" "io" "io/ioutil" + "path/filepath" + "strings" "github.com/Shopify/kubeaudit/internal/k8sinternal" "github.com/Shopify/kubeaudit/pkg/k8s" @@ -138,7 +140,7 @@ func New(auditors []Auditable, opts ...Option) (*Kubeaudit, error) { } // AuditManifest audits the Kubernetes resources in the provided manifest -func (a *Kubeaudit) AuditManifest(manifest io.Reader) (*Report, error) { +func (a *Kubeaudit) AuditManifest(manifestPath string, manifest io.Reader) (*Report, error) { manifestBytes, err := ioutil.ReadAll(manifest) if err != nil { return nil, err @@ -154,6 +156,18 @@ func (a *Kubeaudit) AuditManifest(manifest io.Reader) (*Report, error) { return nil, err } + for _, result := range results { + auditResults := result.GetAuditResults() + + if !filepath.IsAbs(manifestPath) { + manifestPath = strings.TrimPrefix(filepath.Clean("/"+manifestPath), "/") + } + + for _, ar := range auditResults { + ar.FilePath = manifestPath + } + } + report := &Report{results: results} return report, nil diff --git a/pkg/override/override.go b/pkg/override/override.go index ce099085..018ed951 100644 --- a/pkg/override/override.go +++ b/pkg/override/override.go @@ -24,9 +24,10 @@ func GetOverriddenResultName(resultName string) string { // NewRedundantOverrideResult creates a new AuditResult at warning level telling the user to remove the override // label because there are no security issues found, so the label is redundant -func NewRedundantOverrideResult(containerName string, overrideReason, overrideLabel string) *kubeaudit.AuditResult { +func NewRedundantOverrideResult(auditorName, containerName, overrideReason, overrideLabel string) *kubeaudit.AuditResult { return &kubeaudit.AuditResult{ - Name: kubeaudit.RedundantAuditorOverride, + Auditor: auditorName, + Rule: kubeaudit.RedundantAuditorOverride, Severity: kubeaudit.Warn, Message: "Auditor is disabled via label but there were no security issues found by the auditor. The label should be removed.", Metadata: kubeaudit.Metadata{ @@ -38,7 +39,7 @@ func NewRedundantOverrideResult(containerName string, overrideReason, overrideLa // ApplyOverride checks if hasOverride is true. If it is, it changes the severity of the audit result from error to // warn, adds the override reason to the metadata and removes the pending fix -func ApplyOverride(auditResult *kubeaudit.AuditResult, containerName string, resource k8s.Resource, overrideLabel string) *kubeaudit.AuditResult { +func ApplyOverride(auditResult *kubeaudit.AuditResult, auditorName, containerName string, resource k8s.Resource, overrideLabel string) *kubeaudit.AuditResult { hasOverride, overrideReason := GetContainerOverrideReason(containerName, resource, overrideLabel) if !hasOverride { @@ -46,10 +47,10 @@ func ApplyOverride(auditResult *kubeaudit.AuditResult, containerName string, res } if auditResult == nil { - return NewRedundantOverrideResult(containerName, overrideReason, overrideLabel) + return NewRedundantOverrideResult(auditorName, containerName, overrideReason, overrideLabel) } - auditResult.Name = GetOverriddenResultName(auditResult.Name) + auditResult.Rule = GetOverriddenResultName(auditResult.Rule) auditResult.PendingFix = nil auditResult.Severity = kubeaudit.Info auditResult.Message = "Audit result overridden: " + auditResult.Message diff --git a/printer.go b/printer.go index 437e656a..538276a0 100644 --- a/printer.go +++ b/printer.go @@ -109,7 +109,7 @@ func (p *Printer) prettyPrintReport(report *Report) { } p.print("-- ") p.printColor(severityColor, "["+auditResult.Severity.String()+"] ") - p.print(auditResult.Name + "\n") + p.print(auditResult.Rule + "\n") p.print(" Message: " + auditResult.Message + "\n") if len(auditResult.Metadata) > 0 { p.print(" Metadata:\n") @@ -166,7 +166,7 @@ func (p *Printer) getLogFieldsForResult(resource k8s.Resource, result *AuditResu objectMeta := k8s.GetObjectMeta(resource) fields := log.Fields{ - "AuditResultName": result.Name, + "AuditResultName": result.Rule, "ResourceKind": kind, "ResourceApiVersion": apiVersion, } diff --git a/result.go b/result.go index fda3339c..d23e0675 100644 --- a/result.go +++ b/result.go @@ -37,11 +37,13 @@ func (s SeverityLevel) String() string { // 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 + Auditor string // Auditor name + Rule string // Rule uniquely identifies a type of violation 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 + FilePath string // Manifest file path } func (result *AuditResult) Fix(resource k8s.Resource) (newResources []k8s.Resource) { diff --git a/util_test.go b/util_test.go index bce54c8d..2a3b46fc 100644 --- a/util_test.go +++ b/util_test.go @@ -56,7 +56,7 @@ func TestPrintResults(t *testing.T) { func newTestAuditResult(severity SeverityLevel) *AuditResult { return &AuditResult{ - Name: "MyAuditResult", + Rule: "MyAuditResult", Severity: severity, Metadata: Metadata{"Foo": "bar"}, }