From fa172f29c328fd52763f8f5531b08b4bd086aad5 Mon Sep 17 00:00:00 2001 From: Jeremie Lagarde Date: Thu, 30 Jun 2022 17:39:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Adds=20"deprecated=20apis"=20audito?= =?UTF-8?q?r=20(#428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 37 +++-- auditors/all/all.go | 4 + auditors/all/all_test.go | 7 +- auditors/deprecatedapis/config.go | 50 +++++++ auditors/deprecatedapis/depreceatedapis.go | 133 ++++++++++++++++++ .../deprecatedapis/depreceatedapis_test.go | 72 ++++++++++ auditors/deprecatedapis/fixtures/cronjob.yml | 19 +++ cmd/commands/deprecatedapis.go | 45 ++++++ config/config.go | 10 +- config/config.yaml | 4 + docs/auditors/deprecatedapis.md | 102 ++++++++++++++ internal/k8sinternal/client.go | 3 + internal/k8sinternal/scheme.go | 67 +-------- internal/test/test.go | 13 +- 14 files changed, 479 insertions(+), 87 deletions(-) create mode 100644 auditors/deprecatedapis/config.go create mode 100644 auditors/deprecatedapis/depreceatedapis.go create mode 100644 auditors/deprecatedapis/depreceatedapis_test.go create mode 100644 auditors/deprecatedapis/fixtures/cronjob.yml create mode 100644 cmd/commands/deprecatedapis.go create mode 100644 docs/auditors/deprecatedapis.md diff --git a/README.md b/README.md index 5333af8f..eefbcbc8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -237,6 +238,7 @@ enabledAuditors: apparmor: false asat: false capabilities: true + deprecatedapis: true hostns: true image: true limits: true @@ -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 diff --git a/auditors/all/all.go b/auditors/all/all.go index fd5d7999..93b12adc 100644 --- a/auditors/all/all.go +++ b/auditors/all/all.go @@ -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" @@ -27,6 +28,7 @@ var AuditorNames = []string{ apparmor.Name, asat.Name, capabilities.Name, + deprecatedapis.Name, hostns.Name, image.Name, limits.Name, @@ -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: diff --git a/auditors/all/all_test.go b/auditors/all/all_test.go index a1d56f30..8a94562b 100644 --- a/auditors/all/all_test.go +++ b/auditors/all/all_test.go @@ -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" @@ -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) { @@ -121,6 +124,7 @@ func TestGetEnabledAuditors(t *testing.T) { expectedAuditors: []string{ asat.Name, capabilities.Name, + deprecatedapis.Name, hostns.Name, image.Name, limits.Name, @@ -152,6 +156,7 @@ func TestGetEnabledAuditors(t *testing.T) { expectedAuditors: []string{ asat.Name, capabilities.Name, + deprecatedapis.Name, hostns.Name, image.Name, limits.Name, diff --git a/auditors/deprecatedapis/config.go b/auditors/deprecatedapis/config.go new file mode 100644 index 00000000..c61694d1 --- /dev/null +++ b/auditors/deprecatedapis/config.go @@ -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 +} diff --git a/auditors/deprecatedapis/depreceatedapis.go b/auditors/deprecatedapis/depreceatedapis.go new file mode 100644 index 00000000..eaad682c --- /dev/null +++ b/auditors/deprecatedapis/depreceatedapis.go @@ -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 +} diff --git a/auditors/deprecatedapis/depreceatedapis_test.go b/auditors/deprecatedapis/depreceatedapis_test.go new file mode 100644 index 00000000..749617f6 --- /dev/null +++ b/auditors/deprecatedapis/depreceatedapis_test.go @@ -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) + } + } +} diff --git a/auditors/deprecatedapis/fixtures/cronjob.yml b/auditors/deprecatedapis/fixtures/cronjob.yml new file mode 100644 index 00000000..f075c097 --- /dev/null +++ b/auditors/deprecatedapis/fixtures/cronjob.yml @@ -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 \ No newline at end of file diff --git a/cmd/commands/deprecatedapis.go b/cmd/commands/deprecatedapis.go new file mode 100644 index 00000000..e53dba2c --- /dev/null +++ b/cmd/commands/deprecatedapis.go @@ -0,0 +1,45 @@ +package commands + +import ( + "github.com/Shopify/kubeaudit/auditors/deprecatedapis" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var deprecatedapisConfig deprecatedapis.Config + +const ( + currentVersionFlagName = "current-k8s-version" + targetedVersionFlagName = "targeted-k8s-version" +) + +var deprecatedapisCmd = &cobra.Command{ + Use: "deprecatedapis", + Short: "Audit resource API version deprecations", + Long: `This command determines which resource is defined with a deprecated API version. + +An ERROR result is generated for API version not available in the targeted version +A WARN result is generated for API version deprecated in the current version +An INFO result is generated for API version not yet deprecated in the current version + +Example usage: +kubeaudit deprecatedapis +kubeaudit deprecatedapis --current-k8s-version 1.22 --targeted-k8s-version 1.24`, + Run: func(cmd *cobra.Command, args []string) { + auditor, err := deprecatedapis.New(deprecatedapisConfig) + if err != nil { + log.Fatal("failed to create deprecatedapis auditor") + } + runAudit(auditor)(cmd, args) + }, +} + +func setdeprecatedapisFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&deprecatedapisConfig.CurrentVersion, currentVersionFlagName, "", "Kubernetes current version (eg 1.22)") + cmd.Flags().StringVar(&deprecatedapisConfig.TargetedVersion, targetedVersionFlagName, "", "Kubernetes version to migrate to (eg 1.24)") +} + +func init() { + RootCmd.AddCommand(deprecatedapisCmd) + setdeprecatedapisFlags(deprecatedapisCmd) +} diff --git a/config/config.go b/config/config.go index f6e8e8b5..4a5e8b3d 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ import ( "io" "io/ioutil" + "github.com/Shopify/kubeaudit/auditors/deprecatedapis" "github.com/Shopify/kubeaudit/auditors/mounts" "github.com/Shopify/kubeaudit/auditors/capabilities" @@ -47,8 +48,9 @@ func (conf *KubeauditConfig) GetAuditorConfigs() AuditorConfig { } type AuditorConfig struct { - Capabilities capabilities.Config `yaml:"capabilities"` - Image image.Config `yaml:"image"` - Limits limits.Config `yaml:"limits"` - Mounts mounts.Config `yaml:"mounts"` + Capabilities capabilities.Config `yaml:"capabilities"` + DeprecatedAPIs deprecatedapis.Config `yaml:"config"` + Image image.Config `yaml:"image"` + Limits limits.Config `yaml:"limits"` + Mounts mounts.Config `yaml:"mounts"` } diff --git a/config/config.yaml b/config/config.yaml index d4399456..28e271a4 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,6 +5,7 @@ enabledAuditors: apparmor: true asat: true capabilities: true + deprecatedapis: true hostns: true image: true limits: true @@ -19,6 +20,9 @@ auditors: capabilities: # add capabilities needed to the add list, so kubeaudit won't report errors add: ["AUDIT_WRITE", "CHOWN", "KILL"] + deprecatedapis: + currentVersion: "1.22" + targetedVersion: "1.25" image: image: "myimage:mytag" limits: diff --git a/docs/auditors/deprecatedapis.md b/docs/auditors/deprecatedapis.md new file mode 100644 index 00000000..4a028c26 --- /dev/null +++ b/docs/auditors/deprecatedapis.md @@ -0,0 +1,102 @@ +# Kubernetes Deprecated API Auditor (deprecatedapis) + +Finds any resource defined with a deprecated API version. + +## General Usage + +``` +kubeaudit deprecatedapis [flags] +``` + +### Flags +| Short | Long | Description | Default | +| :------ | :--------------------- | :-------------------------------------------- | :------------------ | +| | --current-k8s-version | Kubernetes current version | | +| | --targeted-k8s-version | Kubernetes version to migrate to | | + + +Also see [Global Flags](/README.md#global-flags) + +## Examples + +The `deprecatedapis` auditor finds the deprecated APIs in use, reports the versions where they will be removed, and recommends replacement APIs. +``` +$ kubeaudit deprecatedapis -f "auditors/deprecatedapis/fixtures/cronjob.yml" + +---------------- Results for --------------- + + apiVersion: batch/v1beta1 + kind: CronJob + metadata: + name: hello + +-------------------------------------------- + +-- [warning] DeprecatedAPIUsed + Message: batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+, introduced in v1.8+; use batch/v1 CronJob + Metadata: + DeprecatedMajor: 1 + DeprecatedMinor: 21 + IntroducedMajor: 1 + IntroducedMinor: 8 + RemovedMajor: 1 + RemovedMinor: 25 + ReplacementKind: CronJob + ReplacementGroup: batch/v1 +``` + +The `deprecatedapis` auditor can be used with the `--current-k8s-version` flag. If the API is not yet deprecated for this version the auditor will produce an `info` otherwise a `warning`. +``` +$ kubeaudit deprecatedapis --current-k8s-version 1.20 -f "auditors/deprecatedapis/fixtures/cronjob.yml" + +---------------- Results for --------------- + + apiVersion: batch/v1beta1 + kind: CronJob + metadata: + name: hello + +-------------------------------------------- + +-- [info] DeprecatedAPIUsed + Message: batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+, introduced in v1.8+; use batch/v1 CronJob + Metadata: + DeprecatedMajor: 1 + DeprecatedMinor: 21 + IntroducedMajor: 1 + IntroducedMinor: 8 + RemovedMajor: 1 + RemovedMinor: 25 + ReplacementKind: CronJob + ReplacementGroup: batch/v1 +``` + +The `deprecatedapis` auditor can be used with the `--targeted-k8s-version` flag. If the API is not available for the targeted version the auditor will produce an `error` otherwise a `warning` or `info` if the API is not yet deprecated for this version. +``` +$ kubeaudit deprecatedapis --current-k8s-version 1.20 --targeted-k8s-version 1.25 -f "auditors/deprecatedapis/fixtures/cronjob.yml" + +---------------- Results for --------------- + + apiVersion: batch/v1beta1 + kind: CronJob + metadata: + name: hello + +-------------------------------------------- + +-- [error] DeprecatedAPIUsed + Message: batch/v1beta1 CronJob is deprecated in v1.21+, unavailable in v1.25+, introduced in v1.8+; use batch/v1 CronJob + Metadata: + DeprecatedMajor: 1 + DeprecatedMinor: 21 + IntroducedMajor: 1 + IntroducedMinor: 8 + RemovedMajor: 1 + RemovedMinor: 25 + ReplacementKind: CronJob + ReplacementGroup: batch/v1 +``` + +## Override Errors + +Overrides are not currently supported for `deprecatedapis`. diff --git a/internal/k8sinternal/client.go b/internal/k8sinternal/client.go index d5c8314e..c8821f2a 100644 --- a/internal/k8sinternal/client.go +++ b/internal/k8sinternal/client.go @@ -65,6 +65,9 @@ func NewKubeClientLocal(configPath string, context string) (KubeClient, error) { return nil, err } + // Ignore warnings from kubeclient as they are expected to be reported by the deprecatedapi auditor. + kubeconfig.WarningHandler = rest.NoWarnings{} + return newKubeClientFromConfig(kubeconfig) } diff --git a/internal/k8sinternal/scheme.go b/internal/k8sinternal/scheme.go index 963fac27..a1211585 100644 --- a/internal/k8sinternal/scheme.go +++ b/internal/k8sinternal/scheme.go @@ -2,77 +2,22 @@ package k8sinternal import ( certmanagerv1alpha2 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" - admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" - appsv1 "k8s.io/api/apps/v1" - authenticationv1 "k8s.io/api/authentication/v1" - authenticationv1beta1 "k8s.io/api/authentication/v1beta1" - authorizationv1 "k8s.io/api/authorization/v1" - authorizationv1beta1 "k8s.io/api/authorization/v1beta1" - autoscalingv1 "k8s.io/api/autoscaling/v1" - autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" - autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" - batchv1 "k8s.io/api/batch/v1" - batchv1beta1 "k8s.io/api/batch/v1beta1" - certificatesv1beta1 "k8s.io/api/certificates/v1beta1" - coordinationv1 "k8s.io/api/coordination/v1" - coordinationv1beta1 "k8s.io/api/coordination/v1beta1" - corev1 "k8s.io/api/core/v1" - eventsv1beta1 "k8s.io/api/events/v1beta1" - networkingv1 "k8s.io/api/networking/v1" - networkingv1beta1 "k8s.io/api/networking/v1beta1" - nodev1alpha1 "k8s.io/api/node/v1alpha1" - nodev1beta1 "k8s.io/api/node/v1beta1" - policyv1beta1 "k8s.io/api/policy/v1beta1" - rbacv1 "k8s.io/api/rbac/v1" - rbacv1alpha1 "k8s.io/api/rbac/v1alpha1" - rbacv1beta1 "k8s.io/api/rbac/v1beta1" - schedulingv1 "k8s.io/api/scheduling/v1" - schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1" - schedulingv1beta1 "k8s.io/api/scheduling/v1beta1" - storagev1 "k8s.io/api/storage/v1" - storagev1alpha1 "k8s.io/api/storage/v1alpha1" - storagev1beta1 "k8s.io/api/storage/v1beta1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + kubescheme "k8s.io/client-go/kubernetes/scheme" ) -var scheme = runtime.NewScheme() +var scheme = kubescheme.Scheme var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ - admissionregistrationv1beta1.AddToScheme, certmanagerv1alpha2.AddToScheme, - appsv1.AddToScheme, - authenticationv1.AddToScheme, - authenticationv1beta1.AddToScheme, - authorizationv1.AddToScheme, - authorizationv1beta1.AddToScheme, - autoscalingv1.AddToScheme, - autoscalingv2beta1.AddToScheme, - autoscalingv2beta2.AddToScheme, - batchv1.AddToScheme, - batchv1beta1.AddToScheme, - certificatesv1beta1.AddToScheme, - coordinationv1beta1.AddToScheme, - coordinationv1.AddToScheme, - corev1.AddToScheme, - eventsv1beta1.AddToScheme, - networkingv1.AddToScheme, - networkingv1beta1.AddToScheme, - nodev1alpha1.AddToScheme, - nodev1beta1.AddToScheme, - policyv1beta1.AddToScheme, - rbacv1.AddToScheme, - rbacv1beta1.AddToScheme, - rbacv1alpha1.AddToScheme, - schedulingv1alpha1.AddToScheme, - schedulingv1beta1.AddToScheme, - schedulingv1.AddToScheme, - storagev1beta1.AddToScheme, - storagev1.AddToScheme, - storagev1alpha1.AddToScheme, + apiextensionsv1.AddToScheme, + apiextensionsv1beta1.AddToScheme, } // AddToScheme adds localScheme to Scheme diff --git a/internal/test/test.go b/internal/test/test.go index 06995cac..ead07904 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -22,17 +22,17 @@ const SharedFixturesDir = "../../internal/test/fixtures" const MANIFEST_MODE = "manifest" const LOCAL_MODE = "local" -func AuditManifest(t *testing.T, fixtureDir, fixture string, auditable kubeaudit.Auditable, expectedErrors []string) { - AuditMultiple(t, fixtureDir, fixture, []kubeaudit.Auditable{auditable}, expectedErrors, "", MANIFEST_MODE) +func AuditManifest(t *testing.T, fixtureDir, fixture string, auditable kubeaudit.Auditable, expectedErrors []string) *kubeaudit.Report { + return AuditMultiple(t, fixtureDir, fixture, []kubeaudit.Auditable{auditable}, expectedErrors, "", MANIFEST_MODE) } -func AuditLocal(t *testing.T, fixtureDir, fixture string, auditable kubeaudit.Auditable, namespace string, expectedErrors []string) { - AuditMultiple(t, fixtureDir, fixture, []kubeaudit.Auditable{auditable}, expectedErrors, namespace, LOCAL_MODE) +func AuditLocal(t *testing.T, fixtureDir, fixture string, auditable kubeaudit.Auditable, namespace string, expectedErrors []string) *kubeaudit.Report { + return AuditMultiple(t, fixtureDir, fixture, []kubeaudit.Auditable{auditable}, expectedErrors, namespace, LOCAL_MODE) } -func AuditMultiple(t *testing.T, fixtureDir, fixture string, auditables []kubeaudit.Auditable, expectedErrors []string, namespace string, mode string) { +func AuditMultiple(t *testing.T, fixtureDir, fixture string, auditables []kubeaudit.Auditable, expectedErrors []string, namespace string, mode string) *kubeaudit.Report { if mode == LOCAL_MODE && !UseKind() { - return + return nil } expected := make(map[string]bool, len(expectedErrors)) @@ -51,6 +51,7 @@ func AuditMultiple(t *testing.T, fixtureDir, fixture string, auditables []kubeau } assert.Equal(t, expected, errors) + return report } func FixSetup(t *testing.T, fixtureDir, fixture string, auditable kubeaudit.Auditable) (fixedResources []k8s.Resource, report *kubeaudit.Report) {