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',