From 5ac6659cb822c7fffba4052ac7e9a2599726a6fd Mon Sep 17 00:00:00 2001 From: Robert Bendun Date: Tue, 8 Sep 2026 14:47:25 +0200 Subject: [PATCH 1/4] fix(web): Close span in process list in network overview (#3221) --- web/templates/analysis/network/_hosts_not_ajax.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/templates/analysis/network/_hosts_not_ajax.html b/web/templates/analysis/network/_hosts_not_ajax.html index aa34730a6d1..e62771a0a0b 100644 --- a/web/templates/analysis/network/_hosts_not_ajax.html +++ b/web/templates/analysis/network/_hosts_not_ajax.html @@ -44,7 +44,7 @@ {% if host.processes %} {% for p in host.processes %} {% if p.process_name %}{{ p.process_name }}{% else %}(unknown){% endif %}{% if p.pid %} ({{ p.pid }}){% endif %} {% endfor %} From ab62147a83b43e7d9b2970b1ea28eb683dd165ae Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 8 Sep 2026 15:09:48 +0200 Subject: [PATCH 2/4] python (#3220) * python * Update python.py --- analyzer/windows/modules/packages/python.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/analyzer/windows/modules/packages/python.py b/analyzer/windows/modules/packages/python.py index c5721368ca4..4322184e581 100644 --- a/analyzer/windows/modules/packages/python.py +++ b/analyzer/windows/modules/packages/python.py @@ -2,6 +2,8 @@ # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org # See the file 'docs/LICENSE' for copying permission. +import os + from lib.common.abstracts import Package from lib.common.common import check_file_extension from lib.common.constants import OPT_ARGUMENTS @@ -24,6 +26,13 @@ def start(self, path): except CuckooPackageError: python = self.get_path_glob("py.exe") + # Set PYTHONHOME to help Python locate its standard library during initialization. + # Python may fail to load the 'encodings' module, resulting in: + # "ModuleNotFoundError: No module named 'encodings'" + # Might break if your Python is in VENV + python_home = os.path.dirname(python) + os.environ["PYTHONHOME"] = python_home + arguments = self.options.get(OPT_ARGUMENTS, "") path = check_file_extension(path, ".py") From 4bc90156d87ff4de92cc1bb4d382d841c89e9eff Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 8 Sep 2026 15:14:06 +0200 Subject: [PATCH 3/4] azure sniffer improvements (#3222) * azure sniffer improvements --- modules/auxiliary/AzSniffer.py | 44 ++++- poetry.lock | 290 ++++++++++++++++++++++++++++++++- pyproject.toml | 9 + tests/test_az_sniffer.py | 217 ++++++++++++++++++++++++ 4 files changed, 554 insertions(+), 6 deletions(-) create mode 100644 tests/test_az_sniffer.py diff --git a/modules/auxiliary/AzSniffer.py b/modules/auxiliary/AzSniffer.py index 68538834813..a03a7cccbb2 100644 --- a/modules/auxiliary/AzSniffer.py +++ b/modules/auxiliary/AzSniffer.py @@ -19,10 +19,7 @@ HAVE_AZURE = True except ImportError: HAVE_AZURE = False - print("Missing machinery-required libraries.") - print( - "poetry run python -m pip install azure-identity msrest msrestazure azure-mgmt-compute azure-mgmt-network azure-mgmt-storage azure-storage-blob" - ) + print("Missing machinery-required libraries.Run: poetry install --extras azure") from lib.cuckoo.common.abstracts import Auxiliary from lib.cuckoo.common.config import Config @@ -72,13 +69,33 @@ def start(self): self.create_packet_capture(custom_filters) def create_packet_capture(self, custom_filters): + # Determine the target resource ID for packet capture (standalone VM or specific VMSS instance) + target = None + if hasattr(self, "machine") and self.machine and hasattr(self.machine, "label") and self.machine.label: + parts = self.machine.label.rsplit("_", 1) + if len(parts) == 2 and parts[1].isdigit() and self.vmss_name: + instance_id = parts[1] + target = f"/subscriptions/{self.subscription_id}/resourceGroups/{self.resource_group}/providers/Microsoft.Compute/virtualMachineScaleSets/{self.vmss_name}/virtualMachines/{instance_id}" + else: + # Standalone VM target fallback + target = f"/subscriptions/{self.subscription_id}/resourceGroups/{self.resource_group}/providers/Microsoft.Compute/virtualMachines/{self.machine.label}" + + # Ultimate fallback to VMSS if target is still None + if not target and self.vmss_name: + target = f"/subscriptions/{self.subscription_id}/resourceGroups/{self.resource_group}/providers/Microsoft.Compute/virtualMachineScaleSets/{self.vmss_name}" + + if not target: + raise ValueError("No target VM or VMSS could be determined for AzSniffer") + + log.debug("AzSniffer targeting resource ID: %s", target) + storage_location = PacketCaptureStorageLocation( storage_id=f"/subscriptions/{self.subscription_id}/resourceGroups/{self.resource_group}/providers/Microsoft.Storage/storageAccounts/{self.storage_account}", storage_path=f"https://{self.storage_account}.blob.core.windows.net/network-watcher-logs/{self.capture_name}.cap", ) packet_capture = PacketCapture( - target=f"/subscriptions/{self.subscription_id}/resourceGroups/{self.resource_group}/providers/Microsoft.Compute/virtualMachineScaleSets/{self.vmss_name}", + target=target, storage_location=storage_location, time_limit_in_seconds=18000, total_bytes_per_session=1073741824, @@ -146,6 +163,23 @@ def download_packet_capture(self): blob_client = self.blob_service_client.get_blob_client(container=container_name, blob=blob_name) + # Check if the blob exists. If not (e.g. due to Azure-appended subfolders or timestamps), + # list blobs in the container to find any match for our capture name. + if not blob_client.exists(): + log.info("Blob %s not found directly. Searching container %s for blobs matching %s", blob_name, container_name, self.capture_name) + container_client = self.blob_service_client.get_container_client(container_name) + matched_blob_name = None + for blob in container_client.list_blobs(): + if (self.capture_name in blob.name or f"_{self.task.id}" in blob.name) and blob.name.endswith(".cap"): + matched_blob_name = blob.name + break + + if matched_blob_name: + log.info("Found matching blob: %s", matched_blob_name) + blob_client = self.blob_service_client.get_blob_client(container=container_name, blob=matched_blob_name) + else: + log.error("No matching blob found in container %s containing %s", container_name, self.capture_name) + self._download_to_file(blob_client, primary_output_file) log.info("Downloaded packet capture for task %s to %s", str(self.task.id), primary_output_file) self.convert_cap_to_pcap(primary_output_file) diff --git a/poetry.lock b/poetry.lock index 96c0bc2ae9e..7561e5b41e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +[[package]] +name = "adal" +version = "1.2.7" +description = "Note: This library is already replaced by MSAL Python, available here: https://pypi.org/project/msal/ .ADAL Python remains available here as a legacy. The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "adal-1.2.7-py2.py3-none-any.whl", hash = "sha256:2a7451ed7441ddbc57703042204a3e30ef747478eea022c70f789fc7f084bc3d"}, + {file = "adal-1.2.7.tar.gz", hash = "sha256:d74f45b81317454d96e982fd1c50e6fb5c99ac2223728aea8764433a39f566f1"}, +] + +[package.dependencies] +cryptography = ">=1.1.0" +PyJWT = ">=1.0.0,<3" +python-dateutil = ">=2.1.0,<3" +requests = ">=2.0.0,<3" + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -334,6 +353,139 @@ files = [ [package.extras] visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] +[[package]] +name = "azure-core" +version = "1.41.0" +description = "Microsoft Azure Core Library for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d"}, + {file = "azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a"}, +] + +[package.dependencies] +requests = ">=2.21.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] +tracing = ["opentelemetry-api (>=1.26,<2.0)"] + +[[package]] +name = "azure-identity" +version = "1.25.3" +description = "Microsoft Azure Identity Library for Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c"}, + {file = "azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +cryptography = ">=2.5" +msal = ">=1.35.1" +msal-extensions = ">=1.2.0" +typing-extensions = ">=4.0.0" + +[[package]] +name = "azure-mgmt-compute" +version = "38.3.0" +description = "Microsoft Azure Compute Management Client Library for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_mgmt_compute-38.3.0-py3-none-any.whl", hash = "sha256:2349f0a88b2be9d77f1449db65dcdf7c3c11141c91bc355b29060531779396f6"}, + {file = "azure_mgmt_compute-38.3.0.tar.gz", hash = "sha256:2310e608f78fb3a4d38be206ec25ff07d67b1c9236fd4568f857af674cb1773a"}, +] + +[package.dependencies] +azure-mgmt-core = ">=1.6.0" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[[package]] +name = "azure-mgmt-core" +version = "1.6.0" +description = "Microsoft Azure Management Core Library for Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4"}, + {file = "azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c"}, +] + +[package.dependencies] +azure-core = ">=1.32.0" + +[[package]] +name = "azure-mgmt-network" +version = "32.0.0" +description = "Microsoft Azure Network Management Client Library for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_mgmt_network-32.0.0-py3-none-any.whl", hash = "sha256:915a6465edfb3b5c30a44027ed92851173d07054682682d0b362fbe9e5c78697"}, + {file = "azure_mgmt_network-32.0.0.tar.gz", hash = "sha256:cd5707de7bac945c5407047cd764e64cc4b4ce8f51d1f4fe32bfc204d9229093"}, +] + +[package.dependencies] +azure-mgmt-core = ">=1.6.0" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[[package]] +name = "azure-mgmt-storage" +version = "25.1.0" +description = "Microsoft Azure Storage Management Client Library for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_mgmt_storage-25.1.0-py3-none-any.whl", hash = "sha256:46b51cb0491f9aac303436c28bd18954d0c3aca39d4882e7d332e6ad0605b25e"}, + {file = "azure_mgmt_storage-25.1.0.tar.gz", hash = "sha256:cc4bd2158fd0dd80639142af06dc70c119bbfd6f79d53da4482e0b2c849af7ca"}, +] + +[package.dependencies] +azure-mgmt-core = ">=1.6.0" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[[package]] +name = "azure-storage-blob" +version = "12.30.1" +description = "Microsoft Azure Blob Storage Client Library for Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "azure_storage_blob-12.30.1-py3-none-any.whl", hash = "sha256:7dc09c37f4f58508e20532b4b4c178f4763f41b01e0b9063835b994fd9d2a7b3"}, + {file = "azure_storage_blob-12.30.1.tar.gz", hash = "sha256:7a24f978c51d56a0375beebffcbe8453e59ae390d2695705848edc75083e4184"}, +] + +[package.dependencies] +azure-core = ">=1.37.0" +cryptography = ">=2.1.4" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["azure-core[aio] (>=1.37.0)"] + [[package]] name = "bcrypt" version = "4.2.1" @@ -2658,6 +2810,19 @@ files = [ [package.dependencies] sortedcontainers = ">=2.0,<3.0" +[[package]] +name = "isodate" +version = "0.7.2" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, + {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, +] + [[package]] name = "isort" version = "8.0.1" @@ -3132,6 +3297,46 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "msal" +version = "1.38.0" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "msal-1.38.0-py3-none-any.whl", hash = "sha256:765b9b98b6aa380ee8b8f1c75636e08863edaf0a953498955bd668650dde5d49"}, + {file = "msal-1.38.0.tar.gz", hash = "sha256:4f10ff1257bacfd1781f22e85bd2b8d43ad1b490f3b6aafd7906671cadedd464"}, +] + +[package.dependencies] +cryptography = ">=2.5,<51" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.20,<0.21) ; python_version >= \"3.9\" and platform_system == \"Darwin\"", "pymsalruntime (>=0.20,<0.21) ; python_version >= \"3.9\" and platform_system == \"Linux\"", "pymsalruntime (>=0.20,<0.21) ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, + {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, +] + +[package.dependencies] +msal = ">=1.29,<2" + +[package.extras] +portalocker = ["portalocker (>=1.4,<4)"] + [[package]] name = "msgpack" version = "1.0.8" @@ -3268,6 +3473,47 @@ files = [ cryptography = ">=39.0" olefile = ">=0.46" +[[package]] +name = "msrest" +version = "0.7.1" +description = "AutoRest swagger generator Python client runtime." +optional = true +python-versions = ">=3.6" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, + {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, +] + +[package.dependencies] +azure-core = ">=1.24.0" +certifi = ">=2017.4.17" +isodate = ">=0.6.0" +requests = ">=2.16,<3.0" +requests-oauthlib = ">=0.5.0" + +[package.extras] +async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version >= \"3.5\""] + +[[package]] +name = "msrestazure" +version = "0.6.4.post1" +description = "AutoRest swagger generator Python client runtime. Azure-specific module." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "msrestazure-0.6.4.post1-py2.py3-none-any.whl", hash = "sha256:2264493b086c2a0a82ddf5fd87b35b3fffc443819127fed992ac5028354c151e"}, + {file = "msrestazure-0.6.4.post1.tar.gz", hash = "sha256:39842007569e8c77885ace5c46e4bf2a9108fcb09b1e6efdf85b6e2c642b55d4"}, +] + +[package.dependencies] +adal = ">=0.6.0,<2.0.0" +msrest = ">=0.6.0,<2.0.0" +six = "*" + [[package]] name = "multidict" version = "6.7.0" @@ -3542,6 +3788,24 @@ files = [ {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "olefile" version = "0.47" @@ -4728,6 +4992,9 @@ files = [ {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] @@ -5484,6 +5751,26 @@ files = [ [package.dependencies] requests = ">=1.0.0" +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = true +python-versions = ">=3.4" +groups = ["main"] +markers = "extra == \"azure\"" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rich" version = "13.9.4" @@ -7114,6 +7401,7 @@ test = ["coverage[toml]", "zope.event", "zope.testing"] testing = ["coverage[toml]", "zope.event", "zope.testing"] [extras] +azure = ["azure-identity", "azure-mgmt-compute", "azure-mgmt-network", "azure-mgmt-storage", "azure-storage-blob", "msrest", "msrestazure"] dist = ["fastapi"] gcp = ["google-cloud-pubsub", "google-cloud-storage"] maco = ["maco"] @@ -7123,4 +7411,4 @@ yara = ["plyara"] [metadata] lock-version = "2.1" python-versions = ">=3.10, <4.0" -content-hash = "eeb9792819922ef1fbcea6564ccb83430b50cf44140d7619dd429f0c7434afe7" +content-hash = "d29867c0688f7a69c36be70021845644e7a171ff1139dabee069127bb8a2e396" diff --git a/pyproject.toml b/pyproject.toml index 5ee002eb7e6..1919320d1ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,15 @@ maco = ["maco"] gcp = ["google-cloud-storage", "google-cloud-pubsub"] yara = ["plyara"] mcp = ["fastmcp", "httpx"] +azure = [ + "azure-identity", + "msrest", + "msrestazure", + "azure-mgmt-compute", + "azure-mgmt-network", + "azure-mgmt-storage", + "azure-storage-blob", +] dist = [ "fastapi", ] diff --git a/tests/test_az_sniffer.py b/tests/test_az_sniffer.py new file mode 100644 index 00000000000..d9863a8a381 --- /dev/null +++ b/tests/test_az_sniffer.py @@ -0,0 +1,217 @@ +import sys +from unittest.mock import MagicMock, patch + +# Inject mock modules for Azure SDK to allow importing/instantiating even if the Azure SDK is not installed +azure_core_mock = MagicMock() +azure_mgmt_network_mock = MagicMock() +azure_mgmt_network_models_mock = MagicMock() +azure_identity_mock = MagicMock() +azure_mgmt_storage_mock = MagicMock() +azure_storage_blob_mock = MagicMock() + +sys.modules["azure"] = azure_core_mock +sys.modules["azure.core"] = azure_core_mock +sys.modules["azure.core.exceptions"] = azure_core_mock +sys.modules["azure.identity"] = azure_identity_mock +sys.modules["azure.mgmt"] = azure_mgmt_network_mock +sys.modules["azure.mgmt.network"] = azure_mgmt_network_mock +sys.modules["azure.mgmt.network.models"] = azure_mgmt_network_models_mock +sys.modules["azure.mgmt.storage"] = azure_mgmt_storage_mock +sys.modules["azure.storage"] = azure_storage_blob_mock +sys.modules["azure.storage.blob"] = azure_storage_blob_mock + +# Mock the specific classes imported from azure.mgmt.network.models +class MockPacketCapture: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + +class MockPacketCaptureStorageLocation: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + +azure_mgmt_network_models_mock.PacketCapture = MockPacketCapture +azure_mgmt_network_models_mock.PacketCaptureStorageLocation = MockPacketCaptureStorageLocation + + +# Now we can import AzSniffer safely +import modules.auxiliary.AzSniffer as az_sniffer_module +from modules.auxiliary.AzSniffer import AzSniffer + +# Ensure mock classes are present in the module namespace even if import was skipped +if not hasattr(az_sniffer_module, "ClientSecretCredential"): + az_sniffer_module.ClientSecretCredential = MagicMock() +if not hasattr(az_sniffer_module, "NetworkManagementClient"): + az_sniffer_module.NetworkManagementClient = MagicMock() +if not hasattr(az_sniffer_module, "StorageManagementClient"): + az_sniffer_module.StorageManagementClient = MagicMock() +if not hasattr(az_sniffer_module, "BlobServiceClient"): + az_sniffer_module.BlobServiceClient = MagicMock() +if not hasattr(az_sniffer_module, "PacketCapture"): + az_sniffer_module.PacketCapture = MockPacketCapture +if not hasattr(az_sniffer_module, "PacketCaptureStorageLocation"): + az_sniffer_module.PacketCaptureStorageLocation = MockPacketCaptureStorageLocation +if not hasattr(az_sniffer_module, "AzureError"): + az_sniffer_module.AzureError = Exception + + +class DummyMachine: + def __init__(self, label): + self.label = label + + +@patch("modules.auxiliary.AzSniffer.HAVE_AZURE", True) +@patch("modules.auxiliary.AzSniffer.Config") +def test_az_sniffer_target_resolution(mock_config_class): + # Set up mock configuration values + mock_aux_config = MagicMock() + mock_aux_config.enabled = True + + mock_az_config = MagicMock() + mock_az_config.resource_group = "my-rg" + mock_az_config.storage_account = "mystorage" + mock_az_config.vmss_name = "myvmss" + mock_az_config.location = "eastus" + mock_az_config.subscription_id = "sub-123" + mock_az_config.connection_string = "DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=key;EndpointSuffix=core.windows.net" + mock_az_config.tenant_id = "tenant-123" + mock_az_config.client_id = "client-123" + mock_az_config.client_secret = "secret-123" + + # Route Config() calls to appropriate mocks + def config_side_effect(name=None): + if name == "auxiliary": + mock_inst = MagicMock() + mock_inst.get.return_value = mock_aux_config + return mock_inst + elif name == "az": + mock_inst = MagicMock() + mock_inst.get.return_value = mock_az_config + return mock_inst + # Default fallback + mock_inst = MagicMock() + return mock_inst + + mock_config_class.side_effect = config_side_effect + + # Instantiate AzSniffer + sniffer = AzSniffer() + sniffer.task = MagicMock() + sniffer.task.id = 42 + sniffer.capture_name = "PacketCapture_42" + + # Test Case 1: Target VMSS instance (e.g. label is myvmss_10) + sniffer.set_machine(DummyMachine("myvmss_10")) + with patch.object(sniffer.network_client.packet_captures, "begin_create") as mock_begin_create: + mock_poller = MagicMock() + mock_result = MagicMock() + mock_result.storage_location.storage_path = "https://mystorage.blob.core.windows.net/network-watcher-logs/PacketCapture_42.cap" + mock_poller.result.return_value = mock_result + mock_begin_create.return_value = mock_poller + + sniffer.create_packet_capture([]) + + # Verify that begin_create was called + mock_begin_create.assert_called_once() + # Verify the target set on the PacketCapture parameters + args, kwargs = mock_begin_create.call_args + packet_capture_param = kwargs.get("parameters") or args[0] # depending on positional vs kwarg + if not packet_capture_param and len(args) >= 4: + packet_capture_param = args[3] + + expected_target = "/subscriptions/sub-123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/myvmss/virtualMachines/10" + assert packet_capture_param.target == expected_target + + # Test Case 2: Target Standalone VM (e.g. label is standalone_vm) + sniffer.set_machine(DummyMachine("standalone_vm")) + with patch.object(sniffer.network_client.packet_captures, "begin_create") as mock_begin_create: + mock_poller = MagicMock() + mock_result = MagicMock() + mock_result.storage_location.storage_path = "https://mystorage.blob.core.windows.net/network-watcher-logs/PacketCapture_42.cap" + mock_poller.result.return_value = mock_result + mock_begin_create.return_value = mock_poller + + sniffer.create_packet_capture([]) + + mock_begin_create.assert_called_once() + args, kwargs = mock_begin_create.call_args + packet_capture_param = kwargs.get("parameters") or args[3] + + expected_target = "/subscriptions/sub-123/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/standalone_vm" + assert packet_capture_param.target == expected_target + + +@patch("modules.auxiliary.AzSniffer.HAVE_AZURE", True) +@patch("modules.auxiliary.AzSniffer.Config") +def test_az_sniffer_download_fallback(mock_config_class): + # Set up mock configuration values + mock_aux_config = MagicMock() + mock_aux_config.enabled = True + + mock_az_config = MagicMock() + mock_az_config.resource_group = "my-rg" + mock_az_config.storage_account = "mystorage" + mock_az_config.vmss_name = "myvmss" + mock_az_config.location = "eastus" + mock_az_config.subscription_id = "sub-123" + mock_az_config.connection_string = "DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=key;EndpointSuffix=core.windows.net" + mock_az_config.tenant_id = "tenant-123" + mock_az_config.client_id = "client-123" + mock_az_config.client_secret = "secret-123" + + def config_side_effect(name=None): + if name == "auxiliary": + mock_inst = MagicMock() + mock_inst.get.return_value = mock_aux_config + return mock_inst + elif name == "az": + mock_inst = MagicMock() + mock_inst.get.return_value = mock_az_config + return mock_inst + mock_inst = MagicMock() + return mock_inst + + mock_config_class.side_effect = config_side_effect + + # Instantiate AzSniffer + sniffer = AzSniffer() + sniffer.task = MagicMock() + sniffer.task.id = 42 + sniffer.capture_name = "PacketCapture_42" + sniffer.blob_url = "https://mystorage.blob.core.windows.net/network-watcher-logs/PacketCapture_42.cap" + + # Mock the blob service client + mock_blob_client = MagicMock() + mock_container_client = MagicMock() + + # Simulate that the direct blob does NOT exist + mock_blob_client.exists.return_value = False + + # Simulate that there is a matching blob found via container listing + mock_found_blob = MagicMock() + mock_found_blob.name = "subscriptions/sub-123/.../PacketCapture_42_20260908.cap" + mock_container_client.list_blobs.return_value = [mock_found_blob] + + sniffer.blob_service_client.get_blob_client.return_value = mock_blob_client + sniffer.blob_service_client.get_container_client.return_value = mock_container_client + + with patch.object(sniffer, "_download_to_file") as mock_download, \ + patch.object(sniffer, "convert_cap_to_pcap") as mock_convert: + sniffer.download_packet_capture() + + # It should have called list_blobs + mock_container_client.list_blobs.assert_called_once() + # It should have downloaded using the resolved/matched blob client + mock_download.assert_called_once() + mock_convert.assert_called_once() + + +if __name__ == "__main__": + print("Running AzSniffer unit tests directly...") + # Mock Config inside main execution to avoid import errors + from unittest.mock import patch + with patch("modules.auxiliary.AzSniffer.Config") as mock_cfg: + test_az_sniffer_target_resolution(mock_cfg) + test_az_sniffer_download_fallback(mock_cfg) + print("All AzSniffer tests PASSED successfully!") From 2cf0166dbd278ff53bae5d8078fb32b26b4f2837 Mon Sep 17 00:00:00 2001 From: doomedraven Date: Tue, 8 Sep 2026 15:27:42 +0200 Subject: [PATCH 4/4] Update python.py --- analyzer/windows/modules/packages/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyzer/windows/modules/packages/python.py b/analyzer/windows/modules/packages/python.py index 4322184e581..5ca843a2998 100644 --- a/analyzer/windows/modules/packages/python.py +++ b/analyzer/windows/modules/packages/python.py @@ -27,7 +27,7 @@ def start(self, path): python = self.get_path_glob("py.exe") # Set PYTHONHOME to help Python locate its standard library during initialization. - # Python may fail to load the 'encodings' module, resulting in: + # Python may fail to load the 'encodings' module, resulting in: # "ModuleNotFoundError: No module named 'encodings'" # Might break if your Python is in VENV python_home = os.path.dirname(python)