Skip to content
This repository has been archived by the owner on Oct 30, 2024. It is now read-only.

✨ Adds "deprecated apis" auditor #428

Merged
merged 7 commits into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -236,6 +237,7 @@ enabledAuditors:
apparmor: false
asat: false
capabilities: true
deprecatedapis: true
hostns: true
image: true
limits: true
Expand All @@ -250,6 +252,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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we maybe add a comment with a reference link for these functions? I assume they're defined somewhere in the kubernetes codebase?

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this interface used anywhere?

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we ignoring this error?

}
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)
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to also test.AuditLocal?

}

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
Loading