From bc3546e0c176ebb216b3c834e1523b0b532b72ad Mon Sep 17 00:00:00 2001 From: Edwin Bernal Date: Fri, 7 Aug 2026 08:26:22 -0700 Subject: [PATCH] vm-repair: skip extension version check when installed version is null On Azure CLI 2.87 the installed extension metadata (metadata.json) is missing because newer setuptools no longer generates it, so the extension version resolves to None. check_extension_version() compared a version string against None, raising TypeError and aborting every vm-repair command (all validators call it first). Guard against a null installed/available version so the up-to-date check is skipped instead of crashing. Adds a regression unit test and bumps to 2.2.3 (py312-compat takes 2.2.2). CLI 2.88 fixes the underlying metadata generation. --- src/vm-repair/HISTORY.rst | 4 ++ src/vm-repair/azext_vm_repair/repair_utils.py | 10 +++- .../tests/latest/test_repair_utils.py | 54 +++++++++++++++++++ src/vm-repair/setup.py | 2 +- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/vm-repair/azext_vm_repair/tests/latest/test_repair_utils.py diff --git a/src/vm-repair/HISTORY.rst b/src/vm-repair/HISTORY.rst index a1e7bdee50a..8354075665c 100644 --- a/src/vm-repair/HISTORY.rst +++ b/src/vm-repair/HISTORY.rst @@ -2,6 +2,10 @@ Release History =============== +2.2.3 +++++++ +Fixing a crash ("version: null") when running any ``vm repair`` command on Azure CLI 2.87. Newer ``setuptools`` no longer generates the ``metadata.json`` that CLI 2.87 relied on to read the installed extension version, so the version resolved to ``None`` and the extension's version check raised a ``TypeError`` before the command could run. The version check now handles a missing version gracefully instead of failing. Azure CLI 2.88 also fixes the underlying metadata issue, so upgrading the CLI remains the recommended long-term resolution. + 2.2.1 ++++++ Fixing a command injection vulnerability (MSRC 115198 / VULN-185362). Source VM tag values copied via ``--copy-tags`` could contain shell metacharacters that, on Windows, were interpreted by ``cmd.exe`` and executed as arbitrary commands on the operator's workstation. Tag keys and values are now validated and quoted before being interpolated into the ``az`` command, and ``_call_az_command`` quotes every argument so ``cmd.exe`` treats shell metacharacters as literal text. Minimum fixed version: 2.2.1. diff --git a/src/vm-repair/azext_vm_repair/repair_utils.py b/src/vm-repair/azext_vm_repair/repair_utils.py index dad85d04817..d8cc00a9b53 100644 --- a/src/vm-repair/azext_vm_repair/repair_utils.py +++ b/src/vm-repair/azext_vm_repair/repair_utils.py @@ -239,8 +239,16 @@ def check_extension_version(extension_name): extension_to_check = extension_to_check[0] + # On some Azure CLI versions (e.g. 2.87) the installed extension metadata is missing because + # newer setuptools no longer generates 'metadata.json', so the version resolves to None. Skip + # the up-to-date check instead of crashing the command on a None comparison (fixed in CLI 2.88). + installed_version = extension_to_check.get('version') + if not installed_version: + logger.debug('Could not determine the installed version of the %s extension; skipping version check.', extension_name) + return + for ext in available_extensions: - if ext['name'] == extension_name and ext['version'] > extension_to_check['version']: + if ext['name'] == extension_name and ext.get('version') and ext['version'] > installed_version: logger.warning('The %s extension is not up to date, please update with az extension update -n %s', extension_name, extension_name) return diff --git a/src/vm-repair/azext_vm_repair/tests/latest/test_repair_utils.py b/src/vm-repair/azext_vm_repair/tests/latest/test_repair_utils.py new file mode 100644 index 00000000000..b8459bf84cb --- /dev/null +++ b/src/vm-repair/azext_vm_repair/tests/latest/test_repair_utils.py @@ -0,0 +1,54 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long +import unittest +from unittest import mock + +from azext_vm_repair.repair_utils import check_extension_version + + +class CheckExtensionVersionTest(unittest.TestCase): + + def _run(self, installed, available): + with mock.patch('azure.cli.core.extension.operations.list_extensions', return_value=installed), \ + mock.patch('azure.cli.core.extension.operations.list_available_extensions', return_value=available): + check_extension_version('vm-repair') + + def test_none_installed_version_does_not_raise(self): + # Regression for the "version: null" crash on Azure CLI 2.87, where the installed + # extension metadata is missing and the version resolves to None. Comparing a version + # string against None used to raise TypeError and abort every vm-repair command. + installed = [{'name': 'vm-repair', 'version': None}] + available = [{'name': 'vm-repair', 'version': '2.2.2'}] + try: + self._run(installed, available) + except TypeError: + self.fail('check_extension_version raised TypeError on a None installed version') + + def test_none_available_version_does_not_raise(self): + installed = [{'name': 'vm-repair', 'version': '2.2.2'}] + available = [{'name': 'vm-repair', 'version': None}] + try: + self._run(installed, available) + except TypeError: + self.fail('check_extension_version raised TypeError on a None available version') + + def test_newer_available_version_warns(self): + installed = [{'name': 'vm-repair', 'version': '2.2.1'}] + available = [{'name': 'vm-repair', 'version': '2.2.2'}] + with mock.patch('azext_vm_repair.repair_utils.logger') as mock_logger: + self._run(installed, available) + mock_logger.warning.assert_called_once() + + def test_up_to_date_does_not_warn(self): + installed = [{'name': 'vm-repair', 'version': '2.2.2'}] + available = [{'name': 'vm-repair', 'version': '2.2.2'}] + with mock.patch('azext_vm_repair.repair_utils.logger') as mock_logger: + self._run(installed, available) + mock_logger.warning.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/src/vm-repair/setup.py b/src/vm-repair/setup.py index cedf28498cb..9a7a7b0151c 100644 --- a/src/vm-repair/setup.py +++ b/src/vm-repair/setup.py @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "2.2.1" +VERSION = "2.2.3" CLASSIFIERS = [ 'Development Status :: 4 - Beta',