Skip to content

Commit a102b31

Browse files
Add loader for ESXi vm-support (#1492)
1 parent 0041dd4 commit a102b31

File tree

7 files changed

+195
-1
lines changed

7 files changed

+195
-1
lines changed

dissect/target/loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def open(path: str | Path, *, fallbacks: list[type[Loader]] | None = None, **kwa
278278
register("overlay", "OverlayLoader")
279279
register("phobos", "PhobosLoader")
280280
register("uac", "UacLoader")
281+
register("vmsupport", "VmSupportLoader")
281282
# tar and zip loaders should be low priority to give other loaders a chance first
282283
register("tar", "TarLoader")
283284
register("zip", "ZipLoader")

dissect/target/loaders/nscollector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def detect(path: Path, tarfile: tf.TarFile) -> bool:
3131
return required_paths.issubset(names)
3232

3333
def map(self, target: Target) -> None:
34-
fs = TarFilesystem(tarfile=self.tar, base=self.tar.getnames()[0], fh=self.tar.fileobj)
34+
fs = TarFilesystem(self.tar.fileobj, base=self.tar.getnames()[0])
3535
target.filesystems.add(fs)
3636

3737
# Symlink /nsconfig to /flash/nsconfig to make Citrix parsers compatible

dissect/target/loaders/tar.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class TarLoader(Loader):
137137
import_lazy("dissect.target.loaders.acquire").AcquireTarSubLoader,
138138
import_lazy("dissect.target.loaders.uac").UacTarSubloader,
139139
import_lazy("dissect.target.loaders.nscollector").NsCollectorTarSubLoader,
140+
import_lazy("dissect.target.loaders.vmsupport").VmSupportTarSubloader,
140141
GenericTarSubLoader, # should be last
141142
)
142143

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from dissect.target.filesystems.tar import TarFilesystem
6+
from dissect.target.loader import Loader
7+
from dissect.target.loaders.dir import map_dirs
8+
from dissect.target.loaders.tar import TarSubLoader
9+
from dissect.target.plugin import OperatingSystem
10+
11+
if TYPE_CHECKING:
12+
import tarfile as tf
13+
from pathlib import Path
14+
15+
from dissect.target.target import Target
16+
17+
18+
# From tech support readme
19+
# Files/Directories of Interest:
20+
# ------------------------------
21+
#
22+
# error.log: log containing errors that ocurred while running vm-support
23+
# action.log: a log of all commands, and/or actions run during vm-support
24+
# commands: a directory containing output files for all commands run
25+
#
26+
# All other directories and files should be mirrors of the the ESXi system
27+
# vm-support was run on.
28+
29+
EXPECTED_FILES_OR_DIR = ["etc/vmware/esx.conf", "error.log", "action.log"]
30+
31+
32+
class VmSupportLoader(Loader):
33+
"""Loader for extracted ESXi vm-support.
34+
35+
References:
36+
- https://knowledge.broadcom.com/external/article/313542
37+
"""
38+
39+
@staticmethod
40+
def detect(path: Path) -> bool:
41+
if not path.is_dir():
42+
return False
43+
root_dir = next(path.iterdir())
44+
return all(root_dir.joinpath(f).exists() for f in EXPECTED_FILES_OR_DIR)
45+
46+
def map(self, target: Target) -> None:
47+
map_dirs(target, [next(self.absolute_path.iterdir())], OperatingSystem.ESXI)
48+
49+
50+
class VmSupportTarSubloader(TarSubLoader):
51+
"""Loader for tar-based ESXi vm-support.
52+
53+
References:
54+
- https://knowledge.broadcom.com/external/article/313542
55+
"""
56+
57+
@staticmethod
58+
def detect(path: Path, tarfile: tf.TarFile) -> bool:
59+
if not (names := tarfile.getnames()):
60+
return False
61+
root = names[0].split("/")[0]
62+
required_paths = {f"{root}/{f}" for f in EXPECTED_FILES_OR_DIR}
63+
return required_paths.issubset(names)
64+
65+
def map(self, target: Target) -> None:
66+
fs = TarFilesystem(self.tar.fileobj, tarfile=self.tar, base=self.tar.getnames()[0].split("/")[0])
67+
target.filesystems.add(fs)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:fe64d0a2b663e79c2056c21dc753fe02ead6804e33636d947ce7af8ebf3423bf
3+
size 3994299
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:732e75c90fc430e0f28603689f6a2ec1ce1edc42f95f04950eed27520dad37bd
3+
size 4519237

tests/loaders/test_vmsupport.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from unittest.mock import patch
5+
6+
import pytest
7+
8+
from dissect.target.loader import open as loader_open
9+
from dissect.target.loaders.tar import TarLoader
10+
from dissect.target.loaders.vmsupport import VmSupportLoader, VmSupportTarSubloader
11+
from dissect.target.target import Target
12+
from tests._utils import absolute_path
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Callable
16+
from pathlib import Path
17+
18+
from pytest_benchmark.fixture import BenchmarkFixture
19+
20+
from dissect.target.loader import Loader
21+
22+
23+
@pytest.fixture
24+
def mock_vmsupport_dir(tmp_path: Path) -> Path:
25+
root = tmp_path / "esx-localhost-2026-01-09--16.04-135806"
26+
27+
(root / "etc" / "vmware").mkdir(parents=True)
28+
(root / "action.log").write_bytes(b"")
29+
(root / "error.log").write_bytes(b"")
30+
(root / "etc" / "vmware" / "esx.conf").write_bytes(b'/resourceGroups/version = "8.0.3"\n')
31+
return tmp_path
32+
33+
34+
@pytest.mark.parametrize(
35+
("opener"),
36+
[
37+
pytest.param(Target.open, id="target-open"),
38+
pytest.param(lambda x: next(Target.open_all([x])), id="target-open-all"),
39+
],
40+
)
41+
# vmsupport files with commands/vsi_traverse_-s* removed (~300mo)
42+
@pytest.mark.parametrize(
43+
("path", "loader"),
44+
[
45+
("_data/loaders/vmsupport/esx-localhost6-2026-01-12--13.56-2107676.tar.gz", TarLoader),
46+
("_data/loaders/vmsupport/esx-localhost8-2026-01-09--16.04-135806.tgz", TarLoader),
47+
("mock_vmsupport_dir", VmSupportLoader),
48+
],
49+
)
50+
def test_target_open(
51+
opener: Callable[[str | Path], Target], path: str, loader: type[Loader], mock_vmsupport_dir: Path
52+
) -> None:
53+
"""Test that we correctly use the ESXi vm-support loaders when opening a ``Target``."""
54+
path = mock_vmsupport_dir if path == "mock_vmsupport_dir" else absolute_path(path)
55+
56+
with patch("dissect.target.target.Target.apply"):
57+
target = opener(path)
58+
59+
assert isinstance(target._loader, loader)
60+
if isinstance(target._loader, TarLoader):
61+
assert isinstance(target._loader.subloader, VmSupportTarSubloader)
62+
assert target.path == path
63+
64+
65+
@pytest.mark.parametrize(
66+
"data_path",
67+
[
68+
"_data/loaders/vmsupport/esx-localhost6-2026-01-12--13.56-2107676.tar.gz",
69+
"_data/loaders/vmsupport/esx-localhost8-2026-01-09--16.04-135806.tgz",
70+
],
71+
)
72+
def test_compressed_tar(data_path: str) -> None:
73+
"""Test if we map a compressed vm support tar image correctly."""
74+
path = absolute_path(data_path)
75+
76+
loader = loader_open(path)
77+
assert isinstance(loader, TarLoader)
78+
79+
t = Target()
80+
loader.map(t)
81+
assert isinstance(loader.subloader, VmSupportTarSubloader)
82+
assert len(t.filesystems) == 1
83+
84+
t.apply()
85+
test_file = t.fs.path("/etc/vmware/esx.conf")
86+
assert test_file.exists()
87+
assert test_file.is_file()
88+
assert b"/resourceGroups/version" in test_file.open().read()
89+
90+
91+
def test_dir(mock_vmsupport_dir: Path) -> None:
92+
"""Test if we map an extracted vm support directory correctly."""
93+
94+
loader = loader_open(mock_vmsupport_dir)
95+
assert isinstance(loader, VmSupportLoader)
96+
97+
t = Target()
98+
loader.map(t)
99+
assert len(t.filesystems) == 1
100+
101+
t.apply()
102+
test_file = t.fs.path("etc/vmware/esx.conf")
103+
assert test_file.exists()
104+
assert test_file.is_file()
105+
assert test_file.open().readline() == b'/resourceGroups/version = "8.0.3"\n'
106+
107+
108+
@pytest.mark.parametrize(
109+
("archive", "loader"),
110+
[
111+
("_data/loaders/vmsupport/esx-localhost6-2026-01-12--13.56-2107676.tar.gz", TarLoader),
112+
("_data/loaders/vmsupport/esx-localhost8-2026-01-09--16.04-135806.tgz", TarLoader),
113+
],
114+
)
115+
@pytest.mark.benchmark
116+
def test_benchmark(benchmark: BenchmarkFixture, archive: str, loader: type[Loader]) -> None:
117+
file = absolute_path(archive)
118+
119+
benchmark(lambda: loader(file).map(Target()))

0 commit comments

Comments
 (0)