Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dissect/target/helpers/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def DynamicDescriptor(types: Sequence[str]) -> RecordDescriptor:
"target/child",
[
("string", "type"),
("string", "name"),
("path", "path"),
],
)
Expand Down
2 changes: 1 addition & 1 deletion dissect/target/plugins/apps/container/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
("datetime", "started"),
("datetime", "finished"),
("string[]", "ports"),
("string", "names"),
("string", "name"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JSCU-CNI I changed this from names to name, seemed to be an oversight in naming since it's seemingly always a singular name, but let me know if I'm overlooking anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentionally called names since that is how Docker, Podman (and iirc other) container platforms tag their container name (see for example output of docker ps -a or podman ps -a).

https://docs.docker.com/reference/cli/docker/container/ls/#format
https://docs.podman.io/en/v5.0.2/markdown/podman-ps.1.html#format-format

From the dissect perspective I guess it makes sense to make the field singular if it is not a string[] field.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that, but I think that's silly 😄. In the linked example of Docker there's a redis,webapp/db container names though, do you know how that occurs, and if we support that already?

("string[]", "volumes"),
("string[]", "environment"),
("path", "mount_path"),
Expand Down
2 changes: 1 addition & 1 deletion dissect/target/plugins/apps/container/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def containers(self) -> Iterator[DockerContainerRecord]:
started=convert_timestamp(config.get("State", {}).get("StartedAt")),
finished=convert_timestamp(config.get("State", {}).get("FinishedAt")),
ports=list(convert_ports(ports)),
names=config.get("Name", "").replace("/", "", 1),
name=config.get("Name", "").replace("/", "", 1),
volumes=volumes,
environment=config.get("Config", {}).get("Env", []),
mount_path=mount_path,
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/apps/container/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def _find_containers_sqlite(self, path: Path) -> Iterator[PodmanContainerRecord]
started=container.get("startedTime"),
finished=container.get("finishedTime"),
ports=list(convert_ports(container.get("newPortMappings", []))), # TODO: research "exposedPorts"
names=container.get("name"),
name=container.get("name"),
volumes=volumes,
environment=container.get("spec", {}).get("process", {}).get("env", []),
mount_path=mount_path,
Expand Down Expand Up @@ -271,7 +271,7 @@ def _find_containers_fs(self, path: Path) -> Iterator[PodmanContainerRecord]:
image_id=other_config.get("image"),
command=" ".join(config.get("process", {}).get("args", [])),
created=other_config.get("created"),
names=other_config.get("names"),
name=other_config.get("names"),
environment=config.get("process", {}).get("env", []),
mount_path=path.joinpath(f"storage/overlay/{other_config.get('layer')}") if other_config else None,
config_path=config_path,
Expand Down
54 changes: 34 additions & 20 deletions dissect/target/plugins/child/colima.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,56 @@
from dissect.target import Target


def find_containers(paths: list[Path]) -> Iterator[Path]:
for path in paths:
for config_path in path.iterdir():
if (config_file := config_path.joinpath("colima.yaml")).exists():
name = f"-{config_file.parts[-2]}" if config_file.parts[-2] != "default" else ""
if (disk_path := path.joinpath("_lima", f"colima{name}", "diffdisk")).exists():
yield disk_path
def find_vms(path: Path) -> Iterator[tuple[str, Path, Path]]:
"""Find the Lima VMs from Colima and yield the name, Colima configuration path and Lima VM path.

References:
- https://github.com/abiosoft/colima/blob/5ddf1e0dc67772f6e28f84c7c7b32f2343ad4bfb/config/profile.go#L19-L39
"""
for config_file in path.glob("*/colima.yaml"):
profile = config_file.parent.name
if profile == "default":
lima_id = "colima"
else:
profile = profile.removeprefix("colima-")
lima_id = f"colima-{profile}"

if (lima_path := path.joinpath("_lima", lima_id)).exists():
yield profile, config_file, lima_path


class ColimaChildTargetPlugin(ChildTargetPlugin):
"""Child target plugin that yields Colima containers.
"""Child target plugin that yields Colima VMs.

Colima is a container runtime for macOS and Linux.

References:
- https://github.com/abiosoft/colima/blob/5d2e91c4a491d4ae35d69fb2583f4f959401bc37
- https://github.com/abiosoft/colima
"""

__type__ = "colima"

def __init__(self, target: Target):
super().__init__(target)
self.paths = [
path
for user in self.target.user_details.all_with_home()
if (path := user.home_path.joinpath(".colima")).exists()
]
self.paths = []
for user in self.target.user_details.all_with_home():
# check .colima folder in ~/
if (path := user.home_path.joinpath(".colima")).exists():
self.paths.append(path)
# check .colima folder in ~/.config/
if (path := user.home_path.joinpath(".config", "colima")).exists():
self.paths.append(path)

def check_compatible(self) -> None:
if not self.paths:
raise UnsupportedPluginError("No Colima configurations found")

def list_children(self) -> Iterator[ChildTargetRecord]:
for container in find_containers(self.paths):
yield ChildTargetRecord(
type=self.__type__,
path=container,
_target=self.target,
)
for path in self.paths:
for name, _, lima_path in find_vms(path):
yield ChildTargetRecord(
type=self.__type__,
name=name,
path=lima_path.joinpath("diffdisk"),
_target=self.target,
)
1 change: 1 addition & 0 deletions dissect/target/plugins/child/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def list_children(self) -> Iterator[ChildTargetRecord]:
if container.mount_path:
yield ChildTargetRecord(
type=self.__type__,
name=container.name,
path=container.mount_path,
_target=self.target,
)
11 changes: 11 additions & 0 deletions dissect/target/plugins/child/esxi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import TYPE_CHECKING

from dissect.hypervisor import vmx

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import ChildTargetRecord
from dissect.target.plugin import ChildTargetPlugin
Expand All @@ -21,8 +23,17 @@ def check_compatible(self) -> None:

def list_children(self) -> Iterator[ChildTargetRecord]:
for vm in self.target.vm_inventory():
try:
name = vmx.VMX.parse(self.target.fs.path(vm.path).read_text()).attr.get("displayname")
except Exception as e:
self.target.log.exception("Failed parsing displayname from VMX: %s", vm.path)
self.target.log.debug("", exc_info=e)

name = None

yield ChildTargetRecord(
type=self.__type__,
name=name,
path=vm.path,
_target=self.target,
)
35 changes: 32 additions & 3 deletions dissect/target/plugins/child/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING

from defusedxml import ElementTree
from dissect.hypervisor import hyperv

from dissect.target.exceptions import UnsupportedPluginError
Expand All @@ -10,6 +11,7 @@

if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path

from dissect.target.target import Target

Expand Down Expand Up @@ -45,16 +47,43 @@ def list_children(self) -> Iterator[ChildTargetRecord]:
data = hyperv.HyperVFile(self.data_vmcx.open()).as_dict()

if virtual_machines := data["Configurations"].get("VirtualMachines"):
for vm_path in virtual_machines.values():
for path in virtual_machines.values():
vm_path = self.target.fs.path(path)
yield ChildTargetRecord(
type=self.__type__,
path=self.target.fs.path(vm_path),
name=self._name_from_vmcx(vm_path),
path=vm_path,
_target=self.target,
)

for xml_path in self.vm_xml:
vm_path = xml_path.resolve()
yield ChildTargetRecord(
type=self.__type__,
path=xml_path.resolve(),
name=self._name_from_xml(vm_path),
path=vm_path,
_target=self.target,
)

def _name_from_vmcx(self, path: Path) -> str | None:
try:
with path.open("rb") as fh:
config = hyperv.HyperVFile(fh).as_dict()
return config.get("configuration", {}).get("properties", {}).get("name")
except Exception as e:
self.target.log.error("Failed parsing name from VMCX: %s", path) # noqa: TRY400
self.target.log.debug("", exc_info=e)

return None

def _name_from_xml(self, path: Path) -> str | None:
try:
xml = ElementTree.fromstring(path.read_bytes())

if (name := xml.find(".//properties/name")) is not None:
return name.text
except Exception as e:
self.target.log.error("Failed parsing name from XML: %s", path) # noqa: TRY400
self.target.log.debug("", exc_info=e)

return None
52 changes: 52 additions & 0 deletions dissect/target/plugins/child/lima.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import ChildTargetRecord
from dissect.target.plugin import ChildTargetPlugin

if TYPE_CHECKING:
from collections.abc import Iterator

from dissect.target import Target


class LimaChildTargetPlugin(ChildTargetPlugin):
"""Child target plugin that yields Lima VMs.

Lima (Linux Machines) is a Linux VM or container runtime for macOS and Linux.

References:
- https://github.com/lima-vm/lima
"""

__type__ = "lima"

def __init__(self, target: Target):
super().__init__(target)
self.paths = []
for user in self.target.user_details.all_with_home():
# check .lima folder in ~/
if (path := user.home_path.joinpath(".lima")).exists():
self.paths.append(path)
# check .lima folder in ~/.config/
if (path := user.home_path.joinpath(".config", "lima")).exists():
self.paths.append(path)

def check_compatible(self) -> None:
if not self.paths:
raise UnsupportedPluginError("No Lima configurations found")

def list_children(self) -> Iterator[ChildTargetRecord]:
for path in self.paths:
for instance in path.iterdir():
if instance.name.startswith((".", "_")) or not instance.is_dir():
continue

yield ChildTargetRecord(
type=self.__type__,
name=instance.name,
path=instance.joinpath("diffdisk"),
_target=self.target,
)
12 changes: 12 additions & 0 deletions dissect/target/plugins/child/parallels.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import TYPE_CHECKING

from defusedxml import ElementTree

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import ChildTargetRecord
from dissect.target.plugin import ChildTargetPlugin
Expand Down Expand Up @@ -67,8 +69,18 @@ def check_compatible(self) -> None:

def list_children(self) -> Iterator[ChildTargetRecord]:
for pvm in self.pvms:
try:
config = ElementTree.fromstring(pvm.joinpath("config.pvs").read_bytes())
name = config.find(".//VmName").text
except Exception as e:
self.target.log.error("Failed parsing VmName from config.pvs: %s", pvm) # noqa: TRY400
self.target.log.debug("", exc_info=e)

name = None

yield ChildTargetRecord(
type=self.__type__,
name=name,
path=pvm,
_target=self.target,
)
1 change: 1 addition & 0 deletions dissect/target/plugins/child/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def list_children(self) -> Iterator[ChildTargetRecord]:
if container.mount_path:
yield ChildTargetRecord(
type=self.__type__,
name=container.name,
path=container.mount_path,
_target=self.target,
)
19 changes: 18 additions & 1 deletion dissect/target/plugins/child/proxmox.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,25 @@ def check_compatible(self) -> None:

def list_children(self) -> Iterator[ChildTargetRecord]:
for vm in self.target.vmlist():
vm_path = self.target.fs.path(vm.path)

name = None
try:
with vm_path.open("rt") as fh:
for line in fh:
if not (line := line.strip()):
continue

if (key_value := line.split(":", 1)) and key_value[0] == "name":
name = key_value[1].strip()
break
except Exception as e:
self.target.log.error("Failed parsing name from VM config: %s", vm_path) # noqa: TRY400
self.target.log.debug("", exc_info=e)

yield ChildTargetRecord(
type=self.__type__,
path=vm.path,
name=name,
path=vm_path,
_target=self.target,
)
16 changes: 15 additions & 1 deletion dissect/target/plugins/child/qemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import TYPE_CHECKING

from defusedxml import ElementTree

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import ChildTargetRecord
from dissect.target.plugin import ChildTargetPlugin
Expand All @@ -21,4 +23,16 @@ def check_compatible(self) -> None:

def list_children(self) -> Iterator[ChildTargetRecord]:
for domain in self.target.fs.path("/etc/libvirt/qemu").glob("*.xml"):
yield ChildTargetRecord(type=self.__type__, path=domain)
try:
name = ElementTree.fromstring(domain.read_bytes()).find("name").text
except Exception as e:
self.target.log.error("Failed to parse name from QEMU config: %s", domain) # noqa: TRY400
self.target.log.debug("", exc_info=e)
name = None

yield ChildTargetRecord(
type=self.__type__,
name=name,
path=domain,
_target=self.target,
)
14 changes: 12 additions & 2 deletions dissect/target/plugins/child/virtualbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING

from defusedxml import ElementTree as ET
from defusedxml import ElementTree
from dissect.hypervisor.descriptor.vbox import VBox

from dissect.target.exceptions import UnsupportedPluginError
Expand Down Expand Up @@ -63,7 +63,7 @@ def find_vms(self) -> Iterator[Path]:
continue

try:
config = ET.fromstring(path.read_text())
config = ElementTree.fromstring(path.read_text())
except Exception as e:
self.target.log.warning("Unable to parse %s: %s", path, e)
self.target.log.debug("", exc_info=e)
Expand Down Expand Up @@ -95,8 +95,18 @@ def check_compatible(self) -> None:

def list_children(self) -> Iterator[ChildTargetRecord]:
for vbox in self.vboxes:
try:
config = ElementTree.fromstring(vbox.read_bytes())
name = config.find(f".//{VBox.VBOX_XML_NAMESPACE}Machine").attrib["name"]
except Exception as e:
self.target.log.error("Failed to parse name from VirtualBox XML: %s", vbox) # noqa: TRY400
self.target.log.debug("", exc_info=e)

name = None

yield ChildTargetRecord(
type=self.__type__,
name=name,
path=vbox,
_target=self.target,
)
Loading
Loading