diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index ed24d989e7..4ef065cbbe 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523) - `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031) - `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032) +- `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027) - Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602) - TLS certificate verification in the `codepipeline_project_repo_private` check, which previously used an unverified SSL context, leaving the repository-visibility probe open to MITM tampering [(#11603)](https://github.com/prowler-cloud/prowler/pull/11603) diff --git a/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/__init__.py b/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.metadata.json b/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.metadata.json new file mode 100644 index 0000000000..77096d6a20 --- /dev/null +++ b/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "azure", + "CheckID": "aks_cluster_auto_upgrade_enabled", + "CheckTitle": "AKS cluster has automatic upgrade enabled", + "CheckType": [], + "ServiceName": "aks", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.containerservice/managedclusters", + "ResourceGroup": "container", + "Description": "**AKS clusters** are evaluated for an **automatic upgrade channel** other than `none`. Automatic upgrades keep the Kubernetes control plane and node pools on supported patch/minor versions and reduce version drift across clusters.", + "Risk": "Without automatic upgrades, AKS clusters can remain on unsupported or vulnerable Kubernetes versions. Delayed patching increases exposure to **known CVEs**, control plane/node defects, and exploit chains that can affect workload **confidentiality**, **integrity**, and **availability**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-cluster", + "https://learn.microsoft.com/en-us/azure/aks/supported-kubernetes-versions" + ], + "Remediation": { + "Code": { + "CLI": "az aks update --resource-group --name --auto-upgrade-channel patch", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure an AKS automatic upgrade channel so clusters receive Kubernetes version updates and security patches without relying only on manual upgrade processes. Use `patch` or `stable` for conservative production upgrades, reserve faster channels such as `rapid` for environments that can absorb quicker version changes, and avoid `none` unless a documented exception and manual patching process exists.", + "Url": "https://hub.prowler.com/check/aks_cluster_auto_upgrade_enabled" + } + }, + "Categories": [ + "vulnerabilities", + "cluster-security" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Automatic upgrade channels can change cluster versions during Azure-managed upgrade windows. Select a channel that matches the workload change tolerance and use planned maintenance windows, staging validation, and documented rollback procedures for production clusters." +} diff --git a/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.py b/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.py new file mode 100644 index 0000000000..9d1f8b48a1 --- /dev/null +++ b/prowler/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.aks.aks_client import aks_client + + +class aks_cluster_auto_upgrade_enabled(Check): + def execute(self) -> list[Check_Report_Azure]: + findings = [] + + for subscription_name, clusters in aks_client.clusters.items(): + for cluster in clusters.values(): + report = Check_Report_Azure(metadata=self.metadata(), resource=cluster) + report.subscription = subscription_name + + auto_upgrade_channel = ( + (cluster.auto_upgrade_channel or "").strip().lower() + ) + if auto_upgrade_channel and auto_upgrade_channel != "none": + report.status = "PASS" + report.status_extended = ( + f"Cluster '{cluster.name}' has auto-upgrade channel." + ) + else: + report.status = "FAIL" + report.status_extended = f"Cluster '{cluster.name}' does not have auto-upgrade configured." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/aks/aks_service.py b/prowler/providers/azure/services/aks/aks_service.py index 3d158a2f70..081edd7b17 100644 --- a/prowler/providers/azure/services/aks/aks_service.py +++ b/prowler/providers/azure/services/aks/aks_service.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List +from typing import List, Optional from azure.mgmt.containerservice import ContainerServiceClient @@ -55,6 +55,56 @@ class AKS(AzureService): ) ], rbac_enabled=getattr(cluster, "enable_rbac", False), + auto_upgrade_channel=getattr( + getattr(cluster, "auto_upgrade_profile", None), + "upgrade_channel", + None, + ), + defender_enabled=bool( + getattr( + getattr( + getattr( + getattr( + cluster, + "security_profile", + None, + ), + "defender", + None, + ), + "security_monitoring", + None, + ), + "enabled", + False, + ) + ), + azure_monitor_enabled=( + bool( + getattr( + getattr( + getattr( + cluster, + "azure_monitor_profile", + None, + ), + "metrics", + None, + ), + "enabled", + False, + ) + ) + if getattr( + cluster, "azure_monitor_profile", None + ) + else False + ), + local_accounts_disabled=bool( + getattr( + cluster, "disable_local_accounts", False + ) + ), ) } ) @@ -82,3 +132,7 @@ class Cluster: agent_pool_profiles: List[ManagedClusterAgentPoolProfile] rbac_enabled: bool location: str + auto_upgrade_channel: Optional[str] = None + defender_enabled: bool = False + azure_monitor_enabled: bool = False + local_accounts_disabled: bool = False diff --git a/tests/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled_test.py b/tests/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled_test.py new file mode 100644 index 0000000000..b7c2ac3bd9 --- /dev/null +++ b/tests/providers/azure/services/aks/aks_cluster_auto_upgrade_enabled/aks_cluster_auto_upgrade_enabled_test.py @@ -0,0 +1,99 @@ +from unittest import mock + +import pytest + +from prowler.providers.azure.services.aks.aks_service import Cluster +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +def build_cluster(auto_upgrade_channel): + return Cluster( + id="/sub/rg/cluster1", + name="test-cluster", + public_fqdn="test.azmk8s.io", + private_fqdn=None, + network_policy=None, + agent_pool_profiles=[], + rbac_enabled=True, + location="eastus", + auto_upgrade_channel=auto_upgrade_channel, + ) + + +class Test_aks_cluster_auto_upgrade_enabled: + def test_no_subscriptions(self): + aks_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled.aks_client", + new=aks_client, + ), + ): + from prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled import ( + aks_cluster_auto_upgrade_enabled, + ) + + aks_client.clusters = {} + + check = aks_cluster_auto_upgrade_enabled() + result = check.execute() + assert len(result) == 0 + + def test_pass(self): + aks_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled.aks_client", + new=aks_client, + ), + ): + from prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled import ( + aks_cluster_auto_upgrade_enabled, + ) + + cluster = build_cluster(auto_upgrade_channel="stable") + aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {cluster.id: cluster}} + + check = aks_cluster_auto_upgrade_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + @pytest.mark.parametrize("auto_upgrade_channel", [None, "", "none", "None"]) + def test_fail(self, auto_upgrade_channel): + aks_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled.aks_client", + new=aks_client, + ), + ): + from prowler.providers.azure.services.aks.aks_cluster_auto_upgrade_enabled.aks_cluster_auto_upgrade_enabled import ( + aks_cluster_auto_upgrade_enabled, + ) + + cluster = build_cluster(auto_upgrade_channel=auto_upgrade_channel) + aks_client.clusters = {AZURE_SUBSCRIPTION_ID: {cluster.id: cluster}} + + check = aks_cluster_auto_upgrade_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL"