Skip to content

Commit

Permalink
✨ Adds "deprecated apis" auditor (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerr committed Jun 30, 2022
1 parent 7eaf685 commit fa172f2
Show file tree
Hide file tree
Showing 14 changed files with 479 additions and 87 deletions.
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,21 +191,22 @@ For all the ways kubeaudit can be customized, see [Global Flags](#global-flags).

Auditors can also be run individually.

| Command | Description | Documentation |
| :------------- | :------------------------------------------------------------------------------------------------------------- | :------------------------------------ |
| `apparmor` | Finds containers running without AppArmor. | [docs](docs/auditors/apparmor.md) |
| `asat` | Finds pods using an automatically mounted default service account | [docs](docs/auditors/asat.md) |
| `capabilities` | Finds containers that do not drop the recommended capabilities or add new ones. | [docs](docs/auditors/capabilities.md) |
| `hostns` | Finds containers that have HostPID, HostIPC or HostNetwork enabled. | [docs](docs/auditors/hostns.md) |
| `image` | Finds containers which do not use the desired version of an image (via the tag) or use an image without a tag. | [docs](docs/auditors/image.md) |
| `limits` | Finds containers which exceed the specified CPU and memory limits or do not specify any. | [docs](docs/auditors/limits.md) |
| `mounts` | Finds containers that have sensitive host paths mounted. | [docs](docs/auditors/mounts.md) |
| `netpols` | Finds namespaces that do not have a default-deny network policy. | [docs](docs/auditors/netpols.md) |
| `nonroot` | Finds containers running as root. | [docs](docs/auditors/nonroot.md) |
| `privesc` | Finds containers that allow privilege escalation. | [docs](docs/auditors/privesc.md) |
| `privileged` | Finds containers running as privileged. | [docs](docs/auditors/privileged.md) |
| `rootfs` | Finds containers which do not have a read-only filesystem. | [docs](docs/auditors/rootfs.md) |
| `seccomp` | Finds containers running without Seccomp. | [docs](docs/auditors/seccomp.md) |
| Command | Description | Documentation |
| :--------------- | :------------------------------------------------------------------------------------------------------------- | :-------------------------------------- |
| `apparmor` | Finds containers running without AppArmor. | [docs](docs/auditors/apparmor.md) |
| `asat` | Finds pods using an automatically mounted default service account | [docs](docs/auditors/asat.md) |
| `capabilities` | Finds containers that do not drop the recommended capabilities or add new ones. | [docs](docs/auditors/capabilities.md) |
| `deprecatedapis` | Finds any resource defined with a deprecated API version. | [docs](docs/auditors/deprecatedapis.md) |
| `hostns` | Finds containers that have HostPID, HostIPC or HostNetwork enabled. | [docs](docs/auditors/hostns.md) |
| `image` | Finds containers which do not use the desired version of an image (via the tag) or use an image without a tag. | [docs](docs/auditors/image.md) |
| `limits` | Finds containers which exceed the specified CPU and memory limits or do not specify any. | [docs](docs/auditors/limits.md) |
| `mounts` | Finds containers that have sensitive host paths mounted. | [docs](docs/auditors/mounts.md) |
| `netpols` | Finds namespaces that do not have a default-deny network policy. | [docs](docs/auditors/netpols.md) |
| `nonroot` | Finds containers running as root. | [docs](docs/auditors/nonroot.md) |
| `privesc` | Finds containers that allow privilege escalation. | [docs](docs/auditors/privesc.md) |
| `privileged` | Finds containers running as privileged. | [docs](docs/auditors/privileged.md) |
| `rootfs` | Finds containers which do not have a read-only filesystem. | [docs](docs/auditors/rootfs.md) |
| `seccomp` | Finds containers running without Seccomp. | [docs](docs/auditors/seccomp.md) |

### Global Flags

Expand Down Expand Up @@ -237,6 +238,7 @@ enabledAuditors:
apparmor: false
asat: false
capabilities: true
deprecatedapis: true
hostns: true
image: true
limits: true
Expand All @@ -251,6 +253,11 @@ auditors:
capabilities:
# add capabilities needed to the add list, so kubeaudit won't report errors
allowAddList: ['AUDIT_WRITE', 'CHOWN']
deprecatedapis:
# If no versions are specified and the'deprecatedapis' auditor is enabled, WARN
# results will be genereted for the resources defined with a deprecated API.
currentVersion: '1.22'
targetedVersion: '1.25'
image:
# If no image is specified and the 'image' auditor is enabled, WARN results
# will be generated for containers which use an image without a tag
Expand Down
4 changes: 4 additions & 0 deletions auditors/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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"
Expand All @@ -27,6 +28,7 @@ var AuditorNames = []string{
apparmor.Name,
asat.Name,
capabilities.Name,
deprecatedapis.Name,
hostns.Name,
image.Name,
limits.Name,
Expand Down Expand Up @@ -73,6 +75,8 @@ func initAuditor(name string, conf config.KubeauditConfig) (kubeaudit.Auditable,
return asat.New(), nil
case capabilities.Name:
return capabilities.New(conf.GetAuditorConfigs().Capabilities), nil
case deprecatedapis.Name:
return deprecatedapis.New(conf.GetAuditorConfigs().DeprecatedAPIs)
case hostns.Name:
return hostns.New(), nil
case image.Name:
Expand Down
7 changes: 6 additions & 1 deletion auditors/all/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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/mounts"

"github.com/Shopify/kubeaudit/auditors/hostns"
Expand Down Expand Up @@ -45,7 +46,9 @@ func TestAuditAll(t *testing.T) {
seccomp.SeccompAnnotationMissing,
}

allAuditors, err := Auditors(config.KubeauditConfig{})
allAuditors, err := Auditors(
// Not all the tested resources raise an deprecated API error
config.KubeauditConfig{EnabledAuditors: map[string]bool{deprecatedapis.Name: false}})
require.NoError(t, err)

for _, file := range test.GetAllFileNames(t, fixtureDir) {
Expand Down Expand Up @@ -121,6 +124,7 @@ func TestGetEnabledAuditors(t *testing.T) {
expectedAuditors: []string{
asat.Name,
capabilities.Name,
deprecatedapis.Name,
hostns.Name,
image.Name,
limits.Name,
Expand Down Expand Up @@ -152,6 +156,7 @@ func TestGetEnabledAuditors(t *testing.T) {
expectedAuditors: []string{
asat.Name,
capabilities.Name,
deprecatedapis.Name,
hostns.Name,
image.Name,
limits.Name,
Expand Down
50 changes: 50 additions & 0 deletions auditors/deprecatedapis/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package deprecatedapis

import (
"fmt"
"regexp"
"strconv"
)

type Config struct {
CurrentVersion string `yaml:"currentVersion"`
TargetedVersion string `yaml:"targetedVersion"`
}

type Version struct {
Major int
Minor int
}

func (config *Config) GetCurrentVersion() (*Version, error) {
if config == nil {
return nil, nil
}
return toMajorMinor(config.CurrentVersion)
}

func (config *Config) GetTargetedVersion() (*Version, error) {
if config == nil {
return nil, nil
}
return toMajorMinor(config.TargetedVersion)
}

func toMajorMinor(version string) (*Version, error) {
if len(version) == 0 {
return nil, nil
}
re := regexp.MustCompile(`^(\d{1,2})\.(\d{1,2})$`)
if !re.MatchString(version) {
return nil, fmt.Errorf("error parsing version: %s", version)
}
major, err := strconv.Atoi(re.FindStringSubmatch(version)[1])
if err != nil {
return nil, err
}
minor, err := strconv.Atoi(re.FindStringSubmatch(version)[2])
if err != nil {
return nil, err
}
return &Version{major, minor}, nil
}
133 changes: 133 additions & 0 deletions auditors/deprecatedapis/depreceatedapis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package deprecatedapis

import (
"fmt"
"strconv"

"github.com/Shopify/kubeaudit"
"github.com/Shopify/kubeaudit/internal/k8sinternal"
"github.com/Shopify/kubeaudit/pkg/k8s"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)

const Name = "deprecatedapis"

const (
// DeprecatedAPIUsed occurs when a deprecated resource type version is used
DeprecatedAPIUsed = "DeprecatedAPIUsed"
)

// DeprecatedAPIs implements Auditable
type DeprecatedAPIs struct {
CurrentVersion *Version
TargetedVersion *Version
}

func New(config Config) (*DeprecatedAPIs, error) {
currentVersion, err := config.GetCurrentVersion()
if err != nil {
return nil, fmt.Errorf("error creating DeprecatedAPIs auditor: %w", err)
}

targetedVersion, err := config.GetTargetedVersion()
if err != nil {
return nil, fmt.Errorf("error creating DeprecatedAPIs auditor: %w", err)
}

return &DeprecatedAPIs{
CurrentVersion: currentVersion,
TargetedVersion: targetedVersion,
}, nil
}

// APILifecycleDeprecated is a generated function on the available APIs, returning the release in which the API struct was or will be deprecated as int versions of major and minor for comparison.
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L475-L479
type apiLifecycleDeprecated interface {
APILifecycleDeprecated() (major, minor int)
}

// APILifecycleRemoved is a generated function on the available APIs, returning the release in which the API is no longer served as int versions of major and minor for comparison.
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L491-L495
type apiLifecycleRemoved interface {
APILifecycleRemoved() (major, minor int)
}

// APILifecycleReplacement is a generated function on the available APIs, returning the group, version, and kind that should be used instead of this deprecated type.
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L482-L487
type apiLifecycleReplacement interface {
APILifecycleReplacement() schema.GroupVersionKind
}

// APILifecycleIntroduced is a generated function on the available APIs, returning the release in which the API struct was introduced as int versions of major and minor for comparison.
// https://github.com/kubernetes/code-generator/blob/v0.24.1/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go#L467-L473
type apiLifecycleIntroduced interface {
APILifecycleIntroduced() (major, minor int)
}

// Audit checks that the resource API version is not deprecated
func (deprecatedAPIs *DeprecatedAPIs) Audit(resource k8s.Resource, _ []k8s.Resource) ([]*kubeaudit.AuditResult, error) {
var auditResults []*kubeaudit.AuditResult
lastApplied, ok := k8s.GetAnnotations(resource)[v1.LastAppliedConfigAnnotation]
if ok && len(lastApplied) > 0 {
resource, _ = k8sinternal.DecodeResource([]byte(lastApplied))
}
deprecated, isDeprecated := resource.(apiLifecycleDeprecated)
if isDeprecated {
deprecatedMajor, deprecatedMinor := deprecated.APILifecycleDeprecated()
if deprecatedMajor == 0 && deprecatedMinor == 0 {
return nil, fmt.Errorf("version not found %s (%d.%d)", deprecated, deprecatedMajor, deprecatedMinor)
} else {
severity := kubeaudit.Warn
metadata := kubeaudit.Metadata{
"DeprecatedMajor": strconv.Itoa(deprecatedMajor),
"DeprecatedMinor": strconv.Itoa(deprecatedMinor),
}
if deprecatedAPIs.CurrentVersion != nil && (deprecatedAPIs.CurrentVersion.Major < deprecatedMajor || deprecatedAPIs.CurrentVersion.Major == deprecatedMajor && deprecatedAPIs.CurrentVersion.Minor < deprecatedMinor) {
severity = kubeaudit.Info
}
gvk := resource.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
return nil, fmt.Errorf("GroupVersionKind not found %s", resource)
} else {
deprecationMessage := fmt.Sprintf("%s %s is deprecated in v%d.%d+", gvk.GroupVersion().String(), gvk.Kind, deprecatedMajor, deprecatedMinor)
if removed, hasRemovalInfo := resource.(apiLifecycleRemoved); hasRemovalInfo {
removedMajor, removedMinor := removed.APILifecycleRemoved()
if removedMajor != 0 || removedMinor != 0 {
deprecationMessage = deprecationMessage + fmt.Sprintf(", unavailable in v%d.%d+", removedMajor, removedMinor)
metadata["RemovedMajor"] = strconv.Itoa(removedMajor)
metadata["RemovedMinor"] = strconv.Itoa(removedMinor)
}
if deprecatedAPIs.TargetedVersion != nil && deprecatedAPIs.TargetedVersion.Major >= removedMajor && deprecatedAPIs.TargetedVersion.Minor >= removedMinor {
severity = kubeaudit.Error
}
}
if introduced, hasIntroduced := resource.(apiLifecycleIntroduced); hasIntroduced {
introducedMajor, introducedMinor := introduced.APILifecycleIntroduced()
if introducedMajor != 0 || introducedMinor != 0 {
deprecationMessage = deprecationMessage + fmt.Sprintf(", introduced in v%d.%d+", introducedMajor, introducedMinor)
metadata["IntroducedMajor"] = strconv.Itoa(introducedMajor)
metadata["IntroducedMinor"] = strconv.Itoa(introducedMinor)
}
}
if replaced, hasReplacement := resource.(apiLifecycleReplacement); hasReplacement {
replacement := replaced.APILifecycleReplacement()
if !replacement.Empty() {
deprecationMessage = deprecationMessage + fmt.Sprintf("; use %s %s", replacement.GroupVersion().String(), replacement.Kind)
metadata["ReplacementGroup"] = replacement.GroupVersion().String()
metadata["ReplacementKind"] = replacement.Kind
}
}
auditResult := &kubeaudit.AuditResult{
Name: DeprecatedAPIUsed,
Severity: severity,
Message: deprecationMessage,
Metadata: metadata,
}
auditResults = append(auditResults, auditResult)
}
}

}
return auditResults, nil
}
72 changes: 72 additions & 0 deletions auditors/deprecatedapis/depreceatedapis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package deprecatedapis

import (
"fmt"
"strings"
"testing"

"github.com/Shopify/kubeaudit"
"github.com/Shopify/kubeaudit/internal/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const fixtureDir = "fixtures"

func TestAuditDeprecatedAPIs(t *testing.T) {
cases := []struct {
file string
currentVersion string
targetedVersion string
expectedSeverity kubeaudit.SeverityLevel
}{
{"cronjob.yml", "", "", kubeaudit.Warn}, // Warn is the serverity by default
{"cronjob.yml", "1.20", "1.21", kubeaudit.Info}, // Info, not yet deprecated in the current version
{"cronjob.yml", "1.21", "1.22", kubeaudit.Warn}, // Warn, deprecated in the current version
{"cronjob.yml", "1.22", "1.25", kubeaudit.Error}, // Error, not available in the targeted version
{"cronjob.yml", "1.20", "1.25", kubeaudit.Error}, // Error, not yet deprecated in the current version but not available in the targeted version
{"cronjob.yml", "1.20", "", kubeaudit.Info}, // Info, not yet deprecated in the current version and no targeted version defined
{"cronjob.yml", "1.21", "", kubeaudit.Warn}, // Warn, deprecated in the current version
{"cronjob.yml", "", "1.20", kubeaudit.Warn}, // Warn is the serverity by default if no current version
{"cronjob.yml", "", "1.25", kubeaudit.Error}, // Error, not available in the targeted version
}

message := "batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+, introduced in v1.8+; use batch/v1 CronJob"
metadata := kubeaudit.Metadata{
"DeprecatedMajor": "1",
"DeprecatedMinor": "21",
"RemovedMajor": "1",
"RemovedMinor": "25",
"IntroducedMajor": "1",
"IntroducedMinor": "8",
"ReplacementGroup": "batch/v1",
"ReplacementKind": "CronJob",
}

for i, tc := range cases {
// These lines are needed because of how scopes work with parallel tests (see https://gist.github.com/posener/92a55c4cd441fc5e5e85f27bca008721)
tc := tc
i := i
t.Run(tc.file+"-"+tc.currentVersion+"-"+tc.targetedVersion, func(t *testing.T) {
t.Parallel()
auditor, err := New(Config{CurrentVersion: tc.currentVersion, TargetedVersion: tc.targetedVersion})
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)
})
}
}

func assertReport(t *testing.T, report *kubeaudit.Report, expectedSeverity kubeaudit.SeverityLevel, message string, metadata map[string]string) {
assert.Equal(t, 1, len(report.Results()))
for _, result := range report.Results() {
assert.Equal(t, 1, len(result.GetAuditResults()))
for _, auditResult := range result.GetAuditResults() {
require.Equal(t, expectedSeverity, auditResult.Severity)
require.Equal(t, message, auditResult.Message)
require.Equal(t, metadata, auditResult.Metadata)
}
}
}
19 changes: 19 additions & 0 deletions auditors/deprecatedapis/fixtures/cronjob.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "* * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure

0 comments on commit fa172f2

Please sign in to comment.