From 898b07abe75206dc6bd18f8e6df9207a1aec23c6 Mon Sep 17 00:00:00 2001 From: qmadev <190383216+qmadev@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:57:33 +0100 Subject: [PATCH 01/52] some working stuff --- .../os/unix/bsd/darwin/macos/install.py | 123 +++++++++++++++ .../os/unix/bsd/darwin/macos/launchers.py | 144 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/locale.py | 73 +++++++++ .../os/unix/bsd/darwin/macos/shadow.py | 86 +++++++++++ .../os/unix/bsd/darwin/macos/test_install.py | 0 5 files changed, 426 insertions(+) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/install.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_install.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py new file mode 100644 index 0000000000..11692bb44f --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import re +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +# Do we call it macos or osx? +OSXInstallLogRecord = TargetRecordDescriptor( + "osx/install", + [ + ("datetime", "ts"), + ("string", "host"), + ("string", "component"), + ("string", "message"), + ], +) + +RE_TIMESTAMP_PATTERN = re.compile( + r"^(?:" + r"(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}" + r"|" + r"\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}[+-]\d{1,2}(?::?\d{2})?" + r")" +) + + +class InstallLog(Plugin): + """Return information related software installations and updates on OS X. + + References: + - https://sansorg.egnyte.com/dl/m9ftGF7heI + """ + + INSTALL_LOG_PATH = "/var/log/install.log" + + def __init__(self, target: Target): + super().__init__(target) + + def check_compatible(self) -> None: + if not self.target.fs.exists(self.INSTALL_LOG_PATH): + raise UnsupportedPluginError("No install.log file found.") + + @export(record=OSXInstallLogRecord) + def installlog(self) -> Iterator[OSXInstallLogRecord]: + """Return all OS X install log messages. + + Yields OSXInstallLogRecord instances with fields: + + .. code-block:: text + + ts (datetime): Timestamp of the log line. + host (str): Hostname. + component (str): Component name. + message (str): Log message. + + References: + - https://sansorg.egnyte.com/dl/m9ftGF7heI + """ + + logfile = self.target.fs.path(self.INSTALL_LOG_PATH).open(mode="rt") + + current_ts: re.Match[str] | None = None + current_buf = "" + + for line in logfile.readlines(): + # If we have a buffer with a timestamp and + # our current line also has a timestamp, + # we should have a complete record in our buffer. + if ts_match := RE_TIMESTAMP_PATTERN.match(line): + if current_ts: + # Add 1 to skip the whitespace after the timestamp. + asdf = current_buf[len(current_ts.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield OSXInstallLogRecord( + ts=parse_timestamp(current_ts), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + _target=self.target, + ) + + current_ts = ts_match + current_buf = line + elif current_buf: + current_buf += line + + # For the last line + if current_ts and current_buf: + asdf = current_buf[len(current_ts.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield OSXInstallLogRecord( + ts=parse_timestamp(current_ts), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + _target=self.target, + ) + + +def parse_timestamp(timestamp: re.Match) -> datetime: + # I could not find docs about this but it seems to be the case that when you + # start installing your macbook, it outputs the timestamp in this BSD style + # format without any timezone info. After some messages it starts outputting + # ISO timestamps with timezone info. From those timestamps I kind of inferred + # that this is actually just UTC. + ts = None + try: + ts = datetime.fromisoformat(timestamp.group()) + except ValueError: + ts = datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) + + return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py new file mode 100644 index 0000000000..02afb0dc7d --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import plistlib +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + from pathlib import Path + + from dissect.target.target import Target + from flow.record.base import Record + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +class UserPlugin(Plugin): + """macOS user plugin.""" + + LAUNCH_AGENT_PATHS = [ + "/System/Library/LaunchAgents/*.plist", + "/Library/LaunchAgents/*.plist", + "~/Library/LaunchAgents/*.plist", + ] + LAUNCH_DAEMON_PATHS = [ + "/System/Library/LaunchDaemons/*.plist", + "/Library/LaunchDaemons/*.plist", + ] + LAUNCH_AGENT_FILES = set() + LAUNCH_DAEMON_FILES = set() + + def __init__(self, target: Target): + super().__init__(target) + self._find_files() + + def check_compatible(self) -> None: + if not (self.LAUNCH_AGENT_FILES or self.LAUNCH_DAEMON_FILES): + raise UnsupportedPluginError("No Agent or Deamon files found") + + def _find_files(self): + for glob in self.LAUNCH_AGENT_PATHS: + for path in self.target.fs.glob(glob): + self.LAUNCH_AGENT_FILES.add(path) + + for glob in self.LAUNCH_DAEMON_PATHS: + for path in self.target.fs.glob(glob): + self.LAUNCH_DAEMON_FILES.add(path) + + + @export(record=DynamicDescriptor(["string"])) + # @export(output="yield") + def launch_agents(self) -> Iterator[DynamicDescriptor]: + """Yield OS X launch agent plist files.""" + + for file in self.LAUNCH_AGENT_FILES: + file = self.target.fs.path(file) + fh = file.open(mode="rb") + try: + data = plistlib.load(fh) + flat_data = {} + extract_nested_dict(flat_data, data) + + yield self._build_record("osx/launch_agent", flat_data, file) + except Exception: + self.target.log.exception("Failed to parse %s", file) + + @export(record=DynamicDescriptor(["string"])) + def launch_daemons(self) -> Iterator[DynamicDescriptor]: + """Yield OS X launch daemon plist files.""" + + for file in self.LAUNCH_DAEMON_FILES: + file = self.target.fs.path(file) + fh = file.open(mode="rb") + try: + data = plistlib.load(fh) + flat_data = {} + extract_nested_dict(flat_data, data) + + yield self._build_record("osx/launch_daemon", flat_data, file) + except Exception: + self.target.log.exception("Failed to parse %s", file) + + def _build_record(self, record_name: str, rdict: dict, source: Path | None) -> Record: + # predictable order of fields in the list is important, since we'll + # be constructing a record descriptor from it. + record_fields = sorted(rdict.items()) + + record_values = { + "_target": self.target, + "source": source, + } + record_fields = [] + + for k, v in rdict.items(): + k = format_key(k) + + if isinstance(v, bool): + record_fields.append(("boolean", k)) + elif isinstance(v, int): + record_fields.append(("varint", k)) + else: + record_fields.append(("string", k)) + + record_values[k] = v + + record_fields.append(("path", "source")) + + # tuple conversion here is needed for lru_cache + desc = self._create_event_descriptor(record_name, tuple(record_fields)) + return desc(**record_values) + + def _create_event_descriptor(self, record_name, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: + return TargetRecordDescriptor(record_name, record_fields) + +def format_key(key: str) -> str: + # A lot of "malformed" keys + key = key.replace(".", "_") + key = key.replace("-", "_") + key = key.replace("@", "_") + key = key.replace(" ", "_") + key = key.replace("()", "") + key = key.removeprefix("#") + key = key.removeprefix("_") + key = key.lstrip("_") + + if "/" in key: + key = key.rsplit("/", 1)[-1] + + if key == "0": + key = "zero" + + return key + + +def extract_nested_dict(flat, nested): + for k, v in nested.items(): + if isinstance(v, dict): + extract_nested_dict(flat, v) + else: + flat[k] = v + diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py new file mode 100644 index 0000000000..be7305a24b --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.localeutil import normalize_language, normalize_timezone +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import export +from dissect.target.plugins.os.default.locale import LocalePlugin +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +from datetime import datetime, timezone +import plistlib +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +WindowsKeyboardRecord = TargetRecordDescriptor( + "windows/keyboard", + [ + ("string", "layout"), + ("string", "language_id"), + ], +) + + +class OSXLocalePlugin(LocalePlugin): + """Windows locale plugin.""" + + GLOBAL = "/Library/Preferences/.GlobalPreferences.plist" + + def __init__(self, target: Target): + super().__init__(target) + + def check_compatible(self) -> None: + if not self.target.os == "macos": + raise UnsupportedPluginError("Unsupported Plugin") + + @export(property=True) + def timezone(self) -> str | None: + """Get the configured timezone of the system in IANA TZ standard format.""" + + preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) + tz_data = preferences["com.apple.TimeZonePref.Last_Selected_City"] + tz = None + + for entry in tz_data: + try: + tz = ZoneInfo(entry).key + except ZoneInfoNotFoundError: + continue + + return tz + + @export(property=True) + def language(self) -> str | None: + """Get a list of installed languages on the system.""" + # HKCU\\Control Panel\\International\\User Profile" Languages + + preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) + languages = preferences["AppleLanguages"] + clean_languages = [] + for lang in languages: + language = normalize_language(lang.replace("-", "_")) + if language not in clean_languages: + clean_languages.append(language) + + return clean_languages + + @export(property=True) + def install_date(self) -> str | None: + mtime = self.target.fs.path("/private/var/db/.AppleSetupDone").lstat().st_mtime + return datetime.fromtimestamp(mtime, timezone.utc) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py new file mode 100644 index 0000000000..3e59b304fc --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +import plistlib + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +OSXShadowRecord = TargetRecordDescriptor( + "osx/shadow", + [ + ("string", "name"), + ("string", "hash"), + ("string", "salt"), + ("varint", "iterations"), + ("string", "algorithm"), + ("path", "source") + ], +) + + +class ShadowPlugin(Plugin): + """Unix shadow passwords plugin.""" + + SHADOW_FILES = ("/etc/shadow", "/etc/shadow-") + GLOB_PATTERN = "/var/db/dslocal/nodes/Default/users/*.plist" + USER_FILES = set() + + def __init__(self, target: Target): + super().__init__(target) + self._resolve_files() + + def check_compatible(self) -> None: + if not self.USER_FILES: + raise UnsupportedPluginError("No shadow file found") + + def _resolve_files(self) -> None: + for file in self.target.fs.glob(self.GLOB_PATTERN): + self.USER_FILES.add(file) + + @export(record=OSXShadowRecord) + def passwords(self) -> Iterator[OSXShadowRecord]: + """Yield shadow records from /etc/shadow files. + + References: + - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html + - https://linux.die.net/man/5/shadow + """ + + try: + for path in self.target.fs.path("/var/db/dslocal/nodes/Default/users/").glob("*.plist"): + user = plistlib.load(path.open()) + if user.get("ShadowHashData") is None: + continue + + shadow = plistlib.loads(user["ShadowHashData"][0]) + username = user["name"][0] + + for key in shadow: + if shadow[key].get("entropy") is None: + continue + + hash = shadow[key]["entropy"].hex() + salt = shadow[key]["salt"].hex() + iterations = shadow[key]["iterations"] + + + yield OSXShadowRecord( + name=username, + hash=hash, + salt=salt, + iterations=iterations, + algorithm=key, + source=path, + _target=self.target, + ) + except FileNotFoundError: + pass diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_install.py b/tests/plugins/os/unix/bsd/darwin/macos/test_install.py new file mode 100644 index 0000000000..e69de29bb2 From 1e8d95ce3e60f59201d6d2ce4e9a8e06f573c546 Mon Sep 17 00:00:00 2001 From: qmadev <190383216+qmadev@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:27:58 +0100 Subject: [PATCH 02/52] lint stuff --- .../os/unix/bsd/darwin/macos/install.py | 1 - .../os/unix/bsd/darwin/macos/launchers.py | 78 +++++++++---------- .../os/unix/bsd/darwin/macos/locale.py | 11 ++- .../os/unix/bsd/darwin/macos/shadow.py | 67 +++++++--------- 4 files changed, 71 insertions(+), 86 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py index 11692bb44f..d625a20a0c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py @@ -65,7 +65,6 @@ def installlog(self) -> Iterator[OSXInstallLogRecord]: References: - https://sansorg.egnyte.com/dl/m9ftGF7heI """ - logfile = self.target.fs.path(self.INSTALL_LOG_PATH).open(mode="rt") current_ts: re.Match[str] | None = None diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py index 02afb0dc7d..8180c6782f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py @@ -12,49 +12,50 @@ from collections.abc import Iterator from pathlib import Path - from dissect.target.target import Target from flow.record.base import Record + from dissect.target.target import Target + re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + class UserPlugin(Plugin): """macOS user plugin.""" - LAUNCH_AGENT_PATHS = [ + LAUNCH_AGENT_PATHS = ( "/System/Library/LaunchAgents/*.plist", "/Library/LaunchAgents/*.plist", "~/Library/LaunchAgents/*.plist", - ] - LAUNCH_DAEMON_PATHS = [ + ) + LAUNCH_DAEMON_PATHS = ( "/System/Library/LaunchDaemons/*.plist", "/Library/LaunchDaemons/*.plist", - ] - LAUNCH_AGENT_FILES = set() - LAUNCH_DAEMON_FILES = set() + ) def __init__(self, target: Target): super().__init__(target) + + self.launch_agent_files = set() + self.launch_daemon_files = set() self._find_files() def check_compatible(self) -> None: if not (self.LAUNCH_AGENT_FILES or self.LAUNCH_DAEMON_FILES): raise UnsupportedPluginError("No Agent or Deamon files found") - def _find_files(self): + def _find_files(self) -> None: for glob in self.LAUNCH_AGENT_PATHS: for path in self.target.fs.glob(glob): - self.LAUNCH_AGENT_FILES.add(path) + self.launch_agent_files.add(path) for glob in self.LAUNCH_DAEMON_PATHS: for path in self.target.fs.glob(glob): - self.LAUNCH_DAEMON_FILES.add(path) - + self.launch_daemon_files.add(path) @export(record=DynamicDescriptor(["string"])) # @export(output="yield") def launch_agents(self) -> Iterator[DynamicDescriptor]: """Yield OS X launch agent plist files.""" - for file in self.LAUNCH_AGENT_FILES: file = self.target.fs.path(file) fh = file.open(mode="rb") @@ -70,7 +71,6 @@ def launch_agents(self) -> Iterator[DynamicDescriptor]: @export(record=DynamicDescriptor(["string"])) def launch_daemons(self) -> Iterator[DynamicDescriptor]: """Yield OS X launch daemon plist files.""" - for file in self.LAUNCH_DAEMON_FILES: file = self.target.fs.path(file) fh = file.open(mode="rb") @@ -83,38 +83,39 @@ def launch_daemons(self) -> Iterator[DynamicDescriptor]: except Exception: self.target.log.exception("Failed to parse %s", file) - def _build_record(self, record_name: str, rdict: dict, source: Path | None) -> Record: - # predictable order of fields in the list is important, since we'll - # be constructing a record descriptor from it. - record_fields = sorted(rdict.items()) + def build_record(self, record_name: str, rdict: dict, source: Path | None) -> Record: + # predictable order of fields in the list is important, since we'll + # be constructing a record descriptor from it. + record_fields = sorted(rdict.items()) - record_values = { - "_target": self.target, - "source": source, - } - record_fields = [] + record_values = { + "_target": self.target, + "source": source, + } + record_fields = [] - for k, v in rdict.items(): - k = format_key(k) + for k, v in rdict.items(): + k = format_key(k) - if isinstance(v, bool): - record_fields.append(("boolean", k)) - elif isinstance(v, int): - record_fields.append(("varint", k)) - else: - record_fields.append(("string", k)) + if isinstance(v, bool): + record_fields.append(("boolean", k)) + elif isinstance(v, int): + record_fields.append(("varint", k)) + else: + record_fields.append(("string", k)) - record_values[k] = v + record_values[k] = v - record_fields.append(("path", "source")) + record_fields.append(("path", "source")) - # tuple conversion here is needed for lru_cache - desc = self._create_event_descriptor(record_name, tuple(record_fields)) - return desc(**record_values) + # tuple conversion here is needed for lru_cache + desc = self._create_event_descriptor(record_name, tuple(record_fields)) + return desc(**record_values) - def _create_event_descriptor(self, record_name, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: + def create_event_descriptor(self, record_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: return TargetRecordDescriptor(record_name, record_fields) + def format_key(key: str) -> str: # A lot of "malformed" keys key = key.replace(".", "_") @@ -135,10 +136,9 @@ def format_key(key: str) -> str: return key -def extract_nested_dict(flat, nested): +def extract_nested_dict(flat: dict, nested: dict) -> None: for k, v in nested.items(): if isinstance(v, dict): - extract_nested_dict(flat, v) + extract_nested_dict(flat, v) else: flat[k] = v - diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index be7305a24b..39c3fe8cda 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -1,17 +1,17 @@ from __future__ import annotations +import plistlib +from datetime import datetime, timezone from typing import TYPE_CHECKING +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.localeutil import normalize_language, normalize_timezone +from dissect.target.helpers.localeutil import normalize_language from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import export from dissect.target.plugins.os.default.locale import LocalePlugin -from zoneinfo import ZoneInfo, ZoneInfoNotFoundError -from datetime import datetime, timezone -import plistlib + if TYPE_CHECKING: - from collections.abc import Iterator from dissect.target.target import Target @@ -39,7 +39,6 @@ def check_compatible(self) -> None: @export(property=True) def timezone(self) -> str | None: """Get the configured timezone of the system in IANA TZ standard format.""" - preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) tz_data = preferences["com.apple.TimeZonePref.Last_Selected_City"] tz = None diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py index 3e59b304fc..a3faadb128 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -1,14 +1,12 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone +import plistlib from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -import plistlib - if TYPE_CHECKING: from collections.abc import Iterator @@ -22,7 +20,7 @@ ("string", "salt"), ("varint", "iterations"), ("string", "algorithm"), - ("path", "source") + ("path", "source"), ], ) @@ -30,12 +28,11 @@ class ShadowPlugin(Plugin): """Unix shadow passwords plugin.""" - SHADOW_FILES = ("/etc/shadow", "/etc/shadow-") - GLOB_PATTERN = "/var/db/dslocal/nodes/Default/users/*.plist" - USER_FILES = set() + USER_FILE_GLOB = "/var/db/dslocal/nodes/Default/users/*.plist" def __init__(self, target: Target): super().__init__(target) + self.user_files = set() self._resolve_files() def check_compatible(self) -> None: @@ -44,43 +41,33 @@ def check_compatible(self) -> None: def _resolve_files(self) -> None: for file in self.target.fs.glob(self.GLOB_PATTERN): - self.USER_FILES.add(file) + self.user_files.add(file) @export(record=OSXShadowRecord) def passwords(self) -> Iterator[OSXShadowRecord]: - """Yield shadow records from /etc/shadow files. + """Yield shadow records from OS X user plist files.""" + for path in self.target.fs.path("/var/db/dslocal/nodes/Default/users/").glob("*.plist"): + user = plistlib.load(path.open()) + if user.get("ShadowHashData") is None: + continue - References: - - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html - - https://linux.die.net/man/5/shadow - """ + shadow = plistlib.loads(user["ShadowHashData"][0]) + username = user["name"][0] - try: - for path in self.target.fs.path("/var/db/dslocal/nodes/Default/users/").glob("*.plist"): - user = plistlib.load(path.open()) - if user.get("ShadowHashData") is None: + for key in shadow: + if shadow[key].get("entropy") is None: continue - shadow = plistlib.loads(user["ShadowHashData"][0]) - username = user["name"][0] - - for key in shadow: - if shadow[key].get("entropy") is None: - continue - - hash = shadow[key]["entropy"].hex() - salt = shadow[key]["salt"].hex() - iterations = shadow[key]["iterations"] - - - yield OSXShadowRecord( - name=username, - hash=hash, - salt=salt, - iterations=iterations, - algorithm=key, - source=path, - _target=self.target, - ) - except FileNotFoundError: - pass + hash = shadow[key]["entropy"].hex() + salt = shadow[key]["salt"].hex() + iterations = shadow[key]["iterations"] + + yield OSXShadowRecord( + name=username, + hash=hash, + salt=salt, + iterations=iterations, + algorithm=key, + source=path, + _target=self.target, + ) From 6db60f2e85518a4bbce45a1adfbb83a66cbe8dbb Mon Sep 17 00:00:00 2001 From: qmadev <190383216+qmadev@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:35:11 +0100 Subject: [PATCH 03/52] add groups and systemlog --- .../os/unix/bsd/darwin/macos/groups.py | 87 +++++++++++++ .../os/unix/bsd/darwin/macos/locale.py | 5 +- .../os/unix/bsd/darwin/macos/shadow.py | 9 +- .../os/unix/bsd/darwin/macos/systemlog.py | 115 ++++++++++++++++++ 4 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py new file mode 100644 index 0000000000..2bbff430c1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +GroupInfoRecord = TargetRecordDescriptor( + "macos/groups", + [ + ("string", "generateduid"), + ("string", "members"), + ("string", "smb_sid"), + ("varint", "gid"), + ("string", "name"), + ("string", "realname"), + ("path", "source"), + ], +) + + +class GroupPlugin(Plugin): + """macOS group plugin.""" + + GROUP_PATH_GLOB = "/var/db/dslocal/nodes/Default/groups/*.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.group_files = set() + self._resolve_files() + + def check_compatible(self) -> None: + if not self.group_files: + raise UnsupportedPluginError("No group files found") + + def _resolve_files(self) -> None: + for file in self.target.fs.glob(self.GROUP_PATH_GLOB): + self.group_files.add(file) + + @export(record=GroupInfoRecord) + def groups(self) -> Iterator[GroupInfoRecord]: + """Yield user account policy information.""" + for file in self.group_files: + file = self.target.fs.path(file) + group_data = plistlib.load(file.open()) + + if uuid := group_data.get("generateduid"): # noqa: SIM102 + if len(uuid) == 1: + uuid = uuid[0] + + if smb_sid := group_data.get("smb_sid"): # noqa: SIM102 + if len(smb_sid): + smb_sid = smb_sid[0] + + if gid := group_data.get("gid"): # noqa: SIM102 + if len(gid) == 1: + gid = gid[0] + + if members := group_data.get("users"): # noqa: SIM102 + if len(members) == 1: + members = members[0] + + if realname := group_data.get("realname"): # noqa: SIM102 + if len(realname) == 1: + realname = realname[0] + + if name := group_data.get("name"): # noqa: SIM102 + if len(name) == 1: + name = name[0] + + yield GroupInfoRecord( + generateduid=uuid, + members=members, + smb_sid=smb_sid, + gid=gid, + name=name, + realname=realname, + source=file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 39c3fe8cda..76e3615fb6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -12,7 +12,6 @@ from dissect.target.plugins.os.default.locale import LocalePlugin if TYPE_CHECKING: - from dissect.target.target import Target WindowsKeyboardRecord = TargetRecordDescriptor( @@ -46,7 +45,7 @@ def timezone(self) -> str | None: for entry in tz_data: try: tz = ZoneInfo(entry).key - except ZoneInfoNotFoundError: + except ZoneInfoNotFoundError: # noqa: PERF203 continue return tz @@ -54,8 +53,6 @@ def timezone(self) -> str | None: @export(property=True) def language(self) -> str | None: """Get a list of installed languages on the system.""" - # HKCU\\Control Panel\\International\\User Profile" Languages - preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) languages = preferences["AppleLanguages"] clean_languages = [] diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py index a3faadb128..eb1b2a58e2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -36,17 +36,18 @@ def __init__(self, target: Target): self._resolve_files() def check_compatible(self) -> None: - if not self.USER_FILES: - raise UnsupportedPluginError("No shadow file found") + if not self.user_files: + raise UnsupportedPluginError("No shadow files found") def _resolve_files(self) -> None: - for file in self.target.fs.glob(self.GLOB_PATTERN): + for file in self.target.fs.glob(self.USER_FILE_GLOB): self.user_files.add(file) @export(record=OSXShadowRecord) def passwords(self) -> Iterator[OSXShadowRecord]: """Yield shadow records from OS X user plist files.""" - for path in self.target.fs.path("/var/db/dslocal/nodes/Default/users/").glob("*.plist"): + for path in self.user_files: + path = self.target.fs.path(path) user = plistlib.load(path.open()) if user.get("ShadowHashData") is None: continue diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py new file mode 100644 index 0000000000..6304913a4a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import gzip +import re +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + from datetime import tzinfo + + from dissect.target.target import Target + +# Do we call it macos or osx? +OSXInstallLogRecord = TargetRecordDescriptor( + "osx/system", + [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], +) + +RE_TIMESTAMP_PATTERN = re.compile(r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}") + + +class SystemLog(Plugin): + """Return information related software installations and updates on OS X. + + References: + - https://sansorg.egnyte.com/dl/m9ftGF7heI + """ + + SYSTEM_LOG_GLOB = "/var/log/system.log*" + + def __init__(self, target: Target): + super().__init__(target) + self.log_files = set() + self._resolve_files() + + def check_compatible(self) -> None: + if not self.log_files: + raise UnsupportedPluginError("No system log files found.") + + def _resolve_files(self) -> None: + for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): + self.log_files.add(file) + + @export(record=OSXInstallLogRecord) + def systemlog(self) -> Iterator[OSXInstallLogRecord]: + """Return all OS X install log messages. + + Yields OSXInstallLogRecord instances with fields: + + .. code-block:: text + + ts (datetime): Timestamp of the log line. + host (str): Hostname. + component (str): Component name. + message (str): Log message. + + References: + - https://sansorg.egnyte.com/dl/m9ftGF7heI + """ + for file in self.log_files: + filepath = self.target.fs.path(file) + + logfile = gzip.open(filepath, mode="rt") if file.endswith(".gz") else filepath.open(mode="rt") # noqa: SIM115 + + current_ts: re.Match[str] | None = None + current_buf = "" + + for line in logfile.readlines(): + # If we have a buffer with a timestamp and + # our current line also has a timestamp, + # we should have a complete record in our buffer. + if ts_match := RE_TIMESTAMP_PATTERN.match(line): + if current_ts: + # Add 1 to skip the whitespace after the timestamp. + asdf = current_buf[len(current_ts.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield OSXInstallLogRecord( + ts=self.parse_timestamp(current_ts), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + source=filepath, + _target=self.target, + ) + + current_ts = ts_match + current_buf = line + elif current_buf: + current_buf += line + + # For the last line + if current_ts and current_buf: + asdf = current_buf[len(current_ts.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield OSXInstallLogRecord( + ts=self.parse_timestamp(current_ts), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + source=filepath, + _target=self.target, + ) + + logfile.close() + + +def parse_timestamp(timestamp: re.Match, tzinfo: tzinfo = timezone.utc) -> datetime: + return datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=tzinfo) From a33ccabf64ed9d8dd129cb8fc2f238573a11d918 Mon Sep 17 00:00:00 2001 From: qmadev <190383216+qmadev@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:47:42 +0100 Subject: [PATCH 04/52] add empty test files --- tests/plugins/os/unix/bsd/darwin/macos/test_groups.py | 0 tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py | 0 tests/plugins/os/unix/bsd/darwin/macos/test_locale.py | 0 tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py | 0 tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_groups.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_locale.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py b/tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py new file mode 100644 index 0000000000..e69de29bb2 From bc39d35f7440e81ea6714217172afe31daadea59 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:33:07 +0200 Subject: [PATCH 05/52] Fix macOS parsers and add tests --- dissect/target/container.py | 1 + .../os/unix/bsd/darwin/macos/locale.py | 2 +- .../os/unix/bsd/darwin/macos/logs/__init__.py | 0 .../macos/{install.py => logs/installlog.py} | 21 +- .../bsd/darwin/macos/{ => logs}/systemlog.py | 29 ++- .../bsd/darwin/macos/persistence/__init__.py | 0 .../macos/{ => persistence}/launchers.py | 76 +++++-- .../os/unix/bsd/darwin/macos/shadow.py | 12 +- .../bsd/darwin/macos/groups/_applepay.plist | 3 + .../darwin/macos/groups/_eligibilityd.plist | 3 + .../unix/bsd/darwin/macos/groups/nobody.plist | 3 + .../bsd/darwin/macos/locale/.AppleSetupDone} | 0 .../macos/locale/.GlobalPreferences.plist | 3 + .../os/unix/bsd/darwin/macos/logs/install.log | 3 + .../os/unix/bsd/darwin/macos/logs/system.log | 3 + .../launchers/com.apple.AMPArtworkAgent.plist | 3 + .../com.apple.WirelessRadioManager-osx.plist | 3 + .../com.apple.cfprefsd.xpc.daemon.plist | 3 + .../com.apple.familynotificationd.plist | 3 + .../com.apple.perfpowermetricd.plist | 3 + .../launchers/com.apple.seserviced.plist | 3 + .../com.apple.sidecar-hid-relay.plist | 3 + .../darwin/macos/shadow/_mbsetupuser.plist | 3 + .../unix/bsd/darwin/macos/shadow/user.plist | 3 + .../os/unix/bsd/darwin/macos/logs/__init__.py | 0 .../bsd/darwin/macos/logs/test_installlog.py | 53 +++++ .../bsd/darwin/macos/logs/test_systemlog.py | 60 ++++++ .../bsd/darwin/macos/persistence/__init__.py | 0 .../macos/persistence/test_launchers.py | 193 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/test_groups.py | 61 ++++++ .../os/unix/bsd/darwin/macos/test_locale.py | 65 ++++++ .../os/unix/bsd/darwin/macos/test_shadow.py | 57 ++++++ 32 files changed, 624 insertions(+), 51 deletions(-) rename tests/plugins/os/unix/bsd/darwin/macos/test_install.py => dissect/target/plugins/os/unix/bsd/darwin/macos/logs/__init__.py (100%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{install.py => logs/installlog.py} (89%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{ => logs}/systemlog.py (83%) rename tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py => dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py (100%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{ => persistence}/launchers.py (60%) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_applepay.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_eligibilityd.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/groups/nobody.plist rename tests/{plugins/os/unix/bsd/darwin/macos/test_systemlog.py => _data/plugins/os/unix/bsd/darwin/macos/locale/.AppleSetupDone} (100%) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.GlobalPreferences.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/install.log create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/_mbsetupuser.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/user.plist create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/__init__.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py diff --git a/dissect/target/container.py b/dissect/target/container.py index 0295fadfd3..c132d2a171 100644 --- a/dissect/target/container.py +++ b/dissect/target/container.py @@ -257,3 +257,4 @@ def open(item: list | str | BinaryIO | Path, *args, **kwargs) -> Container: register("hds", "HdsContainer") register("split", "SplitContainer") register("fortifw", "FortiFirmwareContainer") +register("asif", "AsifContainer") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 76e3615fb6..5d36033b78 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -23,7 +23,7 @@ ) -class OSXLocalePlugin(LocalePlugin): +class macOSLocalePlugin(LocalePlugin): """Windows locale plugin.""" GLOBAL = "/Library/Preferences/.GlobalPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_install.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/__init__.py similarity index 100% rename from tests/plugins/os/unix/bsd/darwin/macos/test_install.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/logs/__init__.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py similarity index 89% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/install.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py index d625a20a0c..8da0a2d82d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/install.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py @@ -13,9 +13,8 @@ from dissect.target.target import Target -# Do we call it macos or osx? -OSXInstallLogRecord = TargetRecordDescriptor( - "osx/install", +macOSInstallLogRecord = TargetRecordDescriptor( + "macos/install", [ ("datetime", "ts"), ("string", "host"), @@ -33,8 +32,8 @@ ) -class InstallLog(Plugin): - """Return information related software installations and updates on OS X. +class InstallLogPlugin(Plugin): + """Return information related software installations and updates on macOS. References: - https://sansorg.egnyte.com/dl/m9ftGF7heI @@ -49,11 +48,11 @@ def check_compatible(self) -> None: if not self.target.fs.exists(self.INSTALL_LOG_PATH): raise UnsupportedPluginError("No install.log file found.") - @export(record=OSXInstallLogRecord) - def installlog(self) -> Iterator[OSXInstallLogRecord]: - """Return all OS X install log messages. + @export(record=macOSInstallLogRecord) + def installlog(self) -> Iterator[macOSInstallLogRecord]: + """Return all macOS install log messages. - Yields OSXInstallLogRecord instances with fields: + Yields macOSInstallLogRecord instances with fields: .. code-block:: text @@ -80,7 +79,7 @@ def installlog(self) -> Iterator[OSXInstallLogRecord]: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield OSXInstallLogRecord( + yield macOSInstallLogRecord( ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip(), @@ -98,7 +97,7 @@ def installlog(self) -> Iterator[OSXInstallLogRecord]: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield OSXInstallLogRecord( + yield macOSInstallLogRecord( ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip(), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py similarity index 83% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py index 6304913a4a..d9adaf6dc4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/systemlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py @@ -15,17 +15,16 @@ from dissect.target.target import Target -# Do we call it macos or osx? -OSXInstallLogRecord = TargetRecordDescriptor( - "osx/system", +macOSSystemLogRecord = TargetRecordDescriptor( + "macos/system", [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], ) RE_TIMESTAMP_PATTERN = re.compile(r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}") -class SystemLog(Plugin): - """Return information related software installations and updates on OS X. +class SystemLogPlugin(Plugin): + """Return information related software installations and updates on macOS. References: - https://sansorg.egnyte.com/dl/m9ftGF7heI @@ -46,11 +45,11 @@ def _resolve_files(self) -> None: for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): self.log_files.add(file) - @export(record=OSXInstallLogRecord) - def systemlog(self) -> Iterator[OSXInstallLogRecord]: - """Return all OS X install log messages. + @export(record=macOSSystemLogRecord) + def systemlog(self) -> Iterator[macOSSystemLogRecord]: + """Return all macOS install log messages. - Yields OSXInstallLogRecord instances with fields: + Yields macOSSystemLogRecord instances with fields: .. code-block:: text @@ -80,12 +79,12 @@ def systemlog(self) -> Iterator[OSXInstallLogRecord]: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield OSXInstallLogRecord( - ts=self.parse_timestamp(current_ts), + yield macOSSystemLogRecord( + ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip(), message=message.strip(), - source=filepath, + source=filepath, # What benefit does this field have??? _target=self.target, ) @@ -99,12 +98,12 @@ def systemlog(self) -> Iterator[OSXInstallLogRecord]: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield OSXInstallLogRecord( - ts=self.parse_timestamp(current_ts), + yield macOSSystemLogRecord( + ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip(), message=message.strip(), - source=filepath, + source=filepath, # What benefit does this field have??? _target=self.target, ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py similarity index 100% rename from tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py similarity index 60% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index 8180c6782f..0772a7f6b0 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -14,24 +14,29 @@ from flow.record.base import Record + from dissect.target.plugins.general.users import UserDetails from dissect.target.target import Target re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -class UserPlugin(Plugin): - """macOS user plugin.""" +class LaunchersPlugin(Plugin): + """macOS launchers plugin.""" - LAUNCH_AGENT_PATHS = ( + SYSTEM_LAUNCH_AGENT_PATHS = ( "/System/Library/LaunchAgents/*.plist", "/Library/LaunchAgents/*.plist", - "~/Library/LaunchAgents/*.plist", ) - LAUNCH_DAEMON_PATHS = ( + + SYSTEM_LAUNCH_DAEMON_PATHS = ( "/System/Library/LaunchDaemons/*.plist", "/Library/LaunchDaemons/*.plist", ) + USER_LAUNCH_AGENT_PATHS = ("Library/LaunchAgents/*.plist",) + + USER_LAUNCH_DAEMON_PATHS = ("Library/LaunchDaemons/*.plist",) + def __init__(self, target: Target): super().__init__(target) @@ -39,39 +44,72 @@ def __init__(self, target: Target): self.launch_daemon_files = set() self._find_files() + def _build_userdirs(self, hist_paths: list[str]) -> set[tuple[UserDetails, Path]]: + """Join the selected browser dirs with the user home path. + + Args: + hist_paths: A list with browser paths as strings. + + Returns: + List of tuples containing user and unique file path objects. + """ + users_dirs: set[tuple] = set() + for user_details in self.target.user_details.all_with_home(): + for d in hist_paths: + home_dir: Path = user_details.home_path + for cur_dir in home_dir.glob(d): + cur_dir = cur_dir.resolve() + if cur_dir.exists(): + users_dirs.add((user_details, cur_dir)) + return users_dirs + def check_compatible(self) -> None: - if not (self.LAUNCH_AGENT_FILES or self.LAUNCH_DAEMON_FILES): + if not (self.launch_agent_files or self.launch_daemon_files): raise UnsupportedPluginError("No Agent or Deamon files found") def _find_files(self) -> None: - for glob in self.LAUNCH_AGENT_PATHS: - for path in self.target.fs.glob(glob): + # --- System-wide LaunchAgents --- + for pattern in self.SYSTEM_LAUNCH_AGENT_PATHS: + for path in self.target.fs.glob(pattern): self.launch_agent_files.add(path) - for glob in self.LAUNCH_DAEMON_PATHS: - for path in self.target.fs.glob(glob): + # --- Per-user LaunchAgents --- + for _, path in self._build_userdirs(self.USER_LAUNCH_AGENT_PATHS): + self.launch_agent_files.add(path) + + # --- System-wide LaunchDaemons --- + for pattern in self.SYSTEM_LAUNCH_DAEMON_PATHS: + for path in self.target.fs.glob(pattern): self.launch_daemon_files.add(path) + # --- Per-user LaunchDaemons --- + for _, path in self._build_userdirs(self.USER_LAUNCH_DAEMON_PATHS): + self.launch_daemon_files.add(path) + @export(record=DynamicDescriptor(["string"])) # @export(output="yield") def launch_agents(self) -> Iterator[DynamicDescriptor]: - """Yield OS X launch agent plist files.""" - for file in self.LAUNCH_AGENT_FILES: + """Yield macOS launch agent plist files.""" + for file in self.launch_agent_files: file = self.target.fs.path(file) - fh = file.open(mode="rb") + try: + fh = file.open(mode="rb") + except FileNotFoundError: + self.target.log.exception("LaunchAgent missing target: %s", {file}) + continue try: data = plistlib.load(fh) flat_data = {} extract_nested_dict(flat_data, data) - yield self._build_record("osx/launch_agent", flat_data, file) + yield self.build_record("macos/launch_agent", flat_data, file) except Exception: self.target.log.exception("Failed to parse %s", file) @export(record=DynamicDescriptor(["string"])) def launch_daemons(self) -> Iterator[DynamicDescriptor]: - """Yield OS X launch daemon plist files.""" - for file in self.LAUNCH_DAEMON_FILES: + """Yield macOS launch daemon plist files.""" + for file in self.launch_daemon_files: file = self.target.fs.path(file) fh = file.open(mode="rb") try: @@ -79,7 +117,7 @@ def launch_daemons(self) -> Iterator[DynamicDescriptor]: flat_data = {} extract_nested_dict(flat_data, data) - yield self._build_record("osx/launch_daemon", flat_data, file) + yield self.build_record("macos/launch_daemon", flat_data, file) except Exception: self.target.log.exception("Failed to parse %s", file) @@ -88,6 +126,8 @@ def build_record(self, record_name: str, rdict: dict, source: Path | None) -> Re # be constructing a record descriptor from it. record_fields = sorted(rdict.items()) + print(record_fields) + record_values = { "_target": self.target, "source": source, @@ -109,7 +149,7 @@ def build_record(self, record_name: str, rdict: dict, source: Path | None) -> Re record_fields.append(("path", "source")) # tuple conversion here is needed for lru_cache - desc = self._create_event_descriptor(record_name, tuple(record_fields)) + desc = self.create_event_descriptor(record_name, tuple(record_fields)) return desc(**record_values) def create_event_descriptor(self, record_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py index eb1b2a58e2..48e221af59 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -12,8 +12,8 @@ from dissect.target import Target -OSXShadowRecord = TargetRecordDescriptor( - "osx/shadow", +macOSShadowRecord = TargetRecordDescriptor( + "macos/shadow", [ ("string", "name"), ("string", "hash"), @@ -43,9 +43,9 @@ def _resolve_files(self) -> None: for file in self.target.fs.glob(self.USER_FILE_GLOB): self.user_files.add(file) - @export(record=OSXShadowRecord) - def passwords(self) -> Iterator[OSXShadowRecord]: - """Yield shadow records from OS X user plist files.""" + @export(record=macOSShadowRecord) + def passwords(self) -> Iterator[macOSShadowRecord]: + """Yield shadow records from macOS user plist files.""" for path in self.user_files: path = self.target.fs.path(path) user = plistlib.load(path.open()) @@ -63,7 +63,7 @@ def passwords(self) -> Iterator[OSXShadowRecord]: salt = shadow[key]["salt"].hex() iterations = shadow[key]["iterations"] - yield OSXShadowRecord( + yield macOSShadowRecord( name=username, hash=hash, salt=salt, diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_applepay.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_applepay.plist new file mode 100644 index 0000000000..5e307bea33 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_applepay.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36ea4b5b5397ffd62838451eebd65a6821e60cbd30082227a42e141babdfd414 +size 188 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_eligibilityd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_eligibilityd.plist new file mode 100644 index 0000000000..10d6effb7e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/_eligibilityd.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39b1f19df1fd22d9e16061cad3207d0eebff2a75a6e82fcfd2c65d0a9147185c +size 269 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/nobody.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/nobody.plist new file mode 100644 index 0000000000..2d4a2510d4 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/groups/nobody.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ac4dc2192f02c744164d6fbd44bd8ea688f770030f78718ec6039e4a5a18ccb +size 213 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.AppleSetupDone similarity index 100% rename from tests/plugins/os/unix/bsd/darwin/macos/test_systemlog.py rename to tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.AppleSetupDone diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.GlobalPreferences.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.GlobalPreferences.plist new file mode 100644 index 0000000000..f3f17c4922 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.GlobalPreferences.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2918e637565580a6d784428ae51d674fa6b69797ca92e92026df8e76d3f0deb0 +size 558 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/install.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/install.log new file mode 100644 index 0000000000..e538e28743 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/install.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa736920d6262f9cfc35bd451d12f02ba2a060031817ba87ede3bb89b6338161 +size 328 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log new file mode 100644 index 0000000000..ea5388deb1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cdb1129922d0d4aede061f739857a3ace99fdcaea2c0bd7f4af9664670f3184 +size 355 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist new file mode 100644 index 0000000000..598191f177 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:476b1f6ae8ee5a5f8c456a8f463608c1506e5436f0ca61a96397010eafa6d492 +size 656 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist new file mode 100644 index 0000000000..6c16adb107 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc3a62a161ff90fe68e375709acd444decc7ea038380bd074734af26db5be9f5 +size 695 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist new file mode 100644 index 0000000000..5852dd6bc2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a92e72216f285f1cfc547731aa57694824602fbaa1723b49ee1bef966890598 +size 386 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist new file mode 100644 index 0000000000..6a21f1a5b4 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a550178868f9daa0db2c32b637a4898545debab27e26f17a25e94d574d76bed +size 1876 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist new file mode 100644 index 0000000000..9b469a3fe1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:568b6bd1ff8e8043a59f72291a5828f3a66304277a043914392b43b3586a72fa +size 282 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist new file mode 100644 index 0000000000..1b6b216483 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dc00f6312728dbbe92d9d782bdde68e5974225cce5a0296a06d768480bda66b +size 1249 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist new file mode 100644 index 0000000000..7f4b3987cd --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98d30b55d0d2feb9fe5b6e57e775299aeb77fc530b6eb98508ba0661cd1d18af +size 236 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/_mbsetupuser.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/_mbsetupuser.plist new file mode 100644 index 0000000000..72ae34e7e1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/_mbsetupuser.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8592d09647af9c8681e8a1e83edb9edf05c88fff7a6ab805860db0fd63b3592 +size 2930 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/user.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/user.plist new file mode 100644 index 0000000000..f5a2954868 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/shadow/user.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cae224de709dfab01fe951c2cac4a036daec49d534bcafc1d3552ef84f722ce +size 710014 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/__init__.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py new file mode 100644 index 0000000000..c52a82a884 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.installlog import InstallLogPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "install.log", + ], +) +def test_install_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") + fs_unix.map_file(f"/var/log/{test_file}", data_file) + + entry = fs_unix.get(f"/var/log/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(InstallLogPlugin) + + results = list(target_unix.installlog()) + assert len(results) == 3 + + assert results[0].ts == datetime(2026, 3, 25, 14, 6, 59, tzinfo=tz) + assert results[0].host == "localhost" + assert results[0].component == "Installer" + assert results[0].message == "Progress[57]: Progress UI App Starting" + + assert results[1].ts == datetime(2026, 3, 25, 14, 7, tzinfo=tz) + assert results[1].host == "localhost" + assert results[1].component == "Installer" + assert results[1].message == "Progress[57]: Logging also using os_log, installerProgressLog = 0xc6501c080" + + assert results[-1].ts == datetime(2026, 3, 25, 15, 18, 58, tzinfo=tz) + assert results[-1].host == "users-Virtual-Machine" + assert results[-1].component == "loginwindow[1042]:" + assert results[-1].message == "+[SUOSULoginCredentialPolicy currentLoginCredentialPolicy] = 0" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py new file mode 100644 index 0000000000..2046e255c6 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.systemlog import SystemLogPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "system.log", + ], +) +def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") + fs_unix.map_file(f"/var/log/{test_file}", data_file) + + entry = fs_unix.get(f"/var/log/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(SystemLogPlugin) + + results = list(target_unix.systemlog()) + assert len(results) == 3 + + assert results[0].ts == datetime(1900, 3, 25, 7, 6, 57, tzinfo=tz) + assert results[0].host == "localhost" + assert results[0].component == "bootlog[0]:" + assert results[0].message == "BOOT_TIME 1774447617 0" + assert results[0].source == "/var/log/system.log" + + assert results[1].ts == datetime(1900, 3, 25, 7, 7, tzinfo=tz) + assert results[1].host == "localhost" + assert results[1].component == "syslogd[60]:" + assert results[1].message == ( + "Configuration Notice:\n\t" + 'ASL Module "com.apple.cdscheduler" claims selected messages.\n\t' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[1].source == "/var/log/system.log" + + assert results[-1].ts == datetime(1900, 3, 25, 16, 19, 5, tzinfo=tz) + assert results[-1].host == "users-Virtual-Machine" + assert results[-1].component == "shutdown[1785]:" + assert results[-1].message == "SHUTDOWN_TIME: 1774451945 577079" + assert results[-1].source == "/var/log/system.log" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py new file mode 100644 index 0000000000..fbca628c5e --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.launchers import LaunchersPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "com.apple.seserviced.plist", + "com.apple.AMPArtworkAgent.plist", + "com.apple.familynotificationd.plist", + "com.apple.sidecar-hid-relay.plist", + "com.apple.WirelessRadioManager-osx.plist", + ), + ( + "/Users/user/Library/LaunchAgents/com.apple.seserviced.plist", + "/System/Library/LaunchAgents/com.apple.AMPArtworkAgent.plist", + "/System/Library/LaunchAgents/com.apple.familynotificationd.plist", + "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist", + "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist", + ), + ), + ], +) +def test_launch_agents( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [user] + + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(LaunchersPlugin) + + results = list(target_unix.launch_agents()) + results.sort(key=lambda r: r.source) + + assert len(results) == 4 + + assert results[0].EnablePressuredExit + assert results[0].EnableTransactions + assert results[0].Label == "com.apple.sidecar-display-agent" + assert results[0].ProcessType == "Interactive" + assert results[0].Program == "/usr/libexec/SidecarDisplayAgent" + assert results[0].com_apple_sidecar_display_agent + assert results[0].source == "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist" + + assert results[1].EnablePressuredExit + assert results[1].EnableTransactions + assert results[1].Label == "com.apple.AMPArtworkAgent" + assert results[1].ProcessType == "Adaptive" + assert results[1].ProgramArguments == ( + "['/System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPArtworkAgent', '--launchd']" + ) + assert results[1].com_apple_amp_artworkd + assert results[1].source == "/System/Library/LaunchAgents/com.apple.AMPArtworkAgent.plist" + + assert results[2].EnableTransactions + assert results[2].Label == "com.apple.familynotificationd" + assert results[2].Program == ( + "/System/Library/PrivateFrameworks/FamilyNotification.framework/familynotificationd" + ) + assert results[2].LimitLoadToSessionType == "['LoginWindow', 'Aqua']" + assert results[2].bundleid == "com.apple.familyalert" + assert results[2].POSIXSpawnType == "Adaptive" + assert results[2].ExitTimeOut == 1 + assert results[2].delay_registration + assert results[2].com_apple_familynotification_agent + assert results[2].com_apple_usernotifications_delegate_com_apple_familynotifications + assert results[2].events == ( + "['didDismissAlert', 'didActivateNotification', 'didDeliverNotification', " + "'didSnoozeAlert', 'didRemoveDeliveredNotifications', " + "'didExpireNotifications']" + ) + assert results[2].source == "/System/Library/LaunchAgents/com.apple.familynotificationd.plist" + + assert results[3].EnablePressuredExit + assert results[3].EnableTransactions + assert results[3].Label == "com.apple.seserviced" + assert results[3].ProcessType == "Adaptive" + assert results[3].Program == "/usr/libexec/seserviced" + assert results[3].Repeating + assert results[3].LimitLoadFromVariant == "HasFactoryContent" + assert results[3].com_apple_seserviced + assert results[3].com_apple_seserviced_sereservation_client + assert results[3].source == "/Users/user/Library/LaunchAgents/com.apple.seserviced.plist" + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "com.apple.WirelessRadioManager-osx.plist", + "com.apple.cfprefsd.xpc.daemon.plist", + "com.apple.perfpowermetricd.plist", + "com.apple.sidecar-hid-relay.plist", + ), + ( + "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist", + "/Users/user/Library/LaunchDaemons/com.apple.cfprefsd.xpc.daemon.plist", + "/Library/LaunchDaemons/com.apple.perfpowermetricd.plist", + "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist", + ), + ), + ], +) +def test_launch_daemons( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [user] + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(LaunchersPlugin) + + results = list(target_unix.launch_daemons()) + results.sort(key=lambda r: r.source) + + assert len(results) == 3 + + assert results[0].EnablePressuredExit + assert results[0].EnableTransactions + assert results[0].Label == "com.apple.perfpowermetricd" + assert results[0].ProcessType == "Interactive" + assert results[0].com_apple_PerfPowerMetricMonitor_xpc + assert results[0].ProgramArguments == "['/usr/libexec/perfpowermetricd']" + assert results[0].source == "/Library/LaunchDaemons/com.apple.perfpowermetricd.plist" + + assert results[1].EnablePressuredExit + assert results[1].Label == "com.apple.WirelessRadioManager" + assert results[1].POSIXSpawnType == "Interactive" + assert results[1].ThrottleInterval == 10 + assert results[1].com_apple_WirelessCoexManager + assert results[1].com_apple_WirelessRadioManager + assert results[1].ProgramArguments == "['/usr/sbin/WirelessRadioManagerd']" + assert results[1].source == "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist" + + assert not results[2].EnablePressuredExit + assert results[2].EnableTransactions + assert results[2].Label == "com.apple.cfprefsd.xpc.daemon" + assert results[2].POSIXSpawnType == "Adaptive" + assert results[2].NumberOfFiles == 512 + assert results[2].com_apple_cfprefsd_daemon + assert results[2].ProgramArguments == "['/usr/sbin/cfprefsd', 'daemon']" + assert results[2].source == "/Users/user/Library/LaunchDaemons/com.apple.cfprefsd.xpc.daemon.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py index e69de29bb2..54bade6e45 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.groups import GroupPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + ["nobody.plist", "_eligibilityd.plist", "_applepay.plist"], + ], +) +def test_groups(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/groups/{test_file}") + fs_unix.map_file(f"/var/db/dslocal/nodes/Default/groups/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/groups/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(GroupPlugin) + + results = list(target_unix.groups()) + results.sort(key=lambda r: r.realname) + assert len(results) == 3 + + assert results[0].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEFFFFFFFFE" + assert results[0].members is None + assert results[0].smb_sid == "S-1-0-0" + assert results[0].gid == -2 + assert results[0].name == "['nobody', 'BUILTIN\\\\Nobody']" + assert results[0].realname == "Nobody" + assert results[0].source == "/var/db/dslocal/nodes/Default/groups/nobody.plist" + + assert results[-1].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000104" + assert results[-1].members is None + assert results[-1].smb_sid is None + assert results[-1].gid == 260 + assert results[-1].name == "_applepay" + assert results[-1].realname == "applepay Daemon" + assert results[-1].source == "/var/db/dslocal/nodes/Default/groups/_applepay.plist" + + assert results[1].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000129" + assert results[1].members == "_eligibilityd" + assert results[1].smb_sid is None + assert results[1].gid == 297 + assert results[1].name == "_eligibilityd" + assert results[1].realname == "OS Eligibility Daemon" + assert results[1].source == "/var/db/dslocal/nodes/Default/groups/_eligibilityd.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py index e69de29bb2..43291383c3 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import os +import shutil +import tempfile +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.locale import macOSLocalePlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + (".GlobalPreferences.plist", ".AppleSetupDone"), + ( + "/Library/Preferences/.GlobalPreferences.plist", + "/private/var/db/.AppleSetupDone", + ), + ), + ], +) +def test_locale( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + tz = timezone.utc + apple_setup_time = 1704067199 + + tmpdir = tempfile.mkdtemp() + try: + for name, path in zip(names, paths, strict=True): + if name == ".AppleSetupDone": + src = absolute_path("_data/plugins/os/unix/bsd/darwin/macos/locale/.AppleSetupDone") + + tmp_file = os.path.join(tmpdir, name) # noqa: PTH118 + shutil.copyfile(src, tmp_file) + + os.utime(tmp_file, (apple_setup_time, apple_setup_time)) + else: + tmp_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/locale/{name}") + + fs_unix.map_file(path, tmp_file) + + target_unix.fs.mount("/", fs_unix) + target_unix.os = "macos" + target_unix.add_plugin(macOSLocalePlugin) + + assert target_unix.timezone == "Europe/Amsterdam" + assert target_unix.language == ["en_US", "nl_NL"] + assert target_unix.install_date == datetime.fromtimestamp(apple_setup_time, tz=tz) + + finally: + target_unix.fs = None + os.system(f'rmdir /s /q "{tmpdir}"' if os.name == "nt" else f'rm -rf "{tmpdir}"') diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py index e69de29bb2..28ef55fa0f 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.shadow import ShadowPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + ["_mbsetupuser.plist", "user.plist"], + ], +) +def test_passwords(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/shadow/{test_file}") + fs_unix.map_file(f"/var/db/dslocal/nodes/Default/users/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/users/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(ShadowPlugin) + + results = list(target_unix.passwords()) + results.sort(key=lambda r: r.name) + assert len(results) == 2 + + assert results[0].name == "_mbsetupuser" + assert ( + results[0].hash + == "46e8c10ca67d3d8a3998fa83b9dbb845d3a5c8b257d60272a22912a1a0fd6bff5d315b6f5b50577e465a8ae57f5a0f0fd52ea708d614141c428911d3273b67125d9599198590376ad38f43d6ed3a1173fd86c378b9879c5a026809c802df5ae5bd72e53bb033f9aa9e0e24600c03d0ec287e466f5c79eb1be42fac7afeee2b7e" # noqa: E501 + ) + assert results[0].salt == "225ea6a940b08c6985c792a7195ef008527b5c83829ca0850bdcae77e517c9b5" + assert results[0].iterations == 78125 + assert results[0].algorithm == "SALTED-SHA512-PBKDF2" + assert results[0].source == "/var/db/dslocal/nodes/Default/users/_mbsetupuser.plist" + + assert results[-1].name == "user" + assert ( + results[-1].hash + == "f6e502079b9eb8b2f49b099c235aed2debb0b80084dca99f9a23db41dbfca88be0266bf62dfca5923ccd20d4c8f81140e2cb09b7951cce001ccb37b9fa3cee46b68a0edfc8e055e7f523feaec444f775eaddcf7d4e91e5e918a0dd715a4a749fd92974b023db4cb8851f4fdee3bd091755686be92a3f8ff906c6552907a8b0dc" # noqa: E501 + ) + assert results[-1].salt == "64e7869eef2d9bf35bc9b72fddcf90055be833eed2c6c7d58ca91f0c0754f5f6" + assert results[-1].iterations == 128205 + assert results[-1].algorithm == "SALTED-SHA512-PBKDF2" + assert results[-1].source == "/var/db/dslocal/nodes/Default/users/user.plist" From 5a31d6e9d3e26ee81d2fcfd85cc7bb58f35ec046 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:15:09 +0200 Subject: [PATCH 06/52] Update comments --- .../plugins/os/unix/bsd/darwin/macos/persistence/launchers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index 0772a7f6b0..bfb9246aad 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -45,10 +45,10 @@ def __init__(self, target: Target): self._find_files() def _build_userdirs(self, hist_paths: list[str]) -> set[tuple[UserDetails, Path]]: - """Join the selected browser dirs with the user home path. + """Join the selected dirs with the user home path. Args: - hist_paths: A list with browser paths as strings. + hist_paths: A list with paths as strings. Returns: List of tuples containing user and unique file path objects. From 4742b4a1ab9b7959ce7dc8022940dbd7685e4223 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:08:49 +0200 Subject: [PATCH 07/52] Remove unnecessary print --- .../plugins/os/unix/bsd/darwin/macos/persistence/launchers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index bfb9246aad..6ea6e50e7e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -126,8 +126,6 @@ def build_record(self, record_name: str, rdict: dict, source: Path | None) -> Re # be constructing a record descriptor from it. record_fields = sorted(rdict.items()) - print(record_fields) - record_values = { "_target": self.target, "source": source, From 4a9acd176c230b74d3d161df0c75c5c7ae9ac307 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:25:42 +0200 Subject: [PATCH 08/52] Add macOS .plist parsers --- .../bsd/darwin/macos/airport_preferences.py | 63 ++ .../macos/code_signature_coderesources.py | 91 ++ .../os/unix/bsd/darwin/macos/contents_info.py | 55 ++ .../unix/bsd/darwin/macos/contents_version.py | 72 ++ .../darwin/macos/global_user_preferences.py | 42 + .../unix/bsd/darwin/macos/helpers/__init__.py | 0 .../os/unix/bsd/darwin/macos/helpers/plist.py | 251 ++++++ .../unix/bsd/darwin/macos/helpers/userdirs.py | 29 + .../bsd/darwin/macos/installation_history.py | 64 ++ .../unix/bsd/darwin/macos/keyboard_layout.py | 63 ++ .../os/unix/bsd/darwin/macos/locale.py | 14 + .../os/unix/bsd/darwin/macos/login_window.py | 59 ++ .../bsd/darwin/macos/persistence/launchers.py | 376 ++++++--- .../darwin/macos/persistence/login_items.py | 68 ++ .../darwin/macos/resources_info_strings.py | 52 ++ .../macos/resources_localizable_strings.py | 50 ++ .../macos/software_update_preferences.py | 78 ++ .../bsd/darwin/macos/system_preferences.py | 41 + .../os/unix/bsd/darwin/macos/time_machine.py | 52 ++ .../bsd/darwin/macos/.GlobalPreferences.plist | 3 + .../bsd/darwin/macos/InstallHistory.plist | 3 + .../bsd/darwin/macos/code_signature/Ethernet | 3 + .../darwin/macos/code_signature/FileProvider | 3 + .../unix/bsd/darwin/macos/code_signature/Host | 3 + .../darwin/macos/com.apple.HIToolbox.plist | 3 + .../macos/com.apple.SoftwareUpdate.plist | 3 + .../darwin/macos/com.apple.TimeMachine.plist | 3 + .../macos/com.apple.airport.preferences.plist | 3 + .../contents_info/AppleEventLogHandler.plist | 3 + .../macos/contents_info/AppleHPET.plist | 3 + .../contents_info/UnmountAssistantAgent.plist | 3 + .../contents_version/IOBluetoothUI.plist | 3 + .../macos/contents_version/SwiftUICore.plist | 3 + .../contents_version/UniversalControl.plist | 3 + .../macos/global_user_preferences/root.plist | 3 + .../securityagent.plist | 3 + .../macos/global_user_preferences/user.plist | 3 + .../locale/com.apple.timezone.auto.plist | 3 + ...E253F552-3A40-5010-9ACE-98662C9CFE20.plist | 3 + .../macos/login_window/loginwindow.plist | 3 + .../macos/persistence/BackgroundItems-v16.btm | 3 + .../resources_info_strings/InfoPlist.strings | 3 + .../macos/persistence/test_launchers.py | 774 +++++++++++++++++- .../macos/persistence/test_login_items.py | 88 ++ .../darwin/macos/test_airport_preferences.py | 41 + .../test_code_signature_coderesources.py | 77 ++ .../bsd/darwin/macos/test_contents_info.py | 107 +++ .../bsd/darwin/macos/test_contents_version.py | 80 ++ .../macos/test_global_user_preferences.py | 112 +++ .../darwin/macos/test_installation_history.py | 43 + .../bsd/darwin/macos/test_keyboard_layout.py | 51 ++ .../os/unix/bsd/darwin/macos/test_locale.py | 8 +- .../bsd/darwin/macos/test_login_window.py | 74 ++ .../macos/test_resources_info_strings.py | 45 + .../test_resources_localizable_strings.py | 1 + .../macos/test_software_update_preferences.py | 53 ++ .../darwin/macos/test_system_preferences.py | 41 + .../bsd/darwin/macos/test_time_machine.py | 38 + 58 files changed, 3064 insertions(+), 158 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/__init__.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/.GlobalPreferences.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/InstallHistory.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.HIToolbox.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.SoftwareUpdate.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.airport.preferences.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/IOBluetoothUI.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/SwiftUICore.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/UniversalControl.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/root.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/securityagent.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/user.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/locale/com.apple.timezone.auto.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/loginwindow.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py new file mode 100644 index 0000000000..641dea7069 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +AirportPreferencesRecord = TargetRecordDescriptor( + "macos/airport_preferences", + [ + ("varint", "counter"), + ("string", "device_uuid"), + ("varint", "version"), + ("string", "preferred_order"), + ("path", "source"), + ], +) + + +class AirportPreferencesPlugin(Plugin): + """macOS AirPort (WiFi) preferences plugin.""" + + PATH = "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found") + + @export(record=AirportPreferencesRecord) + def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: + """Yield AirPort preference information.""" + plist = plistlib.load(self.file.open()) + + counter = plist.get("Counter") + version = plist.get("Version") + device_uuid = plist.get("DeviceUUID") + preferred_order = plist.get("PreferredOrder") + + yield AirportPreferencesRecord( + counter=counter, + device_uuid=device_uuid, + version=version, + preferred_order=preferred_order, + source=self.file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py new file mode 100644 index 0000000000..246bb9ee4f --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +CodeSignatureCodeResourcesRecord1 = TargetRecordDescriptor( + "macos/code_signature_coderesources", + [ + ("boolean", "omit"), + ("string", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +CodeSignatureCodeResourcesRecord2 = TargetRecordDescriptor( + "macos/code_signature_coderesources", + [ + ("boolean", "nested"), + ("string", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +CodeSignatureCodeResourcesRecord3 = TargetRecordDescriptor( + "macos/code_signature_coderesources", + [ + ("string", "cdhash"), + ("string", "requirement"), + ("string", "plist_path"), + ("path", "source"), + ], +) + + +CodeSignatureCodeResourcesRecords = ( + CodeSignatureCodeResourcesRecord1, + CodeSignatureCodeResourcesRecord2, + CodeSignatureCodeResourcesRecord3, +) + + +class CodeSignatureCodeResourcesPlugin(Plugin): + """macOS Code signature CodeResources plugin.""" + + PATHS = ( + "/Applications/Utilities/*.app/Contents/_CodeSignature/CodeResources", + "/System/Library/CoreServices/*.app/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/*.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources", + "/System/Library/Filesystems/*/*.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/Filesystems/*/Encodings/*.kext/Contents/_CodeSignature/CodeResource", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/_CodeSignature/CodeResources", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No code signature coderesources files found") + + @export(record=DynamicDescriptor(["string"])) + def code_signature_coderesources(self) -> Iterator[DynamicDescriptor]: + """Yield code signature coderesources information.""" + yield from build_records( + self, "macos/code_signature_coderesources", self.files, CodeSignatureCodeResourcesRecords + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py new file mode 100644 index 0000000000..0a3e2d3238 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class ContentsInfoPlugin(Plugin): + """macOS contents info plugin.""" + + PATHS = ( + "/Applications/*/*.app/Contents/Info.plist", + "/Applications/*/*.app/Contents/Resources/*.help/Contents/Info.plist", + "/System/Library/CoreServices/*.app/Contents/Info.plist", + "/System/Library/Extensions/*.kext/Contents/Info.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Info.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/Info.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/Info.plist", + "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/Info.plist", + "/System/Library/Extensions/*.kext/PlugIns/*.kext/Info.plist", + "/System/Library/Filesystems/*/*.kext/Contents/Info.plist", + "/System/Library/Filesystems/*/Encodings/*.kext/Contents/Info.plist", + "/System/Library/Frameworks/*.framework/Versions/A/Resources/Info.plist", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/Info.plist", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No contents info files found") + + @export(record=DynamicDescriptor(["string"])) + def contents_info(self) -> Iterator[DynamicDescriptor]: + """Yield contents info information.""" + yield from build_records(self, "macos/contents_info", self.files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py new file mode 100644 index 0000000000..3ae0903064 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +ContentsVersionRecord = TargetRecordDescriptor( + "macos/contents_version", + [ + ("string", "BuildAliasOf"), + ("string", "RelevancePlatform"), + ("string", "BuildVersion"), + ("string", "CFBundleShortVersionString"), + ("string", "CFBundleVersion"), + ("string", "ProjectName"), + ("string", "SourceVersion"), + ("path", "source"), + ], +) + + +ContentsVersionRecords = (ContentsVersionRecord,) + + +class MacOSContentsVersionPlugin(Plugin): + """macOS Contents version.plist file.""" + + PATHS = ( + "/Applications/*/*.app/Contents/version.plist", + "/Applications/*/*.app/Contents/Resources/*.help/Contents/version.plist", + "/System/Library/CoreServices/*.app/Contents/version.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/version.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/version.plist", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/version.plist", + "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/version.plist", + "/System/Library/Extensions/*.kext/Contents/version.plist", + "/System/Library/Extensions/*.kext/PlugIns/*.kext/version.plist", + "/System/Library/Filesystems/*/*.kext/Contents/version.plist", + "/System/Library/Filesystems/*/Encodings/*.kext/Contents/version.plist", + "/System/Library/Frameworks/*.framework/Versions/A/Resources/version.plist", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/version.plist", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No contents version.plist files found") + + @export(record=DynamicDescriptor(["string"])) + def contents_version(self) -> Iterator[DynamicDescriptor]: + """Yield contents version.plist information.""" + yield from build_records(self, "macos/contents_version", self.files, ContentsVersionRecords) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py new file mode 100644 index 0000000000..da4c241edf --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class GlobalUserPreferencesPlugin(Plugin): + """macOS global user preferences plugin.""" + + PATHS = ("Library/Preferences/.GlobalPreferences.plist",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No global user preferences files found") + + def _find_files(self) -> None: + for _, path in _build_userdirs(self, self.PATHS): + self.files.add(path) + + @export(record=DynamicDescriptor(["string"])) + def global_user_preferences(self) -> Iterator[DynamicDescriptor]: + """Yield global user preference information.""" + yield from build_records(self, "macos/global_user_preferences", self.files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/__init__.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py new file mode 100644 index 0000000000..2402d274f6 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import plistlib +import re +import uuid +from datetime import datetime +from typing import TYPE_CHECKING, Any, BinaryIO + +from dissect.util.plist import NSDictionary, NSKeyedArchiver + +from dissect.target.helpers.record import TargetRecordDescriptor + +if TYPE_CHECKING: + from collections.abc import Iterator + from pathlib import Path + + from flow.record.base import Record + + from dissect.target.plugin import Plugin + +re_non_identifier = re.compile(r"[^A-Za-z0-9_]") + + +def build_records( + plugin: Plugin, + record_name: str, + files: set[str], + record_descriptors: tuple | None = None, + collapse_paths: set[tuple[str, bool]] | None = None, +) -> Iterator[Record]: + for file in files: + file = plugin.target.fs.path(file) + try: + fh = file.open(mode="rb") + except FileNotFoundError: + plugin.target.log.exception("File not found: %s", file) + continue + + try: + if b"$archiver" in fh.peek(64): + fh.seek(0) + data = load_plist_data(fh) + else: + data = plistlib.load(fh) + + yield from emit_dict_records( + plugin, + record_name, + data, + file, + record_descriptors=record_descriptors, + collapse_paths=collapse_paths, + ) + + except Exception: + plugin.target.log.exception("Failed to parse %s", file) + + +def dynamic_build_record(plugin: Plugin, record_name: str, rdict: dict, source: Path | None) -> Record: + record_fields = sorted(rdict.items()) + + record_values = { + "_target": plugin.target, + "source": source, + } + record_fields = [] + + for k, v in rdict.items(): + k = format_key(k) + + if isinstance(v, bool): + record_fields.append(("boolean", k)) + elif isinstance(v, int): + record_fields.append(("varint", k)) + else: + record_fields.append(("string", k)) + + record_values[k] = v + + record_fields.append(("path", "source")) + + desc = create_event_descriptor(record_name, tuple(record_fields)) + + return desc(**record_values) + + +def select_descriptor( + record_descriptors: tuple, + rdict: dict, +) -> TargetRecordDescriptor | None: + rdict_keys = {format_key(k) for k in rdict} + + for record in record_descriptors: + record_keys = set(record.fields.keys()) + if rdict_keys.issubset(record_keys): + return record + + return None + + +def build_record( + plugin: Plugin, + rdict: dict, + source: Path | None, + record_descriptors: tuple | None = None, +) -> Record: + desc = select_descriptor(record_descriptors, rdict) + + if desc is None: + plugin.target.log.exception( + "No matching record descriptor for %s with fields %s", + source, + sorted(map(format_key, rdict)), + ) + return None + + record_values = { + "_target": plugin.target, + "source": source, + } + + for k, v in rdict.items(): + record_values[format_key(k)] = v + + return desc(**record_values) + + +def create_event_descriptor(record_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: + return TargetRecordDescriptor(record_name, record_fields) + + +def format_key(key: str) -> str: + key = re_non_identifier.sub("_", key) + + key = re.sub(r"_+", "_", key) + + key = key.lstrip("_") + + if not key or key[0].isdigit(): + key = f"k_{key}" + + return key + + +UUID_RE = re.compile( + r"^[0-9A-Fa-f]{8}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{12}$" +) + + +def is_collapsed_path(child_path: str, collapse_paths: set[tuple[str, bool]]) -> bool: + for collapse_path, exact in collapse_paths: + if child_path == collapse_path: + return True + if not exact and collapse_path and child_path.startswith(f"{collapse_path}/"): + return True + return False + + +def emit_dict_records( + plugin: Plugin, + record_name: str, + node: dict, + source: Path | None, + *, + section: str | None = None, + path: str | None = None, + record_descriptors: tuple | None = None, + collapse_paths: set[tuple[str, bool]] | None = None, +) -> Iterator[Record]: + if path and path.endswith("$class"): + return + + attributes = {} + child_dicts = {} + + for k, v in node.items(): + child_path = f"{path}/{k}" if path else k + + if collapse_paths and isinstance(v, dict) and is_collapsed_path(child_path, collapse_paths): + attributes[k] = list(v.items()) + continue + + if isinstance(v, dict): + child_dicts[k] = v + else: + attributes[k] = v + + if node and all(isinstance(k, str) and UUID_RE.fullmatch(k) for k in node): + attributes = {} + + if attributes: + record_data = dict(attributes) + + if section is not None: + record_data["section"] = section + if path is not None: + record_data["plist_path"] = path + + if record_descriptors is None: + yield dynamic_build_record(plugin, record_name, record_data, source) + else: + yield build_record(plugin, record_data, source, record_descriptors) + + for k, child in child_dicts.items(): + child_path = f"{path}/{k}" if path else k + yield from emit_dict_records( + plugin, + record_name, + child, + source, + section=section, + path=child_path, + record_descriptors=record_descriptors, + collapse_paths=collapse_paths, + ) + + +def normalize_nsobj(obj: Any) -> Any: + """Convert NSKeyedArchiver output to plain Python types.""" + if isinstance(obj, NSDictionary): + return {k: normalize_nsobj(v) for k, v in obj.items()} + + if isinstance(obj, dict): + return {k: normalize_nsobj(v) for k, v in obj.items()} + + if isinstance(obj, list): + return [normalize_nsobj(v) for v in obj] + + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + + if isinstance(obj, datetime): + return obj + + if isinstance(obj, uuid.UUID): + return str(obj) + + if hasattr(obj, "keys"): + return {k: normalize_nsobj(obj.get(k)) for k in obj.keys()} # noqa: SIM118 + + return obj + + +def load_plist_data(fh: BinaryIO) -> Any: + ns = NSKeyedArchiver(fh) + root = ns.get("store") + return normalize_nsobj(root) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py new file mode 100644 index 0000000000..f56a1b18f1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + from dissect.target.plugin import Plugin + from dissect.target.plugins.general.users import UserDetails + + +def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDetails, Path]]: + """Join the selected dirs with the user home path. + + Args: + hist_paths: A list with paths as strings. + + Returns: + List of tuples containing user and unique file path objects. + """ + users_dirs: set[tuple] = set() + for user_details in plugin.target.user_details.all_with_home(): + for d in hist_paths: + home_dir: Path = user_details.home_path + for cur_dir in home_dir.glob(d): + cur_dir = cur_dir.resolve() + if cur_dir.exists(): + users_dirs.add((user_details, cur_dir)) + return users_dirs diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py new file mode 100644 index 0000000000..398efcb9f7 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +InstallationHistoryRecord = TargetRecordDescriptor( + "macos/installation_history", + [ + ("datetime", "date"), + ("string", "display_name"), + ("string", "display_version"), + ("string", "process_name"), + ("path", "source"), + ], +) + + +class InstallationHistoryPlugin(Plugin): + """macOS Software installation history property list plugin.""" + + PATH = "/Library/Receipts/InstallHistory.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No InstallHistory.plis file found") + + @export(record=InstallationHistoryRecord) + def installation_history(self) -> Iterator[InstallationHistoryRecord]: + """Yield installation history information.""" + plist = plistlib.load(self.file.open()) + data = plist[0] + + display_name = data.get("displayName") + display_version = data.get("displayVersion") + process_name = data.get("processName") + date = data.get("date") + + yield InstallationHistoryRecord( + date=date, + display_name=display_name, + display_version=display_version, + process_name=process_name, + source=self.file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py new file mode 100644 index 0000000000..da144f33db --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +KeyboardLayoutRecord = TargetRecordDescriptor( + "macos/keyboard_layout", + [ + ("string", "input_source_kind"), + ("string", "keyboard_layout_name"), + ("varint", "keyboard_layout_id"), + ("boolean", "enabled"), + ("boolean", "selected"), + ("boolean", "current"), + ("path", "source"), + ], +) + + +class KeyboardLayoutPlugin(Plugin): + """macOS keyboard layout plugin.""" + + PATH = "/Library/Preferences/com.apple.HIToolbox.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No com.apple.HIToolbox.plist file found") + + @export(record=KeyboardLayoutRecord) + def keyboard_layout(self) -> Iterator[KeyboardLayoutRecord]: + """Yield macOS keyboard layout information.""" + plist = plistlib.loads(self.file.read_bytes()) + + for source in plist.get("AppleEnabledInputSources", []): + yield KeyboardLayoutRecord( + input_source_kind=source.get("InputSourceKind"), + keyboard_layout_name=source.get("KeyboardLayout Name"), + keyboard_layout_id=source.get("KeyboardLayout ID"), + enabled=True, + selected=source in plist.get("AppleSelectedInputSources", []), + current=(source.get("KeyboardLayout ID") == plist.get("AppleCurrentKeyboardLayoutInputSourceID")), + source=self.file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 5d36033b78..3dabb4f545 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -67,3 +67,17 @@ def language(self) -> str | None: def install_date(self) -> str | None: mtime = self.target.fs.path("/private/var/db/.AppleSetupDone").lstat().st_mtime return datetime.fromtimestamp(mtime, timezone.utc) + + @export(property=True) + def location_services_active(self) -> bool | None: + path = self.target.fs.path("/Library/Preferences/com.apple.timezone.auto.plist") + + if not path.exists(): + return None + + try: + with path.open("rb") as fh: + plist = plistlib.load(fh) + return plist.get("Active") + except Exception: + return None diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py new file mode 100644 index 0000000000..3d5c4fc6df --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class LoginWindowPlugin(Plugin): + """macOS login window plugin.""" + + SYSTEM_LOGIN_WINDOW_PATHS = ( + "/Library/Preferences/com.apple.loginwindow.plist", + "/var/root/Library/Preferences/com.apple.loginwindow.plist", + "/private/var/root/Library/Preferences/com.apple.loginwindow.plist", + ) + + USER_LOGIN_WINDOW_PATHS = ( + "Library/Preferences/loginwindow.plist", + "Library/Preferences/ByHost/com.apple.loginwindow.plist", + "Library/Preferences/ByHost/com.apple.loginwindow.*.plist", + ) + + def __init__(self, target: Target): + super().__init__(target) + + self.login_window_files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.login_window_files): + raise UnsupportedPluginError("No Login Window files found") + + def _find_files(self) -> None: + # --- System-wide --- + for pattern in self.SYSTEM_LOGIN_WINDOW_PATHS: + for path in self.target.fs.glob(pattern): + self.login_window_files.add(path) + + # --- Per-user --- + for _, path in _build_userdirs(self, self.USER_LOGIN_WINDOW_PATHS): + self.login_window_files.add(path) + + @export(record=DynamicDescriptor(["string"])) + # @export(output="yield") + def login_window(self) -> Iterator[DynamicDescriptor]: + """Yield macOS login window plist files.""" + yield from build_records(self, "macos/login_window", self.login_window_files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index 6ea6e50e7e..791c7d60bb 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -1,24 +1,275 @@ from __future__ import annotations -import plistlib import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator - from pathlib import Path - from flow.record.base import Record - - from dissect.target.plugins.general.users import UserDetails from dissect.target.target import Target re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") +LauncherRecord1 = [ + ("string", "Label"), + ("string", "Disabled"), + ("string", "UserName"), + ("string", "GroupName"), + ("string", "Group"), + ("string", "CFBundleIdentifier"), + ("string", "CFBundleDevelopmentRegion"), + ("string", "CFBundleInfoDictionaryVersion"), + ("string", "CFBundleName"), + ("boolean", "InitGroups"), + ("string", "Program"), + ("string", "BundleProgram"), + ("string[]", "ProgramArguments"), + ("boolean", "EnableGlobbing"), + ("boolean", "EnableTransactions"), + ("boolean", "PressuredExit"), + ("boolean", "EnablePressureExit"), + ("string", "EnablePressuredExit"), + ("string", "KeepAlive"), + ("boolean", "SuccessfulExit"), + ("boolean", "Crashed"), + ("boolean", "RunAtLoad"), + ("string", "RootDirectory"), + ("string", "WorkingDirectory"), + ("varint", "Umask"), + ("varint", "TimeOut"), + ("varint", "ExitTimeOut"), + ("varint", "ThrottleInterval"), + ("varint", "StartInterval"), + ("boolean", "StartOnMount"), + ("string[]", "WatchPaths"), + ("string[]", "QueueDirectories"), + ("string", "StandardInPath"), + ("string", "StandardOutPath"), + ("string", "StandardErrorPath"), + ("boolean", "Debug"), + ("boolean", "WaitForDebugger"), + ("varint", "Nice"), + ("string", "ProcessType"), + ("boolean", "PowerNap"), + ("boolean", "AbandonProcessGroup"), + ("boolean", "LowPriorityIO"), + ("boolean", "LowPriorityBackgroundIO"), + ("boolean", "MaterializeDatalessFiles"), + ("boolean", "LaunchOnlyOnce"), + ("boolean", "BootShell"), + ("boolean", "SessionCreate"), + ("boolean", "LegacyTimers"), + ("boolean", "TransactionTimeLimitEnabled"), + ("boolean", "LimitLoadToDeveloperMode"), + ("string", "LimitLoadToSessionType"), + ("boolean", "Wait"), + ("string[]", "AssociatedBundleIdentifiers"), + ("string", "plist_path"), + ("string", "POSIXSpawnType"), + ("string", "PosixSpawnType"), + ("string", "MultipleInstances"), + ("boolean", "DisabledInSafeBoot"), + ("boolean", "BeginTransactionAtShutdown"), + ("boolean", "OnDemand"), + ("boolean", "AlwaysSIGTERMOnShutdown"), + ("boolean", "MinimalBootProfiles"), + ("boolean", "MinimalBootProfile"), + ("boolean", "LSBackgroundOnly"), + ("boolean", "HopefullyExitsLast"), + ("boolean", "ExponentialThrottling"), + ("boolean", "IgnoreProcessGroupAtShutdown"), + ("boolean", "EventMonitor"), + ("boolean", "AuxiliaryBootstrapper"), + ("boolean", "AuxiliaryBootstrapperAllowDemand"), + ("boolean", "DrainMessagesAfterFailedInit"), + ("boolean", "DrainMessagesOnFailedInit"), + ("string", "LimitLoadFromVariant"), + ("string", "LimitLoadFromBootMode"), + ("string", "EfficiencyMode"), + ("string", "Conclave"), + ("string[]", "LaunchEvents"), + ("string[]", "MachServices"), + ("string[]", "SoftResourceLimits"), + ("string[]", "HardResourceLimits"), + ("string[]", "EnvironmentVariables"), + ("string[]", "NoEnvironmentVariables"), + ("string[]", "NO_EnvironmentVariables"), + ("string", "SHAuthorizationRight"), + ("string", "PublishesEvents"), + ("string", "LimitLoadToVariant"), + ("string", "RunLoopType"), + ("string[]", "RemoteServices"), + ("string[]", "JetsamProperties"), + ("string[]", "LimitLoadToHardware"), + ("string[]", "PanicOnCrash"), + ("string[]", "LimitLoadToBootMode"), + ("string", "Cryptex"), + ("string", "ServiceType"), + ("string", "ServiceIPC"), + ("string[]", "UrgentLogSubmission"), + ("string[]", "AppIntents"), + ("string[]", "BinaryOrderPreference"), + ("string[]", "LimitLoadFromHardware"), + ("string[]", "StartCalendarInterval"), + ("string[]", "AdditionalProperties"), + ("string[]", "NSAppTransportSecurity"), + ("string[]", "com_apple_usbcd"), + ("string[]", "com_apple_private_tcc_allow"), + ("string[]", "com_apple_security_application_groups"), + ("string[]", "com_apple_private_security_restricted_application_groups"), + ("string[]", "com_apple_security_exception_files_home_relative_path_read_write"), + ("string[]", "com_apple_security_exception_mach_lookup_global_name"), + ("string[]", "com_apple_security_exception_sysctl_read_only"), + ("boolean", "com_apple_private_security_no_sandbox"), + ("boolean", "com_apple_ane_iokit_user_access"), + ("boolean", "com_apple_alarm"), + ("boolean", "com_apple_imdpersistence_IMDPersistenceAgent_GroupMetadata"), + ("boolean", "com_apple_imdpersistence_IMDPersistenceAgent_Syndication"), + ("path", "source"), +] + +LauncherRecord2 = [ + ("boolean", "Wait"), + ("varint", "Instances"), + ("string", "plist_path"), + ("path", "source"), +] + +LauncherRecord3 = [ + ("string", "SocketKey"), + ("string", "SockType"), + ("boolean", "SockPassive"), + ("string", "SockNodeName"), + ("string", "SockServiceName"), + ("string", "SockFamily"), + ("string", "SockProtocol"), + ("varint", "SockPathMode"), + ("string", "SockPathName"), + ("string", "SecureSocketWithKey"), + ("varint", "SockPathOwner"), + ("varint", "SockPathGroup"), + ("varint", "SockPathMode"), + ("string", "Bonjour"), + ("string", "MulticastGroup"), + ("boolean", "ReceivePacketInfo"), + ("string", "plist_path"), + ("path", "source"), +] + +LauncherRecord4 = [ + ("string[]", "Version4"), + ("path", "source"), +] + +LauncherRecord5 = [ + ("boolean", "UNSettingAlerts"), + ("boolean", "UNSettingAlwaysShowPreviews"), + ("boolean", "UNSettingLockScreen"), + ("boolean", "UNSettingModalAlertStyle"), + ("boolean", "UNAutomaticallyShowSettings"), + ("boolean", "UNSettingNotificationCenter"), + ("boolean", "UNDaemonShouldReceiveBackgroundResponses"), + ("boolean", "UNSuppressUserAuthorizationPrompt"), + ("string", "plist_path"), + ("path", "source"), +] + +LauncherRecord6 = [ + ("string", "UNNotificationIconDefault"), + ("string", "UNNotificationIconSettings"), + ("string", "plist_path"), + ("path", "source"), +] + +LauncherRecord7 = [ + ("string[]", "Listeners"), + ("string", "plist_path"), + ("path", "source"), +] + +TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord1, +) + +LaunchAgentRecords = ( + TargetRecordDescriptor( + "macos/launch_agents", + LauncherRecord1, + ), + TargetRecordDescriptor( + "macos/launch_agents", + LauncherRecord3, + ), + TargetRecordDescriptor( + "macos/launch_agents", + LauncherRecord5, + ), + TargetRecordDescriptor( + "macos/launch_agents", + LauncherRecord6, + ), +) + +LaunchDaemonRecords = ( + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord1, + ), + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord2, + ), + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord3, + ), + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord4, + ), + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord7, + ), +) + +COLLAPSE_PATHS = { + ("LaunchEvents", False), + ("MachServices", True), + ("EnvironmentVariables", True), + ("KeepAlive", False), + ("SoftResourceLimits", True), + ("HardResourceLimits", True), + ("MultipleInstances", True), + ("UserName", True), + ("GroupName", True), + ("RemoteServices", False), + ("JetsamProperties", True), + ("LimitLoadToSessionType", True), + ("Version4", False), + ("EnablePressuredExit", True), + ("LimitLoadToHardware", True), + ("_PanicOnCrash", True), + ("LimitLoadFromHardware", True), + ("StartCalendarInterval", True), + ("PublishesEvents", False), + ("_AdditionalProperties", False), + ("Disabled", True), + ("com.apple.usbcd", True), + ("NoEnvironmentVariables", True), + ("NO_EnvironmentVariables", True), + ("NSAppTransportSecurity", True), + ("_UrgentLogSubmission", True), + ("AppIntents", True), +} + class LaunchersPlugin(Plugin): """macOS launchers plugin.""" @@ -44,25 +295,6 @@ def __init__(self, target: Target): self.launch_daemon_files = set() self._find_files() - def _build_userdirs(self, hist_paths: list[str]) -> set[tuple[UserDetails, Path]]: - """Join the selected dirs with the user home path. - - Args: - hist_paths: A list with paths as strings. - - Returns: - List of tuples containing user and unique file path objects. - """ - users_dirs: set[tuple] = set() - for user_details in self.target.user_details.all_with_home(): - for d in hist_paths: - home_dir: Path = user_details.home_path - for cur_dir in home_dir.glob(d): - cur_dir = cur_dir.resolve() - if cur_dir.exists(): - users_dirs.add((user_details, cur_dir)) - return users_dirs - def check_compatible(self) -> None: if not (self.launch_agent_files or self.launch_daemon_files): raise UnsupportedPluginError("No Agent or Deamon files found") @@ -74,7 +306,7 @@ def _find_files(self) -> None: self.launch_agent_files.add(path) # --- Per-user LaunchAgents --- - for _, path in self._build_userdirs(self.USER_LAUNCH_AGENT_PATHS): + for _, path in _build_userdirs(self, self.USER_LAUNCH_AGENT_PATHS): self.launch_agent_files.add(path) # --- System-wide LaunchDaemons --- @@ -83,100 +315,20 @@ def _find_files(self) -> None: self.launch_daemon_files.add(path) # --- Per-user LaunchDaemons --- - for _, path in self._build_userdirs(self.USER_LAUNCH_DAEMON_PATHS): + for _, path in _build_userdirs(self, self.USER_LAUNCH_DAEMON_PATHS): self.launch_daemon_files.add(path) @export(record=DynamicDescriptor(["string"])) # @export(output="yield") def launch_agents(self) -> Iterator[DynamicDescriptor]: """Yield macOS launch agent plist files.""" - for file in self.launch_agent_files: - file = self.target.fs.path(file) - try: - fh = file.open(mode="rb") - except FileNotFoundError: - self.target.log.exception("LaunchAgent missing target: %s", {file}) - continue - try: - data = plistlib.load(fh) - flat_data = {} - extract_nested_dict(flat_data, data) - - yield self.build_record("macos/launch_agent", flat_data, file) - except Exception: - self.target.log.exception("Failed to parse %s", file) + yield from build_records( + self, "macos/launch_agents", self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS + ) @export(record=DynamicDescriptor(["string"])) def launch_daemons(self) -> Iterator[DynamicDescriptor]: """Yield macOS launch daemon plist files.""" - for file in self.launch_daemon_files: - file = self.target.fs.path(file) - fh = file.open(mode="rb") - try: - data = plistlib.load(fh) - flat_data = {} - extract_nested_dict(flat_data, data) - - yield self.build_record("macos/launch_daemon", flat_data, file) - except Exception: - self.target.log.exception("Failed to parse %s", file) - - def build_record(self, record_name: str, rdict: dict, source: Path | None) -> Record: - # predictable order of fields in the list is important, since we'll - # be constructing a record descriptor from it. - record_fields = sorted(rdict.items()) - - record_values = { - "_target": self.target, - "source": source, - } - record_fields = [] - - for k, v in rdict.items(): - k = format_key(k) - - if isinstance(v, bool): - record_fields.append(("boolean", k)) - elif isinstance(v, int): - record_fields.append(("varint", k)) - else: - record_fields.append(("string", k)) - - record_values[k] = v - - record_fields.append(("path", "source")) - - # tuple conversion here is needed for lru_cache - desc = self.create_event_descriptor(record_name, tuple(record_fields)) - return desc(**record_values) - - def create_event_descriptor(self, record_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: - return TargetRecordDescriptor(record_name, record_fields) - - -def format_key(key: str) -> str: - # A lot of "malformed" keys - key = key.replace(".", "_") - key = key.replace("-", "_") - key = key.replace("@", "_") - key = key.replace(" ", "_") - key = key.replace("()", "") - key = key.removeprefix("#") - key = key.removeprefix("_") - key = key.lstrip("_") - - if "/" in key: - key = key.rsplit("/", 1)[-1] - - if key == "0": - key = "zero" - - return key - - -def extract_nested_dict(flat: dict, nested: dict) -> None: - for k, v in nested.items(): - if isinstance(v, dict): - extract_nested_dict(flat, v) - else: - flat[k] = v + yield from build_records( + self, "macos/launch_daemons", self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py new file mode 100644 index 0000000000..c8ccaebea1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +LoginItemsRecord = TargetRecordDescriptor( + "macos/login_items", + [ + ("varint", "generation"), + ("varint", "backgroundAppRefreshLoadCount"), + ("boolean", "launchServicesItemsImported"), + ("boolean", "serviceManagementLoginItemsMigrated"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +LoginItemsRecords = (LoginItemsRecord,) + + +class LoginItemsPlugin(Plugin): + """macOS login items plugin.""" + + SYSTEM_LOGIN_ITEMS_PATHS = ("/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v*.btm",) + + USER_LOGIN_ITEMS_PATHS = ( + "Library/Preferences/com.apple.loginitems.plist", + "Library/Application Support/com.apple.backgroundtaskmanagementagent/backgrounditems.btm", + ) + + def __init__(self, target: Target): + super().__init__(target) + + self.login_items_files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.login_items_files): + raise UnsupportedPluginError("No Login Items files found") + + def _find_files(self) -> None: + # --- System-wide --- + for pattern in self.SYSTEM_LOGIN_ITEMS_PATHS: + for path in self.target.fs.glob(pattern): + self.login_items_files.add(path) + + # --- Per-user --- + for _, path in _build_userdirs(self, self.USER_LOGIN_ITEMS_PATHS): + self.login_items_files.add(path) + + @export(record=DynamicDescriptor(["string"])) + # @export(output="yield") + def login_items(self) -> Iterator[DynamicDescriptor]: + """Yield macOS login items plist files.""" + yield from build_records(self, "macos/login_items", self.login_items_files, LoginItemsRecords) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py new file mode 100644 index 0000000000..6971269d9a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class MacOSResourcesInfoStringsPlugin(Plugin): + """macOS Resources InfoPlist.strings plist file.""" + + PATHS = ( + "/Applications/*.app/Contents/Resources/*.help/Contents/Resources/*.lproj/InfoPlist.strings", + "/Applications/*/*.app/Contents/Resources/*.help/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/CoreServices/*.app/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.bundle/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/Extensions/*.kext/Contents/Resources/InfoPlist.strings", + "/System/Library/Extensions/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/Filesystems/*/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/Filesystems/*/Encodings/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No Resources InfoPlist.strings files found") + + @export(record=DynamicDescriptor(["string"])) + def resources_info_strings(self) -> Iterator[DynamicDescriptor]: + """Yield Resources InfoPlist.strings information.""" + yield from build_records(self, "macos/resources_info_strings", self.files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py new file mode 100644 index 0000000000..e4450c277a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class MacOSResourcesLocalizableStringsPlugin(Plugin): + """macOS Resources Localizable.strings plist file.""" + + PATHS = ( + "/System/Library/CoreServices/*.app/Contents/Resources/*.lproj/Localizable.strings", + "/System/Library/Extensions/*.kext/Contents/Resources/*.lproj/Localizable.strings", + "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.lproj/Localizable.strings", + "/System/Library/Frameworks/*.framework/Versions/A/Frameworks/*.framework/Versions/A/Resources/*.lproj/Localizable.strings", + "/System/Library/PreferencePanes/*.prefPane/Contents/Resources/*.lproj/Localizable.strings", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Plugins/*.bundle/Contents/Resources/*.lproj/Localizable.strings", + "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.lproj/Localizable.strings", + "/System/Library/SystemProfiler/*/Contents/Resources/*.lproj/Localizable.strings", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No Resources Localizable.strings files found") + + @export(record=DynamicDescriptor(["string"])) + def resources_localizable_strings(self) -> Iterator[DynamicDescriptor]: + """Yield Resources Localizable.strings information.""" + yield from build_records(self, "macos/resources_localizable_strings", self.files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py new file mode 100644 index 0000000000..23f3b517b0 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +SoftwareUpdatePreferencesRecord = TargetRecordDescriptor( + "macos/software_update_preferences", + [ + ("varint", "last_result_code"), + ("string", "last_attempt_system_version"), + ("string", "last_attempt_build_version"), + ("boolean", "automatic_download"), + ("boolean", "automatically_install_macos_updates"), + ("boolean", "critical_update_install"), + ("boolean", "config_data_install"), + ("string[]", "recommended_updates"), + ("boolean", "splat_enabled"), + ("boolean", "post_logout_notification"), + ("string", "last_recommended_major_os_bundle_id"), + ("string[]", "primary_languages"), + ("datetime", "last_successful_date"), + ("datetime", "last_full_successful_date"), + ("path", "source"), + ], +) + + +class SoftwareUpdatePreferencesPlugin(Plugin): + """macOS software update preferences plugin.""" + + PATH = "/Library/Preferences/com.apple.SoftwareUpdate.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No com.apple.SoftwareUpdate.plist file found") + + @export(record=SoftwareUpdatePreferencesRecord) + def software_update_preferences(self) -> Iterator[SoftwareUpdatePreferencesRecord]: + """Yield software update preference information.""" + plist = plistlib.load(self.file.open()) + + yield SoftwareUpdatePreferencesRecord( + last_result_code=plist.get("LastResultCode"), + last_attempt_system_version=plist.get("LastAttemptSystemVersion"), + last_attempt_build_version=plist.get("LastAttemptBuildVersion"), + automatic_download=plist.get("AutomaticDownload"), + automatically_install_macos_updates=plist.get("AutomaticallyInstallMacOSUpdates"), + critical_update_install=plist.get("CriticalUpdateInstall"), + config_data_install=plist.get("ConfigDataInstall"), + recommended_updates=plist.get("RecommendedUpdates"), + splat_enabled=plist.get("SplatEnabled"), + post_logout_notification=plist.get("PostSuccessfulMinorUpdatePostLogOutNotification"), + last_recommended_major_os_bundle_id=plist.get("LastRecommendedMajorOSBundleIdentifier"), + primary_languages=plist.get("PrimaryLanguages"), + last_successful_date=plist.get("LastSuccessfulDate"), + last_full_successful_date=plist.get("LastFullSuccessfulDate"), + source=self.file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py new file mode 100644 index 0000000000..28c141ee9c --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + + +class SystemPreferencesPlugin(Plugin): + """macOS system preferences plugin.""" + + PATHS = ("/Library/Preferences/**/*.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.files.add(path) + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No system preferences files found") + + @export(record=DynamicDescriptor(["string"])) + def system_preferences(self) -> Iterator[DynamicDescriptor]: + """Yield system preference information.""" + yield from build_records(self, "macos/system_preferences", self.files) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py new file mode 100644 index 0000000000..73fe008c6a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +TimeMachineRecord = TargetRecordDescriptor( + "macos/time_machine", + [ + ("varint", "preferences_version"), + ("path", "source"), + ], +) + + +class TimeMachinePlugin(Plugin): + """macOS time machine plugin.""" + + PATH = "/Library/Preferences/com.apple.TimeMachine.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No com.apple.TimeMachine.plist file found") + + @export(record=TimeMachineRecord) + def time_machine(self) -> Iterator[TimeMachineRecord]: + """Yield time machine information.""" + plist = plistlib.load(self.file.open()) + + yield TimeMachineRecord( + preferences_version=plist.get("PreferencesVersion"), + source=self.file, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/.GlobalPreferences.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/.GlobalPreferences.plist new file mode 100644 index 0000000000..b3fa705101 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/.GlobalPreferences.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62a8bdac60526a4ed80707b31a5fb4f301f42c04bfbb52fbb49afb75c4a8b651 +size 1076 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/InstallHistory.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/InstallHistory.plist new file mode 100644 index 0000000000..77de086d3a --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/InstallHistory.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c60512c2e057379a387fded64e4e900d679c763b12fc20a9435fb748c40d4431 +size 428 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet new file mode 100644 index 0000000000..3275e1676d --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e68ed4d5bad9d248690cf91c9232faf91244afb57a53ac9419395bf0d8dfe56 +size 1013 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider new file mode 100644 index 0000000000..335016a4b6 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c07b4c0c0587dc53fa4b37dec98526993eb4eae392938164f42157b4e90378 +size 747 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host new file mode 100644 index 0000000000..759611c5c8 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4787dd7220f2ab63b8049ab9433a7c923d78f65bac44c4550fd689a8f292466c +size 1021 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.HIToolbox.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.HIToolbox.plist new file mode 100644 index 0000000000..823bb60450 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.HIToolbox.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:faec976255adb7aafde03a0b735abd65f45924df434b53eb3e76bdf32814db6c +size 377 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.SoftwareUpdate.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.SoftwareUpdate.plist new file mode 100644 index 0000000000..010f40c18c --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.SoftwareUpdate.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d14d03f8788851274539ecc42fdfb25ac93d1492ef41ef16bc388da6b323bd8c +size 545 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist new file mode 100644 index 0000000000..19419bb7a6 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1efbcae1efa883b268a7af209f1353e03292b67f3b119969125a8ee90c0b13e0 +size 69 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.airport.preferences.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.airport.preferences.plist new file mode 100644 index 0000000000..1b2c77fd1c --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.airport.preferences.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc8d75b8539e525d00a16b745560e1a3457d61b7c08e5e6fa3dd38115f437d81 +size 390 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist new file mode 100644 index 0000000000..a224846811 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93a3f4a2229e6ebb6ba7c8db70af081e45d4895eb2fa552e5bad52cce56d0854 +size 2449 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist new file mode 100644 index 0000000000..5bc7145a6f --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4037034a0ae07c8c7f721b289ccfe63508bf7f27e3ed2c897432b1264052e1ba +size 2242 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist new file mode 100644 index 0000000000..73d436c0ca --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ab864905a40cfbf50165ac39e75e80bcc6e5d60c2c76a281b9cc8e059114501 +size 1478 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/IOBluetoothUI.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/IOBluetoothUI.plist new file mode 100644 index 0000000000..2011e83def --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/IOBluetoothUI.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec7af55fe1b95fdc55c1c537bc7f98120719fdf7e2bbc6ddad98f203eb88c63f +size 462 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/SwiftUICore.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/SwiftUICore.plist new file mode 100644 index 0000000000..4bf00dbec4 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/SwiftUICore.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da651d1aee926c9cb585ab3b41c40df0c431e1816ce30edb9343754c967c6b89 +size 514 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/UniversalControl.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/UniversalControl.plist new file mode 100644 index 0000000000..b1a5db41c9 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_version/UniversalControl.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7c8e3b4bfe04433539f1eebc7bc58346717b5ae7bd65c3066abda46f73436c8 +size 526 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/root.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/root.plist new file mode 100644 index 0000000000..073c8fb814 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/root.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97c2f1cf684f7d4ea1c4eda1ce519461575516f5c8b30d8db83bf1b97b29e915 +size 127 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/securityagent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/securityagent.plist new file mode 100644 index 0000000000..de08975d4f --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/securityagent.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5214ad4e41e0a5f348d79554fe0e2ac47d3d0dc76b2734bea59de8a585079d4 +size 70 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/user.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/user.plist new file mode 100644 index 0000000000..b3fa705101 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/user.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62a8bdac60526a4ed80707b31a5fb4f301f42c04bfbb52fbb49afb75c4a8b651 +size 1076 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/com.apple.timezone.auto.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/com.apple.timezone.auto.plist new file mode 100644 index 0000000000..a07c6c93ed --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/com.apple.timezone.auto.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55775607ea809eb93b51eaa8c68cc33b6b02e66fa4d95a63fb815dca3db8aab0 +size 54 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist new file mode 100644 index 0000000000..ff75ec1046 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:080deca33a1560caffe316ca3f7f47fc6b42538674c52225f5cb1b92f4be5f85 +size 65 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/loginwindow.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/loginwindow.plist new file mode 100644 index 0000000000..a6fa18a9bb --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/loginwindow.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af63e46d50ae6d7e97abff7cb83d709070c131b8dddcf6ace59b0154e457ca22 +size 194 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm new file mode 100644 index 0000000000..8df472f6d2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cef44aa7617d8eff7753f7cf1be9a24514ef3cb526f49cc6e76374afbf9ecd8 +size 1254 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings new file mode 100644 index 0000000000..9242e828d2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0038c966736bfa285115eab47574a334562598110262f5448905acfb971778c8 +size 172 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py index fbca628c5e..0a032eba8b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py @@ -63,56 +63,443 @@ def test_launch_agents( target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_agents()) - results.sort(key=lambda r: r.source) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) assert len(results) == 4 - assert results[0].EnablePressuredExit - assert results[0].EnableTransactions + assert results[0].hostname == "localhost" + assert results[0].domain is None assert results[0].Label == "com.apple.sidecar-display-agent" - assert results[0].ProcessType == "Interactive" + assert results[0].Disabled is None + assert results[0].UserName is None + assert results[0].GroupName is None + assert results[0].Group is None + assert results[0].CFBundleIdentifier is None + assert results[0].CFBundleDevelopmentRegion is None + assert results[0].CFBundleInfoDictionaryVersion is None + assert results[0].CFBundleName is None + assert results[0].InitGroups is None assert results[0].Program == "/usr/libexec/SidecarDisplayAgent" - assert results[0].com_apple_sidecar_display_agent + assert results[0].BundleProgram is None + assert results[0].ProgramArguments == [] + assert results[0].EnableGlobbing is None + assert results[0].EnableTransactions + assert results[0].PressuredExit is None + assert results[0].EnablePressureExit is None + assert results[0].EnablePressuredExit == "True" + assert results[0].KeepAlive is None + assert results[0].SuccessfulExit is None + assert results[0].Crashed is None + assert results[0].RunAtLoad is None + assert results[0].RootDirectory is None + assert results[0].WorkingDirectory is None + assert results[0].Umask is None + assert results[0].TimeOut is None + assert results[0].ExitTimeOut is None + assert results[0].ThrottleInterval is None + assert results[0].StartInterval is None + assert results[0].StartOnMount is None + assert results[0].WatchPaths == [] + assert results[0].QueueDirectories == [] + assert results[0].StandardInPath is None + assert results[0].StandardOutPath is None + assert results[0].StandardErrorPath is None + assert results[0].Debug is None + assert results[0].WaitForDebugger is None + assert results[0].Nice is None + assert results[0].ProcessType == "Interactive" + assert results[0].PowerNap is None + assert results[0].AbandonProcessGroup is None + assert results[0].LowPriorityIO is None + assert results[0].LowPriorityBackgroundIO is None + assert results[0].MaterializeDatalessFiles is None + assert results[0].LaunchOnlyOnce is None + assert results[0].BootShell is None + assert results[0].SessionCreate is None + assert results[0].LegacyTimers is None + assert results[0].TransactionTimeLimitEnabled is None + assert results[0].LimitLoadToDeveloperMode is None + assert results[0].LimitLoadToSessionType is None + assert results[0].Wait is None + assert results[0].AssociatedBundleIdentifiers == [] + assert results[0].plist_path is None + assert results[0].POSIXSpawnType is None + assert results[0].PosixSpawnType is None + assert results[0].MultipleInstances is None + assert results[0].DisabledInSafeBoot is None + assert results[0].BeginTransactionAtShutdown is None + assert results[0].OnDemand is None + assert results[0].AlwaysSIGTERMOnShutdown is None + assert results[0].MinimalBootProfiles is None + assert results[0].MinimalBootProfile is None + assert results[0].LSBackgroundOnly is None + assert results[0].HopefullyExitsLast is None + assert results[0].ExponentialThrottling is None + assert results[0].IgnoreProcessGroupAtShutdown is None + assert results[0].EventMonitor is None + assert results[0].AuxiliaryBootstrapper is None + assert results[0].AuxiliaryBootstrapperAllowDemand is None + assert results[0].DrainMessagesAfterFailedInit is None + assert results[0].DrainMessagesOnFailedInit is None + assert results[0].LimitLoadFromVariant is None + assert results[0].LimitLoadFromBootMode is None + assert results[0].EfficiencyMode is None + assert results[0].Conclave is None + assert results[0].LaunchEvents == [] + assert results[0].MachServices == ["('com.apple.sidecar-display-agent', True)"] + assert results[0].SoftResourceLimits == [] + assert results[0].HardResourceLimits == [] + assert results[0].EnvironmentVariables == [] + assert results[0].NoEnvironmentVariables == [] + assert results[0].NO_EnvironmentVariables == [] + assert results[0].SHAuthorizationRight is None + assert results[0].PublishesEvents is None + assert results[0].LimitLoadToVariant is None + assert results[0].RunLoopType is None + assert results[0].RemoteServices == [] + assert results[0].JetsamProperties == [] + assert results[0].LimitLoadToHardware == [] + assert results[0].PanicOnCrash == [] + assert results[0].LimitLoadToBootMode == [] + assert results[0].Cryptex is None + assert results[0].ServiceType is None + assert results[0].ServiceIPC is None + assert results[0].UrgentLogSubmission == [] + assert results[0].AppIntents == [] + assert results[0].BinaryOrderPreference == [] + assert results[0].LimitLoadFromHardware == [] + assert results[0].StartCalendarInterval == [] + assert results[0].AdditionalProperties == [] + assert results[0].NSAppTransportSecurity == [] assert results[0].source == "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist" - assert results[1].EnablePressuredExit - assert results[1].EnableTransactions + assert results[1].hostname == "localhost" + assert results[1].domain is None assert results[1].Label == "com.apple.AMPArtworkAgent" + assert results[1].Disabled is None + assert results[1].UserName is None + assert results[1].GroupName is None + assert results[1].Group is None + assert results[1].CFBundleIdentifier is None + assert results[1].CFBundleDevelopmentRegion is None + assert results[1].CFBundleInfoDictionaryVersion is None + assert results[1].CFBundleName is None + assert results[1].InitGroups is None + assert results[1].Program is None + assert results[1].BundleProgram is None + assert results[1].ProgramArguments == [ + "/System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPArtworkAgent", + "--launchd", + ] + assert results[1].EnableGlobbing is None + assert results[1].EnableTransactions + assert results[1].PressuredExit is None + assert results[1].EnablePressureExit is None + assert results[1].EnablePressuredExit == "True" + assert results[1].KeepAlive is None + assert results[1].SuccessfulExit is None + assert results[1].Crashed is None + assert results[1].RunAtLoad is None + assert results[1].RootDirectory is None + assert results[1].WorkingDirectory is None + assert results[1].Umask is None + assert results[1].TimeOut is None + assert results[1].ExitTimeOut is None + assert results[1].ThrottleInterval is None + assert results[1].StartInterval is None + assert results[1].StartOnMount is None + assert results[1].WatchPaths == [] + assert results[1].QueueDirectories == [] + assert results[1].StandardInPath is None + assert results[1].StandardOutPath is None + assert results[1].StandardErrorPath is None + assert results[1].Debug is None + assert results[1].WaitForDebugger is None + assert results[1].Nice is None assert results[1].ProcessType == "Adaptive" - assert results[1].ProgramArguments == ( - "['/System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPArtworkAgent', '--launchd']" - ) - assert results[1].com_apple_amp_artworkd + assert results[1].PowerNap is None + assert results[1].AbandonProcessGroup is None + assert results[1].LowPriorityIO is None + assert results[1].LowPriorityBackgroundIO is None + assert results[1].MaterializeDatalessFiles is None + assert results[1].LaunchOnlyOnce is None + assert results[1].BootShell is None + assert results[1].SessionCreate is None + assert results[1].LegacyTimers is None + assert results[1].TransactionTimeLimitEnabled is None + assert results[1].LimitLoadToDeveloperMode is None + assert results[1].LimitLoadToSessionType is None + assert results[1].Wait is None + assert results[1].AssociatedBundleIdentifiers == [] + assert results[1].plist_path is None + assert results[1].POSIXSpawnType is None + assert results[1].PosixSpawnType is None + assert results[1].MultipleInstances is None + assert results[1].DisabledInSafeBoot is None + assert results[1].BeginTransactionAtShutdown is None + assert results[1].OnDemand is None + assert results[1].AlwaysSIGTERMOnShutdown is None + assert results[1].MinimalBootProfiles is None + assert results[1].MinimalBootProfile is None + assert results[1].LSBackgroundOnly is None + assert results[1].HopefullyExitsLast is None + assert results[1].ExponentialThrottling is None + assert results[1].IgnoreProcessGroupAtShutdown is None + assert results[1].EventMonitor is None + assert results[1].AuxiliaryBootstrapper is None + assert results[1].AuxiliaryBootstrapperAllowDemand is None + assert results[1].DrainMessagesAfterFailedInit is None + assert results[1].DrainMessagesOnFailedInit is None + assert results[1].LimitLoadFromVariant is None + assert results[1].LimitLoadFromBootMode is None + assert results[1].EfficiencyMode is None + assert results[1].Conclave is None + assert results[1].LaunchEvents == [] + assert results[1].MachServices == ["('com.apple.amp.artworkd', True)"] + assert results[1].SoftResourceLimits == [] + assert results[1].HardResourceLimits == [] + assert results[1].EnvironmentVariables == [] + assert results[1].NoEnvironmentVariables == [] + assert results[1].NO_EnvironmentVariables == [] + assert results[1].SHAuthorizationRight is None + assert results[1].PublishesEvents is None + assert results[1].LimitLoadToVariant is None + assert results[1].RunLoopType is None + assert results[1].RemoteServices == [] + assert results[1].JetsamProperties == [] + assert results[1].LimitLoadToHardware == [] + assert results[1].PanicOnCrash == [] + assert results[1].LimitLoadToBootMode == [] + assert results[1].Cryptex is None + assert results[1].ServiceType is None + assert results[1].ServiceIPC is None + assert results[1].UrgentLogSubmission == [] + assert results[1].AppIntents == [] + assert results[1].BinaryOrderPreference == [] + assert results[1].LimitLoadFromHardware == [] + assert results[1].StartCalendarInterval == [] + assert results[1].AdditionalProperties == [] + assert results[1].NSAppTransportSecurity == [] assert results[1].source == "/System/Library/LaunchAgents/com.apple.AMPArtworkAgent.plist" - assert results[2].EnableTransactions + assert results[2].hostname == "localhost" + assert results[2].domain is None assert results[2].Label == "com.apple.familynotificationd" - assert results[2].Program == ( - "/System/Library/PrivateFrameworks/FamilyNotification.framework/familynotificationd" + assert results[2].Disabled is None + assert results[2].UserName is None + assert results[2].GroupName is None + assert results[2].Group is None + assert results[2].CFBundleIdentifier is None + assert results[2].CFBundleDevelopmentRegion is None + assert results[2].CFBundleInfoDictionaryVersion is None + assert results[2].CFBundleName is None + assert results[2].InitGroups is None + assert ( + results[2].Program == "/System/Library/PrivateFrameworks/FamilyNotification.framework/familynotificationd" ) + assert results[2].BundleProgram is None + assert results[2].ProgramArguments == [] + assert results[2].EnableGlobbing is None + assert results[2].EnableTransactions + assert results[2].PressuredExit is None + assert results[2].EnablePressureExit is None + assert results[2].EnablePressuredExit is None + assert results[2].KeepAlive is None + assert results[2].SuccessfulExit is None + assert results[2].Crashed is None + assert results[2].RunAtLoad is None + assert results[2].RootDirectory is None + assert results[2].WorkingDirectory is None + assert results[2].Umask is None + assert results[2].TimeOut is None + assert results[2].ExitTimeOut == 1 + assert results[2].ThrottleInterval is None + assert results[2].StartInterval is None + assert results[2].StartOnMount is None + assert results[2].WatchPaths == [] + assert results[2].QueueDirectories == [] + assert results[2].StandardInPath is None + assert results[2].StandardOutPath is None + assert results[2].StandardErrorPath is None + assert results[2].Debug is None + assert results[2].WaitForDebugger is None + assert results[2].Nice is None + assert results[2].ProcessType is None + assert results[2].PowerNap is None + assert results[2].AbandonProcessGroup is None + assert results[2].LowPriorityIO is None + assert results[2].LowPriorityBackgroundIO is None + assert results[2].MaterializeDatalessFiles is None + assert results[2].LaunchOnlyOnce is None + assert results[2].BootShell is None + assert results[2].SessionCreate is None + assert results[2].LegacyTimers is None + assert results[2].TransactionTimeLimitEnabled is None + assert results[2].LimitLoadToDeveloperMode is None assert results[2].LimitLoadToSessionType == "['LoginWindow', 'Aqua']" - assert results[2].bundleid == "com.apple.familyalert" + assert results[2].Wait is None + assert results[2].AssociatedBundleIdentifiers == [] + assert results[2].plist_path is None assert results[2].POSIXSpawnType == "Adaptive" - assert results[2].ExitTimeOut == 1 - assert results[2].delay_registration - assert results[2].com_apple_familynotification_agent - assert results[2].com_apple_usernotifications_delegate_com_apple_familynotifications - assert results[2].events == ( - "['didDismissAlert', 'didActivateNotification', 'didDeliverNotification', " - "'didSnoozeAlert', 'didRemoveDeliveredNotifications', " - "'didExpireNotifications']" - ) + assert results[2].PosixSpawnType is None + assert results[2].MultipleInstances is None + assert results[2].DisabledInSafeBoot is None + assert results[2].BeginTransactionAtShutdown is None + assert results[2].OnDemand is None + assert results[2].AlwaysSIGTERMOnShutdown is None + assert results[2].MinimalBootProfiles is None + assert results[2].MinimalBootProfile is None + assert results[2].LSBackgroundOnly is None + assert results[2].HopefullyExitsLast is None + assert results[2].ExponentialThrottling is None + assert results[2].IgnoreProcessGroupAtShutdown is None + assert results[2].EventMonitor is None + assert results[2].AuxiliaryBootstrapper is None + assert results[2].AuxiliaryBootstrapperAllowDemand is None + assert results[2].DrainMessagesAfterFailedInit is None + assert results[2].DrainMessagesOnFailedInit is None + assert results[2].LimitLoadFromVariant is None + assert results[2].LimitLoadFromBootMode is None + assert results[2].EfficiencyMode is None + assert results[2].Conclave is None + assert results[2].LaunchEvents != [] + assert results[2].MachServices == [ + "('com.apple.familynotification.agent', True)", + "('com.apple.usernotifications.delegate.com.apple.familynotifications', True)", + ] + assert results[2].SoftResourceLimits == [] + assert results[2].HardResourceLimits == [] + assert results[2].EnvironmentVariables == [] + assert results[2].NoEnvironmentVariables == [] + assert results[2].NO_EnvironmentVariables == [] + assert results[2].SHAuthorizationRight is None + assert results[2].PublishesEvents is None + assert results[2].LimitLoadToVariant is None + assert results[2].RunLoopType is None + assert results[2].RemoteServices == [] + assert results[2].JetsamProperties == [] + assert results[2].LimitLoadToHardware == [] + assert results[2].PanicOnCrash == [] + assert results[2].LimitLoadToBootMode == [] + assert results[2].Cryptex is None + assert results[2].ServiceType is None + assert results[2].ServiceIPC is None + assert results[2].UrgentLogSubmission == [] + assert results[2].AppIntents == [] + assert results[2].BinaryOrderPreference == [] + assert results[2].LimitLoadFromHardware == [] + assert results[2].StartCalendarInterval == [] + assert results[2].AdditionalProperties == [] + assert results[2].NSAppTransportSecurity == [] assert results[2].source == "/System/Library/LaunchAgents/com.apple.familynotificationd.plist" - assert results[3].EnablePressuredExit - assert results[3].EnableTransactions + assert results[3].hostname == "localhost" + assert results[3].domain is None assert results[3].Label == "com.apple.seserviced" - assert results[3].ProcessType == "Adaptive" + assert results[3].Disabled is None + assert results[3].UserName is None + assert results[3].GroupName is None + assert results[3].Group is None + assert results[3].CFBundleIdentifier is None + assert results[3].CFBundleDevelopmentRegion is None + assert results[3].CFBundleInfoDictionaryVersion is None + assert results[3].CFBundleName is None + assert results[3].InitGroups is None assert results[3].Program == "/usr/libexec/seserviced" - assert results[3].Repeating + assert results[3].BundleProgram is None + assert results[3].ProgramArguments == [] + assert results[3].EnableGlobbing is None + assert results[3].EnableTransactions + assert results[3].PressuredExit is None + assert results[3].EnablePressureExit is None + assert results[3].EnablePressuredExit == "True" + assert results[3].KeepAlive is None + assert results[3].SuccessfulExit is None + assert results[3].Crashed is None + assert results[3].RunAtLoad is None + assert results[3].RootDirectory is None + assert results[3].WorkingDirectory is None + assert results[3].Umask is None + assert results[3].TimeOut is None + assert results[3].ExitTimeOut is None + assert results[3].ThrottleInterval is None + assert results[3].StartInterval is None + assert results[3].StartOnMount is None + assert results[3].WatchPaths == [] + assert results[3].QueueDirectories == [] + assert results[3].StandardInPath is None + assert results[3].StandardOutPath is None + assert results[3].StandardErrorPath is None + assert results[3].Debug is None + assert results[3].WaitForDebugger is None + assert results[3].Nice is None + assert results[3].ProcessType == "Adaptive" + assert results[3].PowerNap is None + assert results[3].AbandonProcessGroup is None + assert results[3].LowPriorityIO is None + assert results[3].LowPriorityBackgroundIO is None + assert results[3].MaterializeDatalessFiles is None + assert results[3].LaunchOnlyOnce is None + assert results[3].BootShell is None + assert results[3].SessionCreate is None + assert results[3].LegacyTimers is None + assert results[3].TransactionTimeLimitEnabled is None + assert results[3].LimitLoadToDeveloperMode is None + assert results[3].LimitLoadToSessionType == "['Aqua']" + assert results[3].Wait is None + assert results[3].AssociatedBundleIdentifiers == [] + assert results[3].plist_path is None + assert results[3].POSIXSpawnType is None + assert results[3].PosixSpawnType is None + assert results[3].MultipleInstances is None + assert results[3].DisabledInSafeBoot is None + assert results[3].BeginTransactionAtShutdown is None + assert results[3].OnDemand is None + assert results[3].AlwaysSIGTERMOnShutdown is None + assert results[3].MinimalBootProfiles is None + assert results[3].MinimalBootProfile is None + assert results[3].LSBackgroundOnly is None + assert results[3].HopefullyExitsLast is None + assert results[3].ExponentialThrottling is None + assert results[3].IgnoreProcessGroupAtShutdown is None + assert results[3].EventMonitor is None + assert results[3].AuxiliaryBootstrapper is None + assert results[3].AuxiliaryBootstrapperAllowDemand is None + assert results[3].DrainMessagesAfterFailedInit is None + assert results[3].DrainMessagesOnFailedInit is None assert results[3].LimitLoadFromVariant == "HasFactoryContent" - assert results[3].com_apple_seserviced - assert results[3].com_apple_seserviced_sereservation_client + assert results[3].LimitLoadFromBootMode is None + assert results[3].EfficiencyMode is None + assert results[3].Conclave is None + assert results[3].LaunchEvents != [] + assert results[3].MachServices == [ + "('com.apple.seserviced', True)", + "('com.apple.seserviced.sereservation.client', True)", + ] + assert results[3].SoftResourceLimits == [] + assert results[3].HardResourceLimits == [] + assert results[3].EnvironmentVariables == [] + assert results[3].NoEnvironmentVariables == [] + assert results[3].NO_EnvironmentVariables == [] + assert results[3].SHAuthorizationRight is None + assert results[3].PublishesEvents is None + assert results[3].LimitLoadToVariant is None + assert results[3].RunLoopType is None + assert results[3].RemoteServices == [] + assert results[3].JetsamProperties == [] + assert results[3].LimitLoadToHardware == [] + assert results[3].PanicOnCrash == [] + assert results[3].LimitLoadToBootMode == [] + assert results[3].Cryptex is None + assert results[3].ServiceType is None + assert results[3].ServiceIPC is None + assert results[3].UrgentLogSubmission == [] + assert results[3].AppIntents == [] + assert results[3].BinaryOrderPreference == [] + assert results[3].LimitLoadFromHardware == [] + assert results[3].StartCalendarInterval == [] + assert results[3].AdditionalProperties == [] + assert results[3].NSAppTransportSecurity == [] assert results[3].source == "/Users/user/Library/LaunchAgents/com.apple.seserviced.plist" @@ -162,32 +549,329 @@ def test_launch_daemons( target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_daemons()) - results.sort(key=lambda r: r.source) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) assert len(results) == 3 - assert results[0].EnablePressuredExit - assert results[0].EnableTransactions + assert results[0].hostname == "localhost" + assert results[0].domain is None assert results[0].Label == "com.apple.perfpowermetricd" + assert results[0].Disabled is None + assert results[0].UserName is None + assert results[0].GroupName is None + assert results[0].Group is None + assert results[0].CFBundleIdentifier is None + assert results[0].CFBundleDevelopmentRegion is None + assert results[0].CFBundleInfoDictionaryVersion is None + assert results[0].CFBundleName is None + assert results[0].InitGroups is None + assert results[0].Program is None + assert results[0].BundleProgram is None + assert results[0].ProgramArguments == ["usr/libexec/perfpowermetricd"] or results[0].ProgramArguments == [ + "/usr/libexec/perfpowermetricd" + ] + assert results[0].EnableGlobbing is None + assert results[0].EnableTransactions + assert results[0].PressuredExit is None + assert results[0].EnablePressureExit is None + assert results[0].EnablePressuredExit == "True" + assert results[0].KeepAlive is None + assert results[0].SuccessfulExit is None + assert results[0].Crashed is None + assert results[0].RunAtLoad is None + assert results[0].RootDirectory is None + assert results[0].WorkingDirectory is None + assert results[0].Umask is None + assert results[0].TimeOut is None + assert results[0].ExitTimeOut is None + assert results[0].ThrottleInterval is None + assert results[0].StartInterval is None + assert results[0].StartOnMount is None + assert results[0].WatchPaths == [] + assert results[0].QueueDirectories == [] + assert results[0].StandardInPath is None + assert results[0].StandardOutPath is None + assert results[0].StandardErrorPath is None + assert results[0].Debug is None + assert results[0].WaitForDebugger is None + assert results[0].Nice is None assert results[0].ProcessType == "Interactive" - assert results[0].com_apple_PerfPowerMetricMonitor_xpc - assert results[0].ProgramArguments == "['/usr/libexec/perfpowermetricd']" + assert results[0].PowerNap is None + assert results[0].AbandonProcessGroup is None + assert results[0].LowPriorityIO is None + assert results[0].LowPriorityBackgroundIO is None + assert results[0].MaterializeDatalessFiles is None + assert results[0].LaunchOnlyOnce is None + assert results[0].BootShell is None + assert results[0].SessionCreate is None + assert results[0].LegacyTimers is None + assert results[0].TransactionTimeLimitEnabled is None + assert results[0].LimitLoadToDeveloperMode is None + assert results[0].LimitLoadToSessionType is None + assert results[0].Wait is None + assert results[0].AssociatedBundleIdentifiers == [] + assert results[0].plist_path is None + assert results[0].POSIXSpawnType is None + assert results[0].PosixSpawnType is None + assert results[0].MultipleInstances is None + assert results[0].DisabledInSafeBoot is None + assert results[0].BeginTransactionAtShutdown is None + assert results[0].OnDemand is None + assert results[0].AlwaysSIGTERMOnShutdown is None + assert results[0].MinimalBootProfiles is None + assert results[0].MinimalBootProfile is None + assert results[0].LSBackgroundOnly is None + assert results[0].HopefullyExitsLast is None + assert results[0].ExponentialThrottling is None + assert results[0].IgnoreProcessGroupAtShutdown is None + assert results[0].EventMonitor is None + assert results[0].AuxiliaryBootstrapper is None + assert results[0].AuxiliaryBootstrapperAllowDemand is None + assert results[0].DrainMessagesAfterFailedInit is None + assert results[0].DrainMessagesOnFailedInit is None + assert results[0].LimitLoadFromVariant is None + assert results[0].LimitLoadFromBootMode is None + assert results[0].EfficiencyMode is None + assert results[0].Conclave is None + assert results[0].LaunchEvents == [] + assert results[0].MachServices == ["('com.apple.PerfPowerMetricMonitor.xpc', True)"] + assert results[0].SoftResourceLimits == [] + assert results[0].HardResourceLimits == [] + assert results[0].EnvironmentVariables == [] + assert results[0].NoEnvironmentVariables == [] + assert results[0].NO_EnvironmentVariables == [] + assert results[0].SHAuthorizationRight is None + assert results[0].PublishesEvents is None + assert results[0].LimitLoadToVariant is None + assert results[0].RunLoopType is None + assert results[0].RemoteServices == [] + assert results[0].JetsamProperties == [] + assert results[0].LimitLoadToHardware == [] + assert results[0].PanicOnCrash == [] + assert results[0].LimitLoadToBootMode == [] + assert results[0].Cryptex is None + assert results[0].ServiceType is None + assert results[0].ServiceIPC is None + assert results[0].UrgentLogSubmission == [] + assert results[0].AppIntents == [] + assert results[0].BinaryOrderPreference == [] + assert results[0].LimitLoadFromHardware == [] + assert results[0].StartCalendarInterval == [] + assert results[0].AdditionalProperties == [] + assert results[0].NSAppTransportSecurity == [] assert results[0].source == "/Library/LaunchDaemons/com.apple.perfpowermetricd.plist" - assert results[1].EnablePressuredExit + assert results[1].hostname == "localhost" + assert results[1].domain is None assert results[1].Label == "com.apple.WirelessRadioManager" - assert results[1].POSIXSpawnType == "Interactive" + assert results[1].Disabled is None + assert results[1].UserName is None + assert results[1].GroupName is None + assert results[1].Group is None + assert results[1].CFBundleIdentifier is None + assert results[1].CFBundleDevelopmentRegion is None + assert results[1].CFBundleInfoDictionaryVersion is None + assert results[1].CFBundleName is None + assert results[1].InitGroups is None + assert results[1].Program is None + assert results[1].BundleProgram is None + assert results[1].ProgramArguments == ["/usr/sbin/WirelessRadioManagerd"] + assert results[1].EnableGlobbing is None + assert results[1].EnableTransactions is None + assert results[1].PressuredExit is None + assert results[1].EnablePressureExit is None + assert results[1].EnablePressuredExit == "True" + assert results[1].KeepAlive is None + assert results[1].SuccessfulExit is None + assert results[1].Crashed is None + assert results[1].RunAtLoad is None + assert results[1].RootDirectory is None + assert results[1].WorkingDirectory is None + assert results[1].Umask is None + assert results[1].TimeOut is None + assert results[1].ExitTimeOut is None assert results[1].ThrottleInterval == 10 - assert results[1].com_apple_WirelessCoexManager - assert results[1].com_apple_WirelessRadioManager - assert results[1].ProgramArguments == "['/usr/sbin/WirelessRadioManagerd']" + assert results[1].StartInterval is None + assert results[1].StartOnMount is None + assert results[1].WatchPaths == [] + assert results[1].QueueDirectories == [] + assert results[1].StandardInPath is None + assert results[1].StandardOutPath is None + assert results[1].StandardErrorPath is None + assert results[1].Debug is None + assert results[1].WaitForDebugger is None + assert results[1].Nice is None + assert results[1].ProcessType is None + assert results[1].PowerNap is None + assert results[1].AbandonProcessGroup is None + assert results[1].LowPriorityIO is None + assert results[1].LowPriorityBackgroundIO is None + assert results[1].MaterializeDatalessFiles is None + assert results[1].LaunchOnlyOnce is None + assert results[1].BootShell is None + assert results[1].SessionCreate is None + assert results[1].LegacyTimers is None + assert results[1].TransactionTimeLimitEnabled is None + assert results[1].LimitLoadToDeveloperMode is None + assert results[1].LimitLoadToSessionType is None + assert results[1].Wait is None + assert results[1].AssociatedBundleIdentifiers == [] + assert results[1].plist_path is None + assert results[1].POSIXSpawnType == "Interactive" + assert results[1].PosixSpawnType is None + assert results[1].MultipleInstances is None + assert results[1].DisabledInSafeBoot is None + assert results[1].BeginTransactionAtShutdown is None + assert results[1].OnDemand is None + assert results[1].AlwaysSIGTERMOnShutdown is None + assert results[1].MinimalBootProfiles is None + assert results[1].MinimalBootProfile is None + assert results[1].LSBackgroundOnly is None + assert results[1].HopefullyExitsLast is None + assert results[1].ExponentialThrottling is None + assert results[1].IgnoreProcessGroupAtShutdown is None + assert results[1].EventMonitor is None + assert results[1].AuxiliaryBootstrapper is None + assert results[1].AuxiliaryBootstrapperAllowDemand is None + assert results[1].DrainMessagesAfterFailedInit is None + assert results[1].DrainMessagesOnFailedInit is None + assert results[1].LimitLoadFromVariant is None + assert results[1].LimitLoadFromBootMode is None + assert results[1].EfficiencyMode is None + assert results[1].Conclave is None + assert results[1].LaunchEvents == [] + assert results[1].MachServices == [ + "('com.apple.WirelessCoexManager', True)", + "('com.apple.WirelessRadioManager', True)", + ] + assert results[1].SoftResourceLimits == [] + assert results[1].HardResourceLimits == [] + assert results[1].EnvironmentVariables == [] + assert results[1].NoEnvironmentVariables == [] + assert results[1].NO_EnvironmentVariables == [] + assert results[1].SHAuthorizationRight is None + assert results[1].PublishesEvents is None + assert results[1].LimitLoadToVariant is None + assert results[1].RunLoopType is None + assert results[1].RemoteServices == [] + assert results[1].JetsamProperties == [] + assert results[1].LimitLoadToHardware == [] + assert results[1].PanicOnCrash == [] + assert results[1].LimitLoadToBootMode == [] + assert results[1].Cryptex is None + assert results[1].ServiceType is None + assert results[1].ServiceIPC is None + assert results[1].UrgentLogSubmission == [] + assert results[1].AppIntents == [] + assert results[1].BinaryOrderPreference == [] + assert results[1].LimitLoadFromHardware == [] + assert results[1].StartCalendarInterval == [] + assert results[1].AdditionalProperties == [] + assert results[1].NSAppTransportSecurity == [] assert results[1].source == "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist" - assert not results[2].EnablePressuredExit - assert results[2].EnableTransactions + assert results[2].hostname == "localhost" + assert results[2].domain is None assert results[2].Label == "com.apple.cfprefsd.xpc.daemon" + assert results[2].Disabled is None + assert results[2].UserName is None + assert results[2].GroupName is None + assert results[2].Group is None + assert results[2].CFBundleIdentifier is None + assert results[2].CFBundleDevelopmentRegion is None + assert results[2].CFBundleInfoDictionaryVersion is None + assert results[2].CFBundleName is None + assert results[2].InitGroups is None + assert results[2].Program is None + assert results[2].BundleProgram is None + assert results[2].ProgramArguments == ["/usr/sbin/cfprefsd", "daemon"] + assert results[2].EnableGlobbing is None + assert results[2].EnableTransactions + assert results[2].PressuredExit is None + assert results[2].EnablePressureExit is None + assert results[2].EnablePressuredExit == "False" + assert results[2].KeepAlive is None + assert results[2].SuccessfulExit is None + assert results[2].Crashed is None + assert results[2].RunAtLoad is None + assert results[2].RootDirectory is None + assert results[2].WorkingDirectory is None + assert results[2].Umask is None + assert results[2].TimeOut is None + assert results[2].ExitTimeOut is None + assert results[2].ThrottleInterval is None + assert results[2].StartInterval is None + assert results[2].StartOnMount is None + assert results[2].WatchPaths == [] + assert results[2].QueueDirectories == [] + assert results[2].StandardInPath is None + assert results[2].StandardOutPath is None + assert results[2].StandardErrorPath is None + assert results[2].Debug is None + assert results[2].WaitForDebugger is None + assert results[2].Nice is None + assert results[2].ProcessType is None + assert results[2].PowerNap is None + assert results[2].AbandonProcessGroup is None + assert results[2].LowPriorityIO is None + assert results[2].LowPriorityBackgroundIO is None + assert results[2].MaterializeDatalessFiles is None + assert results[2].LaunchOnlyOnce is None + assert results[2].BootShell is None + assert results[2].SessionCreate is None + assert results[2].LegacyTimers is None + assert results[2].TransactionTimeLimitEnabled is None + assert results[2].LimitLoadToDeveloperMode is None + assert results[2].LimitLoadToSessionType is None + assert results[2].Wait is None + assert results[2].AssociatedBundleIdentifiers == [] + assert results[2].plist_path is None assert results[2].POSIXSpawnType == "Adaptive" - assert results[2].NumberOfFiles == 512 - assert results[2].com_apple_cfprefsd_daemon - assert results[2].ProgramArguments == "['/usr/sbin/cfprefsd', 'daemon']" + assert results[2].PosixSpawnType is None + assert results[2].MultipleInstances is None + assert results[2].DisabledInSafeBoot is None + assert results[2].BeginTransactionAtShutdown is None + assert results[2].OnDemand is None + assert results[2].AlwaysSIGTERMOnShutdown is None + assert results[2].MinimalBootProfiles is None + assert results[2].MinimalBootProfile is None + assert results[2].LSBackgroundOnly is None + assert results[2].HopefullyExitsLast is None + assert results[2].ExponentialThrottling is None + assert results[2].IgnoreProcessGroupAtShutdown is None + assert results[2].EventMonitor is None + assert results[2].AuxiliaryBootstrapper is None + assert results[2].AuxiliaryBootstrapperAllowDemand is None + assert results[2].DrainMessagesAfterFailedInit is None + assert results[2].DrainMessagesOnFailedInit is None + assert results[2].LimitLoadFromVariant is None + assert results[2].LimitLoadFromBootMode is None + assert results[2].EfficiencyMode is None + assert results[2].Conclave is None + assert results[2].LaunchEvents == [] + assert results[2].MachServices == ["('com.apple.cfprefsd.daemon', True)"] + assert results[2].SoftResourceLimits == ["('NumberOfFiles', 512)"] + assert results[2].HardResourceLimits == ["('NumberOfFiles', 512)"] + assert results[2].EnvironmentVariables == [] + assert results[2].NoEnvironmentVariables == [] + assert results[2].NO_EnvironmentVariables == [] + assert results[2].SHAuthorizationRight is None + assert results[2].PublishesEvents is None + assert results[2].LimitLoadToVariant is None + assert results[2].RunLoopType is None + assert results[2].RemoteServices == [] + assert results[2].JetsamProperties == [] + assert results[2].LimitLoadToHardware == [] + assert results[2].PanicOnCrash == [] + assert results[2].LimitLoadToBootMode == [] + assert results[2].Cryptex is None + assert results[2].ServiceType is None + assert results[2].ServiceIPC is None + assert results[2].UrgentLogSubmission == [] + assert results[2].AppIntents == [] + assert results[2].BinaryOrderPreference == [] + assert results[2].LimitLoadFromHardware == [] + assert results[2].StartCalendarInterval == [] + assert results[2].AdditionalProperties == [] + assert results[2].NSAppTransportSecurity == [] assert results[2].source == "/Users/user/Library/LaunchDaemons/com.apple.cfprefsd.xpc.daemon.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py new file mode 100644 index 0000000000..5dba9621f7 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.login_items import LoginItemsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ("BackgroundItems-v16.btm",), + ("/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm",), + ), + ], +) +def test_login_items( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [user] + + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(LoginItemsPlugin) + + results = list(target_unix.login_items()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + + assert len(results) == 4 + + assert results[0].domain is None + assert results[0].generation == 2 + assert results[0].serviceManagementLoginItemsMigrated + assert results[0].launchServicesItemsImported + assert results[0].backgroundAppRefreshLoadCount == 4 + assert results[0].plist_path == ("userSettingsByUserIdentifier/8122F0CD-020B-4E0C-A3AD-2FCB201C9BB0") + assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + + assert results[1].domain is None + assert results[1].generation == 0 + assert not results[1].serviceManagementLoginItemsMigrated + assert not results[1].launchServicesItemsImported + assert results[1].backgroundAppRefreshLoadCount == 0 + assert results[1].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000") + assert results[1].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + + assert results[2].domain is None + assert results[2].generation == 1 + assert results[2].serviceManagementLoginItemsMigrated + assert not results[2].launchServicesItemsImported + assert results[2].backgroundAppRefreshLoadCount == 2 + assert results[2].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA000000F8") + assert results[2].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + + assert results[3].domain is None + assert results[3].generation == 1 + assert results[3].serviceManagementLoginItemsMigrated + assert not results[3].launchServicesItemsImported + assert results[3].backgroundAppRefreshLoadCount == 2 + assert results[3].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE") + assert results[3].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py new file mode 100644 index 0000000000..37139c4d68 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.airport_preferences import AirportPreferencesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.airport.preferences.plist", + ], +) +def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Preferences/SystemConfiguration/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Preferences/SystemConfiguration/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(AirportPreferencesPlugin) + + results = list(target_unix.airport_preferences()) + assert len(results) == 1 + + assert results[0].counter == 2 + assert results[0].device_uuid == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" + assert results[0].version == 7200 + assert results[0].preferred_order == "[]" + assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py new file mode 100644 index 0000000000..c4170fad39 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.code_signature_coderesources import ( + CodeSignatureCodeResourcesPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "Host", + "FileProvider", + "Ethernet", + ), + ( + "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/CoreServices/FileProvider-Feedback.app/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources", + ), + ) + ], +) +def test_code_signature_coderesources( + names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/code_signature/{name}") + fs_unix.map_file(f"{path}", data_file) + entry = fs_unix.get(f"{path}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(CodeSignatureCodeResourcesPlugin) + + results = list(target_unix.code_signature_coderesources()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + + assert len(results) == 11 + + assert results[0].omit + assert results[0].weight == "20.0" + assert results[0].plist_path == "rules/^.*" + assert ( + results[0].source + == "/System/Library/CoreServices/FileProvider-Feedback.app/Contents/_CodeSignature/CodeResources" + ) + + assert results[3].cdhash == "\x18\udc8f\x03\x1f\udcb2f(\udcafD.F\udcdbK\udcc05\udc91B\x1f\x06\udca9" + assert results[3].requirement == 'identifier "com.apple.AppleUSBEthernet_kasan" and anchor apple' + assert results[3].plist_path == "files2/MacOS/AppleUSBEthernet_kasan" + assert ( + results[3].source + == "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[7].cdhash == "B\udcd5uȈ\udcf1K\x03qd\udce6\udcf2T4L\udc95\udcc4\udce0\x06\udc9f" + assert results[7].requirement == 'identifier "com.apple.AppleUSBHostS5L8930X_kasan" and anchor apple' + assert results[7].plist_path == "files2/MacOS/AppleUSBHostS5L8930X_kasan" + assert ( + results[7].source + == "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources" + ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py new file mode 100644 index 0000000000..2ecfbb9fa8 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.contents_info import ContentsInfoPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "AppleEventLogHandler.plist", + "UnmountAssistantAgent.plist", + "AppleHPET.plist", + ), + ( + "/System/Library/Extensions/AppleEventLogHandler.kext/Contents/Info.plist", + "/System/Library/CoreServices/UnmountAssistantAgent.app/Contents/Info.plist", + "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist", + ), + ) + ], +) +def test_contents_info( + names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_info/{name}") + fs_unix.map_file(f"{path}", data_file) + entry = fs_unix.get(f"{path}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(ContentsInfoPlugin) + + results = list(target_unix.contents_info()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + + assert len(results) == 7 + + assert results[0].BuildMachineOSBuild == "23A344017" + assert results[0].CFBundleDevelopmentRegion == "English" + assert results[0].CFBundleExecutable == "UnmountAssistantAgent" + assert results[0].CFBundleIdentifier == "com.apple.UnmountAssistantAgent" + assert results[0].CFBundleInfoDictionaryVersion == "6.0" + assert results[0].CFBundleName == "UnmountAssistantAgent" + assert results[0].CFBundlePackageType == "APPL" + assert results[0].CFBundleShortVersionString == "5.0" + assert results[0].CFBundleSignature == "????" + assert results[0].CFBundleSupportedPlatforms == "['MacOSX']" + assert results[0].CFBundleVersion == "5.0" + assert results[0].DTCompiler == "com.apple.compilers.llvm.clang.1_0" + assert results[0].DTPlatformBuild == "" + assert results[0].DTPlatformName == "macosx" + assert results[0].DTPlatformVersion == "26.4" + assert results[0].DTSDKBuild == "25E222" + assert results[0].DTSDKName == "macosx26.4.internal" + assert results[0].DTXcode == "2630" + assert results[0].DTXcodeBuild == "17E6107" + assert results[0].LSMinimumSystemVersion == "26.4" + assert results[0].LSUIElement + assert results[0].NSPrincipalClass == "NSApplication" + assert results[0].source == "/System/Library/CoreServices/UnmountAssistantAgent.app/Contents/Info.plist" + + assert results[4].BuildMachineOSBuild == "23A344017" + assert results[4].CFBundleDevelopmentRegion == "English" + assert results[4].CFBundleExecutable == "AppleHPET" + assert results[4].CFBundleIdentifier == "com.apple.driver.AppleHPET" + assert results[4].CFBundleInfoDictionaryVersion == "6.0" + assert results[4].CFBundleName == "High Precision Event Timer Driver" + assert results[4].CFBundlePackageType == "KEXT" + assert results[4].CFBundleShortVersionString == "1.8" + assert results[4].CFBundleSignature == "????" + assert results[4].CFBundleSupportedPlatforms == "['MacOSX']" + assert results[4].CFBundleVersion == "1.8" + assert results[4].DTCompiler == "com.apple.compilers.llvm.clang.1_0" + assert results[4].DTPlatformBuild == "25E245" + assert results[4].DTPlatformName == "macosx" + assert results[4].DTPlatformVersion == "26.4" + assert results[4].DTSDKBuild == "25E245" + assert results[4].DTSDKName == "macosx26.4.internal" + assert results[4].DTXcode == "2630" + assert results[4].DTXcodeBuild == "17E6107" + assert results[4].LSMinimumSystemVersion == "26.4" + assert results[4].NSHumanReadableCopyright == ("Copyright © 2005-2012 Apple Inc. All rights reserved.") + assert results[4].OSBundleRequired == "Root" + assert results[4].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" + + assert results[6].com_apple_iokit_IOACPIFamily == "1.1.0" + assert results[6].com_apple_kpi_iokit == "9.0.0" + assert results[6].com_apple_kpi_libkern == "9.0.0" + assert results[6].com_apple_kpi_mach == "9.0.0" + assert results[6].com_apple_kpi_unsupported == "9.0.0" + assert results[6].plist_path == "OSBundleLibraries" + assert results[6].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py new file mode 100644 index 0000000000..58e9e45814 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.contents_version import ( + MacOSContentsVersionPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "UniversalControl.plist", + "IOBluetoothUI.plist", + "SwiftUICore.plist", + ), + ( + "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist", + "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist", + "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist", + ), + ) + ], +) +def test_contents_version( + names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_version/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(MacOSContentsVersionPlugin) + + results = list(target_unix.contents_version()) + results.sort(key=lambda r: r.source) + + assert len(results) == 3 + + assert results[0].BuildAliasOf == "Ensemble" + assert results[0].BuildVersion == "1983" + assert results[0].CFBundleShortVersionString == "1.0" + assert results[0].CFBundleVersion == "174.4.1" + assert results[0].ProjectName == "Ensemble_executables" + assert results[0].SourceVersion == "174004001000000" + assert results[0].source == "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist" + + assert results[1].BuildVersion == "100" + assert results[1].CFBundleShortVersionString == "1.0" + assert results[1].CFBundleVersion == "1" + assert results[1].ProjectName == "MobileBluetooth" + assert results[1].SourceVersion == "194026001000001" + assert ( + results[1].source == "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist" + ) + + assert results[2].BuildAliasOf == "SwiftUI" + assert results[2].BuildVersion == "12" + assert results[2].CFBundleShortVersionString == "7.4.27" + assert results[2].CFBundleVersion == "7.4.27" + assert results[2].ProjectName == "SwiftUICore" + assert results[2].SourceVersion == "7004027000000" + assert ( + results[2].source == "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist" + ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py new file mode 100644 index 0000000000..51f94a22c8 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.global_user_preferences import GlobalUserPreferencesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "user.plist", + "securityagent.plist", + "root.plist", + ), + ( + "/Users/user/Library/Preferences/.GlobalPreferences.plist", + "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist", + "/private/var/root/Library/Preferences/.GlobalPreferences.plist", + ), + ), + ], +) +def test_global_user_preferences( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + securityagent = UnixUserRecord( + name="securityagent", + uid=92, + gid=92, + home="/private/var/db/securityagent", + shell="/usr/bin/false", + ) + root = UnixUserRecord( + name="root", + uid=0, + gid=0, + home="/private/var/root", + shell="/bin/sh", + ) + target_unix.users = lambda: [ + user, + securityagent, + root, + ] + + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(GlobalUserPreferencesPlugin) + + results = list(target_unix.global_user_preferences()) + results.sort(key=lambda r: r.source) + + assert len(results) == 3 + + assert results[0].AKLastLocale == "en_US@rg=nlzzzz" + assert results[0].com_apple_sound_beep_flash == 0 + assert results[0].NSLinguisticDataAssetsRequested == "['en', 'en_US', 'nl', 'nl_NL']" + assert not results[0].AppleMiniaturizeOnDoubleClick + assert results[0].NSAutomaticPeriodSubstitutionEnabled + assert results[0].NSSpellCheckerDictionaryContainerTransitionComplete + assert results[0].com_apple_springing_delay == "0.5" + assert results[0].ACDMonthlyAnalyticsLastPosted == "796140692.59226" + assert results[0].AKLastIDMSEnvironment == 0 + assert results[0].NSAutomaticCapitalizationEnabled + assert results[0].NSLinguisticDataAssetsRequestedByChecker == "[]" + assert results[0].NSUserDictionaryReplacementItems == ("[{'replace': 'omw', 'on': 1, 'with': 'On my way!'}]") + assert results[0].NSLinguisticDataAssetsRequestTime == "2026-03-25 14:12:53.295950" + assert results[0].AppleAntiAliasingThreshold == 4 + assert results[0].com_apple_springing_enabled + assert results[0].AppleLanguages == "['en-US', 'nl-NL']" + assert results[0].AppleLocale == "en_US@rg=nlzzzz" + assert results[0].com_apple_trackpad_forceClick + assert results[0].NSLinguisticDataAssetsRequestLastInterval == "86400.0" + assert results[0].AppleLanguagesSchemaVersion == 5400 + assert results[0].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" + + assert results[1].AppleKeyboardUIMode == 2 + assert results[1].source == ("/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist") + + assert results[2].AppleLocale == "en_US" + assert results[2].AppleKeyboardUIMode == 3 + assert results[2].com_apple_sound_beep_flash == 0 + assert results[2].source == ("/private/var/root/Library/Preferences/.GlobalPreferences.plist") diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py new file mode 100644 index 0000000000..b6fbf6dccd --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.installation_history import InstallationHistoryPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "InstallHistory.plist", + ], +) +def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Receipts/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Receipts/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(InstallationHistoryPlugin) + + results = list(target_unix.installation_history()) + assert len(results) == 1 + + assert results[0].date == datetime(2026, 3, 25, 14, 7, 11, tzinfo=tz) + assert results[0].display_name == "macOS 26.4" + assert results[0].display_version == "26.4" + assert results[0].process_name == "softwareupdated" + assert results[0].source == "/Library/Receipts/InstallHistory.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py new file mode 100644 index 0000000000..6ae9dc9b78 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.keyboard_layout import KeyboardLayoutPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.HIToolbox.plist", + ], +) +def test_keyboard_layout(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Preferences/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(KeyboardLayoutPlugin) + + results = list(target_unix.keyboard_layout()) + assert len(results) == 2 + + assert results[0].input_source_kind == "Keyboard Layout" + assert results[0].keyboard_layout_name == "U.S." + assert results[0].keyboard_layout_id == 0 + assert results[0].enabled + assert results[0].selected + assert not results[0].current + assert results[0].source == "/Library/Preferences/com.apple.HIToolbox.plist" + + assert results[1].input_source_kind == "Keyboard Layout" + assert results[1].keyboard_layout_name == "Dutch" + assert results[1].keyboard_layout_id == 26 + assert results[1].enabled + assert not results[1].selected + assert not results[1].current + assert results[1].source == "/Library/Preferences/com.apple.HIToolbox.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py index 43291383c3..2773a31cff 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py @@ -20,10 +20,15 @@ ("names", "paths"), [ ( - (".GlobalPreferences.plist", ".AppleSetupDone"), + ( + ".GlobalPreferences.plist", + ".AppleSetupDone", + "com.apple.timezone.auto.plist", + ), ( "/Library/Preferences/.GlobalPreferences.plist", "/private/var/db/.AppleSetupDone", + "/Library/Preferences/com.apple.timezone.auto.plist", ), ), ], @@ -59,6 +64,7 @@ def test_locale( assert target_unix.timezone == "Europe/Amsterdam" assert target_unix.language == ["en_US", "nl_NL"] assert target_unix.install_date == datetime.fromtimestamp(apple_setup_time, tz=tz) + assert target_unix.location_services_active finally: target_unix.fs = None diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py new file mode 100644 index 0000000000..9e7ca11c24 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.login_window import LoginWindowPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "loginwindow.plist", + "com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist", + ), + ( + "/Users/user/Library/Preferences/loginwindow.plist", + "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist", + ), + ), + ], +) +def test_login_window( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [user] + + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/login_window/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(LoginWindowPlugin) + + results = list(target_unix.login_window()) + results.sort(key=lambda r: r.source) + + assert len(results) == 2 + + assert not results[0].MiniBuddyLaunch + assert ( + results[0].source + == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist" + ) + + assert results[-1].BuildVersionStampAsNumber == 52698816 + assert results[-1].BuildVersionStampAsString == "25E246" + assert results[-1].SystemVersionStampAsNumber == 436469760 + assert results[-1].SystemVersionStampAsString == "26.4" + assert results[-1].source == "/Users/user/Library/Preferences/loginwindow.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py new file mode 100644 index 0000000000..2bad8353cb --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import MacOSResourcesInfoStringsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ("InfoPlist.strings",), + ("/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings",), + ) + ], +) +def test_resources_info_strings( + names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/{name}") + fs_unix.map_file(f"{path}", data_file) + entry = fs_unix.get(f"{path}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(MacOSResourcesInfoStringsPlugin) + + results = list(target_unix.resources_info_strings()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + assert len(results) == 1 + + assert results[0].NSHumanReadableCopyright == "Copyright © 2004 Apple Inc. All rights reserved." + assert results[0].source == "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py new file mode 100644 index 0000000000..40255f4271 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py @@ -0,0 +1 @@ +# TODO write tests for MacOSResourcesLocalizableStringsPlugin diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py new file mode 100644 index 0000000000..52cd236dd8 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.software_update_preferences import SoftwareUpdatePreferencesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.SoftwareUpdate.plist", + ], +) +def test_software_update_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Preferences/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(SoftwareUpdatePreferencesPlugin) + + results = list(target_unix.software_update_preferences()) + assert len(results) == 1 + + assert results[0].last_result_code == 2 + assert results[0].last_attempt_system_version == "26.4 (25E246)" + assert results[0].last_attempt_build_version == "26.4 (25E246)" + assert results[0].automatic_download + assert results[0].automatically_install_macos_updates + assert results[0].critical_update_install + assert results[0].config_data_install + assert results[0].recommended_updates == [] + assert results[0].splat_enabled + assert not results[0].post_logout_notification + assert results[0].last_recommended_major_os_bundle_id == "" + assert results[0].primary_languages == ["en", "en-US"] + assert results[0].last_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) + assert results[0].last_full_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) + assert results[0].source == "/Library/Preferences/com.apple.SoftwareUpdate.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py new file mode 100644 index 0000000000..5335f6d4d5 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.system_preferences import SystemPreferencesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.airport.preferences.plist", + ], +) +def test_system_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Preferences/SystemConfiguration/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Preferences/SystemConfiguration/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(SystemPreferencesPlugin) + + results = list(target_unix.system_preferences()) + assert len(results) == 1 + + assert results[0].Counter == 2 + assert results[0].DeviceUUID == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" + assert results[0].Version == 7200 + assert results[0].PreferredOrder == "[]" + assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py new file mode 100644 index 0000000000..23f00fbfae --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.time_machine import TimeMachinePlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.TimeMachine.plist", + ], +) +def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) + entry = fs_unix.get(f"/Library/Preferences/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(TimeMachinePlugin) + + results = list(target_unix.time_machine()) + assert len(results) == 1 + + assert results[0].preferences_version == 6 + assert results[0].source == "/Library/Preferences/com.apple.TimeMachine.plist" From 57debefa9a444e09030a44f2c5d9b9e33b246f25 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:19:39 +0200 Subject: [PATCH 09/52] Move parse_timestamp function to helper file and remove DynamicDescriptor where not used --- .../macos/code_signature_coderesources.py | 6 +++--- .../unix/bsd/darwin/macos/contents_version.py | 6 +++--- .../bsd/darwin/macos/global_user_preferences.py | 2 +- .../macos/helpers/{userdirs.py => general.py} | 12 ++++++++++++ .../os/unix/bsd/darwin/macos/login_window.py | 2 +- .../os/unix/bsd/darwin/macos/logs/installlog.py | 17 +---------------- .../os/unix/bsd/darwin/macos/logs/systemlog.py | 7 +------ .../bsd/darwin/macos/persistence/launchers.py | 12 ++++++------ .../bsd/darwin/macos/persistence/login_items.py | 8 ++++---- 9 files changed, 32 insertions(+), 40 deletions(-) rename dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/{userdirs.py => general.py} (74%) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py index 246bb9ee4f..c9679f2ff9 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records @@ -83,8 +83,8 @@ def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No code signature coderesources files found") - @export(record=DynamicDescriptor(["string"])) - def code_signature_coderesources(self) -> Iterator[DynamicDescriptor]: + @export(record=CodeSignatureCodeResourcesRecords) + def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]: """Yield code signature coderesources information.""" yield from build_records( self, "macos/code_signature_coderesources", self.files, CodeSignatureCodeResourcesRecords diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index 3ae0903064..7a4b452c3d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records @@ -66,7 +66,7 @@ def check_compatible(self) -> None: if not self.files: raise UnsupportedPluginError("No contents version.plist files found") - @export(record=DynamicDescriptor(["string"])) - def contents_version(self) -> Iterator[DynamicDescriptor]: + @export(record=ContentsVersionRecord) + def contents_version(self) -> Iterator[ContentsVersionRecord]: """Yield contents version.plist information.""" yield from build_records(self, "macos/contents_version", self.files, ContentsVersionRecords) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py index da4c241edf..4776170b9f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py similarity index 74% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py index f56a1b18f1..568d7fbc55 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/userdirs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py @@ -1,8 +1,10 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING if TYPE_CHECKING: + import re from pathlib import Path from dissect.target.plugin import Plugin @@ -27,3 +29,13 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta if cur_dir.exists(): users_dirs.add((user_details, cur_dir)) return users_dirs + + +def parse_timestamp(timestamp: re.Match) -> datetime: + ts = None + try: + ts = datetime.fromisoformat(timestamp.group()) + except ValueError: + ts = datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) + + return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index 3d5c4fc6df..ae9f680245 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py index 8da0a2d82d..a6e199e282 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py @@ -1,12 +1,12 @@ from __future__ import annotations import re -from datetime import datetime, timezone from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import parse_timestamp if TYPE_CHECKING: from collections.abc import Iterator @@ -104,18 +104,3 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: message=message.strip(), _target=self.target, ) - - -def parse_timestamp(timestamp: re.Match) -> datetime: - # I could not find docs about this but it seems to be the case that when you - # start installing your macbook, it outputs the timestamp in this BSD style - # format without any timezone info. After some messages it starts outputting - # ISO timestamps with timezone info. From those timestamps I kind of inferred - # that this is actually just UTC. - ts = None - try: - ts = datetime.fromisoformat(timestamp.group()) - except ValueError: - ts = datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) - - return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py index d9adaf6dc4..694f8c32c8 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py @@ -2,16 +2,15 @@ import gzip import re -from datetime import datetime, timezone from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import parse_timestamp if TYPE_CHECKING: from collections.abc import Iterator - from datetime import tzinfo from dissect.target.target import Target @@ -108,7 +107,3 @@ def systemlog(self) -> Iterator[macOSSystemLogRecord]: ) logfile.close() - - -def parse_timestamp(timestamp: re.Match, tzinfo: tzinfo = timezone.utc) -> datetime: - return datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=tzinfo) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index 791c7d60bb..2e410b3bdc 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator @@ -318,16 +318,16 @@ def _find_files(self) -> None: for _, path in _build_userdirs(self, self.USER_LAUNCH_DAEMON_PATHS): self.launch_daemon_files.add(path) - @export(record=DynamicDescriptor(["string"])) + @export(record=LaunchAgentRecords) # @export(output="yield") - def launch_agents(self) -> Iterator[DynamicDescriptor]: + def launch_agents(self) -> Iterator[LaunchAgentRecords]: """Yield macOS launch agent plist files.""" yield from build_records( self, "macos/launch_agents", self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS ) - @export(record=DynamicDescriptor(["string"])) - def launch_daemons(self) -> Iterator[DynamicDescriptor]: + @export(record=LaunchDaemonRecords) + def launch_daemons(self) -> Iterator[LaunchDaemonRecords]: """Yield macOS launch daemon plist files.""" yield from build_records( self, "macos/launch_daemons", self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py index c8ccaebea1..9c60b92d7a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.userdirs import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator @@ -61,8 +61,8 @@ def _find_files(self) -> None: for _, path in _build_userdirs(self, self.USER_LOGIN_ITEMS_PATHS): self.login_items_files.add(path) - @export(record=DynamicDescriptor(["string"])) + @export(record=LoginItemsRecord) # @export(output="yield") - def login_items(self) -> Iterator[DynamicDescriptor]: + def login_items(self) -> Iterator[LoginItemsRecord]: """Yield macOS login items plist files.""" yield from build_records(self, "macos/login_items", self.login_items_files, LoginItemsRecords) From f2fc33d7e15ad62b0113dbae342b2f0053b7eed0 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:25:01 +0200 Subject: [PATCH 10/52] add macOS SQLite parsers --- .../bsd/darwin/macos/airport_preferences.py | 2 +- .../bsd/darwin/macos/authorization_rules.py | 107 ++++ .../macos/code_signature_coderesources.py | 28 +- .../os/unix/bsd/darwin/macos/contents_info.py | 4 +- .../unix/bsd/darwin/macos/contents_version.py | 28 +- .../macos/directory_services_local_nodes.py | 111 ++++ .../darwin/macos/duet_activity_scheduler.py | 150 +++++ .../os/unix/bsd/darwin/macos/gkopaque.py | 76 +++ .../darwin/macos/global_user_preferences.py | 4 +- .../bsd/darwin/macos/helpers/build_records.py | 527 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/helpers/plist.py | 251 --------- .../bsd/darwin/macos/identity_services.py | 71 +++ .../unix/bsd/darwin/macos/keyboard_layout.py | 14 +- .../os/unix/bsd/darwin/macos/keychain.py | 133 +++++ .../os/unix/bsd/darwin/macos/locale.py | 13 +- .../os/unix/bsd/darwin/macos/login_window.py | 4 +- .../bsd/darwin/macos/persistence/launchers.py | 24 +- .../darwin/macos/persistence/login_items.py | 16 +- .../darwin/macos/resources_info_strings.py | 4 +- .../macos/resources_localizable_strings.py | 4 +- .../bsd/darwin/macos/system_preferences.py | 4 +- .../plugins/os/unix/bsd/darwin/macos/tcc.py | 85 +++ .../bsd/darwin/macos/text_replacements.py | 170 ++++++ .../os/unix/bsd/darwin/macos/user_accounts.py | 350 ++++++++++++ .../plugins/os/unix/bsd/darwin/macos/auth.db | 3 + .../directory_services_local_nodes/sqlindex | 3 + .../sqlindex-wal | 0 .../DuetActivitySchedulerClassC.db | 3 + .../DuetActivitySchedulerClassC.db-wal | 3 + .../os/unix/bsd/darwin/macos/gkopaque.db | 3 + .../plugins/os/unix/bsd/darwin/macos/ids.db | 3 + .../os/unix/bsd/darwin/macos/keychain-2.db | 3 + .../os/unix/bsd/darwin/macos/tcc/system.db | 3 + .../os/unix/bsd/darwin/macos/tcc/user.db | 3 + .../text_replacements/TextReplacements.db | 3 + .../text_replacements/TextReplacements.db-wal | 3 + .../macos/user_accounts/Accounts4.sqlite | 3 + .../macos/user_accounts/Accounts4.sqlite-wal | 0 .../macos/persistence/test_login_items.py | 28 +- .../darwin/macos/test_airport_preferences.py | 2 +- .../darwin/macos/test_authorization_rules.py | 104 ++++ .../test_code_signature_coderesources.py | 2 +- .../bsd/darwin/macos/test_contents_version.py | 34 +- .../test_directory_services_local_nodes.py | 60 ++ .../macos/test_duet_activity_scheduler.py | 67 +++ .../os/unix/bsd/darwin/macos/test_gkopaque.py | 53 ++ .../macos/test_global_user_preferences.py | 4 +- .../darwin/macos/test_identity_services.py | 58 ++ .../bsd/darwin/macos/test_keyboard_layout.py | 12 +- .../os/unix/bsd/darwin/macos/test_keychain.py | 101 ++++ .../os/unix/bsd/darwin/macos/test_tcc.py | 98 ++++ .../darwin/macos/test_text_replacements.py | 112 ++++ .../bsd/darwin/macos/test_user_accounts.py | 106 ++++ 53 files changed, 2691 insertions(+), 366 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/auth.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/gkopaque.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/ids.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/keychain-2.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/system.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/user.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite-wal create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index 641dea7069..ffdd5ce04f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -17,7 +17,7 @@ [ ("varint", "counter"), ("string", "device_uuid"), - ("varint", "version"), + ("string", "version"), ("string", "preferred_order"), ("path", "source"), ], diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py new file mode 100644 index 0000000000..4a0e7e88b1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +AuthorizationRulesRecord = TargetRecordDescriptor( + "macos/authorization_rules", + [ + ("string[]", "tables"), + ("varint", "rules_id"), + ("string", "rules_name"), + ("varint", "rules_type"), + ("varint", "rules_class"), + ("string", "rules_group"), + ("varint", "rules_kofn"), + ("varint", "rules_timeout"), + ("varint", "rules_flags"), + ("string", "rules_tries"), + ("varint", "rules_version"), + ("datetime", "rules_created"), + ("datetime", "rules_modified"), + ("string", "rules_hash"), + ("string", "rules_identifier"), + ("string", "rules_requirement"), + ("string", "rules_comment"), + ("string[]", "rules_delegates_map"), + ("datetime", "rules_history_timestamp"), + ("string", "rules_history_source"), + ("varint", "rules_history_operation"), + ("varint", "mechanisms_map_m_id"), + ("varint", "mechanisms_map_ord"), + ("string", "mechanisms_plugin"), + ("string", "mechanisms_param"), + ("varint", "mechanisms_privileged"), + ("path", "source"), + ], +) + +ConfigTableRecord = TargetRecordDescriptor( + "macos/authorization_rules/config", + [ + ("string", "table"), + ("string", "key"), + ("string", "value"), + ("path", "source"), + ], +) + +SQLiteSequenceTableRecord = TargetRecordDescriptor( + "macos/authorization_rules/sqlite_sequence", + [ + ("string", "table"), + ("string", "name"), + ("varint", "seq"), + ("path", "source"), + ], +) + +AuthorizationRulesRecords = ( + AuthorizationRulesRecord, + ConfigTableRecord, + SQLiteSequenceTableRecord, +) + +joins = ( + {"table1": "rules", "key1": "name", "table2": "rules_history", "key2": "rule", "join": "iterate"}, + {"table1": "rules", "key1": "version", "table2": "rules_history", "key2": "version", "join": "ignore"}, + {"table1": "rules", "key1": "id", "table2": "mechanisms_map", "key2": "r_id", "join": "iterate"}, + {"table1": "mechanisms_map", "key1": "m_id", "table2": "mechanisms", "key2": "id", "join": "iterate"}, + {"table1": "rules", "key1": "id", "table2": "delegates_map", "key2": "r_id", "join": "nested"}, +) + + +class AuthorizationRulesPlugin(Plugin): + """macOS authorization rules plugin.""" + + PATH = "/var/db/auth.db" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No auth.db file found") + + @export(record=AuthorizationRulesRecords) + def authorization_rules(self) -> Iterator[AuthorizationRulesRecords]: + """Yield authorization rules information.""" + yield from build_sqlite_records(self, (self.file,), AuthorizationRulesRecords, joins) + + # Still missing prompts & buttons tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py index c9679f2ff9..2a3e2ed82c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -15,28 +15,28 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -CodeSignatureCodeResourcesRecord1 = TargetRecordDescriptor( - "macos/code_signature_coderesources", +OmitRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/omit", [ ("boolean", "omit"), - ("string", "weight"), + ("varint", "weight"), ("string", "plist_path"), ("path", "source"), ], ) -CodeSignatureCodeResourcesRecord2 = TargetRecordDescriptor( - "macos/code_signature_coderesources", +NestedRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/nested", [ ("boolean", "nested"), - ("string", "weight"), + ("varint", "weight"), ("string", "plist_path"), ("path", "source"), ], ) -CodeSignatureCodeResourcesRecord3 = TargetRecordDescriptor( - "macos/code_signature_coderesources", +CDHashRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/cdhash", [ ("string", "cdhash"), ("string", "requirement"), @@ -47,9 +47,9 @@ CodeSignatureCodeResourcesRecords = ( - CodeSignatureCodeResourcesRecord1, - CodeSignatureCodeResourcesRecord2, - CodeSignatureCodeResourcesRecord3, + OmitRecord, + NestedRecord, + CDHashRecord, ) @@ -86,6 +86,4 @@ def check_compatible(self) -> None: @export(record=CodeSignatureCodeResourcesRecords) def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]: """Yield code signature coderesources information.""" - yield from build_records( - self, "macos/code_signature_coderesources", self.files, CodeSignatureCodeResourcesRecords - ) + yield from build_plist_records(self, self.files, CodeSignatureCodeResourcesRecords) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py index 0a3e2d3238..71fcd277b3 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -52,4 +52,4 @@ def check_compatible(self) -> None: @export(record=DynamicDescriptor(["string"])) def contents_info(self) -> Iterator[DynamicDescriptor]: """Yield contents info information.""" - yield from build_records(self, "macos/contents_info", self.files) + yield from build_plist_records(self, self.files, function_name="macos/contents_info") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index 7a4b452c3d..1825b630ff 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -18,13 +18,13 @@ ContentsVersionRecord = TargetRecordDescriptor( "macos/contents_version", [ - ("string", "BuildAliasOf"), - ("string", "RelevancePlatform"), - ("string", "BuildVersion"), - ("string", "CFBundleShortVersionString"), - ("string", "CFBundleVersion"), - ("string", "ProjectName"), - ("string", "SourceVersion"), + ("string", "build_alias_of"), + ("string", "relevance_platform"), + ("string", "build_version"), + ("string", "cf_bundle_short_version_string"), + ("string", "cf_bundle_version"), + ("string", "project_name"), + ("string", "source_version"), ("path", "source"), ], ) @@ -32,6 +32,16 @@ ContentsVersionRecords = (ContentsVersionRecord,) +FIELD_MAPPINGS = { + "BuildAliasOf": "build_alias_of", + "RelevancePlatform": "relevance_platform", + "BuildVersion": "build_version", + "CFBundleShortVersionString": "cf_bundle_short_version_string", + "CFBundleVersion": "cf_bundle_version", + "ProjectName": "project_name", + "SourceVersion": "source_version", +} + class MacOSContentsVersionPlugin(Plugin): """macOS Contents version.plist file.""" @@ -69,4 +79,4 @@ def check_compatible(self) -> None: @export(record=ContentsVersionRecord) def contents_version(self) -> Iterator[ContentsVersionRecord]: """Yield contents version.plist information.""" - yield from build_records(self, "macos/contents_version", self.files, ContentsVersionRecords) + yield from build_plist_records(self, self.files, ContentsVersionRecords, field_mappings=FIELD_MAPPINGS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py new file mode 100644 index 0000000000..89c283f3fa --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.database.sqlite3 import SQLite3 + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +DirectoryServicesLocalNodesRecord = TargetRecordDescriptor( + "macos/directory_services_local_nodes", + [ + ("string[]", "tables"), + ("datetime", "filetime"), + ("string", "filename"), + ("string", "recordtype"), + ("string", "value"), + ("path", "source"), + ], +) + + +class DirectoryServicesLocalNodesPlugin(Plugin): + """macOS directory services local nodes plugin.""" + + PATH = "/var/db/dslocal/nodes/Default/sqlindex" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No sqlindex file found") + + @export(record=DirectoryServicesLocalNodesRecord) + def directory_services_local_nodes( + self, + ) -> Iterator[DirectoryServicesLocalNodesRecord]: + """Yield directory services local nodes information.""" + with SQLite3(self.file) as database: + NAME_TABLES = { + "name", + "realname", + "generateduid", + "uid", + "gid", + "ip_address", + "ipv6_address", + "smb_sid", + "smb_rid", + "groupmembers", + "users", + "nestedgroups", + } + + r_rows: list[tuple[str, object]] = [] + n_tables = set() + + for table in database.tables(): + if table.name.startswith("rec:"): + r_rows.extend((table.name, r_row) for r_row in table.rows()) + elif table.name in NAME_TABLES: + n_tables.add(table) + + for table in n_tables: + rec_rows = list(r_rows) + + for n_row in table.rows(): + matched = False + + for idx, (table_name, r_row) in enumerate(rec_rows): + if r_row.filename == n_row.filename: + yield DirectoryServicesLocalNodesRecord( + tables=[table.name, table_name], + filetime=r_row.filetime, + filename=n_row.filename, + recordtype=n_row.recordtype, + value=n_row.value, + source=self.file, + _target=self.target, + ) + + del rec_rows[idx] + matched = True + break + + if not matched: + yield DirectoryServicesLocalNodesRecord( + tables=[table.name], + filetime=None, + filename=n_row.filename, + recordtype=n_row.recordtype, + value=n_row.value, + source=self.file, + _target=self.target, + ) + + # Still missing altsecurityidentities, hardwareuuid, en_address, mail, member tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py new file mode 100644 index 0000000000..e2ea30e41b --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZGroupRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/z_group", + [ + ("string", "table"), + ("varint", "z_max_concurrent"), + ("string", "z_name"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_pk"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/z_plist", + [ + ("varint", "ns_persistence_maximum_framework_version"), + ("string[]", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("string", "ns_auto_vacuum_level"), + ("string", "ns_store_model_version_hashes_digest"), + ("string", "ns_store_model_version_checksum_key"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_store_model_version_hashes_version"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/ns_store_model_version_hashes", + [ + ("string", "tr_cloud_kit_sync_state"), + ("string", "text_replacement_entry"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +ZModelCacheRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler", + [ + ("string", "table"), + ("string", "z_content"), + ("path", "source"), + ], +) + +DuetActivityRecords = ( + ZPrimaryKeyRecord, + ZGroupRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "ZMAXCONCURRENT": "z_max_concurrent", + "ZNAME": "z_name", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "Z_CONTENT": "z_content", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "TRCloudKitSyncState": "tr_cloud_kit_sync_state", + "TextReplacementEntry": "text_replacement_entry", +} + + +class DuetActivitySchedulerPlugin(Plugin): + """macOS duet activity scheduler plugin.""" + + PATH = "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No DuetActivitySchedulerClassC.db file found") + + @export(record=DuetActivityRecords) + def duet_activity_scheduler( + self, + ) -> Iterator[DuetActivityRecords]: + """Yield duet activity scheduler information.""" + yield from build_sqlite_records( + self, + (self.file,), + DuetActivityRecords, + field_mappings=FIELD_MAPPINGS, + ) + + # Still missing ZACTIVITY, Z_1TRIGGERS, ZTRIGGER, + # Z_PRIMARYKEY, Z_METADATA,Z_MODELCACHE tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py new file mode 100644 index 0000000000..3cc256a401 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +WhitelistRecord = TargetRecordDescriptor( + "macos/gkopaque/whitelist", + [ + ("string", "table"), + ("string", "current"), + ("string", "opaque"), + ("path", "source"), + ], +) + +ConditionsRecord = TargetRecordDescriptor( + "macos/gkopaque/conditions", + [ + ("string", "table"), + ("string", "label"), + ("varint", "weight"), + ("string", "conditions_source"), + ("string", "identifier"), + ("string", "version"), + ("string", "conditions"), + ("path", "source"), + ], +) + + +GatekeeperOpaqueConfigurationRecords = ( + WhitelistRecord, + ConditionsRecord, +) + +FIELD_MAPPINGS = { + "source": "conditions_source", +} + + +class GatekeeperOpaqueConfigurationPlugin(Plugin): + """macOS gatekeeper opaque configuration plugin.""" + + PATH = "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No gkopaque.db file found") + + @export(record=GatekeeperOpaqueConfigurationRecords) + def gkopaque(self) -> Iterator[GatekeeperOpaqueConfigurationRecords]: + """Yield gatekeeper opaque configuration information.""" + yield from build_sqlite_records( + self, (self.file,), GatekeeperOpaqueConfigurationRecords, field_mappings=FIELD_MAPPINGS + ) + + # Still missing merged table diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py index 4776170b9f..2065652f46 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records if TYPE_CHECKING: from collections.abc import Iterator @@ -39,4 +39,4 @@ def _find_files(self) -> None: @export(record=DynamicDescriptor(["string"])) def global_user_preferences(self) -> Iterator[DynamicDescriptor]: """Yield global user preference information.""" - yield from build_records(self, "macos/global_user_preferences", self.files) + yield from build_plist_records(self, self.files, function_name="macos/global_user_preferences") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py new file mode 100644 index 0000000000..583b5ff998 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -0,0 +1,527 @@ +from __future__ import annotations + +import io +import plistlib +import re +import uuid +from collections import defaultdict +from datetime import datetime +from io import BytesIO +from itertools import product +from typing import TYPE_CHECKING, Any, BinaryIO + +from dissect.database.sqlite3 import SQLite3 +from dissect.util.plist import NSDictionary, NSKeyedArchiver + +from dissect.target.helpers.record import TargetRecordDescriptor + +if TYPE_CHECKING: + from collections.abc import Iterator + from pathlib import Path + + from flow.record.base import Record + + from dissect.target.plugin import Plugin + +re_non_identifier = re.compile(r"[^A-Za-z0-9_]") + + +def build_sqlite_records( + plugin: Plugin, + files: set[str], + record_descriptors: tuple | None = None, + joins: tuple = (), + field_mappings: dict | None = None, +) -> Iterator[TargetRecordDescriptor]: + for file in files: + with SQLite3(file) as database: + for table in database.tables(): + for row in table.rows(): + row_dict = {k: v for k, v in row} # noqa C416 + + for key, value in list(row_dict.items()): + if isinstance(value, (bytes, bytearray)) and value.startswith(b"bplist00"): + if is_nskeyedarchive_blob(value): + try: + archiver = NSKeyedArchiver(BytesIO(value)) + decoded_value = archiver.top.get("root") + row_dict[key] = decoded_value + except Exception: + plugin.target.log.exception( + "Failed to decode %s value for key %s", + table.name, + key, + ) + else: + yield from build_record_from_data( + plugin, file, value, record_descriptors, field_mappings + ) + row_dict.pop(key) + + if table.name in {j["table2"] for j in joins}: + for j2 in joins: + if j2["table2"] != table.name: + continue + + if row_dict.get(j2["key2"]) is None: + row_dict["table"] = table.name + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + break + else: + match = False + for row1 in database.table(j2["table1"]).rows(): + row1_dict = {k: v for k, v in row1} # noqa C416 + if row1_dict.get(j2["key1"]) == row_dict.get(j2["key2"]): + match = True + break + + if not match: + row_dict["table"] = table.name + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + break + + elif table.name in {j["table1"] for j in joins}: + tables = set() + iterate_rows = defaultdict(list) + tables.add(table.name) + + for j in joins: + if j["table1"] != table.name: + continue + + ignore_joins = [ + ij + for ij in joins + if ij["table1"] == j["table1"] + and ij["table2"] == j["table2"] + and ij["join"] == "ignore" + ] + + if j["join"] == "iterate": + for expanded in handle_iterate_join(database, row_dict, j, joins, tables): + iterate_rows[j["table2"]].append(expanded) + + elif j["join"] == "nested": + handle_nested_join(database, row_dict, j, ignore_joins, tables) + + if len(iterate_rows) > 0: + row_dict = prefix_row(row_dict, table.name) + keys = list(iterate_rows.keys()) + values = list(iterate_rows.values()) + for key in keys: + tables.add(key) + row_dict["tables"] = tables + + for combination in product(*values): + combined_row = dict(row_dict) + for joined_row in combination: + combined_row.update(joined_row) + + yield build_record( + plugin, + combined_row, + file, + record_descriptors, + ) + else: + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + + else: + row_dict["table"] = table.name + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + + +def prefix_row(row_dict: dict, table: str) -> dict: + return {f"{table}_{k}": v for k, v in row_dict.items()} + + +def handle_iterate_join( + database: SQLite3, + parent_dict: dict, + current_join: dict, + joins: tuple, + tables: set[str], +) -> list[dict]: + results: list[dict] = [] + + ignore_joins = [ + ij + for ij in joins + if ij["table1"] == current_join["table1"] and ij["table2"] == current_join["table2"] and ij["join"] == "ignore" + ] + + for child_row in database.table(current_join["table2"]).rows(): + if child_row.get(current_join["key2"]) != parent_dict.get(current_join["key1"]): + continue + + tables.add(current_join["table2"]) + + child_dict = {k: v for k, v in child_row} # noqa C416 + child_dict.pop(current_join["key2"], None) + + for ij in ignore_joins: + if parent_dict.get(ij["key1"]) == child_dict.get(ij["key2"]): + child_dict.pop(ij["key2"], None) + + downstream_iterate_rows = defaultdict(list) + for dj in joins: + if dj["table1"] != current_join["table2"]: + continue + + downstream_ignore = [ + ij + for ij in joins + if ij["table1"] == dj["table1"] and ij["table2"] == dj["table2"] and ij["join"] == "ignore" + ] + + if dj["join"] == "iterate": + for expanded in handle_iterate_join(database, child_dict, dj, joins, tables): + downstream_iterate_rows[dj["table2"]].append(expanded) + + elif dj["join"] == "nested": + tables.add(dj["table2"]) + handle_nested_join(database, child_dict, dj, downstream_ignore) + + if len(downstream_iterate_rows) > 0: + base_prefixed = prefix_row(child_dict, current_join["table2"]) + values = list(downstream_iterate_rows.values()) + + for combination in product(*values): + combined_row = dict(base_prefixed) + for joined_row in combination: + combined_row.update(joined_row) + results.append(combined_row) + else: + results.append(prefix_row(child_dict, current_join["table2"])) + + return results + + +def handle_nested_join( + database: SQLite3, + row_dict: dict, + current_join: dict, + ignore_joins: list[dict], + tables: set[str], +) -> None: + n_rows = [] + for n_row in database.table(current_join["table2"]).rows(): + if n_row[current_join["key2"]] == row_dict[current_join["key1"]]: + tables.add(current_join["table2"]) + n_dict = {k: v for k, v in n_row} # noqa C416 + n_dict.pop(current_join["key2"]) + for ij in ignore_joins: + if row_dict.get(ij["key1"]) == n_dict.get(ij["key2"]): + n_dict.pop(ij["key2"]) + n_rows.append(n_dict) + row_dict[current_join["table2"]] = n_rows + + +def is_nskeyedarchive_blob(value: (bytes, bytearray)) -> bool: + try: + plist_obj = plistlib.loads(value) + except Exception: + return False + + return ( + isinstance(plist_obj, dict) + and plist_obj.get("$archiver") == "NSKeyedArchiver" + and "$objects" in plist_obj + and "$top" in plist_obj + ) + + +def build_plist_records( + plugin: Plugin, + files: set[str], + record_descriptors: tuple | None = None, + collapse_paths: set[tuple[str, bool]] | None = None, + field_mappings: dict | None = None, + function_name: str | None = None, +) -> Iterator[Record]: + for file in files: + file = plugin.target.fs.path(file) + try: + fh = file.open(mode="rb") + except FileNotFoundError: + plugin.target.log.exception("File not found: %s", file) + continue + + try: + if b"$archiver" in fh.peek(64): + fh.seek(0) + data = load_plist_data(fh) + else: + data = plistlib.load(fh) + + yield from emit_dict_records( + plugin, + data, + file, + record_descriptors=record_descriptors, + collapse_paths=collapse_paths, + field_mappings=field_mappings, + function_name=function_name, + ) + + except Exception: + plugin.target.log.exception("Failed to parse %s", file) + + +def build_record_from_data( + plugin: Plugin, + file: str, + raw_data: bytes, + record_descriptors: tuple | None = None, + field_mappings: dict | None = None, + function_name: str | None = None, +) -> Iterator[Record]: + try: + if not raw_data: + return + + if raw_data.startswith(b"bplist00"): + data = load_plist_data(raw_data) if b"$archiver" in raw_data[:128] else plistlib.loads(raw_data) + + else: + data = plistlib.load(io.BytesIO(raw_data)) + + yield from emit_dict_records( + plugin, + data, + file, + record_descriptors=record_descriptors, + field_mappings=field_mappings, + function_name=function_name, + ) + + except Exception: + plugin.target.log.exception("Failed to parse %s", file) + + +def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source: Path | None) -> Record: + record_fields = sorted(rdict.items()) + + record_values = { + "_target": plugin.target, + "source": source, + } + record_fields = [] + + for k, v in rdict.items(): + k = format_key(k) + + if isinstance(v, bool): + record_fields.append(("boolean", k)) + elif isinstance(v, int): + record_fields.append(("varint", k)) + else: + record_fields.append(("string", k)) + + record_values[k] = v + + record_fields.append(("path", "source")) + + desc = create_event_descriptor(function_name, tuple(record_fields)) + + return desc(**record_values) + + +def select_descriptor( + record_descriptors: tuple, + rdict: dict, + plugin: Plugin, + source: Path | None, +) -> TargetRecordDescriptor | None: + rdict_keys = {format_key(k) for k in rdict} + + selected_record = None + best_match_count = 0 + + for record in record_descriptors: + record_keys = set(record.fields.keys()) + + matched_keys = rdict_keys & record_keys + match_count = len(matched_keys) + + if match_count > best_match_count: + best_match_count = match_count + selected_record = record + + if best_match_count == 0: + return None + + missing_fields = rdict_keys - set(selected_record.fields.keys()) + if missing_fields: + plugin.target.log.warning( + "Source %s contains fields not defined in the selected record descriptor: %s", + source, + sorted(missing_fields), + ) + + return selected_record + + +def build_record( + plugin: Plugin, + rdict: dict, + source: Path | None, + record_descriptors: tuple | None = None, + field_mappings: dict | None = None, +) -> Record: + if field_mappings: + for key in list(rdict): + for src, dst in field_mappings.items(): + if format_key(key) == src: + rdict[dst] = rdict.pop(key) + + desc = select_descriptor(record_descriptors, rdict, plugin, source) + + if desc is None: + plugin.target.log.exception( + "No matching record descriptor for %s with fields %s", + source, + sorted(map(format_key, rdict)), + ) + return None + + allowed_keys = set(desc.fields.keys()) + filtered_rdict = {k: v for k, v in rdict.items() if format_key(k) in allowed_keys} + + record_values = { + "_target": plugin.target, + "source": source, + } + + for k, v in filtered_rdict.items(): + record_values[format_key(k)] = v + + return desc(**record_values) + + +def create_event_descriptor(function_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: + return TargetRecordDescriptor(function_name, record_fields) + + +def format_key(key: str) -> str: + key = re_non_identifier.sub("_", key) + + key = re.sub(r"_+", "_", key) + + key = key.lstrip("_") + + if not key or key[0].isdigit(): + key = f"k_{key}" + + return key + + +UUID_RE = re.compile( + r"^[0-9A-Fa-f]{8}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{12}$" +) + + +def is_collapsed_path(child_path: str, collapse_paths: set[tuple[str, bool]]) -> bool: + for collapse_path, exact in collapse_paths: + if child_path == collapse_path: + return True + if not exact and collapse_path and child_path.startswith(f"{collapse_path}/"): + return True + return False + + +def emit_dict_records( + plugin: Plugin, + node: dict, + source: Path | None, + *, + section: str | None = None, + path: str | None = None, + record_descriptors: tuple | None = None, + collapse_paths: set[tuple[str, bool]] | None = None, + field_mappings: dict | None = None, + function_name: str | None = None, +) -> Iterator[Record]: + if path and path.endswith("$class"): + return + + attributes = {} + child_dicts = {} + + for k, v in node.items(): + child_path = f"{path}/{k}" if path else k + + if collapse_paths and isinstance(v, dict) and is_collapsed_path(child_path, collapse_paths): + attributes[k] = list(v.items()) + continue + + if isinstance(v, dict): + child_dicts[k] = v + else: + attributes[k] = v + + if node and all(isinstance(k, str) and UUID_RE.fullmatch(k) for k in node): + attributes = {} + + if attributes: + record_data = dict(attributes) + + if section is not None: + record_data["section"] = section + if path is not None: + record_data["plist_path"] = path + + if record_descriptors is None: + yield dynamic_build_record(plugin, function_name, record_data, source) + else: + yield build_record(plugin, record_data, source, record_descriptors, field_mappings) + + for k, child in child_dicts.items(): + child_path = f"{path}/{k}" if path else k + yield from emit_dict_records( + plugin, + child, + source, + section=section, + path=child_path, + record_descriptors=record_descriptors, + collapse_paths=collapse_paths, + field_mappings=field_mappings, + function_name=function_name, + ) + + +def normalize_nsobj(obj: Any) -> Any: + """Convert NSKeyedArchiver output to plain Python types.""" + if isinstance(obj, NSDictionary): + return {k: normalize_nsobj(v) for k, v in obj.items()} + + if isinstance(obj, dict): + return {k: normalize_nsobj(v) for k, v in obj.items()} + + if isinstance(obj, list): + return [normalize_nsobj(v) for v in obj] + + if isinstance(obj, (str, int, float, bool)) or obj is None: + return obj + + if isinstance(obj, datetime): + return obj + + if isinstance(obj, uuid.UUID): + return str(obj) + + if hasattr(obj, "keys"): + return {k: normalize_nsobj(obj.get(k)) for k in obj.keys()} # noqa: SIM118 + + return obj + + +def load_plist_data(fh: BinaryIO) -> Any: + ns = NSKeyedArchiver(fh) + root = ns.get("store") + return normalize_nsobj(root) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py deleted file mode 100644 index 2402d274f6..0000000000 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/plist.py +++ /dev/null @@ -1,251 +0,0 @@ -from __future__ import annotations - -import plistlib -import re -import uuid -from datetime import datetime -from typing import TYPE_CHECKING, Any, BinaryIO - -from dissect.util.plist import NSDictionary, NSKeyedArchiver - -from dissect.target.helpers.record import TargetRecordDescriptor - -if TYPE_CHECKING: - from collections.abc import Iterator - from pathlib import Path - - from flow.record.base import Record - - from dissect.target.plugin import Plugin - -re_non_identifier = re.compile(r"[^A-Za-z0-9_]") - - -def build_records( - plugin: Plugin, - record_name: str, - files: set[str], - record_descriptors: tuple | None = None, - collapse_paths: set[tuple[str, bool]] | None = None, -) -> Iterator[Record]: - for file in files: - file = plugin.target.fs.path(file) - try: - fh = file.open(mode="rb") - except FileNotFoundError: - plugin.target.log.exception("File not found: %s", file) - continue - - try: - if b"$archiver" in fh.peek(64): - fh.seek(0) - data = load_plist_data(fh) - else: - data = plistlib.load(fh) - - yield from emit_dict_records( - plugin, - record_name, - data, - file, - record_descriptors=record_descriptors, - collapse_paths=collapse_paths, - ) - - except Exception: - plugin.target.log.exception("Failed to parse %s", file) - - -def dynamic_build_record(plugin: Plugin, record_name: str, rdict: dict, source: Path | None) -> Record: - record_fields = sorted(rdict.items()) - - record_values = { - "_target": plugin.target, - "source": source, - } - record_fields = [] - - for k, v in rdict.items(): - k = format_key(k) - - if isinstance(v, bool): - record_fields.append(("boolean", k)) - elif isinstance(v, int): - record_fields.append(("varint", k)) - else: - record_fields.append(("string", k)) - - record_values[k] = v - - record_fields.append(("path", "source")) - - desc = create_event_descriptor(record_name, tuple(record_fields)) - - return desc(**record_values) - - -def select_descriptor( - record_descriptors: tuple, - rdict: dict, -) -> TargetRecordDescriptor | None: - rdict_keys = {format_key(k) for k in rdict} - - for record in record_descriptors: - record_keys = set(record.fields.keys()) - if rdict_keys.issubset(record_keys): - return record - - return None - - -def build_record( - plugin: Plugin, - rdict: dict, - source: Path | None, - record_descriptors: tuple | None = None, -) -> Record: - desc = select_descriptor(record_descriptors, rdict) - - if desc is None: - plugin.target.log.exception( - "No matching record descriptor for %s with fields %s", - source, - sorted(map(format_key, rdict)), - ) - return None - - record_values = { - "_target": plugin.target, - "source": source, - } - - for k, v in rdict.items(): - record_values[format_key(k)] = v - - return desc(**record_values) - - -def create_event_descriptor(record_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: - return TargetRecordDescriptor(record_name, record_fields) - - -def format_key(key: str) -> str: - key = re_non_identifier.sub("_", key) - - key = re.sub(r"_+", "_", key) - - key = key.lstrip("_") - - if not key or key[0].isdigit(): - key = f"k_{key}" - - return key - - -UUID_RE = re.compile( - r"^[0-9A-Fa-f]{8}-" - r"[0-9A-Fa-f]{4}-" - r"[0-9A-Fa-f]{4}-" - r"[0-9A-Fa-f]{4}-" - r"[0-9A-Fa-f]{12}$" -) - - -def is_collapsed_path(child_path: str, collapse_paths: set[tuple[str, bool]]) -> bool: - for collapse_path, exact in collapse_paths: - if child_path == collapse_path: - return True - if not exact and collapse_path and child_path.startswith(f"{collapse_path}/"): - return True - return False - - -def emit_dict_records( - plugin: Plugin, - record_name: str, - node: dict, - source: Path | None, - *, - section: str | None = None, - path: str | None = None, - record_descriptors: tuple | None = None, - collapse_paths: set[tuple[str, bool]] | None = None, -) -> Iterator[Record]: - if path and path.endswith("$class"): - return - - attributes = {} - child_dicts = {} - - for k, v in node.items(): - child_path = f"{path}/{k}" if path else k - - if collapse_paths and isinstance(v, dict) and is_collapsed_path(child_path, collapse_paths): - attributes[k] = list(v.items()) - continue - - if isinstance(v, dict): - child_dicts[k] = v - else: - attributes[k] = v - - if node and all(isinstance(k, str) and UUID_RE.fullmatch(k) for k in node): - attributes = {} - - if attributes: - record_data = dict(attributes) - - if section is not None: - record_data["section"] = section - if path is not None: - record_data["plist_path"] = path - - if record_descriptors is None: - yield dynamic_build_record(plugin, record_name, record_data, source) - else: - yield build_record(plugin, record_data, source, record_descriptors) - - for k, child in child_dicts.items(): - child_path = f"{path}/{k}" if path else k - yield from emit_dict_records( - plugin, - record_name, - child, - source, - section=section, - path=child_path, - record_descriptors=record_descriptors, - collapse_paths=collapse_paths, - ) - - -def normalize_nsobj(obj: Any) -> Any: - """Convert NSKeyedArchiver output to plain Python types.""" - if isinstance(obj, NSDictionary): - return {k: normalize_nsobj(v) for k, v in obj.items()} - - if isinstance(obj, dict): - return {k: normalize_nsobj(v) for k, v in obj.items()} - - if isinstance(obj, list): - return [normalize_nsobj(v) for v in obj] - - if isinstance(obj, (str, int, float, bool)) or obj is None: - return obj - - if isinstance(obj, datetime): - return obj - - if isinstance(obj, uuid.UUID): - return str(obj) - - if hasattr(obj, "keys"): - return {k: normalize_nsobj(obj.get(k)) for k in obj.keys()} # noqa: SIM118 - - return obj - - -def load_plist_data(fh: BinaryIO) -> Any: - ns = NSKeyedArchiver(fh) - root = ns.get("store") - return normalize_nsobj(root) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py new file mode 100644 index 0000000000..e36b04d871 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.database.sqlite3 import SQLite3 + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + + +SqliteDatabasePropertiesRecord = TargetRecordDescriptor( + "macos/identity_services", + [ + ("string", "table"), + ("string", "key"), + ("string", "value"), + ("path", "source"), + ], +) + + +class IdentityServicesPlugin(Plugin): + """macOS identity services plugin.""" + + USER_PATH = ("Library/IdentityServices/ids.db",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No ids.db files found") + + def _find_files(self) -> None: + for _, path in _build_userdirs(self, self.USER_PATH): + self.files.add(path) + + @export( + record=[ + SqliteDatabasePropertiesRecord, + ] + ) + def identity_services( + self, + ) -> Iterator[ + [ + SqliteDatabasePropertiesRecord, + ] + ]: + """Yield identity services information.""" + for file in self.files: + with SQLite3(file) as database: + for row in database.table("_SqliteDatabaseProperties").rows(): + yield SqliteDatabasePropertiesRecord( + table="_SqliteDatabaseProperties", + key=row.key, + value=row.value, + source=file, + ) + + # Still missing outgoing_message, sqlite_sequence, incoming_message, outgoing_messages_to_delete tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py index da144f33db..5350bc2a53 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py @@ -18,9 +18,9 @@ ("string", "input_source_kind"), ("string", "keyboard_layout_name"), ("varint", "keyboard_layout_id"), - ("boolean", "enabled"), - ("boolean", "selected"), - ("boolean", "current"), + ("boolean", "enabled_layout"), + ("boolean", "selected_layout"), + ("boolean", "current_layout"), ("path", "source"), ], ) @@ -55,9 +55,11 @@ def keyboard_layout(self) -> Iterator[KeyboardLayoutRecord]: input_source_kind=source.get("InputSourceKind"), keyboard_layout_name=source.get("KeyboardLayout Name"), keyboard_layout_id=source.get("KeyboardLayout ID"), - enabled=True, - selected=source in plist.get("AppleSelectedInputSources", []), - current=(source.get("KeyboardLayout ID") == plist.get("AppleCurrentKeyboardLayoutInputSourceID")), + enabled_layout=True, + selected_layout=source in plist.get("AppleSelectedInputSources", []), + current_layout=( + source.get("KeyboardLayout ID") == plist.get("AppleCurrentKeyboardLayoutInputSourceID") + ), source=self.file, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py new file mode 100644 index 0000000000..a8a195d339 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +KeychainRecord = TargetRecordDescriptor( + "macos/keychain", + [ + ("string", "table"), + ("varint", "row_id"), + ("float", "cdat"), + ("float", "mdat"), + ("string", "desc"), + ("string", "icmt"), + ("string", "crtr"), + ("string", "type"), + ("string", "scrp"), + ("string", "labl"), + ("string", "alis"), + ("varint", "invi"), + ("varint", "nega"), + ("varint", "cusi"), + ("varint", "prot"), + ("string", "acct"), + ("string", "svce"), + ("string", "gena"), + ("string", "data"), + ("string", "agrp"), + ("string", "pdmn"), + ("varint", "sync"), + ("varint", "tomb"), + ("string", "sha1"), + ("string", "vwht"), + ("string", "tkid"), + ("string", "musr"), + ("string", "UUID"), + ("varint", "sysb"), + ("string", "pcss"), + ("string", "pcsk"), + ("string", "pcsi"), + ("string", "persistref"), + ("varint", "clip"), + ("string", "ggrp"), + ("path", "source"), + ], +) + +SqliteSequenceRecord = TargetRecordDescriptor( + "macos/keychain/sqlite_sequence", + [ + ("string", "table"), + ("string", "name"), + ("varint", "seq"), + ("path", "source"), + ], +) + +TVersionRecord = TargetRecordDescriptor( + "macos/keychain/t_version", + [ + ("string", "table"), + ("varint", "row_id"), + ("string", "version"), + ("varint", "minor"), + ("path", "source"), + ], +) + +MetaDataKeysRecord = TargetRecordDescriptor( + "macos/keychain/meta_data_keys", + [ + ("string", "table"), + ("string", "keyclass"), + ("string", "actual_key_class"), + ("string", "data"), + ("path", "source"), + ], +) + +KeychainRecords = ( + KeychainRecord, + SqliteSequenceRecord, + TVersionRecord, + MetaDataKeysRecord, +) + +FIELD_MAPPINGS = { + "actualKeyclass": "actual_key_class", + "rowid": "row_id", +} + + +class KeychainPlugin(Plugin): + """macOS keychain plugin.""" + + USER_PATH = ("Library/Keychains/*/keychain-2.db",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No keychain-2.db files found") + + def _find_files(self) -> None: + for _, path in _build_userdirs(self, self.USER_PATH): + self.files.add(path) + + @export(record=KeychainRecords) + def keychain( + self, + ) -> Iterator[KeychainRecords]: + """Yield keychain information.""" + yield from build_sqlite_records(self, self.files, KeychainRecords, field_mappings=FIELD_MAPPINGS) + + # Still missing cert, outgoingqueue, incomingqueue, synckeys, ckmirror, currentkeys, + # ckstate, item_backup, backup_keybag, ckmanifest, pending_manifest, ckmanifest_leaf, + # backup_keyarchive, currentkeyarchives, archived_key_backup, pending_manifest_leaf, + # currentitems, ckdevicestate, tlkshare, sharingIncomingQueue, + # sharingMirror, sharingOutgoingQueue tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 3dabb4f545..4b51f63571 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -7,24 +7,15 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.localeutil import normalize_language -from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import export from dissect.target.plugins.os.default.locale import LocalePlugin if TYPE_CHECKING: from dissect.target.target import Target -WindowsKeyboardRecord = TargetRecordDescriptor( - "windows/keyboard", - [ - ("string", "layout"), - ("string", "language_id"), - ], -) - class macOSLocalePlugin(LocalePlugin): - """Windows locale plugin.""" + """macOS locale plugin.""" GLOBAL = "/Library/Preferences/.GlobalPreferences.plist" @@ -65,11 +56,13 @@ def language(self) -> str | None: @export(property=True) def install_date(self) -> str | None: + """Get the installation date of the system.""" mtime = self.target.fs.path("/private/var/db/.AppleSetupDone").lstat().st_mtime return datetime.fromtimestamp(mtime, timezone.utc) @export(property=True) def location_services_active(self) -> bool | None: + """Get the status of location services.""" path = self.target.fs.path("/Library/Preferences/com.apple.timezone.auto.plist") if not path.exists(): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index ae9f680245..06cc6e7cc8 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records if TYPE_CHECKING: from collections.abc import Iterator @@ -56,4 +56,4 @@ def _find_files(self) -> None: # @export(output="yield") def login_window(self) -> Iterator[DynamicDescriptor]: """Yield macOS login window plist files.""" - yield from build_records(self, "macos/login_window", self.login_window_files) + yield from build_plist_records(self, self.login_window_files, function_name="macos/login_window") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index 2e410b3bdc..e60b05e2b7 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records if TYPE_CHECKING: from collections.abc import Iterator @@ -204,15 +204,15 @@ LauncherRecord1, ), TargetRecordDescriptor( - "macos/launch_agents", + "macos/launch_agents/socket", LauncherRecord3, ), TargetRecordDescriptor( - "macos/launch_agents", + "macos/launch_agents/un", LauncherRecord5, ), TargetRecordDescriptor( - "macos/launch_agents", + "macos/launch_agents/un_notification", LauncherRecord6, ), ) @@ -223,19 +223,19 @@ LauncherRecord1, ), TargetRecordDescriptor( - "macos/launch_daemons", + "macos/launch_daemons/wait", LauncherRecord2, ), TargetRecordDescriptor( - "macos/launch_daemons", + "macos/launch_daemons/socket", LauncherRecord3, ), TargetRecordDescriptor( - "macos/launch_daemons", + "macos/launch_daemons/version4", LauncherRecord4, ), TargetRecordDescriptor( - "macos/launch_daemons", + "macos/launch_daemons/listeners", LauncherRecord7, ), ) @@ -322,13 +322,9 @@ def _find_files(self) -> None: # @export(output="yield") def launch_agents(self) -> Iterator[LaunchAgentRecords]: """Yield macOS launch agent plist files.""" - yield from build_records( - self, "macos/launch_agents", self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS - ) + yield from build_plist_records(self, self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS) @export(record=LaunchDaemonRecords) def launch_daemons(self) -> Iterator[LaunchDaemonRecords]: """Yield macOS launch daemon plist files.""" - yield from build_records( - self, "macos/launch_daemons", self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS - ) + yield from build_plist_records(self, self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py index 9c60b92d7a..d9e14a4fe5 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py @@ -6,8 +6,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records if TYPE_CHECKING: from collections.abc import Iterator @@ -20,9 +20,9 @@ "macos/login_items", [ ("varint", "generation"), - ("varint", "backgroundAppRefreshLoadCount"), - ("boolean", "launchServicesItemsImported"), - ("boolean", "serviceManagementLoginItemsMigrated"), + ("varint", "background_app_refresh_load_count"), + ("boolean", "launch_services_items_imported"), + ("boolean", "service_management_login_items_migrated"), ("string", "plist_path"), ("path", "source"), ], @@ -30,6 +30,12 @@ LoginItemsRecords = (LoginItemsRecord,) +FIELD_MAPPINGS = { + "backgroundAppRefreshLoadCount": "background_app_refresh_load_count", + "launchServicesItemsImported": "launch_services_items_imported", + "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", +} + class LoginItemsPlugin(Plugin): """macOS login items plugin.""" @@ -65,4 +71,4 @@ def _find_files(self) -> None: # @export(output="yield") def login_items(self) -> Iterator[LoginItemsRecord]: """Yield macOS login items plist files.""" - yield from build_records(self, "macos/login_items", self.login_items_files, LoginItemsRecords) + yield from build_plist_records(self, self.login_items_files, LoginItemsRecords, field_mappings=FIELD_MAPPINGS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 6971269d9a..f651b734ce 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -49,4 +49,4 @@ def check_compatible(self) -> None: @export(record=DynamicDescriptor(["string"])) def resources_info_strings(self) -> Iterator[DynamicDescriptor]: """Yield Resources InfoPlist.strings information.""" - yield from build_records(self, "macos/resources_info_strings", self.files) + yield from build_plist_records(self, self.files, function_name="macos/resources_info_strings") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index e4450c277a..50c664ab74 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -47,4 +47,4 @@ def check_compatible(self) -> None: @export(record=DynamicDescriptor(["string"])) def resources_localizable_strings(self) -> Iterator[DynamicDescriptor]: """Yield Resources Localizable.strings information.""" - yield from build_records(self, "macos/resources_localizable_strings", self.files) + yield from build_plist_records(self, self.files, function_name="macos/resources_localizable_strings") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py index 28c141ee9c..819f624dca 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py @@ -6,7 +6,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.plist import build_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: from collections.abc import Iterator @@ -38,4 +38,4 @@ def check_compatible(self) -> None: @export(record=DynamicDescriptor(["string"])) def system_preferences(self) -> Iterator[DynamicDescriptor]: """Yield system preference information.""" - yield from build_records(self, "macos/system_preferences", self.files) + yield from build_plist_records(self, self.files, function_name="macos/system_preferences") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py new file mode 100644 index 0000000000..96de5dba20 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +AccessRecord = TargetRecordDescriptor( + "macos/tcc/access", + [ + ("string", "table"), + ("string", "service"), + ("string", "client"), + ("varint", "client_type"), + ("varint", "auth_value"), + ("varint", "auth_reason"), + ("varint", "auth_version"), + ("string", "csreq"), + ("string", "policy_id"), + ("string", "indirect_object_identifier_type"), + ("string", "indirect_object_identifier"), + ("string", "indirect_object_code_identity"), + ("varint", "flags"), + ("datetime", "last_modified"), + ("varint", "pid"), + ("string", "pid_version"), + ("string", "boot_uuid"), + ("datetime", "last_reminded"), + ("path", "source"), + ], +) + +KeyValueRecord = TargetRecordDescriptor( + "macos/tcc/key_value", + [ + ("string", "table"), + ("string", "key"), + ("string", "value"), + ("path", "source"), + ], +) + +TCCRecords = ( + AccessRecord, + KeyValueRecord, +) + + +class TCCPlugin(Plugin): + """macOS transparency, consent, control (tcc) framework plugin.""" + + SYSTEM_PATH = "/Library/Application Support/com.apple.TCC/TCC.db" + USER_PATH = ("Library/Application Support/com.apple.TCC/TCC.db",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No TCC.db files found") + + def _find_files(self) -> None: + self.files.add(self.target.fs.path(self.SYSTEM_PATH)) + for _, path in _build_userdirs(self, self.USER_PATH): + self.files.add(path) + + @export(record=TCCRecords) + def tcc( + self, + ) -> Iterator[TCCRecords]: + """Yield transparency, consent, control (tcc) framework information.""" + yield from build_sqlite_records(self, self.files, TCCRecords) + + # Still missing policies, active_policy, access_overrides, expired tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py new file mode 100644 index 0000000000..466c38db8c --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + + +ZTextReplacementEntryRecord = TargetRecordDescriptor( + "macos/text_replacements/z_text_replacement_entry", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_was_deleted"), + ("varint", "z_needs_save_to_cloud"), + ("string", "z_timestamp"), + ("string", "z_phrase"), + ("string", "z_shortcut"), + ("string", "z_unique_name"), + ("string", "z_remote_record_info"), + ("path", "source"), + ], +) + +ZTrCloudKitSyncStateRecord = TargetRecordDescriptor( + "macos/text_replacements/z_tr_cloud_kit_sync_state", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_did_pull_once"), + ("string", "z_fetch_change_token"), + ("path", "source"), + ], +) + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/text_replacements/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/text_replacements/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/text_replacements/z_plist", + [ + ("varint", "ns_persistence_maximum_framework_version"), + ("string[]", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("string", "ns_auto_vacuum_level"), + ("string", "ns_store_model_version_hashes_digest"), + ("string", "ns_store_model_version_checksum_key"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_store_model_version_hashes_version"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/text_replacements/ns_store_model_version_hashes", + [ + ("string", "tr_cloud_kit_sync_state"), + ("string", "text_replacement_entry"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +ZModelCacheRecord = TargetRecordDescriptor( + "macos/text_replacements/z_model_cache", + [ + ("string", "table"), + ("string", "z_content"), + ("path", "source"), + ], +) + +TextReplacementsRecords = ( + ZTextReplacementEntryRecord, + ZTrCloudKitSyncStateRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "ZWASDELETED": "z_was_deleted", + "ZNEEDSSAVETOCLOUD": "z_needs_save_to_cloud", + "ZTIMESTAMP": "z_timestamp", + "ZPHRASE": "z_phrase", + "ZSHORTCUT": "z_shortcut", + "ZUNIQUENAME": "z_unique_name", + "ZREMOTERECORDINFO": "z_remote_record_info", + "ZDIDPULLONCE": "z_did_pull_once", + "ZFETCHCHANGETOKEN": "z_fetch_change_token", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "Z_CONTENT": "z_content", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "TRCloudKitSyncState": "tr_cloud_kit_sync_state", + "TextReplacementEntry": "text_replacement_entry", +} + + +class TextReplacementsPlugin(Plugin): + """macOS text replacements plugin.""" + + PATH = ("Library/KeyboardServices/TextReplacements.db",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No TextReplacements.db files found") + + def _find_files(self) -> None: + for _, path in _build_userdirs(self, self.PATH): + self.files.add(path) + + @export(record=TextReplacementsRecords) + def text_replacements( + self, + ) -> Iterator[TextReplacementsRecords]: + """Yield text replacements information.""" + yield from build_sqlite_records(self, self.files, TextReplacementsRecords, field_mappings=FIELD_MAPPINGS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py new file mode 100644 index 0000000000..2f13306d62 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import ( + build_sqlite_records, +) +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + + +ZAccessOptionsKeyRecord = TargetRecordDescriptor( + "macos/user_accounts/z_access_options_key", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_enum_value"), + ("string", "z_name"), + ("path", "source"), + ], +) + +ZOwningAccountTypesRecord = TargetRecordDescriptor( + "macos/user_accounts/z_owning_account_types", + [ + ("string", "table"), + ("varint", "z_1_access_keys"), + ("varint", "z_4_owning_account_types"), + ("path", "source"), + ], +) + +ZAccountRecord = TargetRecordDescriptor( + "macos/user_accounts/z_account", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_active"), + ("varint", "z_authenticated"), + ("varint", "z_supports_authentication"), + ("varint", "z_visible"), + ("varint", "z_warming_up"), + ("varint", "z_account_type"), + ("varint", "z_parent_account"), + ("float", "z_date"), + ("float", "z_last_credential_renewal_rejection_date"), + ("string", "z_account_description"), + ("string", "z_authentication_type"), + ("string", "z_credential_type"), + ("string", "z_identifier"), + ("string", "z_modification_id"), + ("string", "z_owning_bundle_id"), + ("string", "z_username"), + ("bytes", "z_dataclass_properties"), + ("path", "source"), + ], +) + +ZEnabledDataClassesRecord = TargetRecordDescriptor( + "macos/user_accounts/z_enabled_dataclasses", + [ + ("string", "table"), + ("varint", "z_2_enabled_accounts"), + ("varint", "z_7_enabled_dataclasses"), + ("path", "source"), + ], +) + +ZAccountPropertyRecord = TargetRecordDescriptor( + "macos/user_accounts/z_account_property", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_owner"), + ("string", "z_key"), + ("string", "z_value"), + ("path", "source"), + ], +) + +ZAccountTypeRecord = TargetRecordDescriptor( + "macos/user_accounts/z_account_type", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_obsolete"), + ("varint", "z_supports_authentication"), + ("varint", "z_supports_multiple_accounts"), + ("varint", "z_visibility"), + ("string", "z_account_type_description"), + ("string", "z_credential_protection_policy"), + ("string", "z_credential_type"), + ("string", "z_identifier"), + ("string", "z_owning_bundle_id"), + ("path", "source"), + ], +) + +ZSupportedDataClassesRecord = TargetRecordDescriptor( + "macos/user_accounts/z_supported_dataclasses", + [ + ("string", "table"), + ("varint", "z_4_supported_types"), + ("varint", "z_7_supported_dataclasses"), + ("path", "source"), + ], +) + +ZSyncableDataClassesRecord = TargetRecordDescriptor( + "macos/user_accounts/z_syncable_dataclasses", + [ + ("string", "table"), + ("varint", "z_4_syncable_types"), + ("varint", "z_7_syncable_dataclasses"), + ("path", "source"), + ], +) + +ZDataClassRecord = TargetRecordDescriptor( + "macos/user_accounts/z_dataclass", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_enum_value"), + ("string", "z_name"), + ("path", "source"), + ], +) + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/user_accounts/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/user_accounts/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/user_accounts/z_plist", + [ + ("string", "ac_account_type_version"), + ("string", "ns_auto_vacuum_level"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_persistence_maximum_framework_version"), + ("string", "ns_store_model_version_checksum_key"), + ("string", "ns_store_model_version_hashes_digest"), + ("varint", "ns_store_model_version_hashes_version"), + ("string", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/user_accounts/ns_store_model_version_hashes", + [ + ("string", "access_options_key"), + ("string", "account"), + ("string", "account_property"), + ("string", "account_type"), + ("string", "authorization"), + ("string", "credential_item"), + ("string", "dataclass"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +ZModelCacheRecord = TargetRecordDescriptor( + "macos/user_accounts/z_model_cache", + [ + ("string", "table"), + ("string", "z_content"), + ("path", "source"), + ], +) + +UserAccountRecords = ( + ZAccessOptionsKeyRecord, + ZOwningAccountTypesRecord, + ZAccountRecord, + ZEnabledDataClassesRecord, + ZAccountPropertyRecord, + ZAccountTypeRecord, + ZSupportedDataClassesRecord, + ZSyncableDataClassesRecord, + ZDataClassRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "ZENUMVALUE": "z_enum_value", + "ZNAME": "z_name", + "Z_1ACCESSKEYS": "z_1_access_keys", + "Z_4OWNINGACCOUNTTYPES": "z_4_owning_account_types", + "ZACTIVE": "z_active", + "ZAUTHENTICATED": "z_authenticated", + "ZSUPPORTSAUTHENTICATION": "z_supports_authentication", + "ZVISIBLE": "z_visible", + "ZWARMINGUP": "z_warming_up", + "ZACCOUNTTYPE": "z_account_type", + "ZPARENTACCOUNT": "z_parent_account", + "ZDATE": "z_date", + "ZLASTCREDENTIALRENEWALREJECTIONDATE": "z_last_credential_renewal_rejection_date", + "ZACCOUNTDESCRIPTION": "z_account_description", + "ZAUTHENTICATIONTYPE": "z_authentication_type", + "ZCREDENTIALTYPE": "z_credential_type", + "ZIDENTIFIER": "z_identifier", + "ZMODIFICATIONID": "z_modification_id", + "ZOWNINGBUNDLEID": "z_owning_bundle_id", + "ZUSERNAME": "z_username", + "ZDATACLASSPROPERTIES": "z_dataclass_properties", + "Z_2ENABLEDACCOUNTS": "z_2_enabled_accounts", + "Z_7ENABLEDDATACLASSES": "z_7_enabled_dataclasses", + "ZOWNER": "z_owner", + "ZKEY": "z_key", + "ZVALUE": "z_value", + "ZOBSOLETE": "z_obsolete", + "ZSUPPORTSMULTIPLEACCOUNTS": "z_supports_multiple_accounts", + "ZVISIBILITY": "z_visibility", + "ZACCOUNTTYPEDESCRIPTION": "z_account_type_description", + "ZCREDENTIALPROTECTIONPOLICY": "z_credential_protection_policy", + "Z_4SUPPORTEDTYPES": "z_4_supported_types", + "Z_7SUPPORTEDDATACLASSES": "z_7_supported_dataclasses", + "Z_4SYNCABLETYPES": "z_4_syncable_types", + "Z_7SYNCABLEDATACLASSES": "z_7_syncable_dataclasses", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "ACAccountTypeVersion": "ac_account_type_version", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "AccessOptionsKey": "access_options_key", + "Account": "account", + "AccountProperty": "account_property", + "AccountType": "account_type", + "Authorization": "authorization", + "CredentialItem": "credential_item", + "Dataclass": "dataclass", + "Z_CONTENT": "z_content", +} + + +class UserAccountsPlugin(Plugin): + """macOS user accounts plugin.""" + + USER_PATH = ("Library/Accounts/Accounts*.sqlite",) + + def __init__(self, target: Target): + super().__init__(target) + + self.files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No user account database files found") + + def _find_files(self) -> None: + for _, path in _build_userdirs(self, self.USER_PATH): + self.files.add(path) + + @export( + record=[ + ZAccessOptionsKeyRecord, + ZOwningAccountTypesRecord, + ZAccountRecord, + ZEnabledDataClassesRecord, + ZAccountPropertyRecord, + ZAccountTypeRecord, + ZSupportedDataClassesRecord, + ZSyncableDataClassesRecord, + ZDataClassRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, + ] + ) + def user_accounts( + self, + ) -> Iterator[ + [ + ZAccessOptionsKeyRecord, + ZOwningAccountTypesRecord, + ZAccountRecord, + ZEnabledDataClassesRecord, + ZAccountPropertyRecord, + ZAccountTypeRecord, + ZSupportedDataClassesRecord, + ZSyncableDataClassesRecord, + ZDataClassRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, + ] + ]: + """Yield user accounts information.""" + yield from build_sqlite_records(self, self.files, UserAccountRecords, field_mappings=FIELD_MAPPINGS) + + # Still missing Z_2PROVISIONEDDATACLASSES, ZAUTHORIZATION, ZCREDENTIALITEM tables diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/auth.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/auth.db new file mode 100644 index 0000000000..5b80e6f81b --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/auth.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed8fbd68825e7a18c4140d51ea6163d52c6a2c849114b77fa6c159a39dd05480 +size 155648 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex b/tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex new file mode 100644 index 0000000000..d8c6fade7a --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71d3022121b116072a9b1d25c64fb6d1abf702406ca9aa1363a2e8193051ae38 +size 409600 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/sqlindex-wal new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db new file mode 100644 index 0000000000..2c34b7bba1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8996d53c6b25ba17e98b0c6ca72b4ce8d27798dba14e2ef2f857f25deedc585a +size 4096 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db-wal new file mode 100644 index 0000000000..abbd804bb4 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/DuetActivitySchedulerClassC.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea44f5136ba2311e6aa084f3391ef54dd4f9cb813426f1f70d4c9da883f106e2 +size 683952 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/gkopaque.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/gkopaque.db new file mode 100644 index 0000000000..605e9ed728 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/gkopaque.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:297a6f9e320fd7fa4cb64eb34e1468d1be6a4f95ad047c201ea7d04ed53c989a +size 6062080 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/ids.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/ids.db new file mode 100644 index 0000000000..4d87410225 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/ids.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d52672ea4e330d7449dfbc4ed221696e773ef9b0234842e2b9b432af7777c2d3 +size 49152 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/keychain-2.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/keychain-2.db new file mode 100644 index 0000000000..8046ceb08b --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/keychain-2.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc48a441120098bb5f1b0629b3c08e347ed01a0af1e88b493c56bd47a48314a0 +size 1253376 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/system.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/system.db new file mode 100644 index 0000000000..53a61cc41b --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/system.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbfb621157968dea208759597fcde8aa2fafb9e02a00431b09d6352004161cf6 +size 65536 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/user.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/user.db new file mode 100644 index 0000000000..095cd41d3b --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/tcc/user.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7e8a85daf819bdd320c625b5bd05e1f257c020435a8bc66db1b81239d29b072 +size 65536 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db new file mode 100644 index 0000000000..bd347f85ca --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83e365bb335881b96aacb0a2c920a6601f0a81401f66a7c4cf6ad7baac314c16 +size 53248 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db-wal new file mode 100644 index 0000000000..4109768f2e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/text_replacements/TextReplacements.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:552222421037e4b2f5ae04da6edcfa89f923c93737407023eaadd46596db328f +size 41232 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite b/tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite new file mode 100644 index 0000000000..91dc97f750 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:171fec3682458506c61b127bdf0126b74552658e072d84eefef939c98cdd5d07 +size 159744 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/user_accounts/Accounts4.sqlite-wal new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py index 5dba9621f7..d90421af2b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py @@ -55,34 +55,30 @@ def test_login_items( assert len(results) == 4 - assert results[0].domain is None assert results[0].generation == 2 - assert results[0].serviceManagementLoginItemsMigrated - assert results[0].launchServicesItemsImported - assert results[0].backgroundAppRefreshLoadCount == 4 + assert results[0].service_management_login_items_migrated + assert results[0].launch_services_items_imported + assert results[0].background_app_refresh_load_count == 4 assert results[0].plist_path == ("userSettingsByUserIdentifier/8122F0CD-020B-4E0C-A3AD-2FCB201C9BB0") assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[1].domain is None assert results[1].generation == 0 - assert not results[1].serviceManagementLoginItemsMigrated - assert not results[1].launchServicesItemsImported - assert results[1].backgroundAppRefreshLoadCount == 0 + assert not results[1].service_management_login_items_migrated + assert not results[1].launch_services_items_imported + assert results[1].background_app_refresh_load_count == 0 assert results[1].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000") assert results[1].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[2].domain is None assert results[2].generation == 1 - assert results[2].serviceManagementLoginItemsMigrated - assert not results[2].launchServicesItemsImported - assert results[2].backgroundAppRefreshLoadCount == 2 + assert results[2].service_management_login_items_migrated + assert not results[2].launch_services_items_imported + assert results[2].background_app_refresh_load_count == 2 assert results[2].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA000000F8") assert results[2].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[3].domain is None assert results[3].generation == 1 - assert results[3].serviceManagementLoginItemsMigrated - assert not results[3].launchServicesItemsImported - assert results[3].backgroundAppRefreshLoadCount == 2 + assert results[3].service_management_login_items_migrated + assert not results[3].launch_services_items_imported + assert results[3].background_app_refresh_load_count == 2 assert results[3].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE") assert results[3].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py index 37139c4d68..06ad350a0e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py @@ -36,6 +36,6 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: Virtua assert results[0].counter == 2 assert results[0].device_uuid == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" - assert results[0].version == 7200 + assert results[0].version == "7200" assert results[0].preferred_order == "[]" assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py new file mode 100644 index 0000000000..2cacb9c70e --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.authorization_rules import AuthorizationRulesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "auth.db", + ], +) +def test_authorization_rules(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/var/db/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(AuthorizationRulesPlugin) + + results = list(target_unix.authorization_rules()) + + assert len(results) == 249 + + assert results[0].table == "sqlite_sequence" + assert results[0].name == "rules" + assert results[0].seq == 205 + assert results[0].source == "/var/db/auth.db" + + assert sorted(results[175].tables) == ["delegates_map", "rules", "rules_history"] + assert results[175].rules_id == 159 + assert results[175].rules_name == "com.apple.tcc.util.admin" + assert results[175].rules_type == 1 + assert results[175].rules_class == 2 + assert results[175].rules_group is None + assert results[175].rules_kofn is None + assert results[175].rules_timeout is None + assert results[175].rules_flags == 0 + assert results[175].rules_tries is None + assert results[175].rules_version == 0 + assert results[175].rules_created == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_modified == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_hash is None + assert results[175].rules_identifier is None + assert results[175].rules_requirement is None + assert results[175].rules_comment == "For modification of TCC settings." + assert results[175].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[175].rules_history_source == "authd" + assert results[175].rules_history_operation == 0 + assert results[175].mechanisms_map_m_id is None + assert results[175].mechanisms_map_ord is None + assert results[175].mechanisms_plugin is None + assert results[175].mechanisms_param is None + assert results[175].mechanisms_privileged is None + assert results[175].rules_delegates_map == ["{'d_id': 9, 'ord': 0}"] + assert results[175].source == "/var/db/auth.db" + + assert sorted(results[177].tables) == ["mechanisms", "mechanisms_map", "rules", "rules_history"] + assert results[177].rules_id == 161 + assert results[177].rules_name == "system.login.filevault" + assert results[177].rules_type == 1 + assert results[177].rules_class == 3 + assert results[177].rules_group is None + assert results[177].rules_kofn is None + assert results[177].rules_timeout is None + assert results[177].rules_flags == 1 + assert results[177].rules_tries == "10000" + assert results[177].rules_version == 0 + assert results[177].rules_created == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_modified == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_hash is None + assert results[177].rules_identifier is None + assert results[177].rules_requirement is None + assert results[177].rules_comment == "Login mechanism based rule for Filevault." + assert results[177].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[177].rules_history_source == "authd" + assert results[177].rules_history_operation == 0 + assert results[177].mechanisms_map_m_id == 24 + assert results[177].mechanisms_map_ord == 0 + assert results[177].mechanisms_plugin == "builtin" + assert results[177].mechanisms_param == "policy-banner" + assert results[177].mechanisms_privileged == 0 + assert results[177].rules_delegates_map == [] + assert results[177].source == "/var/db/auth.db" + + assert results[-1].table == "config" + assert results[-1].key == "data_ts" + assert results[-1].value == "795677157.0" + assert results[-1].source == "/var/db/auth.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py index c4170fad39..7e32d2d927 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -53,7 +53,7 @@ def test_code_signature_coderesources( assert len(results) == 11 assert results[0].omit - assert results[0].weight == "20.0" + assert results[0].weight == 20.0 assert results[0].plist_path == "rules/^.*" assert ( results[0].source diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py index 58e9e45814..c15ce23e04 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -52,29 +52,29 @@ def test_contents_version( assert len(results) == 3 - assert results[0].BuildAliasOf == "Ensemble" - assert results[0].BuildVersion == "1983" - assert results[0].CFBundleShortVersionString == "1.0" - assert results[0].CFBundleVersion == "174.4.1" - assert results[0].ProjectName == "Ensemble_executables" - assert results[0].SourceVersion == "174004001000000" + assert results[0].build_alias_of == "Ensemble" + assert results[0].build_version == "1983" + assert results[0].cf_bundle_short_version_string == "1.0" + assert results[0].cf_bundle_version == "174.4.1" + assert results[0].project_name == "Ensemble_executables" + assert results[0].source_version == "174004001000000" assert results[0].source == "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist" - assert results[1].BuildVersion == "100" - assert results[1].CFBundleShortVersionString == "1.0" - assert results[1].CFBundleVersion == "1" - assert results[1].ProjectName == "MobileBluetooth" - assert results[1].SourceVersion == "194026001000001" + assert results[1].build_version == "100" + assert results[1].cf_bundle_short_version_string == "1.0" + assert results[1].cf_bundle_version == "1" + assert results[1].project_name == "MobileBluetooth" + assert results[1].source_version == "194026001000001" assert ( results[1].source == "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist" ) - assert results[2].BuildAliasOf == "SwiftUI" - assert results[2].BuildVersion == "12" - assert results[2].CFBundleShortVersionString == "7.4.27" - assert results[2].CFBundleVersion == "7.4.27" - assert results[2].ProjectName == "SwiftUICore" - assert results[2].SourceVersion == "7004027000000" + assert results[2].build_alias_of == "SwiftUI" + assert results[2].build_version == "12" + assert results[2].cf_bundle_short_version_string == "7.4.27" + assert results[2].cf_bundle_version == "7.4.27" + assert results[2].project_name == "SwiftUICore" + assert results[2].source_version == "7004027000000" assert ( results[2].source == "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist" ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py new file mode 100644 index 0000000000..baa909cbf0 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.directory_services_local_nodes import ( + DirectoryServicesLocalNodesPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "sqlindex", + "sqlindex-wal", + ] + ], +) +def test_directory_services_local_nodes(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/{test_file}") + fs_unix.map_file(f"/var/db/dslocal/nodes/Default/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(DirectoryServicesLocalNodesPlugin) + + results = list(target_unix.directory_services_local_nodes()) + + assert len(results) == 1452 + + results.sort(key=lambda r: r.tables) + + assert results[0].tables == ["generateduid", "rec:groups"] + assert results[0].filetime == datetime(2026, 3, 20, 4, 25, 57, tzinfo=tz) + assert results[0].filename == "_appowner.plist" + assert results[0].recordtype == "groups" + assert results[0].value == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000057" + assert results[0].source == "/var/db/dslocal/nodes/Default/sqlindex" + + assert results[-69].tables == ["users"] + assert results[-69].filetime is None + assert results[-69].filename == "_appserveradm.plist" + assert results[-69].recordtype == "groups" + assert results[-69].value == "_mbsetupuser" + assert results[-69].source == "/var/db/dslocal/nodes/Default/sqlindex" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py new file mode 100644 index 0000000000..3eb6af9515 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.duet_activity_scheduler import DuetActivitySchedulerPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "DuetActivitySchedulerClassC.db", + "DuetActivitySchedulerClassC.db-wal", + ] + ], +) +def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/{test_file}") + fs_unix.map_file(f"/var/db/DuetActivityScheduler/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/DuetActivityScheduler/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(DuetActivitySchedulerPlugin) + + results = list(target_unix.duet_activity_scheduler()) + + assert len(results) == 55 + + assert results[0].table == "ZGROUP" + assert results[0].z_pk == 1 + assert results[0].z_ent == 2 + assert results[0].z_opt == 1 + assert results[0].z_max_concurrent == 6 + assert results[0].z_name == "com.apple.dasd.defaultNetwork" + assert results[0].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[48].table == "Z_PRIMARYKEY" + assert results[48].z_ent == 1 + assert results[48].z_name == "Activity" + assert results[48].z_super == 0 + assert results[48].z_max == 0 + assert results[48].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py new file mode 100644 index 0000000000..42764b0744 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.gkopaque import GatekeeperOpaqueConfigurationPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "gkopaque.db", + ], +) +def test_gkopaque(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"/var/db/gkopaque.bundle/Contents/Resources/{test_file}", data_file) + entry = fs_unix.get(f"/var/db/gkopaque.bundle/Contents/Resources/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(GatekeeperOpaqueConfigurationPlugin) + + results = list(target_unix.gkopaque()) + + assert len(results) == 74247 + + assert results[0].table == "whitelist" + assert ( + results[0].current + == "\x00\x03'\udcec\udce1\udcfbZ'\udcb5\udcf5\udcc5\x1a\x00\udc99\x00\udcb1\udce4\udc85K\udcb7" + ) + assert results[0].opaque == "\udccb\udce5k\udc97\udc84\udc97N\n\x1c\x01Y\udcc4\x1f9+wB\x1bM#" + assert results[0].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" + + assert results[-1].table == "conditions" + assert results[-1].label == "google chrome (canary)" + assert results[-1].weight == 300 + assert results[-1].conditions_source == "EQHXZ8M8AV" + assert results[-1].identifier == "com.google.Chrome.canary" + assert results[-1].version is None + assert results[-1].conditions == "{errors=[-67013]}" + assert results[-1].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py index 51f94a22c8..c8e3ccf05c 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -104,9 +104,9 @@ def test_global_user_preferences( assert results[0].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" assert results[1].AppleKeyboardUIMode == 2 - assert results[1].source == ("/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist") + assert results[1].source == "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist" assert results[2].AppleLocale == "en_US" assert results[2].AppleKeyboardUIMode == 3 assert results[2].com_apple_sound_beep_flash == 0 - assert results[2].source == ("/private/var/root/Library/Preferences/.GlobalPreferences.plist") + assert results[2].source == "/private/var/root/Library/Preferences/.GlobalPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py b/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py new file mode 100644 index 0000000000..88956ad68a --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.identity_services import IdentityServicesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "ids.db", + ], +) +def test_identity_services(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"Users/user/Library/IdentityServices/{test_file}", data_file) + entry = fs_unix.get(f"Users/user/Library/IdentityServices/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(IdentityServicesPlugin) + + results = list(target_unix.identity_services()) + + assert len(results) == 2 + + assert results[0].table == "_SqliteDatabaseProperties" + assert results[0].key == "_ClientVersion" + assert results[0].value == "10027" + assert results[0].source == "/Users/user/Library/IdentityServices/ids.db" + + assert results[1].table == "_SqliteDatabaseProperties" + assert results[1].key == "InternalMigration" + assert results[1].value == "100" + assert results[1].source == "/Users/user/Library/IdentityServices/ids.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py index 6ae9dc9b78..d8c5097673 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py @@ -37,15 +37,15 @@ def test_keyboard_layout(test_file: str, target_unix: Target, fs_unix: VirtualFi assert results[0].input_source_kind == "Keyboard Layout" assert results[0].keyboard_layout_name == "U.S." assert results[0].keyboard_layout_id == 0 - assert results[0].enabled - assert results[0].selected - assert not results[0].current + assert results[0].enabled_layout + assert results[0].selected_layout + assert not results[0].current_layout assert results[0].source == "/Library/Preferences/com.apple.HIToolbox.plist" assert results[1].input_source_kind == "Keyboard Layout" assert results[1].keyboard_layout_name == "Dutch" assert results[1].keyboard_layout_id == 26 - assert results[1].enabled - assert not results[1].selected - assert not results[1].current + assert results[1].enabled_layout + assert not results[1].selected_layout + assert not results[1].current_layout assert results[1].source == "/Library/Preferences/com.apple.HIToolbox.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py new file mode 100644 index 0000000000..44067b8459 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.keychain import KeychainPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "keychain-2.db", + ], +) +def test_keychain(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/{test_file}", data_file) + entry = fs_unix.get(f"/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(KeychainPlugin) + + results = list(target_unix.keychain()) + + assert len(results) == 100 + + assert results[0].table == "genp" + assert results[0].row_id == 1 + assert results[0].agrp == "com.apple.security.indirect-unlock-key" + assert results[0].pdmn == "ak" + assert results[0].sync == 0 + assert results[0].tomb == 0 + assert results[0].clip == 0 + assert results[0].ggrp == "" + assert results[0].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[84].table == "sqlite_sequence" + assert results[84].name == "tversion" + assert results[84].seq == 1 + assert results[84].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[88].table == "inet" + assert results[88].row_id == 1 + assert results[88].invi == 1 + assert results[88].agrp == "com.apple.security.octagon" + assert results[88].pdmn == "cku" + assert results[88].sync == 0 + assert results[88].tomb == 0 + assert results[88].sysb == 1 + assert results[88].clip == 0 + assert results[88].ggrp == "" + assert results[88].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[89].table == "keys" + assert results[89].row_id == 1 + assert results[89].crtr == "0" + assert results[89].type == "0" + assert results[89].agrp == "com.apple.routined" + assert results[89].pdmn == "ck" + assert results[89].sync == 1 + assert results[89].tomb == 0 + assert results[89].clip == 0 + assert results[89].ggrp == "" + assert results[89].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[94].table == "tversion" + assert results[94].row_id == 1 + assert results[94].version == "12" + assert results[94].minor == 12 + assert results[94].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[95].table == "metadatakeys" + assert results[95].keyclass == "6" + assert results[95].actual_key_class == "6" + assert results[95].data == ( + "r\udcccߨ\udcfdeѽi\udcb5\x16\udcd5+\udcf7\udcfa\udca0\udce4\udce7\x13+\udc9d\udc8b\udcae\x06g\udcb4\udc9b\udca1ѡ\udca2\udca1aY摳K\udc83\udc80" # noqa RUF001 + ) + assert results[95].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py new file mode 100644 index 0000000000..cba996ebba --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.tcc import TCCPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "user.db", + "system.db", + ), + ( + "/Users/user/Library/Application Support/com.apple.TCC/TCC.db", + "/Library/Application Support/com.apple.TCC/TCC.db", + ), + ), + ], +) +def test_tcc( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + tz = timezone.utc + + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/tcc/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(TCCPlugin) + + results = list(target_unix.tcc()) + results.sort(key=lambda r: r.source) + + assert len(results) == 51 + + assert results[0].table == "admin" + assert results[0].key == "version" + assert results[0].value == "32" + assert results[0].source == "/Library/Application Support/com.apple.TCC/TCC.db" + + assert results[-2].table == "access" + assert results[-2].service == "kTCCServiceLiverpool" + assert results[-2].client == "com.apple.homeenergyd" + assert results[-2].client_type == 0 + assert results[-2].auth_value == 2 + assert results[-2].auth_reason == 4 + assert results[-2].auth_version == 1 + assert results[-2].csreq is None + assert results[-2].policy_id is None + assert results[-2].indirect_object_identifier_type == "0" + assert results[-2].indirect_object_identifier == "UNUSED" + assert results[-2].indirect_object_code_identity is None + assert results[-2].indirect_object_identifier_type == "0" + assert results[-2].flags == 0 + assert results[-2].last_modified == datetime(2026, 3, 25, 14, 25, 29, tzinfo=tz) + assert results[-2].pid is None + assert results[-2].pid_version is None + assert results[-2].boot_uuid == "UNUSED" + assert results[-2].last_reminded == datetime(2026, 3, 25, 14, 25, 29, tzinfo=tz) + assert results[-2].source == "/Users/user/Library/Application Support/com.apple.TCC/TCC.db" + + assert results[-1].table == "integrity_flag" + assert results[-1].key == "integrity_flag" + assert results[-1].value == "0" + assert results[-1].source == "/Users/user/Library/Application Support/com.apple.TCC/TCC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py new file mode 100644 index 0000000000..5807cf9e6c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.text_replacements import TextReplacementsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "TextReplacements.db", + "TextReplacements.db-wal", + ] + ], +) +def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/text_replacements/{test_file}") + fs_unix.map_file(f"Users/user/Library/KeyboardServices/{test_file}", data_file) + entry = fs_unix.get(f"Users/user/Library/KeyboardServices/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(TextReplacementsPlugin) + + results = list(target_unix.text_replacements()) + + assert len(results) == 8 + + assert results[0].table == "ZTEXTREPLACEMENTENTRY" + assert results[0].z_pk == 1 + assert results[0].z_ent == 1 + assert results[0].z_opt == 1 + assert results[0].z_needs_save_to_cloud == 1 + assert results[0].z_was_deleted == 0 + assert results[0].z_timestamp == "796140768.339994" + assert results[0].z_phrase == "On my way!" + assert results[0].z_shortcut == "omw" + assert results[0].z_unique_name == "CB36B9A8-B570-4972-BC6C-B12DAA7C0B97" + assert results[0].z_remote_record_info is None + assert results[0].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[1].table == "ZTRCLOUDKITSYNCSTATE" + assert results[1].z_pk == 1 + assert results[1].z_ent == 2 + assert results[1].z_opt == 1 + assert results[1].z_did_pull_once == 0 + assert results[1].z_fetch_change_token is None + assert results[1].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[2].table == "Z_PRIMARYKEY" + assert results[2].z_ent == 1 + assert results[2].z_name == "TextReplacementEntry" + assert results[2].z_super == 0 + assert results[2].z_max == 1 + assert results[2].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[3].table == "Z_PRIMARYKEY" + assert results[3].z_ent == 2 + assert results[3].z_name == "TRCloudKitSyncState" + assert results[3].z_super == 0 + assert results[3].z_max == 1 + assert results[3].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[4].ns_persistence_maximum_framework_version == 1526 + assert results[4].ns_store_type == "SQLite" + assert results[4].ns_auto_vacuum_level == "2" + assert results[4].ns_store_model_version_identifiers == [""] + assert results[4].ns_store_model_version_hashes_version == 3 + assert results[4].ns_store_model_version_hashes_digest == ( + "nb/qJ+hB9auf83oGYKFndhE+Etk/7JNbNosAbVi5Zu0biqTNK/UfC4ofTqRhou6nHBT1ci00ct9E+U9vnbhfPw==" + ) + assert results[4].ns_store_model_version_checksum_key == ("c4ljuOCxf+SvLXrpw0Xcnwe7kkFsMpkUuv43N2r16m4=") + assert results[4].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[5].tr_cloud_kit_sync_state == ( + "\udca2 \udc8cuo\udcd1V\udcd2p\udc89C#\udcbd\udcb1$\udcf1F\udc97X\udce9\x01\x1e\x02\udccb\udcf5\udce5y$\udc90\udcc4\udcee\udcdf" # noqa E501 + ) + assert results[5].text_replacement_entry == ( + "#C\t\udcd2l\udca7\udceaK##S\udcae\udcb7>\udc82\udcb5\udcb5ȯB\udcf4\t\udcc4]\udca2)\udcb0}+٫\x03" # noqa RUF001 + ) + assert results[5].plist_path == "NSStoreModelVersionHashes" + assert results[5].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[6].table == "Z_METADATA" + assert results[6].z_version == 1 + assert results[6].z_uuid == "555547C2-D9F5-4B51-8BEE-EEE6158CDDED" + assert results[6].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py new file mode 100644 index 0000000000..c3d4bedaba --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.user_accounts import UserAccountsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "Accounts4.sqlite", + "Accounts4.sqlite-wal", + ] + ], +) +def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/user_accounts/{test_file}") + fs_unix.map_file(f"Users/user/Library/Accounts/{test_file}", data_file) + entry = fs_unix.get(f"Users/user/Library/Accounts/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(UserAccountsPlugin) + + results = list(target_unix.user_accounts()) + + assert len(results) == 290 + + assert results[0].table == "ZACCESSOPTIONSKEY" + assert results[0].z_pk == 1 + assert results[0].z_ent == 1 + assert results[0].z_opt == 1 + assert results[0].z_enum_value == 0 + assert results[0].z_name == "ACTencentWeiboAppIdKey" + assert results[0].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[7].table == "Z_1OWNINGACCOUNTTYPES" + assert results[7].z_1_access_keys == 7 + assert results[7].z_4_owning_account_types == 43 + assert results[7].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[19].table == "ZACCOUNT" + assert results[19].z_pk == 1 + assert results[19].z_ent == 2 + assert results[19].z_opt == 7 + assert results[19].z_active == 0 + assert results[19].z_authenticated == 0 + assert results[19].z_supports_authentication == 1 + assert results[19].z_visible == 1 + assert results[19].z_warming_up == 0 + assert results[19].z_account_type == 50 + assert results[19].z_parent_account is None + assert results[19].z_username == "local" + assert results[19].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[21].table == "Z_2ENABLEDDATACLASSES" + assert results[21].z_2_enabled_accounts == 2 + assert results[21].z_7_enabled_dataclasses == 20 + assert results[21].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[22].table == "ZACCOUNTPROPERTY" + assert results[22].z_pk == 1 + assert results[22].z_ent == 3 + assert results[22].z_opt == 1 + assert results[22].z_owner == 1 + assert results[22].z_key == "isLocalAccount" + assert results[22].z_value is not None + assert results[22].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[36].table == "ZACCOUNTTYPE" + assert results[36].z_pk == 1 + assert results[36].z_ent == 4 + assert results[36].z_opt == 1 + assert results[36].z_obsolete == 0 + assert results[36].z_supports_authentication == 1 + assert results[36].z_supports_multiple_accounts == 1 + assert results[36].z_visibility == 0 + assert results[36].z_account_type_description == "Gmail" + assert results[36].z_credential_type == "oauth2" + assert results[36].z_identifier == "com.apple.account.Google" + assert results[36].source == "/Users/user/Library/Accounts/Accounts4.sqlite" From 820629c6954b14f7e450d8423af613cd509d6388 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 7 May 2026 13:29:06 +0200 Subject: [PATCH 11/52] add macOS persistence parsers --- .../os/unix/bsd/darwin/macos/login_window.py | 1 - .../bsd/darwin/macos/persistence/cronjobs.py | 124 +++ .../bsd/darwin/macos/persistence/launchers.py | 352 ++++--- .../darwin/macos/persistence/login_items.py | 1 - .../bsd/darwin/macos/persistence/periodic.py | 274 +++++ .../bsd/darwin/macos/persistence/cronjobs/one | 3 + .../bsd/darwin/macos/persistence/cronjobs/two | 3 + .../launchers/com.apple.AMPArtworkAgent.plist | 3 - .../com.apple.WirelessRadioManager-osx.plist | 3 - .../com.apple.cfprefsd.xpc.daemon.plist | 3 - .../launchers/com.apple.ecosystemagent.plist | 3 + .../com.apple.familynotificationd.plist | 3 - .../com.apple.perfpowermetricd.plist | 3 - .../launchers/com.apple.seserviced.plist | 3 - .../com.apple.sidecar-hid-relay.plist | 3 - .../launchers/com.openssh.ssh-agent.plist | 3 + .../launchers/org.cups.cupsd.plist | 3 + .../macos/persistence/periodic/110.clean-tmps | 3 + .../macos/persistence/periodic/199.rotate-fax | 3 + .../macos/persistence/periodic/periodic.conf | 3 + .../darwin/macos/persistence/test_cronjobs.py | 59 ++ .../macos/persistence/test_launchers.py | 932 +++--------------- .../darwin/macos/persistence/test_periodic.py | 149 +++ 23 files changed, 950 insertions(+), 987 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index 06cc6e7cc8..78bb9e037d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -53,7 +53,6 @@ def _find_files(self) -> None: self.login_window_files.add(path) @export(record=DynamicDescriptor(["string"])) - # @export(output="yield") def login_window(self) -> Iterator[DynamicDescriptor]: """Yield macOS login window plist files.""" yield from build_plist_records(self, self.login_window_files, function_name="macos/login_window") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py new file mode 100644 index 0000000000..bdd33cc5ad --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + from pathlib import Path + + from dissect.target.target import Target + + +CronjobRecord = TargetRecordDescriptor( + "macos/cronjob", + [ + ("string", "minute"), + ("string", "hour"), + ("string", "day"), + ("string", "month"), + ("string", "weekday"), + ("string", "command"), + ("path", "source"), + ], +) + +EnvironmentVariableRecord = TargetRecordDescriptor( + "macos/environmentvariable", + [ + ("string", "key"), + ("string", "value"), + ("path", "source"), + ], +) + +RE_CRONJOB = re.compile( + r""" + ^ + (?P\S+) + \s+ + (?P\S+) + \s+ + (?P\S+) + \s+ + (?P\S+) + \s+ + (?P\S+) + \s+ + (?P.+) + $ + """, + re.VERBOSE, +) +RE_ENVVAR = re.compile(r"^(?P[a-zA-Z_]+[a-zA-Z[0-9_])=(?P.*)") + + +class CronjobPlugin(Plugin): + """macOS cronjob plugin.""" + + CRONTAB_DIRS = ( + "/usr/lib/cron/tabs", + "/var/at/tabs", + "/var/cron/tabs", + ) + + CRONTAB_FILES = ("/etc/crontab",) + + def __init__(self, target: Target): + super().__init__(target) + self.crontabs = list(self.find_crontabs()) + + def check_compatible(self) -> None: + if not self.crontabs: + raise UnsupportedPluginError("No crontab(s) found on target") + + def find_crontabs(self) -> Iterator[Path]: + for crontab_dir in self.CRONTAB_DIRS: + if not (dir := self.target.fs.path(crontab_dir)).exists(): + continue + + for file in dir.iterdir(): + if file.resolve().is_file(): + yield file + + for crontab_file in self.CRONTAB_FILES: + if (file := self.target.fs.path(crontab_file)).exists(): + yield file + + @export(record=[CronjobRecord, EnvironmentVariableRecord]) + def cronjobs(self) -> Iterator[CronjobRecord | EnvironmentVariableRecord]: + """Yield cronjobs, and their configured environment variables on a macOS system. + + A cronjob is a scheduled task/command on a macOS system. Adversaries may use cronjobs to gain + persistence on the system. + """ + for file in self.crontabs: + for line in file.open("rt"): + line = line.strip() + if line.startswith("#") or not line: + continue + + if match := RE_CRONJOB.search(line): + match = match.groupdict() + + yield CronjobRecord( + **match, + source=file, + _target=self.target, + ) + + # Some cron implementations allow for environment variables to be set inside crontab files. + elif match := RE_ENVVAR.search(line): + match = match.groupdict() + yield EnvironmentVariableRecord( + **match, + source=file, + _target=self.target, + ) + + else: + self.target.log.warning("Unable to match cronjob line in %s: '%s'", file, line) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index e60b05e2b7..de9876b688 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -16,227 +16,218 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -LauncherRecord1 = [ - ("string", "Label"), - ("string", "Disabled"), - ("string", "UserName"), - ("string", "GroupName"), - ("string", "Group"), - ("string", "CFBundleIdentifier"), - ("string", "CFBundleDevelopmentRegion"), - ("string", "CFBundleInfoDictionaryVersion"), - ("string", "CFBundleName"), - ("boolean", "InitGroups"), - ("string", "Program"), - ("string", "BundleProgram"), - ("string[]", "ProgramArguments"), - ("boolean", "EnableGlobbing"), - ("boolean", "EnableTransactions"), - ("boolean", "PressuredExit"), - ("boolean", "EnablePressureExit"), - ("string", "EnablePressuredExit"), - ("string", "KeepAlive"), - ("boolean", "SuccessfulExit"), - ("boolean", "Crashed"), - ("boolean", "RunAtLoad"), - ("string", "RootDirectory"), - ("string", "WorkingDirectory"), - ("varint", "Umask"), - ("varint", "TimeOut"), - ("varint", "ExitTimeOut"), - ("varint", "ThrottleInterval"), - ("varint", "StartInterval"), - ("boolean", "StartOnMount"), - ("string[]", "WatchPaths"), - ("string[]", "QueueDirectories"), - ("string", "StandardInPath"), - ("string", "StandardOutPath"), - ("string", "StandardErrorPath"), - ("boolean", "Debug"), - ("boolean", "WaitForDebugger"), - ("varint", "Nice"), - ("string", "ProcessType"), - ("boolean", "PowerNap"), - ("boolean", "AbandonProcessGroup"), - ("boolean", "LowPriorityIO"), - ("boolean", "LowPriorityBackgroundIO"), - ("boolean", "MaterializeDatalessFiles"), - ("boolean", "LaunchOnlyOnce"), - ("boolean", "BootShell"), - ("boolean", "SessionCreate"), - ("boolean", "LegacyTimers"), - ("boolean", "TransactionTimeLimitEnabled"), - ("boolean", "LimitLoadToDeveloperMode"), - ("string", "LimitLoadToSessionType"), - ("boolean", "Wait"), - ("string[]", "AssociatedBundleIdentifiers"), +LauncherRecord = [ + ("string", "label"), + ("string", "program"), + ("string[]", "program_arguments"), + ("string", "keep_alive"), + ("boolean", "on_demand"), + ("string", "disabled"), + ("boolean", "run_at_load"), + ("boolean", "launch_only_once"), + ("string", "process_type"), + ("boolean", "wait"), + ("string", "limit_load_to_session_type"), + ("boolean", "limit_load_to_developer_mode"), + ("string", "limit_load_from_variant"), + ("string", "limit_load_to_variant"), + ("string", "limit_load_from_boot_mode"), + ("string[]", "limit_load_to_boot_mode"), + ("string[]", "limit_load_from_hardware"), + ("string[]", "limit_load_to_hardware"), + ("string[]", "launch_events"), + ("string[]", "mach_services"), + ("string", "enable_pressured_exit"), + ("boolean", "enable_transactions"), + ("string[]", "environment_variables"), + ("string", "user_name"), + ("boolean", "init_groups"), + ("string", "group_name"), + ("varint", "start_interval"), + ("string[]", "start_calendar_interval"), + ("varint", "throttle_interval"), + ("boolean", "enable_globbing"), + ("string", "standard_in_path"), + ("string", "standard_out_path"), + ("string", "standard_error_path"), + ("varint", "nice"), + ("boolean", "abandon_process_group"), + ("boolean", "low_priority_io"), + ("string", "root_directory"), + ("string", "working_directory"), + ("varint", "umask"), + ("varint", "time_out"), + ("varint", "exit_time_out"), + ("string[]", "watch_paths"), + ("string[]", "queue_directories"), + ("boolean", "start_on_mount"), + ("string[]", "soft_resource_limits"), + ("string[]", "hard_resource_limits"), + ("boolean", "debug"), + ("boolean", "wait_for_debugger"), ("string", "plist_path"), - ("string", "POSIXSpawnType"), - ("string", "PosixSpawnType"), - ("string", "MultipleInstances"), - ("boolean", "DisabledInSafeBoot"), - ("boolean", "BeginTransactionAtShutdown"), - ("boolean", "OnDemand"), - ("boolean", "AlwaysSIGTERMOnShutdown"), - ("boolean", "MinimalBootProfiles"), - ("boolean", "MinimalBootProfile"), - ("boolean", "LSBackgroundOnly"), - ("boolean", "HopefullyExitsLast"), - ("boolean", "ExponentialThrottling"), - ("boolean", "IgnoreProcessGroupAtShutdown"), - ("boolean", "EventMonitor"), - ("boolean", "AuxiliaryBootstrapper"), - ("boolean", "AuxiliaryBootstrapperAllowDemand"), - ("boolean", "DrainMessagesAfterFailedInit"), - ("boolean", "DrainMessagesOnFailedInit"), - ("string", "LimitLoadFromVariant"), - ("string", "LimitLoadFromBootMode"), - ("string", "EfficiencyMode"), - ("string", "Conclave"), - ("string[]", "LaunchEvents"), - ("string[]", "MachServices"), - ("string[]", "SoftResourceLimits"), - ("string[]", "HardResourceLimits"), - ("string[]", "EnvironmentVariables"), - ("string[]", "NoEnvironmentVariables"), - ("string[]", "NO_EnvironmentVariables"), - ("string", "SHAuthorizationRight"), - ("string", "PublishesEvents"), - ("string", "LimitLoadToVariant"), - ("string", "RunLoopType"), - ("string[]", "RemoteServices"), - ("string[]", "JetsamProperties"), - ("string[]", "LimitLoadToHardware"), - ("string[]", "PanicOnCrash"), - ("string[]", "LimitLoadToBootMode"), - ("string", "Cryptex"), - ("string", "ServiceType"), - ("string", "ServiceIPC"), - ("string[]", "UrgentLogSubmission"), - ("string[]", "AppIntents"), - ("string[]", "BinaryOrderPreference"), - ("string[]", "LimitLoadFromHardware"), - ("string[]", "StartCalendarInterval"), - ("string[]", "AdditionalProperties"), - ("string[]", "NSAppTransportSecurity"), - ("string[]", "com_apple_usbcd"), - ("string[]", "com_apple_private_tcc_allow"), - ("string[]", "com_apple_security_application_groups"), - ("string[]", "com_apple_private_security_restricted_application_groups"), - ("string[]", "com_apple_security_exception_files_home_relative_path_read_write"), - ("string[]", "com_apple_security_exception_mach_lookup_global_name"), - ("string[]", "com_apple_security_exception_sysctl_read_only"), - ("boolean", "com_apple_private_security_no_sandbox"), - ("boolean", "com_apple_ane_iokit_user_access"), - ("boolean", "com_apple_alarm"), - ("boolean", "com_apple_imdpersistence_IMDPersistenceAgent_GroupMetadata"), - ("boolean", "com_apple_imdpersistence_IMDPersistenceAgent_Syndication"), ("path", "source"), ] -LauncherRecord2 = [ - ("boolean", "Wait"), - ("varint", "Instances"), +SocketRecord = [ + ("string", "socket_key"), + ("string", "sock_type"), + ("boolean", "sock_passive"), + ("string", "sock_node_name"), + ("string", "sock_service_name"), + ("string", "sock_family"), + ("string", "sock_protocol"), + ("varint", "sock_path_mode"), + ("string", "sock_path_name"), + ("string", "secure_socket_with_key"), + ("varint", "sock_path_owner"), + ("varint", "sock_path_group"), + ("string", "bonjour"), + ("string", "multicast_group"), + ("boolean", "receive_packet_info"), ("string", "plist_path"), ("path", "source"), ] -LauncherRecord3 = [ - ("string", "SocketKey"), - ("string", "SockType"), - ("boolean", "SockPassive"), - ("string", "SockNodeName"), - ("string", "SockServiceName"), - ("string", "SockFamily"), - ("string", "SockProtocol"), - ("varint", "SockPathMode"), - ("string", "SockPathName"), - ("string", "SecureSocketWithKey"), - ("varint", "SockPathOwner"), - ("varint", "SockPathGroup"), - ("varint", "SockPathMode"), - ("string", "Bonjour"), - ("string", "MulticastGroup"), - ("boolean", "ReceivePacketInfo"), +UNRecord = [ + ("boolean", "un_setting_alerts"), + ("boolean", "un_setting_always_show_previews"), + ("boolean", "un_setting_lock_screen"), + ("boolean", "un_setting_modal_alert_style"), + ("boolean", "un_automatically_show_settings"), + ("boolean", "un_setting_notification_center"), + ("boolean", "un_daemon_should_receive_background_responses"), + ("boolean", "un_suppress_user_authorization_prompt"), ("string", "plist_path"), ("path", "source"), ] -LauncherRecord4 = [ - ("string[]", "Version4"), - ("path", "source"), -] - -LauncherRecord5 = [ - ("boolean", "UNSettingAlerts"), - ("boolean", "UNSettingAlwaysShowPreviews"), - ("boolean", "UNSettingLockScreen"), - ("boolean", "UNSettingModalAlertStyle"), - ("boolean", "UNAutomaticallyShowSettings"), - ("boolean", "UNSettingNotificationCenter"), - ("boolean", "UNDaemonShouldReceiveBackgroundResponses"), - ("boolean", "UNSuppressUserAuthorizationPrompt"), - ("string", "plist_path"), - ("path", "source"), -] - -LauncherRecord6 = [ - ("string", "UNNotificationIconDefault"), - ("string", "UNNotificationIconSettings"), +UNNotificationRecord = [ + ("string", "un_notification_icon_default"), + ("string", "un_notification_icon_settings"), ("string", "plist_path"), ("path", "source"), ] -LauncherRecord7 = [ - ("string[]", "Listeners"), +ListenersRecord = [ + ("string[]", "listeners"), ("string", "plist_path"), ("path", "source"), ] -TargetRecordDescriptor( - "macos/launch_daemons", - LauncherRecord1, -) +FIELD_MAPPINGS = { + # LauncherRecord + "Label": "label", + "Program": "program", + "ProgramArguments": "program_arguments", + "KeepAlive": "keep_alive", + "OnDemand": "on_demand", + "Disabled": "disabled", + "RunAtLoad": "run_at_load", + "LaunchOnlyOnce": "launch_only_once", + "ProcessType": "process_type", + "Wait": "wait", + "LimitLoadToSessionType": "limit_load_to_session_type", + "LimitLoadFromHardware": "limit_load_from_hardware", + "LimitLoadToDeveloperMode": "limit_load_to_developer_mode", + "LimitLoadFromVariant": "limit_load_from_variant", + "LimitLoadToVariant": "limit_load_to_variant", + "LimitLoadFromBootMode": "limit_load_from_boot_mode", + "LimitLoadToBootMode": "limit_load_to_boot_mode", + "LimitLoadToHardware": "limit_load_to_hardware", + "LaunchEvents": "launch_events", + "MachServices": "mach_services", + "EnablePressuredExit": "enable_pressured_exit", + "EnableTransactions": "enable_transactions", + "EnvironmentVariables": "environment_variables", + "UserName": "user_name", + "GroupName": "group_name", + "StartInterval": "start_interval", + "StartCalendarInterval": "start_calendar_interval", + "ThrottleInterval": "throttle_interval", + "EnableGlobbing": "enable_globbing", + "StandardInPath": "standard_in_path", + "StandardOutPath": "standard_out_path", + "StandardErrorPath": "standard_error_path", + "Nice": "nice", + "LowPriorityIO": "low_priority_io", + "AbandonProcessGroup": "abandon_process_group", + "RootDirectory": "root_directory", + "WorkingDirectory": "working_directory", + "Umask": "umask", + "TimeOut": "time_out", + "ExitTimeOut": "exit_time_out", + "InitGroups": "init_groups", + "WatchPaths": "watch_paths", + "QueueDirectories": "queue_directories", + "StartOnMount": "start_on_mount", + "SoftResourceLimits": "soft_resource_limits", + "HardResourceLimits": "hard_resource_limits", + "Debug": "debug", + "WaitForDebugger": "wait_for_debugger", + # SocketRecord + "SocketKey": "socket_key", + "SockType": "sock_type", + "SockPassive": "sock_passive", + "SockNodeName": "sock_node_name", + "SockServiceName": "sock_service_name", + "SockFamily": "sock_family", + "SockProtocol": "sock_protocol", + "SockPathMode": "sock_path_mode", + "SockPathName": "sock_path_name", + "SecureSocketWithKey": "secure_socket_with_key", + "SockPathOwner": "sock_path_owner", + "SockPathGroup": "sock_path_group", + "Bonjour": "bonjour", + "MulticastGroup": "multicast_group", + "ReceivePacketInfo": "receive_packet_info", + # UNRecord + "UNSettingAlerts": "un_setting_alerts", + "UNSettingAlwaysShowPreviews": "un_setting_always_show_previews", + "UNSettingLockScreen": "un_setting_lock_screen", + "UNSettingModalAlertStyle": "un_setting_modal_alert_style", + "UNAutomaticallyShowSettings": "un_automatically_show_settings", + "UNSettingNotificationCenter": "un_setting_notification_center", + "UNDaemonShouldReceiveBackgroundResponses": "un_daemon_should_receive_background_responses", + "UNSuppressUserAuthorizationPrompt": "un_suppress_user_authorization_prompt", + # UNNotificationRecord + "UNNotificationIconDefault": "un_notification_icon_default", + "UNNotificationIconSettings": "un_notification_icon_settings", + # ListenersRecord + "Listeners": "listeners", +} LaunchAgentRecords = ( TargetRecordDescriptor( "macos/launch_agents", - LauncherRecord1, + LauncherRecord, ), TargetRecordDescriptor( "macos/launch_agents/socket", - LauncherRecord3, + SocketRecord, ), TargetRecordDescriptor( "macos/launch_agents/un", - LauncherRecord5, + UNRecord, ), TargetRecordDescriptor( "macos/launch_agents/un_notification", - LauncherRecord6, + UNNotificationRecord, ), ) LaunchDaemonRecords = ( TargetRecordDescriptor( "macos/launch_daemons", - LauncherRecord1, - ), - TargetRecordDescriptor( - "macos/launch_daemons/wait", - LauncherRecord2, + LauncherRecord, ), TargetRecordDescriptor( "macos/launch_daemons/socket", - LauncherRecord3, - ), - TargetRecordDescriptor( - "macos/launch_daemons/version4", - LauncherRecord4, + SocketRecord, ), TargetRecordDescriptor( "macos/launch_daemons/listeners", - LauncherRecord7, + ListenersRecord, ), ) @@ -319,12 +310,15 @@ def _find_files(self) -> None: self.launch_daemon_files.add(path) @export(record=LaunchAgentRecords) - # @export(output="yield") def launch_agents(self) -> Iterator[LaunchAgentRecords]: """Yield macOS launch agent plist files.""" - yield from build_plist_records(self, self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS) + yield from build_plist_records( + self, self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS, FIELD_MAPPINGS + ) @export(record=LaunchDaemonRecords) def launch_daemons(self) -> Iterator[LaunchDaemonRecords]: """Yield macOS launch daemon plist files.""" - yield from build_plist_records(self, self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS) + yield from build_plist_records( + self, self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS, FIELD_MAPPINGS + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py index d9e14a4fe5..a718f4df32 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py @@ -68,7 +68,6 @@ def _find_files(self) -> None: self.login_items_files.add(path) @export(record=LoginItemsRecord) - # @export(output="yield") def login_items(self) -> Iterator[LoginItemsRecord]: """Yield macOS login items plist files.""" yield from build_plist_records(self, self.login_items_files, LoginItemsRecords, field_mappings=FIELD_MAPPINGS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py new file mode 100644 index 0000000000..a70fc0aa1a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +PeriodicScriptsRecord = TargetRecordDescriptor( + "macos/periodic_scripts", + [ + ("path", "source"), + ], +) + +PeriodicConfRecord = TargetRecordDescriptor( + "macos/periodic_conf", + [ + ("string", "local_periodic"), + ("string", "dir_output"), + ("boolean", "dir_show_success"), + ("boolean", "dir_show_info"), + ("boolean", "dir_show_badconfig"), + ("varint", "anticongestion_sleeptime"), + ("path", "source"), + ], +) + +PeriodicConfDailyRecord = TargetRecordDescriptor( + "macos/periodic_conf/daily", + [ + ("boolean", "daily_clean_disks_enable"), + ("string", "daily_clean_disks_files"), + ("varint", "daily_clean_disks_days"), + ("boolean", "daily_clean_disks_verbose"), + ("boolean", "daily_clean_tmps_enable"), + ("string", "daily_clean_tmps_dirs"), + ("varint", "daily_clean_tmps_days"), + ("string", "daily_clean_tmps_ignore"), + ("boolean", "daily_clean_tmps_verbose"), + ("boolean", "daily_clean_preserve_enable"), + ("varint", "daily_clean_preserve_days"), + ("boolean", "daily_clean_preserve_verbose"), + ("boolean", "daily_clean_msgs_enable"), + ("varint", "daily_clean_msgs_days"), + ("boolean", "daily_clean_rwho_enable"), + ("varint", "daily_clean_rwho_days"), + ("boolean", "daily_clean_rwho_verbose"), + ("boolean", "daily_clean_hoststat_enable"), + ("boolean", "daily_backup_efi_enable"), + ("boolean", "daily_backup_gmirror_enable"), + ("boolean", "daily_backup_gmirror_verbose"), + ("boolean", "daily_backup_gpart_enable"), + ("boolean", "daily_backup_gpart_verbose"), + ("boolean", "daily_backup_passwd_enable"), + ("boolean", "daily_backup_aliases_enable"), + ("boolean", "daily_backup_zfs_enable"), + ("string", "daily_backup_zfs_list_flags"), + ("string", "daily_backup_zpool_list_flags"), + ("boolean", "daily_backup_zfs_props_enable"), + ("string", "daily_backup_zfs_get_flags"), + ("string", "daily_backup_zpool_get_flags"), + ("boolean", "daily_backup_zfs_verbose"), + ("boolean", "daily_calendar_enable"), + ("boolean", "daily_accounting_enable"), + ("boolean", "daily_accounting_compress"), + ("varint", "daily_accounting_save"), + ("string", "daily_accounting_flags"), + ("boolean", "daily_status_disks_enable"), + ("string", "daily_status_disks_df_flags"), + ("boolean", "daily_status_zfs_enable"), + ("boolean", "daily_status_zfs_zpool_list_enable"), + ("boolean", "daily_status_gmirror_enable"), + ("boolean", "daily_status_graid3_enable"), + ("boolean", "daily_status_gstripe_enable"), + ("boolean", "daily_status_gconcat_enable"), + ("boolean", "daily_status_mfi_enable"), + ("boolean", "daily_status_network_enable"), + ("string", "daily_status_network_netstat_flags"), + ("boolean", "daily_status_network_usedns"), + ("boolean", "daily_status_uptime_enable"), + ("boolean", "daily_status_mailq_enable"), + ("boolean", "daily_status_mailq_shorten"), + ("boolean", "daily_status_include_submit_mailq"), + ("boolean", "daily_status_security_enable"), + ("boolean", "daily_status_security_inline"), + ("string", "daily_status_security_output"), + ("boolean", "daily_status_mail_rejects_enable"), + ("varint", "daily_status_mail_rejects_logs"), + ("boolean", "daily_status_ntpd_enable"), + ("boolean", "daily_status_world_kernel"), + ("boolean", "daily_queuerun_enable"), + ("boolean", "daily_submit_queuerun"), + ("boolean", "daily_scrub_zfs_enable"), + ("string", "daily_scrub_zfs_pools"), + ("varint", "daily_scrub_zfs_default_threshold"), + ("boolean", "daily_trim_zfs_enable"), + ("string", "daily_trim_zfs_pools"), + ("string", "daily_local"), + ("string", "daily_diff_flags"), + ("path", "source"), + ], +) + +PeriodicConfWeeklyRecord = TargetRecordDescriptor( + "macos/periodic_conf/weekly", + [ + ("boolean", "weekly_locate_enable"), + ("boolean", "weekly_whatis_enable"), + ("boolean", "weekly_noid_enable"), + ("string", "weekly_noid_dirs"), + ("boolean", "weekly_status_security_enable"), + ("boolean", "weekly_status_security_inline"), + ("string", "weekly_status_security_output"), + ("boolean", "weekly_status_pkg_enable"), + ("string", "pkg_version"), + ("string", "pkg_version_index"), + ("string", "weekly_local"), + ("path", "source"), + ], +) + +PeriodicConfMonthlyRecord = TargetRecordDescriptor( + "macos/periodic_conf/monthly", + [ + ("boolean", "monthly_accounting_enable"), + ("boolean", "monthly_status_security_enable"), + ("boolean", "monthly_status_security_inline"), + ("string", "monthly_status_security_output"), + ("string", "monthly_local"), + ("path", "source"), + ], +) + +PeriodicConfSecurityRecord = TargetRecordDescriptor( + "macos/periodic_conf/security", + [ + ("string", "security_status_diff_flags"), + ("boolean", "security_status_chksetuid_enable"), + ("string", "security_status_chksetuid_period"), + ("boolean", "security_status_chkportsum_enable"), + ("string", "security_status_chkportsum_period"), + ("boolean", "security_status_neggrpperm_enable"), + ("string", "security_status_neggrpperm_period"), + ("boolean", "security_status_chkmounts_enable"), + ("string", "security_status_chkmounts_period"), + ("boolean", "security_status_noamd"), + ("boolean", "security_status_chkuid0_enable"), + ("string", "security_status_chkuid0_period"), + ("boolean", "security_status_passwdless_enable"), + ("string", "security_status_passwdless_period"), + ("boolean", "security_status_logincheck_enable"), + ("string", "security_status_logincheck_period"), + ("boolean", "security_status_ipfwdenied_enable"), + ("string", "security_status_ipfwdenied_period"), + ("boolean", "security_status_ipfdenied_enable"), + ("string", "security_status_ipfdenied_period"), + ("boolean", "security_status_pfdenied_enable"), + ("string", "security_status_pfdenied_additionalanchors"), + ("string", "security_status_pfdenied_period"), + ("boolean", "security_status_ipfwlimit_enable"), + ("string", "security_status_ipfwlimit_period"), + ("boolean", "security_status_kernelmsg_enable"), + ("string", "security_status_kernelmsg_period"), + ("boolean", "security_status_loginfail_enable"), + ("string", "security_status_loginfail_period"), + ("boolean", "security_status_tcpwrap_enable"), + ("string", "security_status_tcpwrap_period"), + ("path", "source"), + ], +) + +PeriodicConfRecords = ( + PeriodicConfRecord, + PeriodicConfDailyRecord, + PeriodicConfWeeklyRecord, + PeriodicConfMonthlyRecord, + PeriodicConfSecurityRecord, +) + + +class PeriodicPlugin(Plugin): + """macOS periodic plugin.""" + + PERIODIC_SCRIPTS_PATHS = ( + "/etc/daily.local/*", + "/etc/monthly.local/*", + "/etc/periodic/**2", + "/etc/periodic/daily/*", + "/etc/periodic/monthly/*", + "/etc/periodic/weekly/*", + "/etc/weekly.local/*", + "/usr/local/etc/periodic/**2", + ) + + PERIODIC_CONF_PATHS = ( + "/etc/defaults/periodic.conf", + "/etc/periodic.conf", + "/etc/periodic.conf.local", + ) + + def __init__(self, target: Target): + super().__init__(target) + + self.periodic_scripts_files = set() + self.periodic_conf_files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.periodic_scripts_files or self.periodic_conf_files): + raise UnsupportedPluginError("No periodic files found") + + def _find_files(self) -> None: + for pattern in self.PERIODIC_SCRIPTS_PATHS: + for path in self.target.fs.glob(pattern): + self.periodic_scripts_files.add(path) + + for path in self.PERIODIC_CONF_PATHS: + p = self.target.fs.path(path) + if p.exists(): + self.periodic_conf_files.add(p) + + @export(record=PeriodicScriptsRecord) + def periodic_scripts(self) -> Iterator[PeriodicScriptsRecord]: + """Yield macOS periodic script paths.""" + for file in self.periodic_scripts_files: + yield PeriodicScriptsRecord( + source=file, + ) + + @export(record=PeriodicConfRecords) + def periodic_conf(self) -> Iterator[PeriodicConfRecords]: + """Yield macOS periodic configuration information.""" + for file in self.periodic_conf_files: + for record in PeriodicConfRecords: + record_keys = set(record.fields.keys()) + record_dict = {} + + with file.open("r") as f: + for line in f: + line = line.strip() + + if not line or line.startswith("#"): + continue + + line = line.split("#", 1)[0].strip() + + if "=" in line: + key, value = line.split("=", 1) + key = key.strip() + + if key in record_keys: + value = value.strip().strip('"') + if value == "": + continue + elif value == "YES": + value = True + elif value == "NO": + value = False + record_dict[key] = value + + if record_dict: + record_dict["source"] = file + yield record( + **record_dict, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one new file mode 100644 index 0000000000..10568b6a42 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3067b0b0cd5d93d07fd389c50c91741a0c97562695135579d291dd2da62c12a1 +size 260 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two new file mode 100644 index 0000000000..10568b6a42 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3067b0b0cd5d93d07fd389c50c91741a0c97562695135579d291dd2da62c12a1 +size 260 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist deleted file mode 100644 index 598191f177..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.AMPArtworkAgent.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:476b1f6ae8ee5a5f8c456a8f463608c1506e5436f0ca61a96397010eafa6d492 -size 656 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist deleted file mode 100644 index 6c16adb107..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.WirelessRadioManager-osx.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc3a62a161ff90fe68e375709acd444decc7ea038380bd074734af26db5be9f5 -size 695 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist deleted file mode 100644 index 5852dd6bc2..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.cfprefsd.xpc.daemon.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a92e72216f285f1cfc547731aa57694824602fbaa1723b49ee1bef966890598 -size 386 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist new file mode 100644 index 0000000000..cd072ecbb1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c492f0cf292e0fc7cbb29dec47992cfbe87ae98756ee082821ddfc7ae1cb88fb +size 1636 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist deleted file mode 100644 index 6a21f1a5b4..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.familynotificationd.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a550178868f9daa0db2c32b637a4898545debab27e26f17a25e94d574d76bed -size 1876 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist deleted file mode 100644 index 9b469a3fe1..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.perfpowermetricd.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:568b6bd1ff8e8043a59f72291a5828f3a66304277a043914392b43b3586a72fa -size 282 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist deleted file mode 100644 index 1b6b216483..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.seserviced.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5dc00f6312728dbbe92d9d782bdde68e5974225cce5a0296a06d768480bda66b -size 1249 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist deleted file mode 100644 index 7f4b3987cd..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.sidecar-hid-relay.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:98d30b55d0d2feb9fe5b6e57e775299aeb77fc530b6eb98508ba0661cd1d18af -size 236 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist new file mode 100644 index 0000000000..0ec0d20dc2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c88ce7ee57624843ab9e982939a7b13326748145ffda8121a870ff88016929f +size 541 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist new file mode 100644 index 0000000000..290f696ffb --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7c9a867f295208ce48b5bbf8417d737338f02987915469b9b5a829ddf0d2ff2 +size 1201 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps new file mode 100644 index 0000000000..1153df6257 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f99a4803d9f2f8c48c33866487067fbd3a0d02080e10749e70445813b4d60954 +size 1642 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax new file mode 100644 index 0000000000..8fa82b514e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd89ec314b4e26aa2481a315c7e71404ccff929ead511269e934406be0e61143 +size 888 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf new file mode 100644 index 0000000000..a79145da40 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee479a7a0dd839c4a08e73d5a6fcffbe750bd47b82cf7c633ed20c5e63b8ed03 +size 4954 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py new file mode 100644 index 0000000000..d04a4f7396 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.cronjobs import CronjobPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ("one", "two"), + ("/usr/lib/cron/tabs/user", "/var/at/tabs/user"), + ), + ], +) +def test_cronjobs( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(CronjobPlugin) + + results = list(target_unix.cronjobs()) + + assert len(results) == 2 + + assert results[0].minute == "*" + assert results[0].hour == "*" + assert results[0].day == "*" + assert results[0].month == "*" + assert results[0].command == "/Users/user/cron_test.sh" + assert results[0].source == "/usr/lib/cron/tabs/user" + + assert results[1].minute == "*" + assert results[1].hour == "*" + assert results[1].day == "*" + assert results[1].month == "*" + assert results[1].command == "/Users/user/cron_test.sh" + assert results[1].source == "/var/at/tabs/user" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py index 0a032eba8b..3459fd274e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py @@ -19,18 +19,12 @@ [ ( ( - "com.apple.seserviced.plist", - "com.apple.AMPArtworkAgent.plist", - "com.apple.familynotificationd.plist", - "com.apple.sidecar-hid-relay.plist", - "com.apple.WirelessRadioManager-osx.plist", + "com.apple.ecosystemagent.plist", + "com.openssh.ssh-agent.plist", ), ( - "/Users/user/Library/LaunchAgents/com.apple.seserviced.plist", - "/System/Library/LaunchAgents/com.apple.AMPArtworkAgent.plist", - "/System/Library/LaunchAgents/com.apple.familynotificationd.plist", - "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist", - "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist", + "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist", + "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist", ), ), ], @@ -63,462 +57,105 @@ def test_launch_agents( target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_agents()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 4 + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) - assert results[0].hostname == "localhost" - assert results[0].domain is None - assert results[0].Label == "com.apple.sidecar-display-agent" - assert results[0].Disabled is None - assert results[0].UserName is None - assert results[0].GroupName is None - assert results[0].Group is None - assert results[0].CFBundleIdentifier is None - assert results[0].CFBundleDevelopmentRegion is None - assert results[0].CFBundleInfoDictionaryVersion is None - assert results[0].CFBundleName is None - assert results[0].InitGroups is None - assert results[0].Program == "/usr/libexec/SidecarDisplayAgent" - assert results[0].BundleProgram is None - assert results[0].ProgramArguments == [] - assert results[0].EnableGlobbing is None - assert results[0].EnableTransactions - assert results[0].PressuredExit is None - assert results[0].EnablePressureExit is None - assert results[0].EnablePressuredExit == "True" - assert results[0].KeepAlive is None - assert results[0].SuccessfulExit is None - assert results[0].Crashed is None - assert results[0].RunAtLoad is None - assert results[0].RootDirectory is None - assert results[0].WorkingDirectory is None - assert results[0].Umask is None - assert results[0].TimeOut is None - assert results[0].ExitTimeOut is None - assert results[0].ThrottleInterval is None - assert results[0].StartInterval is None - assert results[0].StartOnMount is None - assert results[0].WatchPaths == [] - assert results[0].QueueDirectories == [] - assert results[0].StandardInPath is None - assert results[0].StandardOutPath is None - assert results[0].StandardErrorPath is None - assert results[0].Debug is None - assert results[0].WaitForDebugger is None - assert results[0].Nice is None - assert results[0].ProcessType == "Interactive" - assert results[0].PowerNap is None - assert results[0].AbandonProcessGroup is None - assert results[0].LowPriorityIO is None - assert results[0].LowPriorityBackgroundIO is None - assert results[0].MaterializeDatalessFiles is None - assert results[0].LaunchOnlyOnce is None - assert results[0].BootShell is None - assert results[0].SessionCreate is None - assert results[0].LegacyTimers is None - assert results[0].TransactionTimeLimitEnabled is None - assert results[0].LimitLoadToDeveloperMode is None - assert results[0].LimitLoadToSessionType is None - assert results[0].Wait is None - assert results[0].AssociatedBundleIdentifiers == [] - assert results[0].plist_path is None - assert results[0].POSIXSpawnType is None - assert results[0].PosixSpawnType is None - assert results[0].MultipleInstances is None - assert results[0].DisabledInSafeBoot is None - assert results[0].BeginTransactionAtShutdown is None - assert results[0].OnDemand is None - assert results[0].AlwaysSIGTERMOnShutdown is None - assert results[0].MinimalBootProfiles is None - assert results[0].MinimalBootProfile is None - assert results[0].LSBackgroundOnly is None - assert results[0].HopefullyExitsLast is None - assert results[0].ExponentialThrottling is None - assert results[0].IgnoreProcessGroupAtShutdown is None - assert results[0].EventMonitor is None - assert results[0].AuxiliaryBootstrapper is None - assert results[0].AuxiliaryBootstrapperAllowDemand is None - assert results[0].DrainMessagesAfterFailedInit is None - assert results[0].DrainMessagesOnFailedInit is None - assert results[0].LimitLoadFromVariant is None - assert results[0].LimitLoadFromBootMode is None - assert results[0].EfficiencyMode is None - assert results[0].Conclave is None - assert results[0].LaunchEvents == [] - assert results[0].MachServices == ["('com.apple.sidecar-display-agent', True)"] - assert results[0].SoftResourceLimits == [] - assert results[0].HardResourceLimits == [] - assert results[0].EnvironmentVariables == [] - assert results[0].NoEnvironmentVariables == [] - assert results[0].NO_EnvironmentVariables == [] - assert results[0].SHAuthorizationRight is None - assert results[0].PublishesEvents is None - assert results[0].LimitLoadToVariant is None - assert results[0].RunLoopType is None - assert results[0].RemoteServices == [] - assert results[0].JetsamProperties == [] - assert results[0].LimitLoadToHardware == [] - assert results[0].PanicOnCrash == [] - assert results[0].LimitLoadToBootMode == [] - assert results[0].Cryptex is None - assert results[0].ServiceType is None - assert results[0].ServiceIPC is None - assert results[0].UrgentLogSubmission == [] - assert results[0].AppIntents == [] - assert results[0].BinaryOrderPreference == [] - assert results[0].LimitLoadFromHardware == [] - assert results[0].StartCalendarInterval == [] - assert results[0].AdditionalProperties == [] - assert results[0].NSAppTransportSecurity == [] - assert results[0].source == "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist" - - assert results[1].hostname == "localhost" - assert results[1].domain is None - assert results[1].Label == "com.apple.AMPArtworkAgent" - assert results[1].Disabled is None - assert results[1].UserName is None - assert results[1].GroupName is None - assert results[1].Group is None - assert results[1].CFBundleIdentifier is None - assert results[1].CFBundleDevelopmentRegion is None - assert results[1].CFBundleInfoDictionaryVersion is None - assert results[1].CFBundleName is None - assert results[1].InitGroups is None - assert results[1].Program is None - assert results[1].BundleProgram is None - assert results[1].ProgramArguments == [ - "/System/Library/PrivateFrameworks/AMPLibrary.framework/Versions/A/Support/AMPArtworkAgent", - "--launchd", - ] - assert results[1].EnableGlobbing is None - assert results[1].EnableTransactions - assert results[1].PressuredExit is None - assert results[1].EnablePressureExit is None - assert results[1].EnablePressuredExit == "True" - assert results[1].KeepAlive is None - assert results[1].SuccessfulExit is None - assert results[1].Crashed is None - assert results[1].RunAtLoad is None - assert results[1].RootDirectory is None - assert results[1].WorkingDirectory is None - assert results[1].Umask is None - assert results[1].TimeOut is None - assert results[1].ExitTimeOut is None - assert results[1].ThrottleInterval is None - assert results[1].StartInterval is None - assert results[1].StartOnMount is None - assert results[1].WatchPaths == [] - assert results[1].QueueDirectories == [] - assert results[1].StandardInPath is None - assert results[1].StandardOutPath is None - assert results[1].StandardErrorPath is None - assert results[1].Debug is None - assert results[1].WaitForDebugger is None - assert results[1].Nice is None - assert results[1].ProcessType == "Adaptive" - assert results[1].PowerNap is None - assert results[1].AbandonProcessGroup is None - assert results[1].LowPriorityIO is None - assert results[1].LowPriorityBackgroundIO is None - assert results[1].MaterializeDatalessFiles is None - assert results[1].LaunchOnlyOnce is None - assert results[1].BootShell is None - assert results[1].SessionCreate is None - assert results[1].LegacyTimers is None - assert results[1].TransactionTimeLimitEnabled is None - assert results[1].LimitLoadToDeveloperMode is None - assert results[1].LimitLoadToSessionType is None - assert results[1].Wait is None - assert results[1].AssociatedBundleIdentifiers == [] - assert results[1].plist_path is None - assert results[1].POSIXSpawnType is None - assert results[1].PosixSpawnType is None - assert results[1].MultipleInstances is None - assert results[1].DisabledInSafeBoot is None - assert results[1].BeginTransactionAtShutdown is None - assert results[1].OnDemand is None - assert results[1].AlwaysSIGTERMOnShutdown is None - assert results[1].MinimalBootProfiles is None - assert results[1].MinimalBootProfile is None - assert results[1].LSBackgroundOnly is None - assert results[1].HopefullyExitsLast is None - assert results[1].ExponentialThrottling is None - assert results[1].IgnoreProcessGroupAtShutdown is None - assert results[1].EventMonitor is None - assert results[1].AuxiliaryBootstrapper is None - assert results[1].AuxiliaryBootstrapperAllowDemand is None - assert results[1].DrainMessagesAfterFailedInit is None - assert results[1].DrainMessagesOnFailedInit is None - assert results[1].LimitLoadFromVariant is None - assert results[1].LimitLoadFromBootMode is None - assert results[1].EfficiencyMode is None - assert results[1].Conclave is None - assert results[1].LaunchEvents == [] - assert results[1].MachServices == ["('com.apple.amp.artworkd', True)"] - assert results[1].SoftResourceLimits == [] - assert results[1].HardResourceLimits == [] - assert results[1].EnvironmentVariables == [] - assert results[1].NoEnvironmentVariables == [] - assert results[1].NO_EnvironmentVariables == [] - assert results[1].SHAuthorizationRight is None - assert results[1].PublishesEvents is None - assert results[1].LimitLoadToVariant is None - assert results[1].RunLoopType is None - assert results[1].RemoteServices == [] - assert results[1].JetsamProperties == [] - assert results[1].LimitLoadToHardware == [] - assert results[1].PanicOnCrash == [] - assert results[1].LimitLoadToBootMode == [] - assert results[1].Cryptex is None - assert results[1].ServiceType is None - assert results[1].ServiceIPC is None - assert results[1].UrgentLogSubmission == [] - assert results[1].AppIntents == [] - assert results[1].BinaryOrderPreference == [] - assert results[1].LimitLoadFromHardware == [] - assert results[1].StartCalendarInterval == [] - assert results[1].AdditionalProperties == [] - assert results[1].NSAppTransportSecurity == [] - assert results[1].source == "/System/Library/LaunchAgents/com.apple.AMPArtworkAgent.plist" + assert len(results) == 6 - assert results[2].hostname == "localhost" - assert results[2].domain is None - assert results[2].Label == "com.apple.familynotificationd" - assert results[2].Disabled is None - assert results[2].UserName is None - assert results[2].GroupName is None - assert results[2].Group is None - assert results[2].CFBundleIdentifier is None - assert results[2].CFBundleDevelopmentRegion is None - assert results[2].CFBundleInfoDictionaryVersion is None - assert results[2].CFBundleName is None - assert results[2].InitGroups is None - assert ( - results[2].Program == "/System/Library/PrivateFrameworks/FamilyNotification.framework/familynotificationd" - ) - assert results[2].BundleProgram is None - assert results[2].ProgramArguments == [] - assert results[2].EnableGlobbing is None - assert results[2].EnableTransactions - assert results[2].PressuredExit is None - assert results[2].EnablePressureExit is None - assert results[2].EnablePressuredExit is None - assert results[2].KeepAlive is None - assert results[2].SuccessfulExit is None - assert results[2].Crashed is None - assert results[2].RunAtLoad is None - assert results[2].RootDirectory is None - assert results[2].WorkingDirectory is None - assert results[2].Umask is None - assert results[2].TimeOut is None - assert results[2].ExitTimeOut == 1 - assert results[2].ThrottleInterval is None - assert results[2].StartInterval is None - assert results[2].StartOnMount is None - assert results[2].WatchPaths == [] - assert results[2].QueueDirectories == [] - assert results[2].StandardInPath is None - assert results[2].StandardOutPath is None - assert results[2].StandardErrorPath is None - assert results[2].Debug is None - assert results[2].WaitForDebugger is None - assert results[2].Nice is None - assert results[2].ProcessType is None - assert results[2].PowerNap is None - assert results[2].AbandonProcessGroup is None - assert results[2].LowPriorityIO is None - assert results[2].LowPriorityBackgroundIO is None - assert results[2].MaterializeDatalessFiles is None - assert results[2].LaunchOnlyOnce is None - assert results[2].BootShell is None - assert results[2].SessionCreate is None - assert results[2].LegacyTimers is None - assert results[2].TransactionTimeLimitEnabled is None - assert results[2].LimitLoadToDeveloperMode is None - assert results[2].LimitLoadToSessionType == "['LoginWindow', 'Aqua']" - assert results[2].Wait is None - assert results[2].AssociatedBundleIdentifiers == [] - assert results[2].plist_path is None - assert results[2].POSIXSpawnType == "Adaptive" - assert results[2].PosixSpawnType is None - assert results[2].MultipleInstances is None - assert results[2].DisabledInSafeBoot is None - assert results[2].BeginTransactionAtShutdown is None - assert results[2].OnDemand is None - assert results[2].AlwaysSIGTERMOnShutdown is None - assert results[2].MinimalBootProfiles is None - assert results[2].MinimalBootProfile is None - assert results[2].LSBackgroundOnly is None - assert results[2].HopefullyExitsLast is None - assert results[2].ExponentialThrottling is None - assert results[2].IgnoreProcessGroupAtShutdown is None - assert results[2].EventMonitor is None - assert results[2].AuxiliaryBootstrapper is None - assert results[2].AuxiliaryBootstrapperAllowDemand is None - assert results[2].DrainMessagesAfterFailedInit is None - assert results[2].DrainMessagesOnFailedInit is None - assert results[2].LimitLoadFromVariant is None - assert results[2].LimitLoadFromBootMode is None - assert results[2].EfficiencyMode is None - assert results[2].Conclave is None - assert results[2].LaunchEvents != [] - assert results[2].MachServices == [ - "('com.apple.familynotification.agent', True)", - "('com.apple.usernotifications.delegate.com.apple.familynotifications', True)", + assert results[0].label == "com.apple.ecosystemagent" + assert results[0].program is None + assert results[0].program_arguments == [ + "/System/Library/PrivateFrameworks/Ecosystem.framework/Support/ecosystemagent" ] - assert results[2].SoftResourceLimits == [] - assert results[2].HardResourceLimits == [] - assert results[2].EnvironmentVariables == [] - assert results[2].NoEnvironmentVariables == [] - assert results[2].NO_EnvironmentVariables == [] - assert results[2].SHAuthorizationRight is None - assert results[2].PublishesEvents is None - assert results[2].LimitLoadToVariant is None - assert results[2].RunLoopType is None - assert results[2].RemoteServices == [] - assert results[2].JetsamProperties == [] - assert results[2].LimitLoadToHardware == [] - assert results[2].PanicOnCrash == [] - assert results[2].LimitLoadToBootMode == [] - assert results[2].Cryptex is None - assert results[2].ServiceType is None - assert results[2].ServiceIPC is None - assert results[2].UrgentLogSubmission == [] - assert results[2].AppIntents == [] - assert results[2].BinaryOrderPreference == [] - assert results[2].LimitLoadFromHardware == [] - assert results[2].StartCalendarInterval == [] - assert results[2].AdditionalProperties == [] - assert results[2].NSAppTransportSecurity == [] - assert results[2].source == "/System/Library/LaunchAgents/com.apple.familynotificationd.plist" - - assert results[3].hostname == "localhost" - assert results[3].domain is None - assert results[3].Label == "com.apple.seserviced" - assert results[3].Disabled is None - assert results[3].UserName is None - assert results[3].GroupName is None - assert results[3].Group is None - assert results[3].CFBundleIdentifier is None - assert results[3].CFBundleDevelopmentRegion is None - assert results[3].CFBundleInfoDictionaryVersion is None - assert results[3].CFBundleName is None - assert results[3].InitGroups is None - assert results[3].Program == "/usr/libexec/seserviced" - assert results[3].BundleProgram is None - assert results[3].ProgramArguments == [] - assert results[3].EnableGlobbing is None - assert results[3].EnableTransactions - assert results[3].PressuredExit is None - assert results[3].EnablePressureExit is None - assert results[3].EnablePressuredExit == "True" - assert results[3].KeepAlive is None - assert results[3].SuccessfulExit is None - assert results[3].Crashed is None - assert results[3].RunAtLoad is None - assert results[3].RootDirectory is None - assert results[3].WorkingDirectory is None - assert results[3].Umask is None - assert results[3].TimeOut is None - assert results[3].ExitTimeOut is None - assert results[3].ThrottleInterval is None - assert results[3].StartInterval is None - assert results[3].StartOnMount is None - assert results[3].WatchPaths == [] - assert results[3].QueueDirectories == [] - assert results[3].StandardInPath is None - assert results[3].StandardOutPath is None - assert results[3].StandardErrorPath is None - assert results[3].Debug is None - assert results[3].WaitForDebugger is None - assert results[3].Nice is None - assert results[3].ProcessType == "Adaptive" - assert results[3].PowerNap is None - assert results[3].AbandonProcessGroup is None - assert results[3].LowPriorityIO is None - assert results[3].LowPriorityBackgroundIO is None - assert results[3].MaterializeDatalessFiles is None - assert results[3].LaunchOnlyOnce is None - assert results[3].BootShell is None - assert results[3].SessionCreate is None - assert results[3].LegacyTimers is None - assert results[3].TransactionTimeLimitEnabled is None - assert results[3].LimitLoadToDeveloperMode is None - assert results[3].LimitLoadToSessionType == "['Aqua']" - assert results[3].Wait is None - assert results[3].AssociatedBundleIdentifiers == [] - assert results[3].plist_path is None - assert results[3].POSIXSpawnType is None - assert results[3].PosixSpawnType is None - assert results[3].MultipleInstances is None - assert results[3].DisabledInSafeBoot is None - assert results[3].BeginTransactionAtShutdown is None - assert results[3].OnDemand is None - assert results[3].AlwaysSIGTERMOnShutdown is None - assert results[3].MinimalBootProfiles is None - assert results[3].MinimalBootProfile is None - assert results[3].LSBackgroundOnly is None - assert results[3].HopefullyExitsLast is None - assert results[3].ExponentialThrottling is None - assert results[3].IgnoreProcessGroupAtShutdown is None - assert results[3].EventMonitor is None - assert results[3].AuxiliaryBootstrapper is None - assert results[3].AuxiliaryBootstrapperAllowDemand is None - assert results[3].DrainMessagesAfterFailedInit is None - assert results[3].DrainMessagesOnFailedInit is None - assert results[3].LimitLoadFromVariant == "HasFactoryContent" - assert results[3].LimitLoadFromBootMode is None - assert results[3].EfficiencyMode is None - assert results[3].Conclave is None - assert results[3].LaunchEvents != [] - assert results[3].MachServices == [ - "('com.apple.seserviced', True)", - "('com.apple.seserviced.sereservation.client', True)", + assert results[0].keep_alive is None + assert results[0].run_at_load is None + assert results[0].process_type == "Background" + assert results[0].limit_load_to_session_type is None + assert results[0].limit_load_from_hardware == [] + assert results[0].launch_events == [] + assert results[0].mach_services == [ + "('com.apple.ecosystem.agent.clear-notifications', True)", + "('com.apple.ecosystem.agent.notifications', True)", + "('com.apple.ecosystem.unsupportedapplicationlist', True)", + "('com.apple.usernotifications.delegate.com.apple.ecosystem.notifications', True)", ] - assert results[3].SoftResourceLimits == [] - assert results[3].HardResourceLimits == [] - assert results[3].EnvironmentVariables == [] - assert results[3].NoEnvironmentVariables == [] - assert results[3].NO_EnvironmentVariables == [] - assert results[3].SHAuthorizationRight is None - assert results[3].PublishesEvents is None - assert results[3].LimitLoadToVariant is None - assert results[3].RunLoopType is None - assert results[3].RemoteServices == [] - assert results[3].JetsamProperties == [] - assert results[3].LimitLoadToHardware == [] - assert results[3].PanicOnCrash == [] - assert results[3].LimitLoadToBootMode == [] - assert results[3].Cryptex is None - assert results[3].ServiceType is None - assert results[3].ServiceIPC is None - assert results[3].UrgentLogSubmission == [] - assert results[3].AppIntents == [] - assert results[3].BinaryOrderPreference == [] - assert results[3].LimitLoadFromHardware == [] - assert results[3].StartCalendarInterval == [] - assert results[3].AdditionalProperties == [] - assert results[3].NSAppTransportSecurity == [] - assert results[3].source == "/Users/user/Library/LaunchAgents/com.apple.seserviced.plist" + assert results[0].enable_pressured_exit == "True" + assert results[0].enable_transactions + assert results[0].environment_variables == [] + assert results[0].user_name is None + assert results[0].low_priority_io + assert results[0].watch_paths == [] + assert results[0].queue_directories == [] + assert results[0].plist_path is None + assert results[0].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + + assert results[1].un_setting_alerts is None + assert results[1].un_setting_always_show_previews is None + assert results[1].un_setting_lock_screen is None + assert results[1].un_setting_modal_alert_style is None + assert results[1].un_automatically_show_settings + assert results[1].un_setting_notification_center is None + assert results[1].un_daemon_should_receive_background_responses + assert results[1].un_suppress_user_authorization_prompt + assert results[1].plist_path == "UNUserNotificationCenter" + assert results[1].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + + assert results[2].un_setting_alerts + assert not results[2].un_setting_always_show_previews + assert results[2].un_setting_lock_screen + assert results[2].un_setting_modal_alert_style + assert results[2].un_automatically_show_settings is None + assert results[2].un_setting_notification_center + assert results[2].un_daemon_should_receive_background_responses is None + assert results[2].un_suppress_user_authorization_prompt is None + assert results[2].plist_path == "UNUserNotificationCenter/UNDefaultSettings" + assert results[2].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + + assert results[3].un_notification_icon_default == "notification-settings" + assert results[3].un_notification_icon_settings == "notification-settings" + + assert results[3].plist_path == "UNUserNotificationCenter/UNNotificationIcons" + assert results[3].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + + assert results[4].label == "com.openssh.ssh-agent" + assert results[4].program is None + assert results[4].program_arguments == ["/usr/bin/ssh-agent", "-l"] + assert results[4].process_type is None + assert results[4].mach_services == [] + assert results[4].enable_pressured_exit is None + assert results[4].enable_transactions + assert results[4].environment_variables == [] + assert results[4].user_name is None + assert results[4].watch_paths == [] + assert results[4].queue_directories == [] + assert results[4].plist_path is None + assert results[4].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" + + assert results[5].socket_key is None + assert results[5].sock_type is None + assert results[5].sock_passive is None + assert results[5].sock_node_name is None + assert results[5].sock_service_name is None + assert results[5].sock_family is None + assert results[5].sock_protocol is None + assert results[5].sock_path_mode is None + assert results[5].sock_path_name is None + assert results[5].secure_socket_with_key == "SSH_AUTH_SOCK" + assert results[5].sock_path_owner is None + assert results[5].sock_path_group is None + assert results[5].bonjour is None + assert results[5].multicast_group is None + assert results[5].receive_packet_info is None + assert results[5].plist_path == "Sockets/Listeners" + assert results[5].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" @pytest.mark.parametrize( ("names", "paths"), [ ( - ( - "com.apple.WirelessRadioManager-osx.plist", - "com.apple.cfprefsd.xpc.daemon.plist", - "com.apple.perfpowermetricd.plist", - "com.apple.sidecar-hid-relay.plist", - ), - ( - "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist", - "/Users/user/Library/LaunchDaemons/com.apple.cfprefsd.xpc.daemon.plist", - "/Library/LaunchDaemons/com.apple.perfpowermetricd.plist", - "/Library/LaunchAgents/com.apple.sidecar-hid-relay.plist", - ), + ("org.cups.cupsd.plist",), + ("/System/Library/LaunchDaemons/org.cups.cupsd.plist",), ), ], ) @@ -528,14 +165,6 @@ def test_launch_daemons( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: - user = UnixUserRecord( - name="user", - uid=501, - gid=20, - home="/Users/user", - shell="/bin/zsh", - ) - target_unix.users = lambda: [user] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") fs_unix.map_file(path, data_file) @@ -549,329 +178,62 @@ def test_launch_daemons( target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_daemons()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - - assert len(results) == 3 - - assert results[0].hostname == "localhost" - assert results[0].domain is None - assert results[0].Label == "com.apple.perfpowermetricd" - assert results[0].Disabled is None - assert results[0].UserName is None - assert results[0].GroupName is None - assert results[0].Group is None - assert results[0].CFBundleIdentifier is None - assert results[0].CFBundleDevelopmentRegion is None - assert results[0].CFBundleInfoDictionaryVersion is None - assert results[0].CFBundleName is None - assert results[0].InitGroups is None - assert results[0].Program is None - assert results[0].BundleProgram is None - assert results[0].ProgramArguments == ["usr/libexec/perfpowermetricd"] or results[0].ProgramArguments == [ - "/usr/libexec/perfpowermetricd" + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) + + assert len(results) == 2 + + assert results[0].label == "org.cups.cupsd" + assert results[0].program is None + assert results[0].program_arguments == ["/usr/sbin/cupsd", "-l"] + assert results[0].keep_alive == "[('PathState', {'/private/var/spool/cups/cache/org.cups.cupsd': True})]" + assert results[0].on_demand is None + assert results[0].disabled is None + assert results[0].run_at_load is None + assert results[0].launch_only_once is None + assert results[0].process_type == "Interactive" + assert results[0].wait is None + assert results[0].limit_load_to_session_type is None + assert results[0].limit_load_to_developer_mode is None + assert results[0].limit_load_from_variant is None + assert results[0].limit_load_to_variant is None + assert results[0].limit_load_from_boot_mode is None + assert results[0].limit_load_to_boot_mode == [] + assert results[0].limit_load_from_hardware == [] + assert results[0].limit_load_to_hardware == [] + assert results[0].launch_events == [] + assert results[0].mach_services == [] + assert results[0].enable_pressured_exit is None + assert results[0].enable_transactions + assert results[0].environment_variables == [ + "('CUPS_DEBUG_LOG', '/var/log/cups/debug_log')", + "('CUPS_DEBUG_LEVEL', '3')", + "('CUPS_DEBUG_FILTER', '^(cupsDo|cupsGet|cupsMake|cupsSet|http|_http|ipp|_ipp|mime).*')", ] - assert results[0].EnableGlobbing is None - assert results[0].EnableTransactions - assert results[0].PressuredExit is None - assert results[0].EnablePressureExit is None - assert results[0].EnablePressuredExit == "True" - assert results[0].KeepAlive is None - assert results[0].SuccessfulExit is None - assert results[0].Crashed is None - assert results[0].RunAtLoad is None - assert results[0].RootDirectory is None - assert results[0].WorkingDirectory is None - assert results[0].Umask is None - assert results[0].TimeOut is None - assert results[0].ExitTimeOut is None - assert results[0].ThrottleInterval is None - assert results[0].StartInterval is None - assert results[0].StartOnMount is None - assert results[0].WatchPaths == [] - assert results[0].QueueDirectories == [] - assert results[0].StandardInPath is None - assert results[0].StandardOutPath is None - assert results[0].StandardErrorPath is None - assert results[0].Debug is None - assert results[0].WaitForDebugger is None - assert results[0].Nice is None - assert results[0].ProcessType == "Interactive" - assert results[0].PowerNap is None - assert results[0].AbandonProcessGroup is None - assert results[0].LowPriorityIO is None - assert results[0].LowPriorityBackgroundIO is None - assert results[0].MaterializeDatalessFiles is None - assert results[0].LaunchOnlyOnce is None - assert results[0].BootShell is None - assert results[0].SessionCreate is None - assert results[0].LegacyTimers is None - assert results[0].TransactionTimeLimitEnabled is None - assert results[0].LimitLoadToDeveloperMode is None - assert results[0].LimitLoadToSessionType is None - assert results[0].Wait is None - assert results[0].AssociatedBundleIdentifiers == [] - assert results[0].plist_path is None - assert results[0].POSIXSpawnType is None - assert results[0].PosixSpawnType is None - assert results[0].MultipleInstances is None - assert results[0].DisabledInSafeBoot is None - assert results[0].BeginTransactionAtShutdown is None - assert results[0].OnDemand is None - assert results[0].AlwaysSIGTERMOnShutdown is None - assert results[0].MinimalBootProfiles is None - assert results[0].MinimalBootProfile is None - assert results[0].LSBackgroundOnly is None - assert results[0].HopefullyExitsLast is None - assert results[0].ExponentialThrottling is None - assert results[0].IgnoreProcessGroupAtShutdown is None - assert results[0].EventMonitor is None - assert results[0].AuxiliaryBootstrapper is None - assert results[0].AuxiliaryBootstrapperAllowDemand is None - assert results[0].DrainMessagesAfterFailedInit is None - assert results[0].DrainMessagesOnFailedInit is None - assert results[0].LimitLoadFromVariant is None - assert results[0].LimitLoadFromBootMode is None - assert results[0].EfficiencyMode is None - assert results[0].Conclave is None - assert results[0].LaunchEvents == [] - assert results[0].MachServices == ["('com.apple.PerfPowerMetricMonitor.xpc', True)"] - assert results[0].SoftResourceLimits == [] - assert results[0].HardResourceLimits == [] - assert results[0].EnvironmentVariables == [] - assert results[0].NoEnvironmentVariables == [] - assert results[0].NO_EnvironmentVariables == [] - assert results[0].SHAuthorizationRight is None - assert results[0].PublishesEvents is None - assert results[0].LimitLoadToVariant is None - assert results[0].RunLoopType is None - assert results[0].RemoteServices == [] - assert results[0].JetsamProperties == [] - assert results[0].LimitLoadToHardware == [] - assert results[0].PanicOnCrash == [] - assert results[0].LimitLoadToBootMode == [] - assert results[0].Cryptex is None - assert results[0].ServiceType is None - assert results[0].ServiceIPC is None - assert results[0].UrgentLogSubmission == [] - assert results[0].AppIntents == [] - assert results[0].BinaryOrderPreference == [] - assert results[0].LimitLoadFromHardware == [] - assert results[0].StartCalendarInterval == [] - assert results[0].AdditionalProperties == [] - assert results[0].NSAppTransportSecurity == [] - assert results[0].source == "/Library/LaunchDaemons/com.apple.perfpowermetricd.plist" + assert results[0].user_name is None + assert results[0].init_groups is None + assert results[0].group_name is None + assert results[0].start_interval is None + assert results[0].start_calendar_interval == [] + assert results[0].throttle_interval is None + assert results[0].enable_globbing is None + assert results[0].standard_in_path is None + assert results[0].standard_out_path is None + assert results[0].standard_error_path is None + assert results[0].nice is None + assert results[0].abandon_process_group is None + assert results[0].low_priority_io is None + assert results[0].root_directory is None + assert results[0].working_directory is None + assert results[0].umask is None + assert results[0].time_out is None + assert results[0].exit_time_out == 60 + assert results[0].watch_paths == [] + assert results[0].queue_directories == [] + assert results[0].start_on_mount is None + assert results[0].soft_resource_limits == [] assert results[1].hostname == "localhost" assert results[1].domain is None - assert results[1].Label == "com.apple.WirelessRadioManager" - assert results[1].Disabled is None - assert results[1].UserName is None - assert results[1].GroupName is None - assert results[1].Group is None - assert results[1].CFBundleIdentifier is None - assert results[1].CFBundleDevelopmentRegion is None - assert results[1].CFBundleInfoDictionaryVersion is None - assert results[1].CFBundleName is None - assert results[1].InitGroups is None - assert results[1].Program is None - assert results[1].BundleProgram is None - assert results[1].ProgramArguments == ["/usr/sbin/WirelessRadioManagerd"] - assert results[1].EnableGlobbing is None - assert results[1].EnableTransactions is None - assert results[1].PressuredExit is None - assert results[1].EnablePressureExit is None - assert results[1].EnablePressuredExit == "True" - assert results[1].KeepAlive is None - assert results[1].SuccessfulExit is None - assert results[1].Crashed is None - assert results[1].RunAtLoad is None - assert results[1].RootDirectory is None - assert results[1].WorkingDirectory is None - assert results[1].Umask is None - assert results[1].TimeOut is None - assert results[1].ExitTimeOut is None - assert results[1].ThrottleInterval == 10 - assert results[1].StartInterval is None - assert results[1].StartOnMount is None - assert results[1].WatchPaths == [] - assert results[1].QueueDirectories == [] - assert results[1].StandardInPath is None - assert results[1].StandardOutPath is None - assert results[1].StandardErrorPath is None - assert results[1].Debug is None - assert results[1].WaitForDebugger is None - assert results[1].Nice is None - assert results[1].ProcessType is None - assert results[1].PowerNap is None - assert results[1].AbandonProcessGroup is None - assert results[1].LowPriorityIO is None - assert results[1].LowPriorityBackgroundIO is None - assert results[1].MaterializeDatalessFiles is None - assert results[1].LaunchOnlyOnce is None - assert results[1].BootShell is None - assert results[1].SessionCreate is None - assert results[1].LegacyTimers is None - assert results[1].TransactionTimeLimitEnabled is None - assert results[1].LimitLoadToDeveloperMode is None - assert results[1].LimitLoadToSessionType is None - assert results[1].Wait is None - assert results[1].AssociatedBundleIdentifiers == [] - assert results[1].plist_path is None - assert results[1].POSIXSpawnType == "Interactive" - assert results[1].PosixSpawnType is None - assert results[1].MultipleInstances is None - assert results[1].DisabledInSafeBoot is None - assert results[1].BeginTransactionAtShutdown is None - assert results[1].OnDemand is None - assert results[1].AlwaysSIGTERMOnShutdown is None - assert results[1].MinimalBootProfiles is None - assert results[1].MinimalBootProfile is None - assert results[1].LSBackgroundOnly is None - assert results[1].HopefullyExitsLast is None - assert results[1].ExponentialThrottling is None - assert results[1].IgnoreProcessGroupAtShutdown is None - assert results[1].EventMonitor is None - assert results[1].AuxiliaryBootstrapper is None - assert results[1].AuxiliaryBootstrapperAllowDemand is None - assert results[1].DrainMessagesAfterFailedInit is None - assert results[1].DrainMessagesOnFailedInit is None - assert results[1].LimitLoadFromVariant is None - assert results[1].LimitLoadFromBootMode is None - assert results[1].EfficiencyMode is None - assert results[1].Conclave is None - assert results[1].LaunchEvents == [] - assert results[1].MachServices == [ - "('com.apple.WirelessCoexManager', True)", - "('com.apple.WirelessRadioManager', True)", - ] - assert results[1].SoftResourceLimits == [] - assert results[1].HardResourceLimits == [] - assert results[1].EnvironmentVariables == [] - assert results[1].NoEnvironmentVariables == [] - assert results[1].NO_EnvironmentVariables == [] - assert results[1].SHAuthorizationRight is None - assert results[1].PublishesEvents is None - assert results[1].LimitLoadToVariant is None - assert results[1].RunLoopType is None - assert results[1].RemoteServices == [] - assert results[1].JetsamProperties == [] - assert results[1].LimitLoadToHardware == [] - assert results[1].PanicOnCrash == [] - assert results[1].LimitLoadToBootMode == [] - assert results[1].Cryptex is None - assert results[1].ServiceType is None - assert results[1].ServiceIPC is None - assert results[1].UrgentLogSubmission == [] - assert results[1].AppIntents == [] - assert results[1].BinaryOrderPreference == [] - assert results[1].LimitLoadFromHardware == [] - assert results[1].StartCalendarInterval == [] - assert results[1].AdditionalProperties == [] - assert results[1].NSAppTransportSecurity == [] - assert results[1].source == "/System/Library/LaunchDaemons/com.apple.WirelessRadioManager-osx.plist" - - assert results[2].hostname == "localhost" - assert results[2].domain is None - assert results[2].Label == "com.apple.cfprefsd.xpc.daemon" - assert results[2].Disabled is None - assert results[2].UserName is None - assert results[2].GroupName is None - assert results[2].Group is None - assert results[2].CFBundleIdentifier is None - assert results[2].CFBundleDevelopmentRegion is None - assert results[2].CFBundleInfoDictionaryVersion is None - assert results[2].CFBundleName is None - assert results[2].InitGroups is None - assert results[2].Program is None - assert results[2].BundleProgram is None - assert results[2].ProgramArguments == ["/usr/sbin/cfprefsd", "daemon"] - assert results[2].EnableGlobbing is None - assert results[2].EnableTransactions - assert results[2].PressuredExit is None - assert results[2].EnablePressureExit is None - assert results[2].EnablePressuredExit == "False" - assert results[2].KeepAlive is None - assert results[2].SuccessfulExit is None - assert results[2].Crashed is None - assert results[2].RunAtLoad is None - assert results[2].RootDirectory is None - assert results[2].WorkingDirectory is None - assert results[2].Umask is None - assert results[2].TimeOut is None - assert results[2].ExitTimeOut is None - assert results[2].ThrottleInterval is None - assert results[2].StartInterval is None - assert results[2].StartOnMount is None - assert results[2].WatchPaths == [] - assert results[2].QueueDirectories == [] - assert results[2].StandardInPath is None - assert results[2].StandardOutPath is None - assert results[2].StandardErrorPath is None - assert results[2].Debug is None - assert results[2].WaitForDebugger is None - assert results[2].Nice is None - assert results[2].ProcessType is None - assert results[2].PowerNap is None - assert results[2].AbandonProcessGroup is None - assert results[2].LowPriorityIO is None - assert results[2].LowPriorityBackgroundIO is None - assert results[2].MaterializeDatalessFiles is None - assert results[2].LaunchOnlyOnce is None - assert results[2].BootShell is None - assert results[2].SessionCreate is None - assert results[2].LegacyTimers is None - assert results[2].TransactionTimeLimitEnabled is None - assert results[2].LimitLoadToDeveloperMode is None - assert results[2].LimitLoadToSessionType is None - assert results[2].Wait is None - assert results[2].AssociatedBundleIdentifiers == [] - assert results[2].plist_path is None - assert results[2].POSIXSpawnType == "Adaptive" - assert results[2].PosixSpawnType is None - assert results[2].MultipleInstances is None - assert results[2].DisabledInSafeBoot is None - assert results[2].BeginTransactionAtShutdown is None - assert results[2].OnDemand is None - assert results[2].AlwaysSIGTERMOnShutdown is None - assert results[2].MinimalBootProfiles is None - assert results[2].MinimalBootProfile is None - assert results[2].LSBackgroundOnly is None - assert results[2].HopefullyExitsLast is None - assert results[2].ExponentialThrottling is None - assert results[2].IgnoreProcessGroupAtShutdown is None - assert results[2].EventMonitor is None - assert results[2].AuxiliaryBootstrapper is None - assert results[2].AuxiliaryBootstrapperAllowDemand is None - assert results[2].DrainMessagesAfterFailedInit is None - assert results[2].DrainMessagesOnFailedInit is None - assert results[2].LimitLoadFromVariant is None - assert results[2].LimitLoadFromBootMode is None - assert results[2].EfficiencyMode is None - assert results[2].Conclave is None - assert results[2].LaunchEvents == [] - assert results[2].MachServices == ["('com.apple.cfprefsd.daemon', True)"] - assert results[2].SoftResourceLimits == ["('NumberOfFiles', 512)"] - assert results[2].HardResourceLimits == ["('NumberOfFiles', 512)"] - assert results[2].EnvironmentVariables == [] - assert results[2].NoEnvironmentVariables == [] - assert results[2].NO_EnvironmentVariables == [] - assert results[2].SHAuthorizationRight is None - assert results[2].PublishesEvents is None - assert results[2].LimitLoadToVariant is None - assert results[2].RunLoopType is None - assert results[2].RemoteServices == [] - assert results[2].JetsamProperties == [] - assert results[2].LimitLoadToHardware == [] - assert results[2].PanicOnCrash == [] - assert results[2].LimitLoadToBootMode == [] - assert results[2].Cryptex is None - assert results[2].ServiceType is None - assert results[2].ServiceIPC is None - assert results[2].UrgentLogSubmission == [] - assert results[2].AppIntents == [] - assert results[2].BinaryOrderPreference == [] - assert results[2].LimitLoadFromHardware == [] - assert results[2].StartCalendarInterval == [] - assert results[2].AdditionalProperties == [] - assert results[2].NSAppTransportSecurity == [] - assert results[2].source == "/Users/user/Library/LaunchDaemons/com.apple.cfprefsd.xpc.daemon.plist" + assert results[1].listeners == ["{'SockPathMode': 49663, 'SockPathName': '/private/var/run/cupsd'}"] + assert results[1].plist_path == "Sockets" + assert results[1].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py new file mode 100644 index 0000000000..bd87b8e848 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.periodic import PeriodicPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "199.rotate-fax", + "110.clean-tmps", + ), + ( + "/etc/periodic/monthly/199.rotate-fax", + "/etc/periodic/daily/110.clean-tmps", + ), + ), + ], +) +def test_periodic_scripts( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(PeriodicPlugin) + + results = list(target_unix.periodic_scripts()) + results.sort(key=lambda r: r.source) + + assert len(results) == 2 + + assert results[0].source == "/etc/periodic/daily/110.clean-tmps" + + assert results[1].source == "/etc/periodic/monthly/199.rotate-fax" + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ("periodic.conf",), + ("/etc/defaults/periodic.conf",), + ), + ], +) +def test_periodic_conf( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") + fs_unix.map_file(path, data_file) + entry = fs_unix.get(path) + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(PeriodicPlugin) + + results = list(target_unix.periodic_conf()) + results.sort(key=lambda r: r.source) + + assert len(results) == 4 + + assert results[0].local_periodic == "/usr/local/etc/periodic" + assert results[0].dir_output is None + assert results[0].dir_show_success is None + assert results[0].dir_show_info is None + assert results[0].dir_show_badconfig is None + assert results[0].anticongestion_sleeptime is None + assert results[0].source == "/etc/defaults/periodic.conf" + + assert results[1].daily_clean_disks_enable is None + assert results[1].daily_clean_disks_files is None + assert results[1].daily_clean_disks_days is None + assert results[1].daily_clean_disks_verbose is None + assert results[1].daily_clean_tmps_enable + assert results[1].daily_clean_tmps_dirs == "/tmp" + assert results[1].daily_clean_tmps_days == 3 + assert results[1].daily_clean_tmps_ignore == "$daily_clean_tmps_ignore quota.user quota.group" + assert results[1].daily_clean_tmps_verbose + assert results[1].daily_clean_preserve_enable is None + assert results[1].daily_clean_preserve_days is None + assert results[1].daily_clean_preserve_verbose is None + assert results[1].daily_clean_msgs_enable + assert results[1].daily_clean_msgs_days is None + assert results[1].daily_clean_rwho_enable + assert results[1].daily_clean_rwho_days == 7 + assert results[1].daily_clean_rwho_verbose + assert results[1].daily_clean_hoststat_enable is None + assert results[1].daily_accounting_enable + assert not results[1].daily_accounting_compress + assert results[1].daily_accounting_save == 3 + assert results[1].daily_accounting_flags == "-q" + assert results[1].daily_status_disks_enable + assert results[1].daily_status_disks_df_flags == "-l -h" + assert results[1].daily_status_network_enable + assert results[1].daily_status_network_usedns + assert results[1].daily_status_mailq_enable + assert not results[1].daily_status_mailq_shorten + assert results[1].daily_status_include_submit_mailq + assert results[1].daily_local == "/etc/daily.local" + assert results[1].source == "/etc/defaults/periodic.conf" + + assert results[2].weekly_locate_enable is None + assert results[2].weekly_whatis_enable is None + assert results[2].weekly_noid_enable is None + assert results[2].weekly_noid_dirs is None + assert results[2].weekly_status_security_enable is None + assert results[2].weekly_status_security_inline is None + assert results[2].weekly_status_security_output is None + assert results[2].weekly_status_pkg_enable is None + assert results[2].pkg_version is None + assert results[2].pkg_version_index is None + assert results[2].weekly_local == "/etc/weekly.local" + assert results[2].source == "/etc/defaults/periodic.conf" + + assert results[3].monthly_accounting_enable + assert results[3].monthly_status_security_enable is None + assert results[3].monthly_status_security_inline is None + assert results[3].monthly_status_security_output is None + assert results[3].monthly_local == "/etc/monthly.local" + assert results[3].source == "/etc/defaults/periodic.conf" From d1d73c890dc68bb6467461b894916d42a7a9cff1 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 7 May 2026 16:14:40 +0200 Subject: [PATCH 12/52] add macOS parser for at jobs --- .../bsd/darwin/macos/persistence/at_jobs.py | 83 +++++++++++++++++++ .../darwin/macos/persistence/a0000701c43af0 | 3 + .../darwin/macos/persistence/test_at_jobs.py | 42 ++++++++++ 3 files changed, 128 insertions(+) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py new file mode 100644 index 0000000000..2ceb6bbc09 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") + +AtJobsRecord = TargetRecordDescriptor( + "macos/at_jobs", + [ + ("string", "queue"), + ("varint", "seq"), + ("datetime", "execution_time"), + ("path", "source"), + ], +) + +FIELD_MAPPINGS = { + "backgroundAppRefreshLoadCount": "background_app_refresh_load_count", + "launchServicesItemsImported": "launch_services_items_imported", + "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", +} + + +class AtJobsPlugin(Plugin): + """macOS at jobs plugin.""" + + PATHS = ("/usr/lib/cron/jobs/*",) + + def __init__(self, target: Target): + super().__init__(target) + + self.at_jobs_files = set() + self._find_files() + + def check_compatible(self) -> None: + if not (self.at_jobs_files): + raise UnsupportedPluginError("No At Jobs files found") + + def _find_files(self) -> None: + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + self.at_jobs_files.add(path) + + @export(record=AtJobsRecord) + def at_jobs(self) -> Iterator[AtJobsRecord]: + """Yield macOS at jobs.""" + for file in self.at_jobs_files: + name = Path(file).name + + if name in (".SEQ", ".lockfile"): + continue + + if len(name) < 6: + continue + + queue = name[0] + seq = int(name[1:6]) + time_hex = name[6:] + + execution_time = None + try: + minutes = int(time_hex, 16) + execution_time = minutes * 60 + except ValueError: + pass + + yield AtJobsRecord( + queue=queue, + seq=seq, + execution_time=execution_time, + source=file, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 new file mode 100644 index 0000000000..5397add753 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bc687e4fe580a6cf8791b085cbc96203de962476846bbe6553083acf4c09eba +size 1427 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py new file mode 100644 index 0000000000..99b70ee911 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.at_jobs import AtJobsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "a0000701c43af0", + ], +) +def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/{test_file}") + fs_unix.map_file(f"/usr/lib/cron/jobs/{test_file}", data_file) + entry = fs_unix.get(f"/usr/lib/cron/jobs/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(AtJobsPlugin) + + results = list(target_unix.at_jobs()) + assert len(results) == 1 + + assert results[0].queue == "a" + assert results[0].seq == 7 + assert results[0].execution_time == datetime(2026, 5, 8, 12, 0, 0, tzinfo=tz) + assert results[0].source == "/usr/lib/cron/jobs/a0000701c43af0" From ba73adb92d927bdf9329d7519573ccc07e9e37f3 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 8 May 2026 13:07:34 +0200 Subject: [PATCH 13/52] Use exsiting unix cronjobs parser instead --- .../bsd/darwin/macos/persistence/cronjobs.py | 124 ------------------ dissect/target/plugins/os/unix/cronjobs.py | 2 + .../darwin/macos/persistence/test_cronjobs.py | 59 --------- 3 files changed, 2 insertions(+), 183 deletions(-) delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py delete mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py deleted file mode 100644 index bdd33cc5ad..0000000000 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import re -from typing import TYPE_CHECKING - -from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import TargetRecordDescriptor -from dissect.target.plugin import Plugin, export - -if TYPE_CHECKING: - from collections.abc import Iterator - from pathlib import Path - - from dissect.target.target import Target - - -CronjobRecord = TargetRecordDescriptor( - "macos/cronjob", - [ - ("string", "minute"), - ("string", "hour"), - ("string", "day"), - ("string", "month"), - ("string", "weekday"), - ("string", "command"), - ("path", "source"), - ], -) - -EnvironmentVariableRecord = TargetRecordDescriptor( - "macos/environmentvariable", - [ - ("string", "key"), - ("string", "value"), - ("path", "source"), - ], -) - -RE_CRONJOB = re.compile( - r""" - ^ - (?P\S+) - \s+ - (?P\S+) - \s+ - (?P\S+) - \s+ - (?P\S+) - \s+ - (?P\S+) - \s+ - (?P.+) - $ - """, - re.VERBOSE, -) -RE_ENVVAR = re.compile(r"^(?P[a-zA-Z_]+[a-zA-Z[0-9_])=(?P.*)") - - -class CronjobPlugin(Plugin): - """macOS cronjob plugin.""" - - CRONTAB_DIRS = ( - "/usr/lib/cron/tabs", - "/var/at/tabs", - "/var/cron/tabs", - ) - - CRONTAB_FILES = ("/etc/crontab",) - - def __init__(self, target: Target): - super().__init__(target) - self.crontabs = list(self.find_crontabs()) - - def check_compatible(self) -> None: - if not self.crontabs: - raise UnsupportedPluginError("No crontab(s) found on target") - - def find_crontabs(self) -> Iterator[Path]: - for crontab_dir in self.CRONTAB_DIRS: - if not (dir := self.target.fs.path(crontab_dir)).exists(): - continue - - for file in dir.iterdir(): - if file.resolve().is_file(): - yield file - - for crontab_file in self.CRONTAB_FILES: - if (file := self.target.fs.path(crontab_file)).exists(): - yield file - - @export(record=[CronjobRecord, EnvironmentVariableRecord]) - def cronjobs(self) -> Iterator[CronjobRecord | EnvironmentVariableRecord]: - """Yield cronjobs, and their configured environment variables on a macOS system. - - A cronjob is a scheduled task/command on a macOS system. Adversaries may use cronjobs to gain - persistence on the system. - """ - for file in self.crontabs: - for line in file.open("rt"): - line = line.strip() - if line.startswith("#") or not line: - continue - - if match := RE_CRONJOB.search(line): - match = match.groupdict() - - yield CronjobRecord( - **match, - source=file, - _target=self.target, - ) - - # Some cron implementations allow for environment variables to be set inside crontab files. - elif match := RE_ENVVAR.search(line): - match = match.groupdict() - yield EnvironmentVariableRecord( - **match, - source=file, - _target=self.target, - ) - - else: - self.target.log.warning("Unable to match cronjob line in %s: '%s'", file, line) diff --git a/dissect/target/plugins/os/unix/cronjobs.py b/dissect/target/plugins/os/unix/cronjobs.py index 6f946d149b..8bf7a4479f 100644 --- a/dissect/target/plugins/os/unix/cronjobs.py +++ b/dissect/target/plugins/os/unix/cronjobs.py @@ -70,6 +70,8 @@ class CronjobPlugin(Plugin): "/var/spool/cron/crontabs", "/etc/cron.d", "/usr/local/etc/cron.d", # FreeBSD + "/usr/lib/cron/tabs", # macOS + "/var/at/tabs", ) CRONTAB_FILES = ( diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py deleted file mode 100644 index d04a4f7396..0000000000 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_cronjobs.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -from unittest.mock import patch - -import pytest - -from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.cronjobs import CronjobPlugin -from tests._utils import absolute_path - -if TYPE_CHECKING: - from dissect.target.filesystem import VirtualFilesystem - from dissect.target.target import Target - - -@pytest.mark.parametrize( - ("names", "paths"), - [ - ( - ("one", "two"), - ("/usr/lib/cron/tabs/user", "/var/at/tabs/user"), - ), - ], -) -def test_cronjobs( - names: tuple[str, ...], - paths: tuple[str, ...], - target_unix: Target, - fs_unix: VirtualFilesystem, -) -> None: - for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/{name}") - fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(CronjobPlugin) - - results = list(target_unix.cronjobs()) - - assert len(results) == 2 - - assert results[0].minute == "*" - assert results[0].hour == "*" - assert results[0].day == "*" - assert results[0].month == "*" - assert results[0].command == "/Users/user/cron_test.sh" - assert results[0].source == "/usr/lib/cron/tabs/user" - - assert results[1].minute == "*" - assert results[1].hour == "*" - assert results[1].day == "*" - assert results[1].month == "*" - assert results[1].command == "/Users/user/cron_test.sh" - assert results[1].source == "/var/at/tabs/user" From 175eda3019b5a7e2425a3ddf2a2216fa0b39f835 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 8 May 2026 16:22:14 +0200 Subject: [PATCH 14/52] Cleaned up installog and systemlog parsers --- .../os/unix/bsd/darwin/macos/logs/installlog.py | 4 ++++ .../plugins/os/unix/bsd/darwin/macos/logs/systemlog.py | 10 +++------- .../os/unix/bsd/darwin/macos/logs/test_installlog.py | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py index a6e199e282..354495c5ae 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py @@ -20,6 +20,7 @@ ("string", "host"), ("string", "component"), ("string", "message"), + ("path", "source"), ], ) @@ -60,6 +61,7 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: host (str): Hostname. component (str): Component name. message (str): Log message. + source (path): Path to the log file. References: - https://sansorg.egnyte.com/dl/m9ftGF7heI @@ -84,6 +86,7 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: host=hostname.strip(), component=component.strip(), message=message.strip(), + source=self.INSTALL_LOG_PATH, _target=self.target, ) @@ -102,5 +105,6 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: host=hostname.strip(), component=component.strip(), message=message.strip(), + source=self.INSTALL_LOG_PATH, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py index 694f8c32c8..528eaa3545 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py @@ -23,11 +23,7 @@ class SystemLogPlugin(Plugin): - """Return information related software installations and updates on macOS. - - References: - - https://sansorg.egnyte.com/dl/m9ftGF7heI - """ + """Return system logs on macOS.""" SYSTEM_LOG_GLOB = "/var/log/system.log*" @@ -46,7 +42,7 @@ def _resolve_files(self) -> None: @export(record=macOSSystemLogRecord) def systemlog(self) -> Iterator[macOSSystemLogRecord]: - """Return all macOS install log messages. + """Return all macOS system log messages. Yields macOSSystemLogRecord instances with fields: @@ -102,7 +98,7 @@ def systemlog(self) -> Iterator[macOSSystemLogRecord]: host=hostname.strip(), component=component.strip(), message=message.strip(), - source=filepath, # What benefit does this field have??? + source=filepath, _target=self.target, ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py index c52a82a884..3b2bd53458 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py @@ -41,13 +41,16 @@ def test_install_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesy assert results[0].host == "localhost" assert results[0].component == "Installer" assert results[0].message == "Progress[57]: Progress UI App Starting" + assert results[0].source == "/var/log/install.log" assert results[1].ts == datetime(2026, 3, 25, 14, 7, tzinfo=tz) assert results[1].host == "localhost" assert results[1].component == "Installer" assert results[1].message == "Progress[57]: Logging also using os_log, installerProgressLog = 0xc6501c080" + assert results[0].source == "/var/log/install.log" assert results[-1].ts == datetime(2026, 3, 25, 15, 18, 58, tzinfo=tz) assert results[-1].host == "users-Virtual-Machine" assert results[-1].component == "loginwindow[1042]:" assert results[-1].message == "+[SUOSULoginCredentialPolicy currentLoginCredentialPolicy] = 0" + assert results[0].source == "/var/log/install.log" From d12c07c9bf1711c03d3f9db616bd8206a217a18e Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 11 May 2026 13:04:21 +0200 Subject: [PATCH 15/52] add command field for at jobs --- .../bsd/darwin/macos/persistence/at_jobs.py | 23 ++++++++++++++++++- dissect/target/plugins/os/unix/cronjobs.py | 2 +- .../darwin/macos/persistence/test_at_jobs.py | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py index 2ceb6bbc09..0e54ce6b7f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py @@ -21,6 +21,7 @@ ("string", "queue"), ("varint", "seq"), ("datetime", "execution_time"), + ("string", "command"), ("path", "source"), ], ) @@ -65,7 +66,7 @@ def at_jobs(self) -> Iterator[AtJobsRecord]: continue queue = name[0] - seq = int(name[1:6]) + seq = int(name[1:6], 16) time_hex = name[6:] execution_time = None @@ -75,9 +76,29 @@ def at_jobs(self) -> Iterator[AtJobsRecord]: except ValueError: pass + command_line = False + command = "" + with self.target.fs.path(file).open("r") as f: + for line in f: + if command_line: + command += line + else: + line = line.strip() + + if not line or line.startswith(("#", "export")): + continue + + line = line.split("#", 1)[0].strip() + + if "export OLDPWD" in line: + command_line = True + + command = command.rstrip("\n") + yield AtJobsRecord( queue=queue, seq=seq, execution_time=execution_time, source=file, + command=command, ) diff --git a/dissect/target/plugins/os/unix/cronjobs.py b/dissect/target/plugins/os/unix/cronjobs.py index 8bf7a4479f..e28bff860f 100644 --- a/dissect/target/plugins/os/unix/cronjobs.py +++ b/dissect/target/plugins/os/unix/cronjobs.py @@ -70,7 +70,7 @@ class CronjobPlugin(Plugin): "/var/spool/cron/crontabs", "/etc/cron.d", "/usr/local/etc/cron.d", # FreeBSD - "/usr/lib/cron/tabs", # macOS + "/usr/lib/cron/tabs", # macOS "/var/at/tabs", ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py index 99b70ee911..eef4c6edee 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py @@ -39,4 +39,5 @@ def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem assert results[0].queue == "a" assert results[0].seq == 7 assert results[0].execution_time == datetime(2026, 5, 8, 12, 0, 0, tzinfo=tz) + assert results[0].command == "periodic_test.sh" assert results[0].source == "/usr/lib/cron/jobs/a0000701c43af0" From 6ffa21e0091106eee1cf6dd313a0618a64be2e65 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 13 May 2026 11:39:58 +0200 Subject: [PATCH 16/52] Improve log plugins & tests --- .../unix/bsd/darwin/macos/helpers/general.py | 7 +- .../logs/{installlog.py => install_log.py} | 17 +-- .../unix/bsd/darwin/macos/logs/system_log.py | 94 ++++++++++++++++ .../unix/bsd/darwin/macos/logs/systemlog.py | 105 ------------------ .../darwin/macos/resources_info_strings.py | 2 +- .../macos/resources_localizable_strings.py | 2 +- .../bsd/darwin/macos/code_signature/Ethernet | 2 +- .../darwin/macos/code_signature/FileProvider | 2 +- .../unix/bsd/darwin/macos/code_signature/Host | 2 +- .../contents_info/AppleEventLogHandler.plist | 2 +- .../macos/contents_info/AppleHPET.plist | 2 +- .../contents_info/UnmountAssistantAgent.plist | 2 +- .../macos/logs/{ => system_log}/system.log | 0 .../macos/logs/system_log/system.log.0.gz | 3 + ...test_installlog.py => test_install_log.py} | 4 +- .../bsd/darwin/macos/logs/test_system_log.py | 77 +++++++++++++ .../bsd/darwin/macos/logs/test_systemlog.py | 60 ---------- .../macos/persistence/test_launchers.py | 22 +++- .../macos/persistence/test_login_items.py | 10 +- .../darwin/macos/persistence/test_periodic.py | 23 ++-- .../test_code_signature_coderesources.py | 16 ++- .../bsd/darwin/macos/test_contents_info.py | 17 ++- .../bsd/darwin/macos/test_contents_version.py | 12 +- .../test_directory_services_local_nodes.py | 12 +- .../macos/test_duet_activity_scheduler.py | 11 +- .../macos/test_global_user_preferences.py | 13 ++- .../os/unix/bsd/darwin/macos/test_groups.py | 12 +- .../bsd/darwin/macos/test_login_window.py | 11 +- .../macos/test_resources_info_strings.py | 14 ++- .../test_resources_localizable_strings.py | 2 +- .../os/unix/bsd/darwin/macos/test_shadow.py | 11 +- .../os/unix/bsd/darwin/macos/test_tcc.py | 11 +- .../darwin/macos/test_text_replacements.py | 11 +- .../bsd/darwin/macos/test_user_accounts.py | 11 +- 34 files changed, 354 insertions(+), 248 deletions(-) rename dissect/target/plugins/os/unix/bsd/darwin/macos/logs/{installlog.py => install_log.py} (87%) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py rename tests/_data/plugins/os/unix/bsd/darwin/macos/logs/{ => system_log}/system.log (100%) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log.0.gz rename tests/plugins/os/unix/bsd/darwin/macos/logs/{test_installlog.py => test_install_log.py} (96%) create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py delete mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py index 568d7fbc55..d89d542ff4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py @@ -33,9 +33,12 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta def parse_timestamp(timestamp: re.Match) -> datetime: ts = None + value = timestamp.group() try: - ts = datetime.fromisoformat(timestamp.group()) + value = value.removesuffix("-0") + + ts = datetime.fromisoformat(value) except ValueError: - ts = datetime.strptime(timestamp.group(), "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) + ts = datetime.strptime(value, "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py similarity index 87% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py index 354495c5ae..76f2714d9a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/installlog.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py @@ -50,7 +50,7 @@ def check_compatible(self) -> None: raise UnsupportedPluginError("No install.log file found.") @export(record=macOSInstallLogRecord) - def installlog(self) -> Iterator[macOSInstallLogRecord]: + def install_log(self) -> Iterator[macOSInstallLogRecord]: """Return all macOS install log messages. Yields macOSInstallLogRecord instances with fields: @@ -72,19 +72,21 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: current_buf = "" for line in logfile.readlines(): - # If we have a buffer with a timestamp and - # our current line also has a timestamp, - # we should have a complete record in our buffer. if ts_match := RE_TIMESTAMP_PATTERN.match(line): if current_ts: - # Add 1 to skip the whitespace after the timestamp. asdf = current_buf[len(current_ts.group()) + 1 :] - hostname, component, message = asdf.split(" ", 2) + parts = asdf.split(" ", 2) + + if len(parts) == 3: + hostname, component, message = parts + elif len(parts) == 2: + hostname, message = parts + component = None yield macOSInstallLogRecord( ts=parse_timestamp(current_ts), host=hostname.strip(), - component=component.strip(), + component=component.strip() if component else None, message=message.strip(), source=self.INSTALL_LOG_PATH, _target=self.target, @@ -95,7 +97,6 @@ def installlog(self) -> Iterator[macOSInstallLogRecord]: elif current_buf: current_buf += line - # For the last line if current_ts and current_buf: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py new file mode 100644 index 0000000000..d2dfdeeb13 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import gzip +import re +from datetime import timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.helpers.utils import year_rollover_helper +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.log.helpers import RE_TS + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +macOSSystemLogRecord = TargetRecordDescriptor( + "macos/system", + [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], +) + +RE_TIMESTAMP_PATTERN = re.compile(r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}") + + +class SystemLogPlugin(Plugin): + """Return system logs on macOS.""" + + SYSTEM_LOG_GLOB = "/var/log/system.log*" + + def __init__(self, target: Target): + super().__init__(target) + self.log_files = set() + self._resolve_files() + + def check_compatible(self) -> None: + if not self.log_files: + raise UnsupportedPluginError("No system log files found.") + + def _resolve_files(self) -> None: + for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): + self.log_files.add(file) + + @export(record=macOSSystemLogRecord) + def system_log(self) -> Iterator[macOSSystemLogRecord]: + for file in self.log_files: + filepath = self.target.fs.path(file) + + timestamps = [ts for ts, _ in year_rollover_helper(filepath, RE_TS, "%b %d %H:%M:%S", timezone.utc)] + timestamps.reverse() + ts_iter = iter(timestamps) + + with ( + gzip.open(filepath.open("rb"), mode="rt") + if file.endswith(".gz") + else filepath.open(mode="rt") as logfile + ): + current_ts_match: re.Match[str] | None = None + current_buf = "" + + for line in logfile.readlines(): + if ts_match := RE_TIMESTAMP_PATTERN.match(line): + if current_ts_match: + asdf = current_buf[len(current_ts_match.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield macOSSystemLogRecord( + ts=next(ts_iter, None), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + source=filepath, + _target=self.target, + ) + + current_ts_match = ts_match + current_buf = line + + elif current_buf: + current_buf += line + + if current_ts_match and current_buf: + asdf = current_buf[len(current_ts_match.group()) + 1 :] + hostname, component, message = asdf.split(" ", 2) + + yield macOSSystemLogRecord( + ts=next(ts_iter, None), + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + source=filepath, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py deleted file mode 100644 index 528eaa3545..0000000000 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/systemlog.py +++ /dev/null @@ -1,105 +0,0 @@ -from __future__ import annotations - -import gzip -import re -from typing import TYPE_CHECKING - -from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import TargetRecordDescriptor -from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import parse_timestamp - -if TYPE_CHECKING: - from collections.abc import Iterator - - from dissect.target.target import Target - -macOSSystemLogRecord = TargetRecordDescriptor( - "macos/system", - [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], -) - -RE_TIMESTAMP_PATTERN = re.compile(r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}") - - -class SystemLogPlugin(Plugin): - """Return system logs on macOS.""" - - SYSTEM_LOG_GLOB = "/var/log/system.log*" - - def __init__(self, target: Target): - super().__init__(target) - self.log_files = set() - self._resolve_files() - - def check_compatible(self) -> None: - if not self.log_files: - raise UnsupportedPluginError("No system log files found.") - - def _resolve_files(self) -> None: - for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): - self.log_files.add(file) - - @export(record=macOSSystemLogRecord) - def systemlog(self) -> Iterator[macOSSystemLogRecord]: - """Return all macOS system log messages. - - Yields macOSSystemLogRecord instances with fields: - - .. code-block:: text - - ts (datetime): Timestamp of the log line. - host (str): Hostname. - component (str): Component name. - message (str): Log message. - - References: - - https://sansorg.egnyte.com/dl/m9ftGF7heI - """ - for file in self.log_files: - filepath = self.target.fs.path(file) - - logfile = gzip.open(filepath, mode="rt") if file.endswith(".gz") else filepath.open(mode="rt") # noqa: SIM115 - - current_ts: re.Match[str] | None = None - current_buf = "" - - for line in logfile.readlines(): - # If we have a buffer with a timestamp and - # our current line also has a timestamp, - # we should have a complete record in our buffer. - if ts_match := RE_TIMESTAMP_PATTERN.match(line): - if current_ts: - # Add 1 to skip the whitespace after the timestamp. - asdf = current_buf[len(current_ts.group()) + 1 :] - hostname, component, message = asdf.split(" ", 2) - - yield macOSSystemLogRecord( - ts=parse_timestamp(current_ts), - host=hostname.strip(), - component=component.strip(), - message=message.strip(), - source=filepath, # What benefit does this field have??? - _target=self.target, - ) - - current_ts = ts_match - current_buf = line - elif current_buf: - current_buf += line - - # For the last line - if current_ts and current_buf: - asdf = current_buf[len(current_ts.group()) + 1 :] - hostname, component, message = asdf.split(" ", 2) - - yield macOSSystemLogRecord( - ts=parse_timestamp(current_ts), - host=hostname.strip(), - component=component.strip(), - message=message.strip(), - source=filepath, - _target=self.target, - ) - - logfile.close() diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index f651b734ce..036692f2ed 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -16,7 +16,7 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -class MacOSResourcesInfoStringsPlugin(Plugin): +class macOSResourcesInfoStringsPlugin(Plugin): """macOS Resources InfoPlist.strings plist file.""" PATHS = ( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index 50c664ab74..7a5e3ef4d5 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -16,7 +16,7 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -class MacOSResourcesLocalizableStringsPlugin(Plugin): +class macOSResourcesLocalizableStringsPlugin(Plugin): """macOS Resources Localizable.strings plist file.""" PATHS = ( diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet index 3275e1676d..a7c10a15fe 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e68ed4d5bad9d248690cf91c9232faf91244afb57a53ac9419395bf0d8dfe56 +oid sha256:2609fe822410dafbb9d644bc20171fb015735258208ac8f60b991eb1cdb60818 size 1013 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider index 335016a4b6..6e2dc2379d 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/FileProvider @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54c07b4c0c0587dc53fa4b37dec98526993eb4eae392938164f42157b4e90378 +oid sha256:095cf24a76ccc72d91552b76a5f6686b6712a16e995eb63b80ca1c26e585c026 size 747 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host index 759611c5c8..432cbc476f 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Host @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4787dd7220f2ab63b8049ab9433a7c923d78f65bac44c4550fd689a8f292466c +oid sha256:4db0d9039339dcadd2a333f76468110c66b0f70656238b515990e732d2adce6a size 1021 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist index a224846811..98cec89b3e 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleEventLogHandler.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a3f4a2229e6ebb6ba7c8db70af081e45d4895eb2fa552e5bad52cce56d0854 +oid sha256:e5e0fe1aa04935fc715305c7dd046b4b59aba33ef1123d33fb93400a4f193380 size 2449 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist index 5bc7145a6f..b63606cc40 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/AppleHPET.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4037034a0ae07c8c7f721b289ccfe63508bf7f27e3ed2c897432b1264052e1ba +oid sha256:56d36fa3ef2fcac073e3f338edb1b64b5f87ca0d33263e0631d4c0efb46746db size 2242 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist index 73d436c0ca..f450a1d749 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/contents_info/UnmountAssistantAgent.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ab864905a40cfbf50165ac39e75e80bcc6e5d60c2c76a281b9cc8e059114501 +oid sha256:020880a0444267fb7cccce89f241c46ddc7b4655c2d4009bf42bb8f8fa0b1c39 size 1478 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system.log rename to tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log.0.gz b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log.0.gz new file mode 100644 index 0000000000..c839759200 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log.0.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f95e9d8c094b11552f472b39a422a78f93ef59e514badf725cb2e4cf61d1d683 +size 2503 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py similarity index 96% rename from tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py rename to tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py index 3b2bd53458..2ffab09699 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_installlog.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py @@ -6,7 +6,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.installlog import InstallLogPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.install_log import InstallLogPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -34,7 +34,7 @@ def test_install_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesy target_unix.add_plugin(InstallLogPlugin) - results = list(target_unix.installlog()) + results = list(target_unix.install_log()) assert len(results) == 3 assert results[0].ts == datetime(2026, 3, 25, 14, 6, 59, tzinfo=tz) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py new file mode 100644 index 0000000000..e9fbbb8d52 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.system_log import SystemLogPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + ("system.log", "system.log.0.gz"), + ], +) +def test_system_log(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + stat_results = [] + + entries = [] + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/{test_file}") + fs_unix.map_file(f"/var/log/{test_file}", data_file) + + entry = fs_unix.get(f"/var/log/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + entries.append(entry) + stat_results.append(stat_result) + + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): + target_unix.add_plugin(SystemLogPlugin) + results = list(target_unix.system_log()) + + results = list(target_unix.system_log()) + results.sort(key=lambda r: r.source) + + assert len(results) == 286 + + assert results[0].ts == datetime(2023, 3, 25, 7, 6, 57, tzinfo=tz) + assert results[0].host == "localhost" + assert results[0].component == "bootlog[0]:" + assert results[0].message == "BOOT_TIME 1774447617 0" + assert results[0].source == "/var/log/system.log" + + assert results[1].ts == datetime(2023, 3, 25, 7, 7, tzinfo=tz) + assert results[1].host == "localhost" + assert results[1].component == "syslogd[60]:" + assert results[1].message == ( + "Configuration Notice:\n\t" + 'ASL Module "com.apple.cdscheduler" claims selected messages.\n\t' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[1].source == "/var/log/system.log" + + assert results[2].ts == datetime(2023, 3, 25, 16, 19, 5, tzinfo=tz) + assert results[2].host == "users-Virtual-Machine" + assert results[2].component == "shutdown[1785]:" + assert results[2].message == "SHUTDOWN_TIME: 1774451945 577079" + assert results[2].source == "/var/log/system.log" + + assert results[-1].ts == datetime(2023, 5, 6, 4, 50, 20, tzinfo=tz) + assert results[-1].host == "localhost" + assert results[-1].component == "syslogd[122]:" + assert results[-1].message == "ASL Sender Statistics" + assert results[-1].source == "/var/log/system.log.0.gz" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py deleted file mode 100644 index 2046e255c6..0000000000 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_systemlog.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from typing import TYPE_CHECKING -from unittest.mock import patch - -import pytest - -from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.systemlog import SystemLogPlugin -from tests._utils import absolute_path - -if TYPE_CHECKING: - from dissect.target.filesystem import VirtualFilesystem - from dissect.target.target import Target - - -@pytest.mark.parametrize( - "test_file", - [ - "system.log", - ], -) -def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: - tz = timezone.utc - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") - fs_unix.map_file(f"/var/log/{test_file}", data_file) - - entry = fs_unix.get(f"/var/log/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(SystemLogPlugin) - - results = list(target_unix.systemlog()) - assert len(results) == 3 - - assert results[0].ts == datetime(1900, 3, 25, 7, 6, 57, tzinfo=tz) - assert results[0].host == "localhost" - assert results[0].component == "bootlog[0]:" - assert results[0].message == "BOOT_TIME 1774447617 0" - assert results[0].source == "/var/log/system.log" - - assert results[1].ts == datetime(1900, 3, 25, 7, 7, tzinfo=tz) - assert results[1].host == "localhost" - assert results[1].component == "syslogd[60]:" - assert results[1].message == ( - "Configuration Notice:\n\t" - 'ASL Module "com.apple.cdscheduler" claims selected messages.\n\t' - "Those messages may not appear in standard system log files or in the ASL database." - ) - assert results[1].source == "/var/log/system.log" - - assert results[-1].ts == datetime(1900, 3, 25, 16, 19, 5, tzinfo=tz) - assert results[-1].host == "users-Virtual-Machine" - assert results[-1].component == "shutdown[1785]:" - assert results[-1].message == "SHUTDOWN_TIME: 1774451945 577079" - assert results[-1].source == "/var/log/system.log" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py index 3459fd274e..88708d958d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py @@ -43,6 +43,8 @@ def test_launch_agents( shell="/bin/zsh", ) target_unix.users = lambda: [user] + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") @@ -50,10 +52,12 @@ def test_launch_agents( entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_agents()) @@ -165,16 +169,22 @@ def test_launch_daemons( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: + stat_results = [] + + entries = [] + for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): target_unix.add_plugin(LaunchersPlugin) results = list(target_unix.launch_daemons()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py index d90421af2b..9a9955a6d3 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py @@ -37,6 +37,8 @@ def test_login_items( shell="/bin/zsh", ) target_unix.users = lambda: [user] + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/{name}") @@ -44,10 +46,12 @@ def test_login_items( entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): target_unix.add_plugin(LoginItemsPlugin) results = list(target_unix.login_items()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py index bd87b8e848..1185160e66 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py @@ -34,16 +34,22 @@ def test_periodic_scripts( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: + stat_results = [] + + entries = [] + for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): target_unix.add_plugin(PeriodicPlugin) results = list(target_unix.periodic_scripts()) @@ -71,16 +77,19 @@ def test_periodic_conf( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + stat_results.append(stat_result) + entries.append(entry) + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): target_unix.add_plugin(PeriodicPlugin) results = list(target_unix.periodic_conf()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py index 7e32d2d927..fc1df28e93 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -35,16 +35,22 @@ def test_code_signature_coderesources( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/code_signature/{name}") fs_unix.map_file(f"{path}", data_file) entry = fs_unix.get(f"{path}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), + ): target_unix.add_plugin(CodeSignatureCodeResourcesPlugin) results = list(target_unix.code_signature_coderesources()) @@ -62,7 +68,7 @@ def test_code_signature_coderesources( assert results[3].cdhash == "\x18\udc8f\x03\x1f\udcb2f(\udcafD.F\udcdbK\udcc05\udc91B\x1f\x06\udca9" assert results[3].requirement == 'identifier "com.apple.AppleUSBEthernet_kasan" and anchor apple' - assert results[3].plist_path == "files2/MacOS/AppleUSBEthernet_kasan" + assert results[3].plist_path == "files2/macOS/AppleUSBEthernet_kasan" assert ( results[3].source == "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources" @@ -70,7 +76,7 @@ def test_code_signature_coderesources( assert results[7].cdhash == "B\udcd5uȈ\udcf1K\x03qd\udce6\udcf2T4L\udc95\udcc4\udce0\x06\udc9f" assert results[7].requirement == 'identifier "com.apple.AppleUSBHostS5L8930X_kasan" and anchor apple' - assert results[7].plist_path == "files2/MacOS/AppleUSBHostS5L8930X_kasan" + assert results[7].plist_path == "files2/macOS/AppleUSBHostS5L8930X_kasan" assert ( results[7].source == "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py index 2ecfbb9fa8..57b36dca65 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py @@ -33,16 +33,23 @@ def test_contents_info( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: + stat_results = [] + + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_info/{name}") fs_unix.map_file(f"{path}", data_file) entry = fs_unix.get(f"{path}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), + ): target_unix.add_plugin(ContentsInfoPlugin) results = list(target_unix.contents_info()) @@ -59,7 +66,7 @@ def test_contents_info( assert results[0].CFBundlePackageType == "APPL" assert results[0].CFBundleShortVersionString == "5.0" assert results[0].CFBundleSignature == "????" - assert results[0].CFBundleSupportedPlatforms == "['MacOSX']" + assert results[0].CFBundleSupportedPlatforms == "['macOSX']" assert results[0].CFBundleVersion == "5.0" assert results[0].DTCompiler == "com.apple.compilers.llvm.clang.1_0" assert results[0].DTPlatformBuild == "" @@ -83,7 +90,7 @@ def test_contents_info( assert results[4].CFBundlePackageType == "KEXT" assert results[4].CFBundleShortVersionString == "1.8" assert results[4].CFBundleSignature == "????" - assert results[4].CFBundleSupportedPlatforms == "['MacOSX']" + assert results[4].CFBundleSupportedPlatforms == "['macOSX']" assert results[4].CFBundleVersion == "1.8" assert results[4].DTCompiler == "com.apple.compilers.llvm.clang.1_0" assert results[4].DTPlatformBuild == "25E245" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py index c15ce23e04..14850946be 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -35,16 +35,22 @@ def test_contents_version( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_version/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), + ): target_unix.add_plugin(MacOSContentsVersionPlugin) results = list(target_unix.contents_version()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py index baa909cbf0..5bab000939 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py @@ -27,16 +27,22 @@ ) def test_directory_services_local_nodes(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc + stat_results = [] + entries = [] + for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/{test_file}", data_file) entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(DirectoryServicesLocalNodesPlugin) results = list(target_unix.directory_services_local_nodes()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index 3eb6af9515..8129997aa2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -35,16 +35,21 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ user, ] + stat_results = [] + entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/{test_file}") fs_unix.map_file(f"/var/db/DuetActivityScheduler/{test_file}", data_file) entry = fs_unix.get(f"/var/db/DuetActivityScheduler/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + entries.append(entry) + stat_results.append(stat_result) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(DuetActivitySchedulerPlugin) results = list(target_unix.duet_activity_scheduler()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py index c8e3ccf05c..81969a60ff 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -63,17 +63,22 @@ def test_global_user_preferences( securityagent, root, ] - + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), + ): target_unix.add_plugin(GlobalUserPreferencesPlugin) results = list(target_unix.global_user_preferences()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py index 54bade6e45..2a37fadaed 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -20,16 +20,22 @@ ], ) def test_groups(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + stat_results = [] + entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/groups/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/groups/{test_file}", data_file) entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/groups/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + entries.append(entry) + stat_results.append(stat_result) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), + ): target_unix.add_plugin(GroupPlugin) results = list(target_unix.groups()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py index 9e7ca11c24..8755ec08e1 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py @@ -35,6 +35,8 @@ def test_login_window( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: + stat_results = [] + entries = [] user = UnixUserRecord( name="user", uid=501, @@ -50,10 +52,13 @@ def test_login_window( entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(LoginWindowPlugin) results = list(target_unix.login_window()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py index 2bad8353cb..d16445aaed 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -5,7 +5,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import MacOSResourcesInfoStringsPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import macOSResourcesInfoStringsPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -25,17 +25,21 @@ def test_resources_info_strings( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/{name}") fs_unix.map_file(f"{path}", data_file) entry = fs_unix.get(f"{path}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(MacOSResourcesInfoStringsPlugin) + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): + target_unix.add_plugin(macOSResourcesInfoStringsPlugin) results = list(target_unix.resources_info_strings()) results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py index 40255f4271..a31d7f2f95 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py @@ -1 +1 @@ -# TODO write tests for MacOSResourcesLocalizableStringsPlugin +# TODO write tests for macOSResourcesLocalizableStringsPlugin diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py index 28ef55fa0f..a953bdd481 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py @@ -20,16 +20,21 @@ ], ) def test_passwords(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + stat_results = [] + entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/shadow/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/users/{test_file}", data_file) entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/users/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + entries.append(entry) + stat_results.append(stat_result) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(ShadowPlugin) results = list(target_unix.passwords()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py index cba996ebba..996be8c884 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -48,17 +48,22 @@ def test_tcc( target_unix.users = lambda: [ user, ] + stat_results = [] + entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/tcc/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() stat_result.st_mtime = 1704067199 + stat_results.append(stat_result) + entries.append(entry) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(TCCPlugin) results = list(target_unix.tcc()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py index 5807cf9e6c..e96d157211 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -35,16 +35,21 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: user, ] + stat_results = [] + entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/text_replacements/{test_file}") fs_unix.map_file(f"Users/user/Library/KeyboardServices/{test_file}", data_file) entry = fs_unix.get(f"Users/user/Library/KeyboardServices/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + entries.append(entry) + stat_results.append(stat_result) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(TextReplacementsPlugin) results = list(target_unix.text_replacements()) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py index c3d4bedaba..4aaf957de1 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -35,16 +35,21 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt user, ] + stat_results = [] + entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/user_accounts/{test_file}") fs_unix.map_file(f"Users/user/Library/Accounts/{test_file}", data_file) entry = fs_unix.get(f"Users/user/Library/Accounts/{test_file}") stat_result = entry.stat() stat_result.st_mtime = 1704067199 + entries.append(entry) + stat_results.append(stat_result) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), + ): target_unix.add_plugin(UserAccountsPlugin) results = list(target_unix.user_accounts()) From 2fd1cbac3315e311920cb2ba43d7e60fd902531e Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 13 May 2026 15:41:23 +0200 Subject: [PATCH 17/52] Plugin renames --- .../unix/bsd/darwin/macos/contents_version.py | 2 +- .../os/unix/bsd/darwin/macos/locale.py | 2 +- .../unix/bsd/darwin/macos/logs/install_log.py | 14 +++++++------- .../unix/bsd/darwin/macos/logs/system_log.py | 19 +++++++++---------- .../darwin/macos/resources_info_strings.py | 2 +- .../macos/resources_localizable_strings.py | 2 +- .../os/unix/bsd/darwin/macos/shadow.py | 8 ++++---- .../bsd/darwin/macos/test_contents_version.py | 4 ++-- .../os/unix/bsd/darwin/macos/test_locale.py | 4 ++-- .../macos/test_resources_info_strings.py | 4 ++-- 10 files changed, 30 insertions(+), 31 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index 1825b630ff..cbb4dc58c6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -43,7 +43,7 @@ } -class MacOSContentsVersionPlugin(Plugin): +class ContentsVersionPlugin(Plugin): """macOS Contents version.plist file.""" PATHS = ( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 4b51f63571..0b9480d8f7 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -14,7 +14,7 @@ from dissect.target.target import Target -class macOSLocalePlugin(LocalePlugin): +class LocalePlugin(LocalePlugin): """macOS locale plugin.""" GLOBAL = "/Library/Preferences/.GlobalPreferences.plist" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py index 76f2714d9a..7d17bc1fdf 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py @@ -13,8 +13,8 @@ from dissect.target.target import Target -macOSInstallLogRecord = TargetRecordDescriptor( - "macos/install", +InstallLogRecord = TargetRecordDescriptor( + "macos/install_log", [ ("datetime", "ts"), ("string", "host"), @@ -49,11 +49,11 @@ def check_compatible(self) -> None: if not self.target.fs.exists(self.INSTALL_LOG_PATH): raise UnsupportedPluginError("No install.log file found.") - @export(record=macOSInstallLogRecord) - def install_log(self) -> Iterator[macOSInstallLogRecord]: + @export(record=InstallLogRecord) + def install_log(self) -> Iterator[InstallLogRecord]: """Return all macOS install log messages. - Yields macOSInstallLogRecord instances with fields: + Yields InstallLogRecord instances with fields: .. code-block:: text @@ -83,7 +83,7 @@ def install_log(self) -> Iterator[macOSInstallLogRecord]: elif len(parts) == 2: hostname, message = parts component = None - yield macOSInstallLogRecord( + yield InstallLogRecord( ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip() if component else None, @@ -101,7 +101,7 @@ def install_log(self) -> Iterator[macOSInstallLogRecord]: asdf = current_buf[len(current_ts.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield macOSInstallLogRecord( + yield InstallLogRecord( ts=parse_timestamp(current_ts), host=hostname.strip(), component=component.strip(), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index d2dfdeeb13..b9355265f8 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -1,7 +1,6 @@ from __future__ import annotations import gzip -import re from datetime import timezone from typing import TYPE_CHECKING @@ -12,17 +11,16 @@ from dissect.target.plugins.os.unix.log.helpers import RE_TS if TYPE_CHECKING: + import re from collections.abc import Iterator from dissect.target.target import Target -macOSSystemLogRecord = TargetRecordDescriptor( - "macos/system", +SystemLogRecord = TargetRecordDescriptor( + "macos/system_log", [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], ) -RE_TIMESTAMP_PATTERN = re.compile(r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}") - class SystemLogPlugin(Plugin): """Return system logs on macOS.""" @@ -42,8 +40,9 @@ def _resolve_files(self) -> None: for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): self.log_files.add(file) - @export(record=macOSSystemLogRecord) - def system_log(self) -> Iterator[macOSSystemLogRecord]: + @export(record=SystemLogRecord) + def system_log(self) -> Iterator[SystemLogRecord]: + """Return all macOS system log messages.""" for file in self.log_files: filepath = self.target.fs.path(file) @@ -60,12 +59,12 @@ def system_log(self) -> Iterator[macOSSystemLogRecord]: current_buf = "" for line in logfile.readlines(): - if ts_match := RE_TIMESTAMP_PATTERN.match(line): + if ts_match := RE_TS.match(line): if current_ts_match: asdf = current_buf[len(current_ts_match.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield macOSSystemLogRecord( + yield SystemLogRecord( ts=next(ts_iter, None), host=hostname.strip(), component=component.strip(), @@ -84,7 +83,7 @@ def system_log(self) -> Iterator[macOSSystemLogRecord]: asdf = current_buf[len(current_ts_match.group()) + 1 :] hostname, component, message = asdf.split(" ", 2) - yield macOSSystemLogRecord( + yield SystemLogRecord( ts=next(ts_iter, None), host=hostname.strip(), component=component.strip(), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 036692f2ed..315e6b21f4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -16,7 +16,7 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -class macOSResourcesInfoStringsPlugin(Plugin): +class ResourcesInfoStringsPlugin(Plugin): """macOS Resources InfoPlist.strings plist file.""" PATHS = ( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index 7a5e3ef4d5..2d4065eeff 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -16,7 +16,7 @@ re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") -class macOSResourcesLocalizableStringsPlugin(Plugin): +class ResourcesLocalizableStringsPlugin(Plugin): """macOS Resources Localizable.strings plist file.""" PATHS = ( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py index 48e221af59..9618f2df09 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -12,7 +12,7 @@ from dissect.target import Target -macOSShadowRecord = TargetRecordDescriptor( +ShadowRecord = TargetRecordDescriptor( "macos/shadow", [ ("string", "name"), @@ -43,8 +43,8 @@ def _resolve_files(self) -> None: for file in self.target.fs.glob(self.USER_FILE_GLOB): self.user_files.add(file) - @export(record=macOSShadowRecord) - def passwords(self) -> Iterator[macOSShadowRecord]: + @export(record=ShadowRecord) + def passwords(self) -> Iterator[ShadowRecord]: """Yield shadow records from macOS user plist files.""" for path in self.user_files: path = self.target.fs.path(path) @@ -63,7 +63,7 @@ def passwords(self) -> Iterator[macOSShadowRecord]: salt = shadow[key]["salt"].hex() iterations = shadow[key]["iterations"] - yield macOSShadowRecord( + yield ShadowRecord( name=username, hash=hash, salt=salt, diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py index 14850946be..2e09d6c99b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -6,7 +6,7 @@ import pytest from dissect.target.plugins.os.unix.bsd.darwin.macos.contents_version import ( - MacOSContentsVersionPlugin, + ContentsVersionPlugin, ) from tests._utils import absolute_path @@ -51,7 +51,7 @@ def test_contents_version( patch.object(entries[1], "stat", return_value=stat_results[1]), patch.object(entries[2], "stat", return_value=stat_results[2]), ): - target_unix.add_plugin(MacOSContentsVersionPlugin) + target_unix.add_plugin(ContentsVersionPlugin) results = list(target_unix.contents_version()) results.sort(key=lambda r: r.source) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py index 2773a31cff..eb304dfc54 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py @@ -8,7 +8,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.locale import macOSLocalePlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.locale import LocalePlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -59,7 +59,7 @@ def test_locale( target_unix.fs.mount("/", fs_unix) target_unix.os = "macos" - target_unix.add_plugin(macOSLocalePlugin) + target_unix.add_plugin(LocalePlugin) assert target_unix.timezone == "Europe/Amsterdam" assert target_unix.language == ["en_US", "nl_NL"] diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py index d16445aaed..6ca775350b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -5,7 +5,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import macOSResourcesInfoStringsPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import ResourcesInfoStringsPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -39,7 +39,7 @@ def test_resources_info_strings( with ( patch.object(entries[0], "stat", return_value=stat_results[0]), ): - target_unix.add_plugin(macOSResourcesInfoStringsPlugin) + target_unix.add_plugin(ResourcesInfoStringsPlugin) results = list(target_unix.resources_info_strings()) results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) From 17da16edc094a0d50f23cb51d8a2b59dc29b6419 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 13 May 2026 16:18:32 +0200 Subject: [PATCH 18/52] Add parser for WiFi logs --- .../os/unix/bsd/darwin/macos/logs/wifi_log.py | 101 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/logs/wifi.log | 3 + .../bsd/darwin/macos/logs/test_install_log.py | 4 +- .../bsd/darwin/macos/logs/test_wifi_log.py | 51 +++++++++ 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/wifi.log create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py new file mode 100644 index 0000000000..8ca39af4a8 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import re +from datetime import timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.helpers.utils import year_rollover_helper +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +RE_TS = re.compile( + r""" + ^ + (?P + [A-Za-z]{3} + \s+ + [A-Za-z]{3} + \s+ + \d{1,2} + \s+ + \d{2}:\d{2}:\d{2} + \.\d{3} + ) + """, + re.VERBOSE, +) + + +WifiLogRecord = TargetRecordDescriptor( + "wifi_log", + [("datetime", "ts"), ("string", "host"), ("string", "message"), ("path", "source")], +) + + +class WifiLogPlugin(Plugin): + """macOS WiFi logs plugin.""" + + PATH = "/var/log/wifi.log" + + def __init__(self, target: Target): + super().__init__(target) + self.file = None + self._resolve_file() + + def _resolve_file(self) -> None: + path = self.target.fs.path(self.PATH) + if path.exists(): + self.file = path + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No wifi.log file found") + + @export(record=WifiLogRecord) + def wifi_log(self) -> Iterator[WifiLogRecord]: + """Return all macOS WiFi log messages.""" + timestamps = [ts for ts, _ in year_rollover_helper(self.file, RE_TS, "%a %b %d %H:%M:%S.%f", timezone.utc)] + timestamps.reverse() + ts_iter = iter(timestamps) + + with self.file.open(mode="rt") as logfile: + current_ts_match: re.Match[str] | None = None + current_buf = "" + + for line in logfile.readlines(): + if ts_match := RE_TS.match(line): + if current_ts_match: + asdf = current_buf[len(current_ts_match.group()) + 1 :] + hostname, message = asdf.split(" ", 1) + + yield WifiLogRecord( + ts=next(ts_iter, None), + host=hostname.strip(), + message=message.strip(), + source=self.file, + _target=self.target, + ) + + current_ts_match = ts_match + current_buf = line + + elif current_buf: + current_buf += line + + if current_ts_match and current_buf: + asdf = current_buf[len(current_ts_match.group()) + 1 :] + hostname, message = asdf.split(" ", 1) + + yield WifiLogRecord( + ts=next(ts_iter, None), + host=hostname.strip(), + message=message.strip(), + source=self.file, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/wifi.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/wifi.log new file mode 100644 index 0000000000..71d7d879f9 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/wifi.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad95ffb6e3a7112e5ef1ef70e7c798f2a36d64f99342d38a3c221fdbf0720305 +size 289 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py index 2ffab09699..35c1259d53 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py @@ -47,10 +47,10 @@ def test_install_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesy assert results[1].host == "localhost" assert results[1].component == "Installer" assert results[1].message == "Progress[57]: Logging also using os_log, installerProgressLog = 0xc6501c080" - assert results[0].source == "/var/log/install.log" + assert results[1].source == "/var/log/install.log" assert results[-1].ts == datetime(2026, 3, 25, 15, 18, 58, tzinfo=tz) assert results[-1].host == "users-Virtual-Machine" assert results[-1].component == "loginwindow[1042]:" assert results[-1].message == "+[SUOSULoginCredentialPolicy currentLoginCredentialPolicy] = 0" - assert results[0].source == "/var/log/install.log" + assert results[-1].source == "/var/log/install.log" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py new file mode 100644 index 0000000000..6d5df89148 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.wifi_log import WifiLogPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "wifi.log", + ], +) +def test_wifi_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") + fs_unix.map_file(f"/var/log/{test_file}", data_file) + + entry = fs_unix.get(f"/var/log/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(WifiLogPlugin) + + results = list(target_unix.wifi_log()) + assert len(results) == 2 + + assert results[0].ts == datetime(2023, 5, 4, 4, 23, 16, 774000, tzinfo=tz) + assert results[0].host == "" + assert results[0].message == "configdStart: ****** [AirPort logger started] ******" + assert results[0].source == "/var/log/wifi.log" + + assert results[1].ts == datetime(2023, 5, 4, 4, 23, 16, 776000, tzinfo=tz) + assert results[1].host == "[airport]/114" + assert ( + results[1].message + == "@[5.319676] (configdIODriverInterface.m:401) dq:'com.apple.main-thread'/tid[0x39e] FreedDeviceNodeListManager initialized, list[0xb2cd122e0] lock[0xb2d0f8380]" # noqa: E501 + ) + assert results[1].source == "/var/log/wifi.log" From 285cd0bcc36a3ebdecc44b5ab16f2f315c358fb6 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 14 May 2026 16:42:13 +0200 Subject: [PATCH 19/52] Return None instead of skipping lines without timestamp in year_rollover_helper --- dissect/target/helpers/utils.py | 4 ++-- tests/plugins/os/unix/log/test_messages.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dissect/target/helpers/utils.py b/dissect/target/helpers/utils.py index b97b6e790d..919a032704 100644 --- a/dissect/target/helpers/utils.py +++ b/dissect/target/helpers/utils.py @@ -110,7 +110,7 @@ def readinto(buffer: bytearray, fh: BinaryIO) -> int: def year_rollover_helper( path: Path, re_ts: str | re.Pattern, ts_format: str, tzinfo: tzinfo = timezone.utc -) -> Iterator[tuple[datetime, str]]: +) -> Iterator[tuple[datetime | None, str]]: """Helper function for determining the correct timestamps for log files without year notation. Supports compressed files by using :func:`open_decompress`. @@ -142,7 +142,7 @@ def year_rollover_helper( if not warned: log.warning("No timestamp found in one of the lines in %s!", path) warned = True - log.debug("Skipping line: %s", line) + yield None, line continue # We have to append the current_year to strptime instead of adding it using replace later. diff --git a/tests/plugins/os/unix/log/test_messages.py b/tests/plugins/os/unix/log/test_messages.py index 1dbe65c1b7..43fb5dccf2 100644 --- a/tests/plugins/os/unix/log/test_messages.py +++ b/tests/plugins/os/unix/log/test_messages.py @@ -84,11 +84,11 @@ def test_unix_log_messages_compressed_timezone_year_rollover() -> None: results = list(target.messages()) results.reverse() - assert len(results) == 2 + assert len(results) == 3 assert isinstance(results[0], type(MessagesRecord())) - assert isinstance(results[1], type(MessagesRecord())) + assert isinstance(results[2], type(MessagesRecord())) assert results[0].ts == datetime(2020, 12, 31, 3, 14, 0, tzinfo=ZoneInfo("America/Chicago")) - assert results[1].ts == datetime(2021, 1, 1, 13, 37, 0, tzinfo=ZoneInfo("America/Chicago")) + assert results[2].ts == datetime(2021, 1, 1, 13, 37, 0, tzinfo=ZoneInfo("America/Chicago")) def test_unix_log_messages_malformed_log_year_rollover(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: @@ -109,7 +109,7 @@ def test_unix_log_messages_malformed_log_year_rollover(target_unix_users: Target target_unix_users.add_plugin(MessagesPlugin) results = list(target_unix_users.messages()) - assert len(results) == 2 + assert len(results) == 3 assert results[0].ts assert results[0].service == "systemd" From ca71719b011e302b6ba900a3019311f217587b65 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 14 May 2026 17:05:45 +0200 Subject: [PATCH 20/52] Only iterate over logs once --- .../unix/bsd/darwin/macos/logs/system_log.py | 48 ++++--------------- .../os/unix/bsd/darwin/macos/logs/wifi_log.py | 46 ++++++------------ .../bsd/darwin/macos/logs/test_system_log.py | 1 + .../bsd/darwin/macos/logs/test_wifi_log.py | 16 +++---- 4 files changed, 34 insertions(+), 77 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index b9355265f8..d76068c3c9 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -1,6 +1,5 @@ from __future__ import annotations -import gzip from datetime import timezone from typing import TYPE_CHECKING @@ -11,7 +10,6 @@ from dissect.target.plugins.os.unix.log.helpers import RE_TS if TYPE_CHECKING: - import re from collections.abc import Iterator from dissect.target.target import Target @@ -46,48 +44,22 @@ def system_log(self) -> Iterator[SystemLogRecord]: for file in self.log_files: filepath = self.target.fs.path(file) - timestamps = [ts for ts, _ in year_rollover_helper(filepath, RE_TS, "%b %d %H:%M:%S", timezone.utc)] - timestamps.reverse() - ts_iter = iter(timestamps) - - with ( - gzip.open(filepath.open("rb"), mode="rt") - if file.endswith(".gz") - else filepath.open(mode="rt") as logfile - ): - current_ts_match: re.Match[str] | None = None - current_buf = "" - - for line in logfile.readlines(): - if ts_match := RE_TS.match(line): - if current_ts_match: - asdf = current_buf[len(current_ts_match.group()) + 1 :] - hostname, component, message = asdf.split(" ", 2) - - yield SystemLogRecord( - ts=next(ts_iter, None), - host=hostname.strip(), - component=component.strip(), - message=message.strip(), - source=filepath, - _target=self.target, - ) - - current_ts_match = ts_match - current_buf = line - - elif current_buf: - current_buf += line - - if current_ts_match and current_buf: - asdf = current_buf[len(current_ts_match.group()) + 1 :] + current_buf = "" + + for ts, line in year_rollover_helper(filepath, RE_TS, "%b %d %H:%M:%S", timezone.utc): + current_buf = line + "\n\t" + current_buf + if ts: + match = RE_TS.match(current_buf) + asdf = current_buf[match.end() :].lstrip(" ") hostname, component, message = asdf.split(" ", 2) yield SystemLogRecord( - ts=next(ts_iter, None), + ts=ts, host=hostname.strip(), component=component.strip(), message=message.strip(), source=filepath, _target=self.target, ) + + current_buf = "" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py index 8ca39af4a8..c18425eb2b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py @@ -60,42 +60,26 @@ def check_compatible(self) -> None: @export(record=WifiLogRecord) def wifi_log(self) -> Iterator[WifiLogRecord]: """Return all macOS WiFi log messages.""" - timestamps = [ts for ts, _ in year_rollover_helper(self.file, RE_TS, "%a %b %d %H:%M:%S.%f", timezone.utc)] - timestamps.reverse() - ts_iter = iter(timestamps) - - with self.file.open(mode="rt") as logfile: - current_ts_match: re.Match[str] | None = None - current_buf = "" - - for line in logfile.readlines(): - if ts_match := RE_TS.match(line): - if current_ts_match: - asdf = current_buf[len(current_ts_match.group()) + 1 :] - hostname, message = asdf.split(" ", 1) - - yield WifiLogRecord( - ts=next(ts_iter, None), - host=hostname.strip(), - message=message.strip(), - source=self.file, - _target=self.target, - ) - - current_ts_match = ts_match - current_buf = line - - elif current_buf: - current_buf += line - - if current_ts_match and current_buf: - asdf = current_buf[len(current_ts_match.group()) + 1 :] + current_buf = "" + + for ts, line in year_rollover_helper( + self.file, + RE_TS, + "%a %b %d %H:%M:%S.%f", + timezone.utc, + ): + current_buf = line + "\n\t" + current_buf + if ts: + match = RE_TS.match(current_buf) + asdf = current_buf[match.end() :].lstrip(" ") hostname, message = asdf.split(" ", 1) yield WifiLogRecord( - ts=next(ts_iter, None), + ts=ts, host=hostname.strip(), message=message.strip(), source=self.file, _target=self.target, ) + + current_buf = "" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py index e9fbbb8d52..67b7cca9fe 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py @@ -44,6 +44,7 @@ def test_system_log(test_files: str, target_unix: Target, fs_unix: VirtualFilesy results = list(target_unix.system_log()) results = list(target_unix.system_log()) + results.reverse() results.sort(key=lambda r: r.source) assert len(results) == 286 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py index 6d5df89148..4a205e21d2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py @@ -37,15 +37,15 @@ def test_wifi_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste results = list(target_unix.wifi_log()) assert len(results) == 2 - assert results[0].ts == datetime(2023, 5, 4, 4, 23, 16, 774000, tzinfo=tz) - assert results[0].host == "" - assert results[0].message == "configdStart: ****** [AirPort logger started] ******" - assert results[0].source == "/var/log/wifi.log" - - assert results[1].ts == datetime(2023, 5, 4, 4, 23, 16, 776000, tzinfo=tz) - assert results[1].host == "[airport]/114" + assert results[0].ts == datetime(2023, 5, 4, 4, 23, 16, 776000, tzinfo=tz) + assert results[0].host == "[airport]/114" assert ( - results[1].message + results[0].message == "@[5.319676] (configdIODriverInterface.m:401) dq:'com.apple.main-thread'/tid[0x39e] FreedDeviceNodeListManager initialized, list[0xb2cd122e0] lock[0xb2d0f8380]" # noqa: E501 ) + assert results[0].source == "/var/log/wifi.log" + + assert results[1].ts == datetime(2023, 5, 4, 4, 23, 16, 774000, tzinfo=tz) + assert results[1].host == "" + assert results[1].message == "configdStart: ****** [AirPort logger started] ******" assert results[1].source == "/var/log/wifi.log" From 4c76edf8f25df74c83129a1eeaeba85dc9e5be89 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 18 May 2026 11:15:53 +0200 Subject: [PATCH 21/52] Improve build_sqlite_records helper function --- .../bsd/darwin/macos/helpers/build_records.py | 215 +++++++++--------- 1 file changed, 105 insertions(+), 110 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 583b5ff998..ba04cca157 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -33,102 +33,107 @@ def build_sqlite_records( joins: tuple = (), field_mappings: dict | None = None, ) -> Iterator[TargetRecordDescriptor]: + joins_by_table1 = defaultdict(list) + joins_by_table2 = defaultdict(list) + ignore_joins_map = defaultdict(list) + + for j in joins: + joins_by_table1[j["table1"]].append(j) + joins_by_table2[j["table2"]].append(j) + + if j["join"] == "ignore": + key = (j["table1"], j["table2"]) + ignore_joins_map[key].append(j) + for file in files: - with SQLite3(file) as database: - for table in database.tables(): - for row in table.rows(): - row_dict = {k: v for k, v in row} # noqa C416 - - for key, value in list(row_dict.items()): - if isinstance(value, (bytes, bytearray)) and value.startswith(b"bplist00"): - if is_nskeyedarchive_blob(value): - try: - archiver = NSKeyedArchiver(BytesIO(value)) - decoded_value = archiver.top.get("root") - row_dict[key] = decoded_value - except Exception: - plugin.target.log.exception( - "Failed to decode %s value for key %s", - table.name, - key, + try: + with SQLite3(file) as database: + for table in database.tables(): + for row in table.rows(): + row_dict = {k: v for k, v in row} # noqa C416 + + for key, value in list(row_dict.items()): + if isinstance(value, (bytes, bytearray)) and value.startswith(b"bplist00"): + if is_nskeyedarchive_blob(value): + try: + archiver = NSKeyedArchiver(BytesIO(value)) + decoded_value = archiver.top.get("root") + row_dict[key] = decoded_value + except Exception: + plugin.target.log.exception( + "Failed to decode %s value for key %s", + table.name, + key, + ) + else: + yield from build_record_from_data( + plugin, file, value, record_descriptors, field_mappings ) - else: - yield from build_record_from_data( - plugin, file, value, record_descriptors, field_mappings - ) - row_dict.pop(key) - - if table.name in {j["table2"] for j in joins}: - for j2 in joins: - if j2["table2"] != table.name: - continue - - if row_dict.get(j2["key2"]) is None: - row_dict["table"] = table.name - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) - break - else: - match = False - for row1 in database.table(j2["table1"]).rows(): - row1_dict = {k: v for k, v in row1} # noqa C416 - if row1_dict.get(j2["key1"]) == row_dict.get(j2["key2"]): - match = True - break + row_dict.pop(key) - if not match: + if table.name in joins_by_table2: + for j2 in joins_by_table2[table.name]: + if row_dict.get(j2["key2"]) is None: row_dict["table"] = table.name yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) break + else: + match = False + for row1 in database.table(j2["table1"]).rows(): + row1_dict = {k: v for k, v in row1} # noqa C416 + if row1_dict.get(j2["key1"]) == row_dict.get(j2["key2"]): + match = True + break + + if not match: + row_dict["table"] = table.name + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + break + + elif table.name in joins_by_table1: + tables = set() + iterate_rows = defaultdict(list) + tables.add(table.name) + + for j in joins_by_table1[table.name]: + ignore_joins = ignore_joins_map[(j["table1"], j["table2"])] + + if j["join"] == "iterate": + for expanded in handle_iterate_join( + database, row_dict, j, joins_by_table1, ignore_joins_map, tables + ): + iterate_rows[j["table2"]].append(expanded) + + elif j["join"] == "nested": + handle_nested_join(database, row_dict, j, ignore_joins, tables) + + if len(iterate_rows) > 0: + row_dict = prefix_row(row_dict, table.name) + keys = list(iterate_rows.keys()) + values = list(iterate_rows.values()) + for key in keys: + tables.add(key) + row_dict["tables"] = tables + + for combination in product(*values): + combined_row = dict(row_dict) + for joined_row in combination: + combined_row.update(joined_row) + + yield build_record( + plugin, + combined_row, + file, + record_descriptors, + ) + else: + yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) - elif table.name in {j["table1"] for j in joins}: - tables = set() - iterate_rows = defaultdict(list) - tables.add(table.name) - - for j in joins: - if j["table1"] != table.name: - continue - - ignore_joins = [ - ij - for ij in joins - if ij["table1"] == j["table1"] - and ij["table2"] == j["table2"] - and ij["join"] == "ignore" - ] - - if j["join"] == "iterate": - for expanded in handle_iterate_join(database, row_dict, j, joins, tables): - iterate_rows[j["table2"]].append(expanded) - - elif j["join"] == "nested": - handle_nested_join(database, row_dict, j, ignore_joins, tables) - - if len(iterate_rows) > 0: - row_dict = prefix_row(row_dict, table.name) - keys = list(iterate_rows.keys()) - values = list(iterate_rows.values()) - for key in keys: - tables.add(key) - row_dict["tables"] = tables - - for combination in product(*values): - combined_row = dict(row_dict) - for joined_row in combination: - combined_row.update(joined_row) - - yield build_record( - plugin, - combined_row, - file, - record_descriptors, - ) else: + row_dict["table"] = table.name yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) - - else: - row_dict["table"] = table.name - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + except Exception: + plugin.target.log.exception("Failed to process SQLite file: %s", file) def prefix_row(row_dict: dict, table: str) -> dict: @@ -139,16 +144,13 @@ def handle_iterate_join( database: SQLite3, parent_dict: dict, current_join: dict, - joins: tuple, + joins_by_table1: defaultdict(list), + ignore_joins_map: defaultdict(list), tables: set[str], ) -> list[dict]: results: list[dict] = [] - ignore_joins = [ - ij - for ij in joins - if ij["table1"] == current_join["table1"] and ij["table2"] == current_join["table2"] and ij["join"] == "ignore" - ] + ignore_joins = ignore_joins_map[(current_join["table1"], current_join["table2"])] for child_row in database.table(current_join["table2"]).rows(): if child_row.get(current_join["key2"]) != parent_dict.get(current_join["key1"]): @@ -164,23 +166,16 @@ def handle_iterate_join( child_dict.pop(ij["key2"], None) downstream_iterate_rows = defaultdict(list) - for dj in joins: - if dj["table1"] != current_join["table2"]: - continue - - downstream_ignore = [ - ij - for ij in joins - if ij["table1"] == dj["table1"] and ij["table2"] == dj["table2"] and ij["join"] == "ignore" - ] - - if dj["join"] == "iterate": - for expanded in handle_iterate_join(database, child_dict, dj, joins, tables): - downstream_iterate_rows[dj["table2"]].append(expanded) - - elif dj["join"] == "nested": - tables.add(dj["table2"]) - handle_nested_join(database, child_dict, dj, downstream_ignore) + for j in joins_by_table1[current_join["table2"]]: + downstream_ignore = ignore_joins_map[(j["table1"], j["table2"])] + + if j["join"] == "iterate": + for expanded in handle_iterate_join(database, child_dict, j, joins_by_table1, ignore_joins_map, tables): + downstream_iterate_rows[j["table2"]].append(expanded) + + elif j["join"] == "nested": + tables.add(j["table2"]) + handle_nested_join(database, child_dict, j, downstream_ignore) if len(downstream_iterate_rows) > 0: base_prefixed = prefix_row(child_dict, current_join["table2"]) From 0c09f0b114c44ff065147803980735106bd04634 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 18 May 2026 14:46:56 +0200 Subject: [PATCH 22/52] Changed at jobs test file --- .../unix/bsd/darwin/macos/persistence/a0000701c43af0 | 3 --- .../unix/bsd/darwin/macos/persistence/a0000901c45260 | 3 +++ .../unix/bsd/darwin/macos/persistence/test_at_jobs.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 deleted file mode 100644 index 5397add753..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000701c43af0 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8bc687e4fe580a6cf8791b085cbc96203de962476846bbe6553083acf4c09eba -size 1427 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 new file mode 100644 index 0000000000..00a19a35d2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dca01c23993861cfdaeb3de764125e6e69693e2ac8bee8b928a30680bc5b81b +size 1452 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py index eef4c6edee..5c70bb8a05 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py @@ -17,7 +17,7 @@ @pytest.mark.parametrize( "test_file", [ - "a0000701c43af0", + "a0000901c45260", ], ) def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: @@ -37,7 +37,7 @@ def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem assert len(results) == 1 assert results[0].queue == "a" - assert results[0].seq == 7 - assert results[0].execution_time == datetime(2026, 5, 8, 12, 0, 0, tzinfo=tz) - assert results[0].command == "periodic_test.sh" - assert results[0].source == "/usr/lib/cron/jobs/a0000701c43af0" + assert results[0].seq == 9 + assert results[0].execution_time == datetime(2026, 5, 12, 16, 0, 0, tzinfo=tz) + assert results[0].command == 'say "hello"' + assert results[0].source == "/usr/lib/cron/jobs/a0000901c45260" From 90a832cd7b0814f66f820ed426065a71c0079c02 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 18 May 2026 14:47:21 +0200 Subject: [PATCH 23/52] add fsck_apfs_log plugin --- .../bsd/darwin/macos/logs/fsck_apfs_log.py | 68 +++++++++++++++++++ .../unix/bsd/darwin/macos/logs/fsck_apfs.log | 3 + .../darwin/macos/logs/test_fsck_apfs_log.py | 53 +++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs.log create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py new file mode 100644 index 0000000000..6f7c70bb35 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import re +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +FsckAPFSLogRecord = TargetRecordDescriptor( + "macos/fsck_apfs_log", + [ + ("string", "disk_path"), + ("string", "message"), + ("datetime", "ts"), + ("path", "source"), + ], +) + + +RE_TIMESTAMP_PATTERN = re.compile( + r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+" + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+" + r"\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4}$" +) + + +class FsckAPFSLogPlugin(Plugin): + """Return information related to fsck_apfs log entries on macOS.""" + + FSCK_APFS_LOG_PATH = "/var/log/fsck_apfs.log" + + def __init__(self, target: Target): + super().__init__(target) + + def check_compatible(self) -> None: + if not self.target.fs.exists(self.FSCK_APFS_LOG_PATH): + raise UnsupportedPluginError("No fsck_apfs.log file found.") + + @export(record=FsckAPFSLogRecord) + def fsck_apfs_log(self) -> Iterator[FsckAPFSLogRecord]: + """Return all fsck_apfs log messages.""" + logfile = self.target.fs.path(self.FSCK_APFS_LOG_PATH).open(mode="rt") + + for line in logfile.readlines(): + if line != "\n": + parts = line.split(" ", 1) + + disk_path, message = parts + disk_path = disk_path.strip(":") + ts = None + + if ts_match := RE_TIMESTAMP_PATTERN.search(message): + ts = datetime.strptime(ts_match.group(), "%a %b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc) + + yield FsckAPFSLogRecord( + disk_path=disk_path.strip(), + message=message.strip(), + ts=ts, + source=self.FSCK_APFS_LOG_PATH, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs.log new file mode 100644 index 0000000000..5092326888 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs.log @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4af304504831599e76e3bcc190d39a8ee55504705ac3d48c82280f234484dbd8 +size 220 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py new file mode 100644 index 0000000000..6841f5e049 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.fsck_apfs_log import FsckAPFSLogPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "fsck_apfs.log", + ], +) +def test_fsck_apfs_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") + fs_unix.map_file(f"/var/log/{test_file}", data_file) + + entry = fs_unix.get(f"/var/log/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + with patch.object(entry, "stat") as mock_stat: + mock_stat.return_value = stat_result + + target_unix.add_plugin(FsckAPFSLogPlugin) + + results = list(target_unix.fsck_apfs_log()) + assert len(results) == 3 + + assert results[0].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) + assert results[0].disk_path == "/dev/rdisk1s1" + assert results[0].message == "fsck_apfs started at Mon May 4 04:23:21 2026" + assert results[0].source == "/var/log/fsck_apfs.log" + + assert results[1].ts is None + assert results[1].disk_path == "/dev/rdisk1s1" + assert results[1].message == "error: container /dev/rdisk1 is mounted with write access; please re-run with -l." + assert results[1].source == "/var/log/fsck_apfs.log" + + assert results[-1].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) + assert results[-1].disk_path == "/dev/rdisk1s1" + assert results[-1].message == "fsck_apfs completed at Mon May 4 04:23:21 2026" + assert results[-1].source == "/var/log/fsck_apfs.log" From 2488a8b58d1cc0dbf08e8da5bdf1f4565a3c74f6 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 26 May 2026 16:18:11 +0200 Subject: [PATCH 24/52] parse ASL logs --- .../os/unix/bsd/darwin/macos/logs/asl.py | 216 ++++++++++++++++++ .../bsd/darwin/macos/logs/2026.05.06.G80.asl | 3 + .../os/unix/bsd/darwin/macos/logs/test_asl.py | 83 +++++++ 3 files changed, 302 insertions(+) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/logs/2026.05.06.G80.asl create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py new file mode 100644 index 0000000000..7f5c354b2e --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import struct +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +from dissect.cstruct import cstruct + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + from typing import Any + + from dissect.target.target import Target + +cs = cstruct(endian=">") + +cs.load(""" +struct asl_record { + uint64 next; + uint64 msg_id; + uint64 time_s; + uint32 unknown; + + uint16 level; + uint16 flags; + + uint32 pid; + + uint32 uid; + uint32 gid; + uint32 ruid; + uint32 rgid; + uint32 rpid; + uint32 kvcount; + + uint64 host_ref; + uint64 sender_ref; + uint64 facility_ref; + uint64 message_ref; + uint64 ref5; + uint64 ref6; +}; +""") + +ASLRecord = TargetRecordDescriptor( + "macos/logs/asl", + [ + ("datetime", "ts"), + ("varint", "severity_level"), + ("varint", "pid"), + ("string", "asl_host"), + ("string", "sender"), + ("string", "facility"), + ("string", "message"), + ("path", "source"), + ], +) + + +def _parse_asl_string_ref(data: bytes, ref: int) -> str | None: + if ref == 0: + return None + + if ref & 0x8000000000000000: + ref_bytes = struct.pack(">Q", ref & 0x7FFFFFFFFFFFFFFF) + slen = ref_bytes[0] + return ref_bytes[1 : 1 + slen].decode("utf-8", errors="replace").rstrip("\x00") + + if ref + 6 < len(data) and data[ref : ref + 2] == b"\x00\x01": + slen = struct.unpack(">I", data[ref + 2 : ref + 6])[0] + if 0 < slen < 65536 and ref + 6 + slen <= len(data): + return data[ref + 6 : ref + 6 + slen].decode("utf-8", errors="replace").rstrip("\x00") + + return None + + +def _valid_value(s: str | None) -> bool: + if not s: + return True + printable = sum(32 <= ord(c) < 127 for c in s) + return printable / len(s) > 0.9 + + +def _valid_ref(data: bytes, ref: int) -> bool: + if ref == 0: + return False + if ref & 0x8000000000000000: + return True + return ref + 6 < len(data) and data[ref : ref + 2] == b"\x00\x01" + + +def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: + """Parse an ASL DB binary file using cstruct.""" + if len(data) < 80 or data[:6] != b"ASL DB": + return + + now = int(datetime.now(tz=timezone.utc).timestamp()) + pos = 0x80 + + while pos < len(data) - 60: + rec_len = int.from_bytes(data[pos : pos + 2], "big") + + # Sanity checks + if rec_len < 120 or rec_len > 65535 or pos + rec_len + 2 > len(data): + pos += 2 + continue + + # Parse struct safely + try: + rec = cs.asl_record(data[pos + 2 : pos + 2 + rec_len]) + except Exception: + pos += 2 + continue + + # Timestamp validation + if not (946684800 < rec.time_s < now + 31536000): + pos += 2 + continue + + refs = [ + rec.host_ref, + rec.sender_ref, + rec.facility_ref, + rec.message_ref, + rec.ref5, + rec.ref6, + ] + + if not any(_valid_ref(data, r) for r in refs): + pos += 2 + continue + + # Decode strings + host = _parse_asl_string_ref(data, rec.host_ref) + sender = _parse_asl_string_ref(data, rec.sender_ref) + facility = _parse_asl_string_ref(data, rec.facility_ref) + message = _parse_asl_string_ref(data, rec.message_ref) + + if not _valid_value(host): + host = None + if not _valid_value(sender): + sender = None + if not _valid_value(facility): + facility = None + if not _valid_value(message): + message = None + + if not any([host, sender, facility, message]): + pos += 2 + continue + + yield { + "ts": datetime.fromtimestamp(rec.time_s, tz=timezone.utc), + "level": rec.level, + "pid": rec.pid, + "host": host, + "sender": sender, + "facility": facility, + "message": message, + } + + pos += rec_len + 2 + + +class ASLPlugin(Plugin): + """Plugin to parse macOS ASL databases.""" + + ASL_PATHS = ( + "var/log/asl/*.asl", + "var/log/powermanagement/*.asl", + "var/log/DiagnosticMessages/*.asl", + ) + + def __init__(self, target: Target): + super().__init__(target) + self._asl_files = set() + self._find_files() + + def _find_files(self) -> None: + for pattern in self.ASL_PATHS: + for path in self.target.fs.path("/").glob(pattern): + if path.is_file(): + self._asl_files.add(path) + + def check_compatible(self) -> None: + if not self._asl_files: + raise UnsupportedPluginError("No .asl files found") + + @export(record=ASLRecord) + def asl(self) -> Iterator[ASLRecord]: + """Return all apple system log messages.""" + for asl_path in self._asl_files: + try: + with asl_path.open("rb") as fh: + data = fh.read() + records = _parse_asl_file(data) + except Exception as e: + self.target.log.warning("Error parsing ASL file %s: %s", asl_path, e) + continue + + for rec in records: + yield ASLRecord( + ts=rec["ts"], + severity_level=rec["level"], + pid=rec["pid"], + asl_host=rec["host"], + sender=rec["sender"], + facility=rec["facility"], + message=rec["message"], + source=asl_path, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/2026.05.06.G80.asl b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/2026.05.06.G80.asl new file mode 100644 index 0000000000..15ff31246d --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/2026.05.06.G80.asl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1f251241851f3101de3a206287d0eaed195dae28c3a43d35aede3b5e700e628 +size 13915 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py new file mode 100644 index 0000000000..f055d6120c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING +from unittest.mock import patch + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.logs.asl import ASLPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + ("2026.05.06.G80.asl"), + ], +) +def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + tz = timezone.utc + stat_results = [] + + entries = [] + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") + fs_unix.map_file(f"/var/log/asl/{test_file}", data_file) + entry = fs_unix.get(f"/var/log/asl/{test_file}") + stat_result = entry.stat() + stat_result.st_mtime = 1704067199 + + entries.append(entry) + stat_results.append(stat_result) + + with ( + patch.object(entries[0], "stat", return_value=stat_results[0]), + ): + target_unix.add_plugin(ASLPlugin) + results = list(target_unix.asl()) + results.sort(key=lambda r: r.source) + + assert len(results) == 11 + + assert results[0].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) + assert results[0].severity_level == 5 + assert results[0].pid == 121 + assert results[0].asl_host == "localhost" + assert results[0].sender == "syslogd" + assert results[0].facility == "syslog" + assert results[0].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.cdscheduler" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[0].source == "/var/log/asl/2026.05.06.G80.asl" + + assert results[1].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) + assert results[1].severity_level == 5 + assert results[1].pid == 121 + assert results[1].asl_host == "localhost" + assert results[1].sender == "syslogd" + assert results[1].facility == "syslog" + assert results[1].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.install" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[1].source == "/var/log/asl/2026.05.06.G80.asl" + + assert results[-1].ts == datetime(2026, 5, 6, 11, 50, 20, tzinfo=tz) + assert results[-1].severity_level == 5 + assert results[-1].pid == 122 + assert results[-1].asl_host == "localhost" + assert results[-1].sender == "syslogd" + assert results[-1].facility == "syslog" + assert results[-1].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.eventmonitor" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[-1].source == "/var/log/asl/2026.05.06.G80.asl" From da8c07195610454b3240f7cf41e183b0f51bcbbb Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 26 May 2026 16:18:41 +0200 Subject: [PATCH 25/52] Process feedback & general improvements --- .../bsd/darwin/macos/airport_preferences.py | 13 +-- .../macos/code_signature_coderesources.py | 24 ++++- .../os/unix/bsd/darwin/macos/contents_info.py | 4 +- .../unix/bsd/darwin/macos/contents_version.py | 4 +- .../macos/directory_services_local_nodes.py | 19 +++- .../darwin/macos/duet_activity_scheduler.py | 2 +- .../darwin/macos/global_user_preferences.py | 5 +- .../helpers/{general.py => build_paths.py} | 15 --- .../bsd/darwin/macos/helpers/build_records.py | 17 ++++ .../darwin/macos/helpers/parse_timestamp.py | 20 ++++ .../bsd/darwin/macos/identity_services.py | 2 +- .../bsd/darwin/macos/installation_history.py | 4 +- .../os/unix/bsd/darwin/macos/keychain.py | 2 +- .../os/unix/bsd/darwin/macos/login_window.py | 5 +- .../bsd/darwin/macos/logs/fsck_apfs_log.py | 45 ++++----- .../unix/bsd/darwin/macos/logs/install_log.py | 92 +++++++++---------- .../bsd/darwin/macos/persistence/at_jobs.py | 3 - .../bsd/darwin/macos/persistence/launchers.py | 5 +- .../darwin/macos/persistence/login_items.py | 38 +++++++- .../bsd/darwin/macos/persistence/periodic.py | 3 - .../darwin/macos/resources_info_strings.py | 3 - .../macos/resources_localizable_strings.py | 3 - .../bsd/darwin/macos/system_preferences.py | 3 - .../plugins/os/unix/bsd/darwin/macos/tcc.py | 2 +- .../bsd/darwin/macos/text_replacements.py | 4 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 4 +- .../macos/persistence/BackgroundItems-v16.btm | 4 +- .../bsd/darwin/macos/logs/test_system_log.py | 6 +- .../macos/persistence/test_launchers.py | 20 +++- .../macos/persistence/test_login_items.py | 58 ++++++++---- .../bsd/darwin/macos/test_contents_info.py | 4 +- .../macos/test_duet_activity_scheduler.py | 27 ++++++ .../macos/test_global_user_preferences.py | 24 ++--- .../os/unix/bsd/darwin/macos/test_groups.py | 2 +- .../darwin/macos/test_installation_history.py | 2 +- .../os/unix/bsd/darwin/macos/test_shadow.py | 2 +- .../darwin/macos/test_system_preferences.py | 2 +- .../darwin/macos/test_text_replacements.py | 4 + 38 files changed, 300 insertions(+), 196 deletions(-) rename dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/{general.py => build_paths.py} (71%) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index ffdd5ce04f..efb9f11c3c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -48,16 +48,11 @@ def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: """Yield AirPort preference information.""" plist = plistlib.load(self.file.open()) - counter = plist.get("Counter") - version = plist.get("Version") - device_uuid = plist.get("DeviceUUID") - preferred_order = plist.get("PreferredOrder") - yield AirportPreferencesRecord( - counter=counter, - device_uuid=device_uuid, - version=version, - preferred_order=preferred_order, + counter=plist.get("Counter"), + device_uuid=plist.get("DeviceUUID"), + version=plist.get("Version"), + preferred_order=plist.get("PreferredOrder"), source=self.file, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py index 2a3e2ed82c..286d40c467 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - OmitRecord = TargetRecordDescriptor( "macos/code_signature_coderesources/omit", [ @@ -35,6 +32,16 @@ ], ) +OptionalRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/optional", + [ + ("boolean", "optional"), + ("varint", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + CDHashRecord = TargetRecordDescriptor( "macos/code_signature_coderesources/cdhash", [ @@ -45,18 +52,23 @@ ], ) - CodeSignatureCodeResourcesRecords = ( OmitRecord, NestedRecord, + OptionalRecord, CDHashRecord, ) +FIELD_MAPPINGS = { + "Resources_PROMISE_icns": "resources_promise_icns", +} + class CodeSignatureCodeResourcesPlugin(Plugin): """macOS Code signature CodeResources plugin.""" PATHS = ( + "/Applications/*.app/Contents/_CodeSignature/CodeResources", "/Applications/Utilities/*.app/Contents/_CodeSignature/CodeResources", "/System/Library/CoreServices/*.app/Contents/_CodeSignature/CodeResources", "/System/Library/Extensions/*.kext/Contents/_CodeSignature/CodeResources", @@ -86,4 +98,6 @@ def check_compatible(self) -> None: @export(record=CodeSignatureCodeResourcesRecords) def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]: """Yield code signature coderesources information.""" - yield from build_plist_records(self, self.files, CodeSignatureCodeResourcesRecords) + yield from build_plist_records( + self, self.files, CodeSignatureCodeResourcesRecords, field_mappings=FIELD_MAPPINGS + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py index 71fcd277b3..eee85cc1b6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,13 +12,12 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class ContentsInfoPlugin(Plugin): """macOS contents info plugin.""" PATHS = ( + "/Applications/*.app/Contents/Info.plist", "/Applications/*/*.app/Contents/Info.plist", "/Applications/*/*.app/Contents/Resources/*.help/Contents/Info.plist", "/System/Library/CoreServices/*.app/Contents/Info.plist", diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index cbb4dc58c6..4a9b09df09 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - ContentsVersionRecord = TargetRecordDescriptor( "macos/contents_version", [ @@ -47,6 +44,7 @@ class ContentsVersionPlugin(Plugin): """macOS Contents version.plist file.""" PATHS = ( + "/Applications/*.app/Contents/version.plist", "/Applications/*/*.app/Contents/version.plist", "/Applications/*/*.app/Contents/Resources/*.help/Contents/version.plist", "/System/Library/CoreServices/*.app/Contents/version.plist", diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py index 89c283f3fa..58f59f6e7b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py @@ -49,9 +49,22 @@ def check_compatible(self) -> None: def directory_services_local_nodes( self, ) -> Iterator[DirectoryServicesLocalNodesRecord]: - """Yield directory services local nodes information.""" + """Yield directory services local nodes information. + + Database schema: + Tables prefixed with "rec:": + Rows contain: + - filename -> name of the backing plist (links to attribute tables) + - filetime -> datetime + + Attribute tables (e.g. "name", "realname", "uid", "gid", etc.): + Rows contain: + - filename -> name of the backing plist (links to "rec:" tables) + - recordtype -> e.g. "users", "groups", "computers" + - value -> content depends on recordtype + """ with SQLite3(self.file) as database: - NAME_TABLES = { + ATTRIBUTE_TABLES = { "name", "realname", "generateduid", @@ -72,7 +85,7 @@ def directory_services_local_nodes( for table in database.tables(): if table.name.startswith("rec:"): r_rows.extend((table.name, r_row) for r_row in table.rows()) - elif table.name in NAME_TABLES: + elif table.name in ATTRIBUTE_TABLES: n_tables.add(table) for table in n_tables: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index e2ea30e41b..5e5721ff59 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -76,7 +76,7 @@ "macos/duet_activity_scheduler", [ ("string", "table"), - ("string", "z_content"), + ("bytes", "z_content"), ("path", "source"), ], ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py index 2065652f46..5757bbb87c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -1,21 +1,18 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class GlobalUserPreferencesPlugin(Plugin): """macOS global user preferences plugin.""" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py similarity index 71% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py index d89d542ff4..f56a1b18f1 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/general.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -1,10 +1,8 @@ from __future__ import annotations -from datetime import datetime, timezone from typing import TYPE_CHECKING if TYPE_CHECKING: - import re from pathlib import Path from dissect.target.plugin import Plugin @@ -29,16 +27,3 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta if cur_dir.exists(): users_dirs.add((user_details, cur_dir)) return users_dirs - - -def parse_timestamp(timestamp: re.Match) -> datetime: - ts = None - value = timestamp.group() - try: - value = value.removesuffix("-0") - - ts = datetime.fromisoformat(value) - except ValueError: - ts = datetime.strptime(value, "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) - - return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index ba04cca157..cfc3c8379c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -310,6 +310,8 @@ def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source record_fields.append(("boolean", k)) elif isinstance(v, int): record_fields.append(("varint", k)) + elif isinstance(v, list): + record_fields.append(("string[]", k)) else: record_fields.append(("string", k)) @@ -348,6 +350,7 @@ def select_descriptor( missing_fields = rdict_keys - set(selected_record.fields.keys()) if missing_fields: + print(rdict) plugin.target.log.warning( "Source %s contains fields not defined in the selected record descriptor: %s", source, @@ -456,6 +459,20 @@ def emit_dict_records( if isinstance(v, dict): child_dicts[k] = v + + elif isinstance(v, list): + cleaned_list = [] + contains_dict = False + for i, item in enumerate(v): + if isinstance(item, dict): + contains_dict = True + child_dicts[f"{k}[{i}]"] = item + else: + cleaned_list.append(item) + + if cleaned_list or not contains_dict: + attributes[k] = cleaned_list + else: attributes[k] = v diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py new file mode 100644 index 0000000000..69bbca796c --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import re + + +def parse_timestamp(timestamp: re.Match) -> datetime: + ts = None + value = timestamp.group() + try: + value = value.removesuffix("-0") + + ts = datetime.fromisoformat(value) + except ValueError: + ts = datetime.strptime(value, "%b %d %H:%M:%S").replace(tzinfo=timezone.utc) + + return ts diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py index e36b04d871..3b52973ad3 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py @@ -7,7 +7,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py index 398efcb9f7..b5eab5bed2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py @@ -15,7 +15,7 @@ InstallationHistoryRecord = TargetRecordDescriptor( "macos/installation_history", [ - ("datetime", "date"), + ("datetime", "ts"), ("string", "display_name"), ("string", "display_version"), ("string", "process_name"), @@ -55,7 +55,7 @@ def installation_history(self) -> Iterator[InstallationHistoryRecord]: date = data.get("date") yield InstallationHistoryRecord( - date=date, + ts=date, display_name=display_name, display_version=display_version, process_name=process_name, diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py index a8a195d339..e83eb12aa7 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py @@ -5,8 +5,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index 78bb9e037d..e0969b41a9 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -1,21 +1,18 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class LoginWindowPlugin(Plugin): """macOS login window plugin.""" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py index 6f7c70bb35..bc18ea7c7a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py @@ -11,14 +11,13 @@ if TYPE_CHECKING: from collections.abc import Iterator - from dissect.target.target import Target FsckAPFSLogRecord = TargetRecordDescriptor( "macos/fsck_apfs_log", [ + ("datetime", "ts"), ("string", "disk_path"), ("string", "message"), - ("datetime", "ts"), ("path", "source"), ], ) @@ -36,9 +35,6 @@ class FsckAPFSLogPlugin(Plugin): FSCK_APFS_LOG_PATH = "/var/log/fsck_apfs.log" - def __init__(self, target: Target): - super().__init__(target) - def check_compatible(self) -> None: if not self.target.fs.exists(self.FSCK_APFS_LOG_PATH): raise UnsupportedPluginError("No fsck_apfs.log file found.") @@ -46,23 +42,22 @@ def check_compatible(self) -> None: @export(record=FsckAPFSLogRecord) def fsck_apfs_log(self) -> Iterator[FsckAPFSLogRecord]: """Return all fsck_apfs log messages.""" - logfile = self.target.fs.path(self.FSCK_APFS_LOG_PATH).open(mode="rt") - - for line in logfile.readlines(): - if line != "\n": - parts = line.split(" ", 1) - - disk_path, message = parts - disk_path = disk_path.strip(":") - ts = None - - if ts_match := RE_TIMESTAMP_PATTERN.search(message): - ts = datetime.strptime(ts_match.group(), "%a %b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc) - - yield FsckAPFSLogRecord( - disk_path=disk_path.strip(), - message=message.strip(), - ts=ts, - source=self.FSCK_APFS_LOG_PATH, - _target=self.target, - ) + with self.target.fs.path(self.FSCK_APFS_LOG_PATH).open(mode="rt") as fh: + for line in fh: + if line != "\n": + parts = line.split(" ", 1) + + disk_path, message = parts + disk_path = disk_path.strip(":") + ts = None + + if ts_match := RE_TIMESTAMP_PATTERN.search(message): + ts = datetime.strptime(ts_match.group(), "%a %b %d %H:%M:%S %Y").replace(tzinfo=timezone.utc) + + yield FsckAPFSLogRecord( + ts=ts, + disk_path=disk_path.strip(), + message=message.strip(), + source=self.FSCK_APFS_LOG_PATH, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py index 7d17bc1fdf..8b02685c6a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py @@ -6,12 +6,11 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import parse_timestamp +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.parse_timestamp import parse_timestamp if TYPE_CHECKING: from collections.abc import Iterator - from dissect.target.target import Target InstallLogRecord = TargetRecordDescriptor( "macos/install_log", @@ -42,13 +41,37 @@ class InstallLogPlugin(Plugin): INSTALL_LOG_PATH = "/var/log/install.log" - def __init__(self, target: Target): - super().__init__(target) - def check_compatible(self) -> None: if not self.target.fs.exists(self.INSTALL_LOG_PATH): raise UnsupportedPluginError("No install.log file found.") + def parse_log(self, current_ts: re.Match[str], current_buf: str) -> Iterator[InstallLogRecord]: + # Log format: " : " + # Strip the timestamp (and following space) to extract the rest of the line + asdf = current_buf[len(current_ts.group()) + 1 :] + + # Split into hostname, component and message + parts = asdf.split(" ", 2) + + if len(parts) == 3: + hostname, component, message = parts + yield InstallLogRecord( + ts=parse_timestamp(current_ts), + host=hostname.strip(), + component=component.strip() if component else None, + message=message.strip(), + source=self.INSTALL_LOG_PATH, + _target=self.target, + ) + elif len(parts) != 3: + self.target.log.warning( + "Skipping malformed install log entry in %s: " + "expected 3 fields (hostname, component, message), got %d -> '%s'", + self.INSTALL_LOG_PATH, + len(parts), + asdf.strip(), + ) + @export(record=InstallLogRecord) def install_log(self) -> Iterator[InstallLogRecord]: """Return all macOS install log messages. @@ -66,46 +89,23 @@ def install_log(self) -> Iterator[InstallLogRecord]: References: - https://sansorg.egnyte.com/dl/m9ftGF7heI """ - logfile = self.target.fs.path(self.INSTALL_LOG_PATH).open(mode="rt") - current_ts: re.Match[str] | None = None current_buf = "" - - for line in logfile.readlines(): - if ts_match := RE_TIMESTAMP_PATTERN.match(line): - if current_ts: - asdf = current_buf[len(current_ts.group()) + 1 :] - - parts = asdf.split(" ", 2) - - if len(parts) == 3: - hostname, component, message = parts - elif len(parts) == 2: - hostname, message = parts - component = None - yield InstallLogRecord( - ts=parse_timestamp(current_ts), - host=hostname.strip(), - component=component.strip() if component else None, - message=message.strip(), - source=self.INSTALL_LOG_PATH, - _target=self.target, - ) - - current_ts = ts_match - current_buf = line - elif current_buf: - current_buf += line - - if current_ts and current_buf: - asdf = current_buf[len(current_ts.group()) + 1 :] - hostname, component, message = asdf.split(" ", 2) - - yield InstallLogRecord( - ts=parse_timestamp(current_ts), - host=hostname.strip(), - component=component.strip(), - message=message.strip(), - source=self.INSTALL_LOG_PATH, - _target=self.target, - ) + with self.target.fs.path(self.INSTALL_LOG_PATH).open(mode="rt") as fh: + for line in fh: + # New timestamp indicates the start of new log entry. + if ts_match := RE_TIMESTAMP_PATTERN.match(line): + # If previous log entry exists, parse it. + if current_ts: + yield from self.parse_log(current_ts, current_buf) + + current_ts = ts_match + current_buf = line + + # Lines without a timestamp are part of the previous log entry. + elif current_buf: + current_buf += line # append continuation + + # If previous log entry exists, parse it. + if current_ts and current_buf: + yield from self.parse_log(current_ts, current_buf) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py index 0e54ce6b7f..41a5d659f2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from pathlib import Path from typing import TYPE_CHECKING @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - AtJobsRecord = TargetRecordDescriptor( "macos/at_jobs", [ diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py index de9876b688..d121bdd6af 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py @@ -1,21 +1,18 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - LauncherRecord = [ ("string", "label"), ("string", "program"), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py index a718f4df32..eb260007b1 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py @@ -1,23 +1,49 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - LoginItemsRecord = TargetRecordDescriptor( "macos/login_items", + [ + ("string", "associatedBundleIdentifiers"), + ("bytes", "bookmark"), + ("string", "bundleIdentifier"), + ("string", "container"), + ("string", "designatedRequirement"), + ("string", "developerName"), + ("varint", "login_item_disposition"), + ("datetime", "executableModificationDate"), + ("path", "executablePath"), + ("varint", "flags"), + ("varint", "generation"), + ("string", "identifier"), + ("bytes", "lightweightRequirement"), + ("datetime", "modificationDate"), + ("string", "name"), + ("string", "programArguments"), + ("string", "sha256"), + ("string", "teamIdentifier"), + ("varint", "login_item_type"), + ("string", "url"), + ("string", "uuid"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +LoginItemsMetadataRecord = TargetRecordDescriptor( + "macos/login_items/metadata", [ ("varint", "generation"), ("varint", "background_app_refresh_load_count"), @@ -28,12 +54,14 @@ ], ) -LoginItemsRecords = (LoginItemsRecord,) +LoginItemsRecords = (LoginItemsRecord, LoginItemsMetadataRecord) FIELD_MAPPINGS = { "backgroundAppRefreshLoadCount": "background_app_refresh_load_count", "launchServicesItemsImported": "launch_services_items_imported", "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", + "type": "login_item_type", + "disposition": "login_item_disposition", } diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py index a70fc0aa1a..26d2b5eb1b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -12,8 +11,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - PeriodicScriptsRecord = TargetRecordDescriptor( "macos/periodic_scripts", [ diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 315e6b21f4..2a250e1f02 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class ResourcesInfoStringsPlugin(Plugin): """macOS Resources InfoPlist.strings plist file.""" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index 2d4065eeff..cfc53f272a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class ResourcesLocalizableStringsPlugin(Plugin): """macOS Resources Localizable.strings plist file.""" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py index 819f624dca..ac39e7d628 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py @@ -1,6 +1,5 @@ from __future__ import annotations -import re from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -13,8 +12,6 @@ from dissect.target.target import Target -re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]") - class SystemPreferencesPlugin(Plugin): """macOS system preferences plugin.""" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py index 96de5dba20..c9a5c00801 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py @@ -5,8 +5,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index 466c38db8c..a4a58e4f63 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -5,8 +5,8 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator @@ -96,7 +96,7 @@ "macos/text_replacements/z_model_cache", [ ("string", "table"), - ("string", "z_content"), + ("bytes", "z_content"), ("path", "source"), ], ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 2f13306d62..5346ea0a29 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -5,10 +5,10 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import ( build_sqlite_records, ) -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.general import _build_userdirs if TYPE_CHECKING: from collections.abc import Iterator @@ -201,7 +201,7 @@ "macos/user_accounts/z_model_cache", [ ("string", "table"), - ("string", "z_content"), + ("bytes", "z_content"), ("path", "source"), ], ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm index 8df472f6d2..d40ee54705 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cef44aa7617d8eff7753f7cf1be9a24514ef3cb526f49cc6e76374afbf9ecd8 -size 1254 +oid sha256:c69561684eb1c6fca809cc223dc21f5a53c5b1d99a6a8e72ebfc3257680cb86e +size 2961 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py index 67b7cca9fe..493f004223 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py @@ -17,10 +17,10 @@ @pytest.mark.parametrize( "test_files", [ - ("system.log", "system.log.0.gz"), + ["system.log", "system.log.0.gz"], ], ) -def test_system_log(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: +def test_system_log(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc stat_results = [] @@ -41,8 +41,6 @@ def test_system_log(test_files: str, target_unix: Target, fs_unix: VirtualFilesy patch.object(entries[1], "stat", return_value=stat_results[1]), ): target_unix.add_plugin(SystemLogPlugin) - results = list(target_unix.system_log()) - results = list(target_unix.system_log()) results.reverse() results.sort(key=lambda r: r.source) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py index 88708d958d..5e8f7af307 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py @@ -242,8 +242,20 @@ def test_launch_daemons( assert results[0].start_on_mount is None assert results[0].soft_resource_limits == [] - assert results[1].hostname == "localhost" - assert results[1].domain is None - assert results[1].listeners == ["{'SockPathMode': 49663, 'SockPathName': '/private/var/run/cupsd'}"] - assert results[1].plist_path == "Sockets" + assert results[1].socket_key is None + assert results[1].sock_type is None + assert results[1].sock_passive is None + assert results[1].sock_node_name is None + assert results[1].sock_service_name is None + assert results[1].sock_family is None + assert results[1].sock_protocol is None + assert results[1].sock_path_mode == 49663 + assert results[1].sock_path_name == "/private/var/run/cupsd" + assert results[1].secure_socket_with_key is None + assert results[1].sock_path_owner is None + assert results[1].sock_path_group is None + assert results[1].bonjour is None + assert results[1].multicast_group is None + assert results[1].receive_packet_info is None + assert results[1].plist_path == "Sockets/Listeners[0]" assert results[1].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py index 9a9955a6d3..0556dc5fc1 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING from unittest.mock import patch @@ -29,6 +30,7 @@ def test_login_items( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: + tz = timezone.utc user = UnixUserRecord( name="user", uid=501, @@ -59,30 +61,50 @@ def test_login_items( assert len(results) == 4 - assert results[0].generation == 2 - assert results[0].service_management_login_items_migrated - assert results[0].launch_services_items_imported - assert results[0].background_app_refresh_load_count == 4 - assert results[0].plist_path == ("userSettingsByUserIdentifier/8122F0CD-020B-4E0C-A3AD-2FCB201C9BB0") + assert results[0].associatedBundleIdentifiers is None + assert isinstance(results[0].bookmark, (bytes, bytearray)) + assert results[0].bundleIdentifier == "com.microsoft.VSCode" + assert results[0].container is None + assert results[0].designatedRequirement == ( + 'identifier "com.microsoft.VSCode" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] ' # noqa E501 + "/* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = UBF8T346G9" # noqa E501 + ) + assert results[0].developerName is None + assert results[0].login_item_disposition == 3 + assert results[0].executableModificationDate == datetime(1970, 1, 1, 0, 0, tzinfo=tz) + assert results[0].executablePath is None + assert results[0].flags == 0 + assert results[0].generation == 0 + assert results[0].identifier == "2.com.microsoft.VSCode" + assert isinstance(results[0].lightweightRequirement, (bytes, bytearray)) + assert results[0].modificationDate == datetime(1995, 4, 29, 15, 46, 38, tzinfo=tz) + assert results[0].name == "Visual Studio Code.app" + assert results[0].programArguments is None + assert results[0].sha256 is None + assert results[0].teamIdentifier == "UBF8T346G9" + assert results[0].login_item_type == 2 + assert results[0].url == "file:///Applications/Visual%20Studio%20Code.app/" + assert results[0].uuid == "6f541698-5211-4cf2-95b7-e97534baee39" + assert results[0].plist_path == "itemsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90[0]" assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[1].generation == 0 - assert not results[1].service_management_login_items_migrated - assert not results[1].launch_services_items_imported - assert results[1].background_app_refresh_load_count == 0 - assert results[1].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000") + assert results[1].generation == 2 + assert results[1].background_app_refresh_load_count == 17 + assert results[1].launch_services_items_imported + assert results[1].service_management_login_items_migrated + assert results[1].plist_path == "userSettingsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90" assert results[1].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[2].generation == 1 - assert results[2].service_management_login_items_migrated + assert results[2].generation == 0 + assert results[2].background_app_refresh_load_count == 13 assert not results[2].launch_services_items_imported - assert results[2].background_app_refresh_load_count == 2 - assert results[2].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA000000F8") + assert not results[2].service_management_login_items_migrated + assert results[2].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000" assert results[2].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[3].generation == 1 - assert results[3].service_management_login_items_migrated + assert results[3].generation == 14 + assert results[3].background_app_refresh_load_count == 41 assert not results[3].launch_services_items_imported - assert results[3].background_app_refresh_load_count == 2 - assert results[3].plist_path == ("userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE") + assert results[3].service_management_login_items_migrated + assert results[3].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE" assert results[3].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py index 57b36dca65..7cdb85f31d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py @@ -66,7 +66,7 @@ def test_contents_info( assert results[0].CFBundlePackageType == "APPL" assert results[0].CFBundleShortVersionString == "5.0" assert results[0].CFBundleSignature == "????" - assert results[0].CFBundleSupportedPlatforms == "['macOSX']" + assert results[0].CFBundleSupportedPlatforms == ["macOSX"] assert results[0].CFBundleVersion == "5.0" assert results[0].DTCompiler == "com.apple.compilers.llvm.clang.1_0" assert results[0].DTPlatformBuild == "" @@ -90,7 +90,7 @@ def test_contents_info( assert results[4].CFBundlePackageType == "KEXT" assert results[4].CFBundleShortVersionString == "1.8" assert results[4].CFBundleSignature == "????" - assert results[4].CFBundleSupportedPlatforms == "['macOSX']" + assert results[4].CFBundleSupportedPlatforms == ["macOSX"] assert results[4].CFBundleVersion == "1.8" assert results[4].DTCompiler == "com.apple.compilers.llvm.clang.1_0" assert results[4].DTPlatformBuild == "25E245" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index 8129997aa2..f165d68df0 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -70,3 +70,30 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[48].z_super == 0 assert results[48].z_max == 0 assert results[48].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[51].ns_persistence_maximum_framework_version == 1526 + assert results[51].ns_store_model_version_identifiers == [""] + assert results[51].ns_store_type == "SQLite" + assert results[51].ns_auto_vacuum_level == "2" + assert ( + results[51].ns_store_model_version_hashes_digest + == "rvkNkhmOezVbzsczB2H+gkUsiGN7C2d7a9TtXbZPD0kn0MZYSVEGM64BycQewlVstp1ROUAOBjEmkbNTkiu6JA==" + ) + assert results[51].ns_store_model_version_checksum_key == "rXdwmenydb+cl65S3tSy9rIL6lkwSXqL7UvaJVK21Lc=" + assert results[51].ns_persistence_framework_version == 1526 + assert results[51].ns_store_model_version_hashes_version == 3 + assert results[51].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[52].tr_cloud_kit_sync_state is None + assert results[52].text_replacement_entry is None + assert results[52].plist_path == "NSStoreModelVersionHashes" + assert results[52].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[53].table == "Z_METADATA" + assert results[53].z_version == 1 + assert results[53].z_uuid == "514A8E5F-DE48-4C3E-9129-3AF14DEAD0E1" + assert results[53].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[54].table == "Z_MODELCACHE" + assert isinstance(results[54].z_content, (bytes, bytearray)) + assert results[54].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py index 81969a60ff..059827fd71 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -84,11 +84,10 @@ def test_global_user_preferences( results = list(target_unix.global_user_preferences()) results.sort(key=lambda r: r.source) - assert len(results) == 3 + assert len(results) == 4 assert results[0].AKLastLocale == "en_US@rg=nlzzzz" assert results[0].com_apple_sound_beep_flash == 0 - assert results[0].NSLinguisticDataAssetsRequested == "['en', 'en_US', 'nl', 'nl_NL']" assert not results[0].AppleMiniaturizeOnDoubleClick assert results[0].NSAutomaticPeriodSubstitutionEnabled assert results[0].NSSpellCheckerDictionaryContainerTransitionComplete @@ -96,22 +95,25 @@ def test_global_user_preferences( assert results[0].ACDMonthlyAnalyticsLastPosted == "796140692.59226" assert results[0].AKLastIDMSEnvironment == 0 assert results[0].NSAutomaticCapitalizationEnabled - assert results[0].NSLinguisticDataAssetsRequestedByChecker == "[]" - assert results[0].NSUserDictionaryReplacementItems == ("[{'replace': 'omw', 'on': 1, 'with': 'On my way!'}]") assert results[0].NSLinguisticDataAssetsRequestTime == "2026-03-25 14:12:53.295950" assert results[0].AppleAntiAliasingThreshold == 4 assert results[0].com_apple_springing_enabled - assert results[0].AppleLanguages == "['en-US', 'nl-NL']" assert results[0].AppleLocale == "en_US@rg=nlzzzz" assert results[0].com_apple_trackpad_forceClick assert results[0].NSLinguisticDataAssetsRequestLastInterval == "86400.0" assert results[0].AppleLanguagesSchemaVersion == 5400 assert results[0].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" - assert results[1].AppleKeyboardUIMode == 2 - assert results[1].source == "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist" + assert results[1].replace == "omw" + assert results[1].on == 1 + assert getattr(results[1], "with") == "On my way!" + assert results[1].plist_path == "NSUserDictionaryReplacementItems[0]" + assert results[1].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" - assert results[2].AppleLocale == "en_US" - assert results[2].AppleKeyboardUIMode == 3 - assert results[2].com_apple_sound_beep_flash == 0 - assert results[2].source == "/private/var/root/Library/Preferences/.GlobalPreferences.plist" + assert results[2].AppleKeyboardUIMode == 2 + assert results[2].source == "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist" + + assert results[3].AppleLocale == "en_US" + assert results[3].AppleKeyboardUIMode == 3 + assert results[3].com_apple_sound_beep_flash == 0 + assert results[3].source == "/private/var/root/Library/Preferences/.GlobalPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py index 2a37fadaed..031f16ded1 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -19,7 +19,7 @@ ["nobody.plist", "_eligibilityd.plist", "_applepay.plist"], ], ) -def test_groups(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: +def test_groups(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: stat_results = [] entries = [] for test_file in test_files: diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py index b6fbf6dccd..d290bcaedb 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py @@ -36,7 +36,7 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: Virtua results = list(target_unix.installation_history()) assert len(results) == 1 - assert results[0].date == datetime(2026, 3, 25, 14, 7, 11, tzinfo=tz) + assert results[0].ts == datetime(2026, 3, 25, 14, 7, 11, tzinfo=tz) assert results[0].display_name == "macOS 26.4" assert results[0].display_version == "26.4" assert results[0].process_name == "softwareupdated" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py index a953bdd481..57d1519af2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py @@ -19,7 +19,7 @@ ["_mbsetupuser.plist", "user.plist"], ], ) -def test_passwords(test_files: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: +def test_passwords(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: stat_results = [] entries = [] for test_file in test_files: diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py index 5335f6d4d5..3528b10243 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py @@ -37,5 +37,5 @@ def test_system_preferences(test_file: str, target_unix: Target, fs_unix: Virtua assert results[0].Counter == 2 assert results[0].DeviceUUID == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" assert results[0].Version == 7200 - assert results[0].PreferredOrder == "[]" + assert results[0].PreferredOrder == [] assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py index e96d157211..107d787e09 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -115,3 +115,7 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[6].z_version == 1 assert results[6].z_uuid == "555547C2-D9F5-4B51-8BEE-EEE6158CDDED" assert results[6].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[7].table == "Z_MODELCACHE" + assert isinstance(results[7].z_content, bytes) + assert results[7].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" From cace2ae96b189b4f4ff8206bf9abd297651ec03d Mon Sep 17 00:00:00 2001 From: Valkorion <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 26 May 2026 16:26:40 +0200 Subject: [PATCH 26/52] Update dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- .../os/unix/bsd/darwin/macos/airport_preferences.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index efb9f11c3c..dd7db8b54c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -41,12 +41,8 @@ def _resolve_file(self) -> None: def check_compatible(self) -> None: if not self.file: - raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found") - - @export(record=AirportPreferencesRecord) - def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: - """Yield AirPort preference information.""" - plist = plistlib.load(self.file.open()) + with self.file.open() as fh: + plist = plistlib.load(fh) yield AirportPreferencesRecord( counter=plist.get("Counter"), From 220c2629419b599fa73e4fefe78d3293dea9190b Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 26 May 2026 16:27:58 +0200 Subject: [PATCH 27/52] Update airport_preferences.py --- .../os/unix/bsd/darwin/macos/airport_preferences.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index dd7db8b54c..cedd0afa9d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -1,3 +1,4 @@ +@ -1,59 +1,55 @@ from __future__ import annotations import plistlib @@ -41,8 +42,12 @@ def _resolve_file(self) -> None: def check_compatible(self) -> None: if not self.file: - with self.file.open() as fh: - plist = plistlib.load(fh) + raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found") + + @export(record=AirportPreferencesRecord) + def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: + """Yield AirPort preference information.""" + plist = plistlib.load(self.file.open()) yield AirportPreferencesRecord( counter=plist.get("Counter"), From 0f2e889af1ece5ff5c2d185b8376440bb4a0a7c0 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 26 May 2026 16:53:18 +0200 Subject: [PATCH 28/52] minor improvements --- .../os/unix/bsd/darwin/macos/airport_preferences.py | 1 - .../plugins/os/unix/bsd/darwin/macos/logs/system_log.py | 8 +++++++- .../plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py | 7 ++++++- .../os/unix/bsd/darwin/macos/persistence/at_jobs.py | 6 ------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index cedd0afa9d..efb9f11c3c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -1,4 +1,3 @@ -@ -1,59 +1,55 @@ from __future__ import annotations import plistlib diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index d76068c3c9..4ae92b4965 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -16,7 +16,13 @@ SystemLogRecord = TargetRecordDescriptor( "macos/system_log", - [("datetime", "ts"), ("string", "host"), ("string", "component"), ("string", "message"), ("path", "source")], + [ + ("datetime", "ts"), + ("string", "host"), + ("string", "component"), + ("string", "message"), + ("path", "source"), + ], ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py index c18425eb2b..f1ba8cd0bf 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py @@ -34,7 +34,12 @@ WifiLogRecord = TargetRecordDescriptor( "wifi_log", - [("datetime", "ts"), ("string", "host"), ("string", "message"), ("path", "source")], + [ + ("datetime", "ts"), + ("string", "host"), + ("string", "message"), + ("path", "source"), + ], ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py index 41a5d659f2..141dd42a7f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py @@ -23,12 +23,6 @@ ], ) -FIELD_MAPPINGS = { - "backgroundAppRefreshLoadCount": "background_app_refresh_load_count", - "launchServicesItemsImported": "launch_services_items_imported", - "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", -} - class AtJobsPlugin(Plugin): """macOS at jobs plugin.""" From 4a4895c6a208175989b6a7a88f73650f94b79f36 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 27 May 2026 09:46:32 +0200 Subject: [PATCH 29/52] minor imrpovements --- .../darwin/macos/{persistence => }/at_jobs.py | 27 +++++++++++++++++-- .../macos/{persistence => }/launchers.py | 0 .../macos/{persistence => }/login_items.py | 0 .../unix/bsd/darwin/macos/logs/system_log.py | 9 ++++--- .../macos/{persistence => }/periodic.py | 0 .../bsd/darwin/macos/persistence/__init__.py | 0 .../{persistence => }/BackgroundItems-v16.btm | 0 .../macos/{persistence => }/a0000901c45260 | 0 .../macos/{persistence => }/cronjobs/one | 0 .../macos/{persistence => }/cronjobs/two | 0 .../launchers/com.apple.ecosystemagent.plist | 0 .../launchers/com.openssh.ssh-agent.plist | 0 .../launchers/org.cups.cupsd.plist | 0 .../{persistence => }/periodic/110.clean-tmps | 0 .../{persistence => }/periodic/199.rotate-fax | 0 .../{persistence => }/periodic/periodic.conf | 0 .../bsd/darwin/macos/persistence/__init__.py | 0 .../macos/{persistence => }/test_at_jobs.py | 4 +-- .../macos/{persistence => }/test_launchers.py | 6 ++--- .../{persistence => }/test_login_items.py | 4 +-- .../macos/{persistence => }/test_periodic.py | 6 ++--- 21 files changed, 41 insertions(+), 15 deletions(-) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{persistence => }/at_jobs.py (77%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{persistence => }/launchers.py (100%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{persistence => }/login_items.py (100%) rename dissect/target/plugins/os/unix/bsd/darwin/macos/{persistence => }/periodic.py (100%) delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/BackgroundItems-v16.btm (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/a0000901c45260 (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/cronjobs/one (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/cronjobs/two (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/launchers/com.apple.ecosystemagent.plist (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/launchers/com.openssh.ssh-agent.plist (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/launchers/org.cups.cupsd.plist (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/periodic/110.clean-tmps (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/periodic/199.rotate-fax (100%) rename tests/_data/plugins/os/unix/bsd/darwin/macos/{persistence => }/periodic/periodic.conf (100%) delete mode 100644 tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py rename tests/plugins/os/unix/bsd/darwin/macos/{persistence => }/test_at_jobs.py (91%) rename tests/plugins/os/unix/bsd/darwin/macos/{persistence => }/test_launchers.py (98%) rename tests/plugins/os/unix/bsd/darwin/macos/{persistence => }/test_login_items.py (97%) rename tests/plugins/os/unix/bsd/darwin/macos/{persistence => }/test_periodic.py (97%) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py similarity index 77% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py index 141dd42a7f..9f4dba5b44 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/at_jobs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py @@ -46,7 +46,30 @@ def _find_files(self) -> None: @export(record=AtJobsRecord) def at_jobs(self) -> Iterator[AtJobsRecord]: - """Yield macOS at jobs.""" + """Yield macOS `at` jobs. + + The filename of an `at` job follows this structure: + + QSSSSSTTTTTTTT + + Where: + Q = queue identifier + S = sequence number (hexadecimal) + T = execution time (hexadecimal, in minutes) + + The execution time is derived from the hexadecimal value and converted to seconds. + + Within the job file, the line: + + OLDPWD=/usr/lib/cron; export OLDPWD + + typically marks the end of environment setup. Future lines are part of + the command to be executed and are extracted as such. + + Yields: + AtJobsRecord: Parsed `at` job record containing queue, sequence number, + execution time and command. + """ for file in self.at_jobs_files: name = Path(file).name @@ -90,6 +113,6 @@ def at_jobs(self) -> Iterator[AtJobsRecord]: queue=queue, seq=seq, execution_time=execution_time, - source=file, command=command, + source=file, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py similarity index 100% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/launchers.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py similarity index 100% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/login_items.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index 4ae92b4965..08e6dd6d0c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -50,11 +50,14 @@ def system_log(self) -> Iterator[SystemLogRecord]: for file in self.log_files: filepath = self.target.fs.path(file) - current_buf = "" + lines = [] for ts, line in year_rollover_helper(filepath, RE_TS, "%b %d %H:%M:%S", timezone.utc): - current_buf = line + "\n\t" + current_buf + lines.insert(0, line) + if ts: + current_buf = "\n\t".join(lines) + match = RE_TS.match(current_buf) asdf = current_buf[match.end() :].lstrip(" ") hostname, component, message = asdf.split(" ", 2) @@ -68,4 +71,4 @@ def system_log(self) -> Iterator[SystemLogRecord]: _target=self.target, ) - current_buf = "" + lines = [] diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py similarity index 100% rename from dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/periodic.py rename to dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm b/tests/_data/plugins/os/unix/bsd/darwin/macos/BackgroundItems-v16.btm similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/BackgroundItems-v16.btm rename to tests/_data/plugins/os/unix/bsd/darwin/macos/BackgroundItems-v16.btm diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 b/tests/_data/plugins/os/unix/bsd/darwin/macos/a0000901c45260 similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/a0000901c45260 rename to tests/_data/plugins/os/unix/bsd/darwin/macos/a0000901c45260 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one b/tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/one similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/one rename to tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/one diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two b/tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/two similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/cronjobs/two rename to tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/two diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.apple.ecosystemagent.plist similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.apple.ecosystemagent.plist rename to tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.apple.ecosystemagent.plist diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.openssh.ssh-agent.plist similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/com.openssh.ssh-agent.plist rename to tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.openssh.ssh-agent.plist diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/org.cups.cupsd.plist similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/org.cups.cupsd.plist rename to tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/org.cups.cupsd.plist diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/110.clean-tmps similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/110.clean-tmps rename to tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/110.clean-tmps diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/199.rotate-fax similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/199.rotate-fax rename to tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/199.rotate-fax diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/periodic.conf similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/periodic.conf rename to tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/periodic.conf diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py b/tests/plugins/os/unix/bsd/darwin/macos/persistence/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py similarity index 91% rename from tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py rename to tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py index 5c70bb8a05..06cbb7bce8 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_at_jobs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py @@ -6,7 +6,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.at_jobs import AtJobsPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.at_jobs import AtJobsPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -22,7 +22,7 @@ ) def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/{test_file}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/usr/lib/cron/jobs/{test_file}", data_file) entry = fs_unix.get(f"/usr/lib/cron/jobs/{test_file}") stat_result = entry.stat() diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py similarity index 98% rename from tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py rename to tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py index 5e8f7af307..166f046a50 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py @@ -6,7 +6,7 @@ import pytest from dissect.target.helpers.record import UnixUserRecord -from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.launchers import LaunchersPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.launchers import LaunchersPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -47,7 +47,7 @@ def test_launch_agents( entries = [] for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/launchers/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() @@ -174,7 +174,7 @@ def test_launch_daemons( entries = [] for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/launchers/{name}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/launchers/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py similarity index 97% rename from tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py rename to tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py index 0556dc5fc1..4126c22e20 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py @@ -7,7 +7,7 @@ import pytest from dissect.target.helpers.record import UnixUserRecord -from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.login_items import LoginItemsPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.login_items import LoginItemsPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -43,7 +43,7 @@ def test_login_items( entries = [] for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/{name}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() diff --git a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py similarity index 97% rename from tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py rename to tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py index 1185160e66..e352b7427e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/persistence/test_periodic.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py @@ -5,7 +5,7 @@ import pytest -from dissect.target.plugins.os.unix.bsd.darwin.macos.persistence.periodic import PeriodicPlugin +from dissect.target.plugins.os.unix.bsd.darwin.macos.periodic import PeriodicPlugin from tests._utils import absolute_path if TYPE_CHECKING: @@ -39,7 +39,7 @@ def test_periodic_scripts( entries = [] for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/periodic/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() @@ -80,7 +80,7 @@ def test_periodic_conf( stat_results = [] entries = [] for name, path in zip(names, paths, strict=True): - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/persistence/periodic/{name}") + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/periodic/{name}") fs_unix.map_file(path, data_file) entry = fs_unix.get(path) stat_result = entry.stat() From 20e87cca31f7d346388d583100136747004f1f3a Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 27 May 2026 13:19:16 +0200 Subject: [PATCH 30/52] helper function for finding bundle files --- .../macos/code_signature_coderesources.py | 23 +------- .../os/unix/bsd/darwin/macos/contents_info.py | 26 +-------- .../unix/bsd/darwin/macos/contents_version.py | 26 +-------- .../bsd/darwin/macos/helpers/build_paths.py | 58 +++++++++++++++++++ .../unix/bsd/darwin/macos/logs/system_log.py | 6 +- .../darwin/macos/resources_info_strings.py | 22 +------ .../macos/resources_localizable_strings.py | 20 +------ 7 files changed, 70 insertions(+), 111 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py index 286d40c467..41508a8c38 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -5,6 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: @@ -67,29 +68,9 @@ class CodeSignatureCodeResourcesPlugin(Plugin): """macOS Code signature CodeResources plugin.""" - PATHS = ( - "/Applications/*.app/Contents/_CodeSignature/CodeResources", - "/Applications/Utilities/*.app/Contents/_CodeSignature/CodeResources", - "/System/Library/CoreServices/*.app/Contents/_CodeSignature/CodeResources", - "/System/Library/Extensions/*.kext/Contents/_CodeSignature/CodeResources", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/_CodeSignature/CodeResources", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/_CodeSignature/CodeResources", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources", - "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources", - "/System/Library/Filesystems/*/*.kext/Contents/_CodeSignature/CodeResources", - "/System/Library/Filesystems/*/Encodings/*.kext/Contents/_CodeSignature/CodeResource", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/_CodeSignature/CodeResources", - ) - def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() - - def _find_files(self) -> None: - for pattern in self.PATHS: - for path in self.target.fs.glob(pattern): - self.files.add(path) + self.files = find_bundle_files(self.target, "/_CodeSignature/CodeResources") def check_compatible(self) -> None: if not (self.files): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py index eee85cc1b6..680b34990c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -5,6 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: @@ -16,32 +17,9 @@ class ContentsInfoPlugin(Plugin): """macOS contents info plugin.""" - PATHS = ( - "/Applications/*.app/Contents/Info.plist", - "/Applications/*/*.app/Contents/Info.plist", - "/Applications/*/*.app/Contents/Resources/*.help/Contents/Info.plist", - "/System/Library/CoreServices/*.app/Contents/Info.plist", - "/System/Library/Extensions/*.kext/Contents/Info.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Info.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/Info.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/Info.plist", - "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/Info.plist", - "/System/Library/Extensions/*.kext/PlugIns/*.kext/Info.plist", - "/System/Library/Filesystems/*/*.kext/Contents/Info.plist", - "/System/Library/Filesystems/*/Encodings/*.kext/Contents/Info.plist", - "/System/Library/Frameworks/*.framework/Versions/A/Resources/Info.plist", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/Info.plist", - ) - def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() - - def _find_files(self) -> None: - for pattern in self.PATHS: - for path in self.target.fs.glob(pattern): - self.files.add(path) + self.files = find_bundle_files(self.target, "Info.plist") def check_compatible(self) -> None: if not (self.files): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index 4a9b09df09..febfa88dfc 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -5,6 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: @@ -43,32 +44,9 @@ class ContentsVersionPlugin(Plugin): """macOS Contents version.plist file.""" - PATHS = ( - "/Applications/*.app/Contents/version.plist", - "/Applications/*/*.app/Contents/version.plist", - "/Applications/*/*.app/Contents/Resources/*.help/Contents/version.plist", - "/System/Library/CoreServices/*.app/Contents/version.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/version.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/version.plist", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/version.plist", - "/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/version.plist", - "/System/Library/Extensions/*.kext/Contents/version.plist", - "/System/Library/Extensions/*.kext/PlugIns/*.kext/version.plist", - "/System/Library/Filesystems/*/*.kext/Contents/version.plist", - "/System/Library/Filesystems/*/Encodings/*.kext/Contents/version.plist", - "/System/Library/Frameworks/*.framework/Versions/A/Resources/version.plist", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/version.plist", - ) - def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() - - def _find_files(self) -> None: - for pattern in self.PATHS: - for path in self.target.fs.glob(pattern): - self.files.add(path) + self.files = self.files = find_bundle_files(self.target, "version.plist") def check_compatible(self) -> None: if not self.files: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py index f56a1b18f1..413449ab51 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -7,6 +7,7 @@ from dissect.target.plugin import Plugin from dissect.target.plugins.general.users import UserDetails + from dissect.target.target import Target def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDetails, Path]]: @@ -27,3 +28,60 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta if cur_dir.exists(): users_dirs.add((user_details, cur_dir)) return users_dirs + + +START_PATHS = { + "/Applications/", + "/Applications/Utilities/", + "/System/Library/CoreServices/", + "/System/Library/Extensions/", + "/System/Library/Filesystems/*/", + "/System/Library/Filesystems/*/Encodings/", + "/System/Library/PrivateFrameworks/", + "/System/Library/SystemProfiler/*/", + "/System/Library/Frameworks", +} + +EXTENSIONS = { + "*.app": ["/Contents/", "/Contents/Resources/"], + "*.kext": ["/Contents/", "/Contents/PlugIns/", "/Contents/Resources/"], + "*.framework": ["/Versions/A/", "/Versions/A/Resources/"], + "*.bundle": ["/Contents/", "/Contents/Resources/"], + "*.plugin": ["/Contents/"], + "*.prefPane": ["/Contents/", "/Contents/Resources/"], + "*.help": ["/Contents/", "/Contents/Resources/"], + "*.spreporter": ["/Contents/Resources/"], +} + + +def find_bundle_files(target: Target, end_path: str) -> set: + results = set() + + for base in START_PATHS: + results.update(find_end_paths(target, end_path, base)) + + return results + +def find_end_paths(target: Target, end_path: str, base_path: str) -> set: + found = set() + + if isinstance(base_path, str): + base_path = target.fs.path(base_path) + + for ext, subpaths in EXTENSIONS.items(): + for bundle_str in target.fs.glob(f"{base_path}/{ext}"): + bundle = target.fs.path(bundle_str) + + for sub in subpaths: + sub_path = bundle.joinpath(sub.lstrip("/")) + + if not sub_path.exists(): + continue + + candidate = sub_path.joinpath(end_path.lstrip("/")) + if candidate.exists(): + found.add(candidate) + + found.update(find_end_paths(target, end_path, sub_path)) + + return found diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index 08e6dd6d0c..ac5284822a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -33,16 +33,14 @@ class SystemLogPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.log_files = set() - self._resolve_files() + self.log_files = self._resolve_files() def check_compatible(self) -> None: if not self.log_files: raise UnsupportedPluginError("No system log files found.") def _resolve_files(self) -> None: - for file in self.target.fs.glob(self.SYSTEM_LOG_GLOB): - self.log_files.add(file) + return set(self.target.fs.glob(self.SYSTEM_LOG_GLOB)) @export(record=SystemLogRecord) def system_log(self) -> Iterator[SystemLogRecord]: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 2a250e1f02..16399ba7d4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -5,6 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: @@ -16,28 +17,9 @@ class ResourcesInfoStringsPlugin(Plugin): """macOS Resources InfoPlist.strings plist file.""" - PATHS = ( - "/Applications/*.app/Contents/Resources/*.help/Contents/Resources/*.lproj/InfoPlist.strings", - "/Applications/*/*.app/Contents/Resources/*.help/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/CoreServices/*.app/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.bundle/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/Extensions/*.kext/Contents/Resources/InfoPlist.strings", - "/System/Library/Extensions/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/Filesystems/*/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/Filesystems/*/Encodings/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/Resources/*.lproj/InfoPlist.strings", - ) - def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() - - def _find_files(self) -> None: - for pattern in self.PATHS: - for path in self.target.fs.glob(pattern): - self.files.add(path) + self.files = find_bundle_files(self.target, "InfoPlist.strings") def check_compatible(self) -> None: if not self.files: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index cfc53f272a..e511758991 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -5,6 +5,7 @@ from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.record import DynamicDescriptor from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records if TYPE_CHECKING: @@ -16,26 +17,9 @@ class ResourcesLocalizableStringsPlugin(Plugin): """macOS Resources Localizable.strings plist file.""" - PATHS = ( - "/System/Library/CoreServices/*.app/Contents/Resources/*.lproj/Localizable.strings", - "/System/Library/Extensions/*.kext/Contents/Resources/*.lproj/Localizable.strings", - "/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.lproj/Localizable.strings", - "/System/Library/Frameworks/*.framework/Versions/A/Frameworks/*.framework/Versions/A/Resources/*.lproj/Localizable.strings", - "/System/Library/PreferencePanes/*.prefPane/Contents/Resources/*.lproj/Localizable.strings", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Plugins/*.bundle/Contents/Resources/*.lproj/Localizable.strings", - "/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.lproj/Localizable.strings", - "/System/Library/SystemProfiler/*/Contents/Resources/*.lproj/Localizable.strings", - ) - def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() - - def _find_files(self) -> None: - for pattern in self.PATHS: - for path in self.target.fs.glob(pattern): - self.files.add(path) + self.files = find_bundle_files(self.target, "Localizable.strings") def check_compatible(self) -> None: if not self.files: From 4d85bebfc08b76a2c53256df69f12d0e5d1e6287 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:10:00 +0200 Subject: [PATCH 31/52] Improve plugins + tests and add docstrings --- .../bsd/darwin/macos/airport_preferences.py | 35 ++- .../os/unix/bsd/darwin/macos/at_jobs.py | 54 ++-- .../bsd/darwin/macos/authorization_rules.py | 89 +++++- .../macos/code_signature_coderesources.py | 80 +++++- .../os/unix/bsd/darwin/macos/contents_info.py | 6 +- .../unix/bsd/darwin/macos/contents_version.py | 30 ++- .../macos/directory_services_local_nodes.py | 49 ++-- .../darwin/macos/duet_activity_scheduler.py | 98 +++++-- .../os/unix/bsd/darwin/macos/gkopaque.py | 53 +++- .../darwin/macos/global_user_preferences.py | 17 +- .../os/unix/bsd/darwin/macos/groups.py | 80 +++--- .../bsd/darwin/macos/helpers/build_paths.py | 1 + .../bsd/darwin/macos/helpers/build_records.py | 69 ++++- .../bsd/darwin/macos/identity_services.py | 31 ++- .../bsd/darwin/macos/installation_history.py | 44 +-- .../unix/bsd/darwin/macos/keyboard_layout.py | 28 +- .../os/unix/bsd/darwin/macos/keychain.py | 184 ++++++++++--- .../os/unix/bsd/darwin/macos/launchers.py | 209 +++++++++++++-- .../os/unix/bsd/darwin/macos/locale.py | 44 ++- .../os/unix/bsd/darwin/macos/login_items.py | 129 +++++++-- .../os/unix/bsd/darwin/macos/login_window.py | 111 ++++++-- .../os/unix/bsd/darwin/macos/logs/asl.py | 89 ++++-- .../bsd/darwin/macos/logs/fsck_apfs_log.py | 26 +- .../unix/bsd/darwin/macos/logs/install_log.py | 21 +- .../unix/bsd/darwin/macos/logs/system_log.py | 24 +- .../os/unix/bsd/darwin/macos/logs/wifi_log.py | 29 +- .../os/unix/bsd/darwin/macos/periodic.py | 158 ++++++++++- .../darwin/macos/resources_info_strings.py | 83 +++++- .../macos/resources_localizable_strings.py | 8 +- .../os/unix/bsd/darwin/macos/shadow.py | 28 +- .../macos/software_update_preferences.py | 37 ++- .../bsd/darwin/macos/system_preferences.py | 9 +- .../plugins/os/unix/bsd/darwin/macos/tcc.py | 85 +++++- .../bsd/darwin/macos/text_replacements.py | 107 +++++++- .../os/unix/bsd/darwin/macos/time_machine.py | 27 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 253 +++++++++++++----- .../code_signature/AudioDMAController_T8140 | 3 + .../macos/code_signature/EndpointSecurity | 3 + .../macos/code_signature/MobileDeviceUpdater | 3 + ...16130786-970B-53D1-A07B-005E50471D95.plist | 3 + ...nfoPlist.strings => OSvKernDSPLib.strings} | 0 .../resources_info_strings/WiFiAgent.strings | 3 + .../os/unix/bsd/darwin/macos/logs/test_asl.py | 6 +- .../darwin/macos/test_airport_preferences.py | 4 +- .../os/unix/bsd/darwin/macos/test_at_jobs.py | 2 + .../darwin/macos/test_authorization_rules.py | 101 ++++++- .../test_code_signature_coderesources.py | 46 +++- .../bsd/darwin/macos/test_contents_version.py | 12 +- .../macos/test_duet_activity_scheduler.py | 11 +- .../os/unix/bsd/darwin/macos/test_gkopaque.py | 7 +- .../os/unix/bsd/darwin/macos/test_groups.py | 40 +-- .../os/unix/bsd/darwin/macos/test_keychain.py | 111 +++++++- .../unix/bsd/darwin/macos/test_launchers.py | 76 +++++- .../unix/bsd/darwin/macos/test_login_items.py | 23 +- .../bsd/darwin/macos/test_login_window.py | 27 +- .../os/unix/bsd/darwin/macos/test_periodic.py | 2 + .../macos/test_resources_info_strings.py | 41 ++- .../os/unix/bsd/darwin/macos/test_tcc.py | 45 ++-- .../darwin/macos/test_text_replacements.py | 10 +- .../bsd/darwin/macos/test_user_accounts.py | 69 ++++- 60 files changed, 2482 insertions(+), 591 deletions(-) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/AudioDMAController_T8140 create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/EndpointSecurity create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/MobileDeviceUpdater create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist rename tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/{InfoPlist.strings => OSvKernDSPLib.strings} (100%) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/WiFiAgent.strings diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py index efb9f11c3c..d7ae42ae4b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/airport_preferences.py @@ -17,27 +17,27 @@ [ ("varint", "counter"), ("string", "device_uuid"), - ("string", "version"), - ("string", "preferred_order"), + ("string[]", "preferred_order"), + ("varint", "version_number"), ("path", "source"), ], ) class AirportPreferencesPlugin(Plugin): - """macOS AirPort (WiFi) preferences plugin.""" + """macOS AirPort (WiFi) preferences plugin. + + Contains WiFi network information. + + References: + - https://apple.stackexchange.com/questions/301346/how-can-i-better-sort-and-set-wifi-network-preferences-on-mac + """ PATH = "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -45,14 +45,25 @@ def check_compatible(self) -> None: @export(record=AirportPreferencesRecord) def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: - """Yield AirPort preference information.""" + """Return macOS AirPort (Wi-Fi) preferences. + + Yields AirportPreferencesRecord with the following fields: + + .. code-block:: text + + counter (varint): The Counter key of the plist. + device_uuid (string): UUID of the device. + preferred_order (string[]): Ordered list of known Wi-Fi network SSIDs. + version_number (varint): The version number of the plist. + source (path): Path to the com.apple.airport.preferences.plist file. + """ plist = plistlib.load(self.file.open()) yield AirportPreferencesRecord( counter=plist.get("Counter"), device_uuid=plist.get("DeviceUUID"), - version=plist.get("Version"), preferred_order=plist.get("PreferredOrder"), + version_number=plist.get("Version"), source=self.file, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py index 9f4dba5b44..259bb457a2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py @@ -1,6 +1,5 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError @@ -25,30 +24,49 @@ class AtJobsPlugin(Plugin): - """macOS at jobs plugin.""" + """macOS at jobs plugin. + + The at utility schedules commands to be executed at a later time. + + References: + - https://man.freebsd.org/cgi/man.cgi?query=at&sektion=1&format=html + - github.com/freebsd/freebsd-src/blob/main/usr.bin/at/at.c + """ PATHS = ("/usr/lib/cron/jobs/*",) def __init__(self, target: Target): super().__init__(target) - - self.at_jobs_files = set() - self._find_files() + self.at_jobs_files = self._find_files() def check_compatible(self) -> None: if not (self.at_jobs_files): raise UnsupportedPluginError("No At Jobs files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() + for pattern in self.PATHS: for path in self.target.fs.glob(pattern): - self.at_jobs_files.add(path) + files.add(self.target.fs.path(path)) + + return files @export(record=AtJobsRecord) def at_jobs(self) -> Iterator[AtJobsRecord]: - """Yield macOS `at` jobs. + """Return macOS `at` job records. + + Yields AtJobsRecord with the following fields: - The filename of an `at` job follows this structure: + .. code-block:: text + + queue (string): Queue identifier derived from the job filename. + seq (varint): Sequence number derived from the job filename. + execution_time (datetime): Execution time derived from the job filename. + command (string): Command contents extracted from the job file. + source (path): Path to the `at` job file. + + The job filename typically follows the structure: QSSSSSTTTTTTTT @@ -57,21 +75,11 @@ def at_jobs(self) -> Iterator[AtJobsRecord]: S = sequence number (hexadecimal) T = execution time (hexadecimal, in minutes) - The execution time is derived from the hexadecimal value and converted to seconds. - - Within the job file, the line: - - OLDPWD=/usr/lib/cron; export OLDPWD - - typically marks the end of environment setup. Future lines are part of - the command to be executed and are extracted as such. - - Yields: - AtJobsRecord: Parsed `at` job record containing queue, sequence number, - execution time and command. + Lines following the environment setup (typically after 'export OLDPWD') are treated + as the command content. """ for file in self.at_jobs_files: - name = Path(file).name + name = file.name if name in (".SEQ", ".lockfile"): continue @@ -92,7 +100,7 @@ def at_jobs(self) -> Iterator[AtJobsRecord]: command_line = False command = "" - with self.target.fs.path(file).open("r") as f: + with file.open("r") as f: for line in f: if command_line: command += line diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py index 4a0e7e88b1..db0185875e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py @@ -30,7 +30,7 @@ ("datetime", "rules_modified"), ("string", "rules_hash"), ("string", "rules_identifier"), - ("string", "rules_requirement"), + ("bytes", "rules_requirement"), ("string", "rules_comment"), ("string[]", "rules_delegates_map"), ("datetime", "rules_history_timestamp"), @@ -79,21 +79,26 @@ {"table1": "rules", "key1": "id", "table2": "delegates_map", "key2": "r_id", "join": "nested"}, ) +CONVERT_TIMESTAMPS = { + "rules_created": "2001", + "rules_modified": "2001", +} + class AuthorizationRulesPlugin(Plugin): - """macOS authorization rules plugin.""" + """macOS authorization rules plugin. + + The database located in /var/db/auth.db is used to store permissions to perform sensitive operations. + + References: + - https://angelica.gitbook.io/hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-security-protections/macos-authorizations-db-and-authd + """ PATH = "/var/db/auth.db" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -101,7 +106,65 @@ def check_compatible(self) -> None: @export(record=AuthorizationRulesRecords) def authorization_rules(self) -> Iterator[AuthorizationRulesRecords]: - """Yield authorization rules information.""" - yield from build_sqlite_records(self, (self.file,), AuthorizationRulesRecords, joins) - - # Still missing prompts & buttons tables + """Return macOS authorization rules from the auth.db database. + + The /var/db/auth.db database stores authorization rules used by macOS + to determine whether a client is allowed to perform privileged operations. + + Yields the following record types: + + .. code-block:: text + + AuthorizationRulesRecord: + tables (string[]): Names of source tables contributing to the record. + rules_id (varint): Unique identifier for the rule. + rules_name (string): Unique name used to identify the authorization rule. + rules_type (varint): Rule type value. + rules_class (varint): Rule class defining how the rule is evaluated. + rules_group (string): User group associated with the rule. + rules_kofn (varint): "k-of-n" parameter indicating how many subrules must be satisfied. + rules_timeout (varint): Duration in seconds before the authorization expires. + rules_flags (varint): Flags modifying rule behavior. + rules_tries (string): Maximum number of allowed authorization attempts. + rules_version (varint): Version of the rule. + rules_created (datetime): Timestamp when the rule was created. + rules_modified (datetime): Timestamp of the last modification. + rules_hash (string): Hash value used to verify rule integrity. + rules_identifier (string): Identifier string used for external reference. + rules_requirement (bytes): Serialized data defining rule requirements and mechanisms. + rules_comment (string): Human-readable description of the rule. + rules_delegates_map (string[]): Delegate mappings for the rule. + rules_history_timestamp (datetime): Timestamp from the rules_history table. + rules_history_source (string): Source of the history entry (e.g. authd). + rules_history_operation (varint): Operation type recorded in rules_history. + mechanisms_map_m_id (varint): Mechanism identifier from mechanisms_map. + mechanisms_map_ord (varint): Order of the mechanism within the rule. + mechanisms_plugin (string): Mechanism plugin name. + mechanisms_param (string): Mechanism parameter value. + mechanisms_privileged (varint): Indicates whether the mechanism runs with privileges. + source (path): Path to the auth.db database file. + + ConfigTableRecord: + table (string): Name of the source table (config). + key (string): Configuration key. + value (string): Stored value associated with the key. + source (path): Path to the auth.db database file. + + SQLiteSequenceTableRecord: + table (string): Name of the source table (sqlite_sequence). + name (string): Name of the table for which the sequence applies. + seq (varint): Current autoincrement value for the table. + source (path): Path to the auth.db database file. + + Records are constructed by joining data from the rules, + rules_history, mechanisms_map, mechanisms, and + delegates_map tables. + + Multiple records may be produced for a single rule when multiple + mechanisms or delegate mappings exist. + """ + yield from build_sqlite_records( + self, (self.file,), AuthorizationRulesRecords, joins, convert_timestamps=CONVERT_TIMESTAMPS + ) + + # TODO: Add prompts & buttons tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py index 41508a8c38..ad8f630165 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -43,6 +43,26 @@ ], ) +HashRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/hash", + [ + ("string", "hash"), + ("boolean", "optional"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +HashTwoRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/hash_two", + [ + ("string", "hash2"), + ("boolean", "optional"), + ("string", "plist_path"), + ("path", "source"), + ], +) + CDHashRecord = TargetRecordDescriptor( "macos/code_signature_coderesources/cdhash", [ @@ -57,6 +77,8 @@ OmitRecord, NestedRecord, OptionalRecord, + HashRecord, + HashTwoRecord, CDHashRecord, ) @@ -66,7 +88,19 @@ class CodeSignatureCodeResourcesPlugin(Plugin): - """macOS Code signature CodeResources plugin.""" + """macOS Code signature CodeResources plugin. + + + _CodeSignature/CodeResources files are part of the macOS code + signing system and store metadata about signed resources within an + application bundle. They contains hashes and rules used to verify the + integrity of code and resources during code signature validation. + + References: + - https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/AboutCS/AboutCS.html + - https://developer.apple.com/documentation/endpointsecurity/es_process_t/cdhash + - https://alfiecg.uk/2024/01/06/Ad-hoc-signing.html + """ def __init__(self, target: Target): super().__init__(target) @@ -78,7 +112,49 @@ def check_compatible(self) -> None: @export(record=CodeSignatureCodeResourcesRecords) def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]: - """Yield code signature coderesources information.""" + """Return macOS CodeResources plist entries. + + Yields the following record types: + + .. code-block:: text + + OmitRecord: + omit (boolean): Flag indicating the entry is marked as omitted. + weight (varint): Priority over other resources. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + + NestedRecord: + nested (boolean): Flag indicating the entry may be associated with nested code, + such as libraries, helper tools, and other bits of code that are embedded in the app. + weight (varint): Priority over other resources. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + + OptionalRecord: + optional (boolean) + weight (varint): Priority over other resources. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + + HashRecord: + hash (string): Hash value. + optional (boolean): Flag indicating the entry is marked as optional. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + + HashTwoRecord: + hash2 (string): Secondary hash value. + optional (boolean): Flag indicating the entry is marked as optional. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + + CDHashRecord: + cdhash (string): The code directory hash value. + requirement (string): Code signing requirement. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CodeResources file. + """ yield from build_plist_records( self, self.files, CodeSignatureCodeResourcesRecords, field_mappings=FIELD_MAPPINGS ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py index 680b34990c..5832be599a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -15,7 +15,11 @@ class ContentsInfoPlugin(Plugin): - """macOS contents info plugin.""" + """macOS contents info plugin. + + The information property list file is a structured file that contains configuration information + for an application. + """ def __init__(self, target: Target): super().__init__(target) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py index febfa88dfc..b84b2993d0 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.py @@ -17,12 +17,11 @@ "macos/contents_version", [ ("string", "build_alias_of"), - ("string", "relevance_platform"), - ("string", "build_version"), + ("varint", "build_version"), ("string", "cf_bundle_short_version_string"), ("string", "cf_bundle_version"), ("string", "project_name"), - ("string", "source_version"), + ("varint", "source_version"), ("path", "source"), ], ) @@ -32,7 +31,6 @@ FIELD_MAPPINGS = { "BuildAliasOf": "build_alias_of", - "RelevancePlatform": "relevance_platform", "BuildVersion": "build_version", "CFBundleShortVersionString": "cf_bundle_short_version_string", "CFBundleVersion": "cf_bundle_version", @@ -42,7 +40,14 @@ class ContentsVersionPlugin(Plugin): - """macOS Contents version.plist file.""" + """macOS contents version plugin. + + The version.plist file is a property list found in macOS bundles. + + References: + - https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion + - https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleshortversionstring + """ def __init__(self, target: Target): super().__init__(target) @@ -54,5 +59,18 @@ def check_compatible(self) -> None: @export(record=ContentsVersionRecord) def contents_version(self) -> Iterator[ContentsVersionRecord]: - """Yield contents version.plist information.""" + """Return macOS version.plist entries. + + Yields ContentsVersionRecord with the following fields: + + .. code-block:: text + + build_alias_of (string): Name of another component this entry is associated with. + build_version (varint): Build version number. + cf_bundle_short_version_string (string): The release or version number of the bundle. + cf_bundle_version (string): The version of the build that identifies an iteration of the bundle. + project_name (string): Project name. + source_version (varint): Internal source version. + source (path): Path to the version.plist file. + """ yield from build_plist_records(self, self.files, ContentsVersionRecords, field_mappings=FIELD_MAPPINGS) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py index 58f59f6e7b..d3a015047a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py @@ -27,19 +27,19 @@ class DirectoryServicesLocalNodesPlugin(Plugin): - """macOS directory services local nodes plugin.""" + """macOS Directory Services local nodes plugin. + + The /var/db/dslocal/sqlindex database tracks metadata for plist files in the directory structure + + References: + - https://web.archive.org/web/20221206190314/https://samsclass.info/121/lec16/ch13.pdf + """ PATH = "/var/db/dslocal/nodes/Default/sqlindex" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -49,19 +49,24 @@ def check_compatible(self) -> None: def directory_services_local_nodes( self, ) -> Iterator[DirectoryServicesLocalNodesRecord]: - """Yield directory services local nodes information. - - Database schema: - Tables prefixed with "rec:": - Rows contain: - - filename -> name of the backing plist (links to attribute tables) - - filetime -> datetime - - Attribute tables (e.g. "name", "realname", "uid", "gid", etc.): - Rows contain: - - filename -> name of the backing plist (links to "rec:" tables) - - recordtype -> e.g. "users", "groups", "computers" - - value -> content depends on recordtype + """Return macOS Directory Services local node entries. + + Yields DirectoryServicesLocalNodesRecord with the following fields: + + .. code-block:: text + + tables (string[]): Names of tables contributing to the record. + filetime (datetime): Timestamp associated with the row. + filename (string): Name of the backing plist file. + recordtype (string): Type of directory record (e.g. users, groups). + value (string): Attribute value associated with the record. + source (path): Path to the sqlindex file. + + Data is derived from the sqlindex database, where: + - Tables prefixed with "rec:" contain rows with plist filenames and associated filetimes. + - Attribute tables (e.g. name, uid, gid) contain rows with filenames, recordtypes, and values. + + Records are created by correlating rows between "rec:" and attribute tables. """ with SQLite3(self.file) as database: ATTRIBUTE_TABLES = { @@ -121,4 +126,4 @@ def directory_services_local_nodes( _target=self.target, ) - # Still missing altsecurityidentities, hardwareuuid, en_address, mail, member tables + # TODO: Add altsecurityidentities, hardwareuuid, en_address, mail, member tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index 5e5721ff59..99962736ec 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -53,7 +53,7 @@ ("varint", "ns_persistence_maximum_framework_version"), ("string[]", "ns_store_model_version_identifiers"), ("string", "ns_store_type"), - ("string", "ns_auto_vacuum_level"), + ("varint", "ns_auto_vacuum_level"), ("string", "ns_store_model_version_hashes_digest"), ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), @@ -62,21 +62,21 @@ ], ) -NSStoreModelVersionHashesRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler/ns_store_model_version_hashes", +ActivityRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/activity", [ - ("string", "tr_cloud_kit_sync_state"), - ("string", "text_replacement_entry"), + ("bytes", "activity"), + ("bytes", "group_binary"), + ("bytes", "trigger"), ("string", "plist_path"), ("path", "source"), ], ) ZModelCacheRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler", + "macos/duet_activity_scheduler/z_cache", [ ("string", "table"), - ("bytes", "z_content"), ("path", "source"), ], ) @@ -86,7 +86,7 @@ ZGroupRecord, ZMetadataRecord, ZPlistRecord, - NSStoreModelVersionHashesRecord, + ActivityRecord, ZModelCacheRecord, ) @@ -101,7 +101,6 @@ "ZNAME": "z_name", "Z_VERSION": "z_version", "Z_UUID": "z_uuid", - "Z_CONTENT": "z_content", "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", "NSStoreType": "ns_store_type", @@ -110,25 +109,29 @@ "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", - "TRCloudKitSyncState": "tr_cloud_kit_sync_state", - "TextReplacementEntry": "text_replacement_entry", + "Activity": "activity", + "Group": "group_binary", + "Trigger": "trigger", } class DuetActivitySchedulerPlugin(Plugin): - """macOS duet activity scheduler plugin.""" + """macOS Duet Activity Scheduler plugin. + + The Duet Activity Scheduler is a macOS background daemon + responsible for scheduling and managing deferred and conditional + activities. + + References: + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ PATH = "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -138,7 +141,60 @@ def check_compatible(self) -> None: def duet_activity_scheduler( self, ) -> Iterator[DuetActivityRecords]: - """Yield duet activity scheduler information.""" + """Return macOS Duet Activity Scheduler database entries. + + Yields the following record types extracted from the + DuetActivitySchedulerClassC.db database: + + .. code-block:: text + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): The ID of the table. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the DuetActivitySchedulerClassC.db file. + + ZGroupRecord: + table (string): Name of the source table (ZGROUP). + z_max_concurrent (varint): Maximum number of concurrent activities allowed. + z_name (string): The name of the entity in the data model. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_pk (varint): The autoincrement primary key of the table. + source (path): Path to the DuetActivitySchedulerClassC.db file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the DuetActivitySchedulerClassC.db file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the hashes. + source (path): Path to the DuetActivitySchedulerClassC.db file. + + ActivityRecord: + activity (bytes): Binary identifier of the activity. + group_binary (bytes): Binary identifier referencing a group. + trigger (bytes): Binary identifier referencing a trigger. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the DuetActivitySchedulerClassC.db file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the DuetActivitySchedulerClassC.db file. + """ yield from build_sqlite_records( self, (self.file,), @@ -146,5 +202,5 @@ def duet_activity_scheduler( field_mappings=FIELD_MAPPINGS, ) - # Still missing ZACTIVITY, Z_1TRIGGERS, ZTRIGGER, + # TODO: Add ZACTIVITY, Z_1TRIGGERS, ZTRIGGER, # Z_PRIMARYKEY, Z_METADATA,Z_MODELCACHE tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py index 3cc256a401..50706ff047 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py @@ -16,8 +16,8 @@ "macos/gkopaque/whitelist", [ ("string", "table"), - ("string", "current"), - ("string", "opaque"), + ("bytes", "current"), + ("bytes", "opaque"), ("path", "source"), ], ) @@ -48,19 +48,26 @@ class GatekeeperOpaqueConfigurationPlugin(Plugin): - """macOS gatekeeper opaque configuration plugin.""" + """macOS gatekeeper opaque configuration plugin. + + Gatekeeper is a macOS security feature that checks the code signing of downloaded + apps and blocks those that don't meet Apple's trust and policy requirements. + /var/db/gkopaque.bundle/Contents/Resources/gkopaque.db contains a whitelist table of + gatekeeper-trusted application code signature hashes and associated opaque values. + It also includes a conditions table, which defines policy rules applied to + specific applications. + + + References: + - https://indiestack.com/2014/10/gatekeepers-opaque-whitelist/ + - https://developer.apple.com/documentation/metal/mtlbinaryarchive/label?changes=_1&language=objc + """ PATH = "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -68,9 +75,31 @@ def check_compatible(self) -> None: @export(record=GatekeeperOpaqueConfigurationRecords) def gkopaque(self) -> Iterator[GatekeeperOpaqueConfigurationRecords]: - """Yield gatekeeper opaque configuration information.""" + """Return macOS Gatekeeper opaque configuration database entries. + + Yields the following record types extracted from the + gkopaque.db database: + + .. code-block:: text + + WhitelistRecord: + table (string): Name of the source table (whitelist). + current (bytes): Code Directory Hash (CDHash) identifying a trusted code object. + opaque (bytes): Associated opaque validation data. + source (path): Path to the gkopaque.db file. + + ConditionsRecord: + table (string): Name of the source table (conditions). + label (string): A string that identifies the library. + weight (varint): Priority value assigned to the condition. + conditions_source (string): Identifier indicating the origin of the condition rule. + identifier (string): Bundle identifier associated with the condition rule. + version (string): Version string for the condition. + conditions (string): Condition expression used by Gatekeeper. + source (path): Path to the gkopaque.db file. + """ yield from build_sqlite_records( self, (self.file,), GatekeeperOpaqueConfigurationRecords, field_mappings=FIELD_MAPPINGS ) - # Still missing merged table + # TODO: Add merged table diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py index 5757bbb87c..950988b1c2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -15,23 +15,28 @@ class GlobalUserPreferencesPlugin(Plugin): - """macOS global user preferences plugin.""" + """macOS global user preferences plugin. + + .GlobalPreferences.plist files are located in each user's + ~/Library/Preferences directory. This property list contains + system-wide preference settings that apply across applications for the user. + """ PATHS = ("Library/Preferences/.GlobalPreferences.plist",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No global user preferences files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for _, path in _build_userdirs(self, self.PATHS): - self.files.add(path) + files.add(path) + return files @export(record=DynamicDescriptor(["string"])) def global_user_preferences(self) -> Iterator[DynamicDescriptor]: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py index 2bbff430c1..194bb8bc32 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py @@ -15,73 +15,69 @@ GroupInfoRecord = TargetRecordDescriptor( "macos/groups", [ - ("string", "generateduid"), - ("string", "members"), - ("string", "smb_sid"), - ("varint", "gid"), - ("string", "name"), - ("string", "realname"), + ("string[]", "generateduid"), + ("string[]", "members"), + ("string[]", "smb_sid"), + ("varint[]", "gid"), + ("string[]", "name"), + ("string[]", "realname"), ("path", "source"), ], ) class GroupPlugin(Plugin): - """macOS group plugin.""" + """macOS group plugin. + + Parses ``/var/db/dslocal/nodes/Default/groups/*.plist`` files, which contain config data for groups. + + References: + - https://xmcyber.com/blog/introducing-machound-a-solution-to-macos-active-directory-based-attacks/ + """ GROUP_PATH_GLOB = "/var/db/dslocal/nodes/Default/groups/*.plist" def __init__(self, target: Target): super().__init__(target) - self.group_files = set() - self._resolve_files() + self.group_files = self._resolve_files() def check_compatible(self) -> None: if not self.group_files: raise UnsupportedPluginError("No group files found") - def _resolve_files(self) -> None: + def _resolve_files(self) -> set(): + files = set() for file in self.target.fs.glob(self.GROUP_PATH_GLOB): - self.group_files.add(file) + files.add(file) + return files @export(record=GroupInfoRecord) def groups(self) -> Iterator[GroupInfoRecord]: - """Yield user account policy information.""" - for file in self.group_files: - file = self.target.fs.path(file) - group_data = plistlib.load(file.open()) - - if uuid := group_data.get("generateduid"): # noqa: SIM102 - if len(uuid) == 1: - uuid = uuid[0] + """Return group information. - if smb_sid := group_data.get("smb_sid"): # noqa: SIM102 - if len(smb_sid): - smb_sid = smb_sid[0] + Yields GroupInfoRecords with the following fields: - if gid := group_data.get("gid"): # noqa: SIM102 - if len(gid) == 1: - gid = gid[0] + .. code-block:: text - if members := group_data.get("users"): # noqa: SIM102 - if len(members) == 1: - members = members[0] - - if realname := group_data.get("realname"): # noqa: SIM102 - if len(realname) == 1: - realname = realname[0] - - if name := group_data.get("name"): # noqa: SIM102 - if len(name) == 1: - name = name[0] + generateduid (string[]): Generated unique identifier(s) for the group. + members (string[]): List of user accounts that are members of the group. + smb_sid (string[]): SMB security identifier(s) associated with the group. + gid (varint[]): Group ID(s) assigned to the group. + name (string[]): Name(s) of the group. + realname (string[]): Realname(s) of the group. + source (path): Path to the group plist file. + """ + for file in self.group_files: + file = self.target.fs.path(file) + group_data = plistlib.load(file.open()) yield GroupInfoRecord( - generateduid=uuid, - members=members, - smb_sid=smb_sid, - gid=gid, - name=name, - realname=realname, + generateduid=group_data.get("generateduid"), + members=group_data.get("users"), + smb_sid=group_data.get("smb_sid"), + gid=group_data.get("gid"), + name=group_data.get("name"), + realname=group_data.get("realname"), source=file, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py index 413449ab51..32255be656 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -62,6 +62,7 @@ def find_bundle_files(target: Target, end_path: str) -> set: return results + def find_end_paths(target: Target, end_path: str, base_path: str) -> set: found = set() diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index cfc3c8379c..7bdc626f60 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -5,7 +5,7 @@ import re import uuid from collections import defaultdict -from datetime import datetime +from datetime import datetime, timedelta, timezone from io import BytesIO from itertools import product from typing import TYPE_CHECKING, Any, BinaryIO @@ -32,6 +32,7 @@ def build_sqlite_records( record_descriptors: tuple | None = None, joins: tuple = (), field_mappings: dict | None = None, + convert_timestamps: dict | None = None, ) -> Iterator[TargetRecordDescriptor]: joins_by_table1 = defaultdict(list) joins_by_table2 = defaultdict(list) @@ -67,7 +68,7 @@ def build_sqlite_records( ) else: yield from build_record_from_data( - plugin, file, value, record_descriptors, field_mappings + plugin, file, value, record_descriptors, field_mappings, convert_timestamps ) row_dict.pop(key) @@ -75,7 +76,9 @@ def build_sqlite_records( for j2 in joins_by_table2[table.name]: if row_dict.get(j2["key2"]) is None: row_dict["table"] = table.name - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + yield build_record( + plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + ) break else: match = False @@ -87,7 +90,14 @@ def build_sqlite_records( if not match: row_dict["table"] = table.name - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + yield build_record( + plugin, + row_dict, + file, + record_descriptors, + field_mappings, + convert_timestamps, + ) break elif table.name in joins_by_table1: @@ -125,13 +135,18 @@ def build_sqlite_records( combined_row, file, record_descriptors, + convert_timestamps=convert_timestamps, ) else: - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + yield build_record( + plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + ) else: row_dict["table"] = table.name - yield build_record(plugin, row_dict, file, record_descriptors, field_mappings) + yield build_record( + plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + ) except Exception: plugin.target.log.exception("Failed to process SQLite file: %s", file) @@ -232,6 +247,7 @@ def build_plist_records( record_descriptors: tuple | None = None, collapse_paths: set[tuple[str, bool]] | None = None, field_mappings: dict | None = None, + convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: for file in files: @@ -256,6 +272,7 @@ def build_plist_records( record_descriptors=record_descriptors, collapse_paths=collapse_paths, field_mappings=field_mappings, + convert_timestamps=convert_timestamps, function_name=function_name, ) @@ -269,6 +286,7 @@ def build_record_from_data( raw_data: bytes, record_descriptors: tuple | None = None, field_mappings: dict | None = None, + convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: try: @@ -287,6 +305,7 @@ def build_record_from_data( file, record_descriptors=record_descriptors, field_mappings=field_mappings, + convert_timestamps=convert_timestamps, function_name=function_name, ) @@ -330,31 +349,38 @@ def select_descriptor( plugin: Plugin, source: Path | None, ) -> TargetRecordDescriptor | None: - rdict_keys = {format_key(k) for k in rdict} + formatted_rdict = {format_key(k): type(v).__name__ for k, v in rdict.items()} selected_record = None best_match_count = 0 + best_match_length = 0 for record in record_descriptors: record_keys = set(record.fields.keys()) - matched_keys = rdict_keys & record_keys + matched_keys = set(formatted_rdict.keys()) & record_keys match_count = len(matched_keys) if match_count > best_match_count: best_match_count = match_count + best_match_length = len(record_keys) + selected_record = record + + elif match_count == best_match_count and len(record_keys) < best_match_length: + best_match_length = len(record_keys) selected_record = record if best_match_count == 0: return None - missing_fields = rdict_keys - set(selected_record.fields.keys()) + missing_fields = {key: type_name for key, type_name in formatted_rdict.items() if key not in selected_record.fields} + if missing_fields: - print(rdict) + formatted = ", ".join(f"{k} ({v})" for k, v in sorted(missing_fields.items())) plugin.target.log.warning( "Source %s contains fields not defined in the selected record descriptor: %s", source, - sorted(missing_fields), + formatted, ) return selected_record @@ -366,6 +392,7 @@ def build_record( source: Path | None, record_descriptors: tuple | None = None, field_mappings: dict | None = None, + convert_timestamps: dict | None = None, ) -> Record: if field_mappings: for key in list(rdict): @@ -392,11 +419,24 @@ def build_record( } for k, v in filtered_rdict.items(): - record_values[format_key(k)] = v + key = format_key(k) + if convert_timestamps and key in convert_timestamps: + v = convert_timestamp(v, convert_timestamps[key]) + record_values[key] = v return desc(**record_values) +def convert_timestamp(value: Any, mode: str) -> datetime | None: + if value is None or isinstance(value, datetime): + return value + + if mode == "2001": + return datetime(2001, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=float(value)) + + return value + + def create_event_descriptor(function_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: return TargetRecordDescriptor(function_name, record_fields) @@ -442,6 +482,7 @@ def emit_dict_records( record_descriptors: tuple | None = None, collapse_paths: set[tuple[str, bool]] | None = None, field_mappings: dict | None = None, + convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: if path and path.endswith("$class"): @@ -472,7 +513,6 @@ def emit_dict_records( if cleaned_list or not contains_dict: attributes[k] = cleaned_list - else: attributes[k] = v @@ -490,7 +530,7 @@ def emit_dict_records( if record_descriptors is None: yield dynamic_build_record(plugin, function_name, record_data, source) else: - yield build_record(plugin, record_data, source, record_descriptors, field_mappings) + yield build_record(plugin, record_data, source, record_descriptors, field_mappings, convert_timestamps) for k, child in child_dicts.items(): child_path = f"{path}/{k}" if path else k @@ -503,6 +543,7 @@ def emit_dict_records( record_descriptors=record_descriptors, collapse_paths=collapse_paths, field_mappings=field_mappings, + convert_timestamps=convert_timestamps, function_name=function_name, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py index 3b52973ad3..8137fe0253 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py @@ -27,23 +27,28 @@ class IdentityServicesPlugin(Plugin): - """macOS identity services plugin.""" + """macOS identity services plugin. + + ids.db is a SQLite database used by macOS's Identity Services framework (IDS), + the system that powers services like iMessage and FaceTime via the identityservicesd + daemon to store local identity-related data and metadata. + """ USER_PATH = ("Library/IdentityServices/ids.db",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No ids.db files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for _, path in _build_userdirs(self, self.USER_PATH): - self.files.add(path) + files.add(path) + return files @export( record=[ @@ -57,7 +62,17 @@ def identity_services( SqliteDatabasePropertiesRecord, ] ]: - """Yield identity services information.""" + """Return identity services information. + + Yields SqliteDatabasePropertiesRecords with the following fields: + + .. code-block:: text + + table (string): Name of the source table (_SqliteDatabaseProperties). + key (string): Key name. + value (string): Value associated with the key. + source (path): Path to the com.apple.airport.preferences.plist file. + """ for file in self.files: with SQLite3(file) as database: for row in database.table("_SqliteDatabaseProperties").rows(): @@ -68,4 +83,4 @@ def identity_services( source=file, ) - # Still missing outgoing_message, sqlite_sequence, incoming_message, outgoing_messages_to_delete tables + # TODO: Add outgoing_message, sqlite_sequence, incoming_message, outgoing_messages_to_delete tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py index b5eab5bed2..07b93c19dc 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py @@ -25,40 +25,46 @@ class InstallationHistoryPlugin(Plugin): - """macOS Software installation history property list plugin.""" + """macOS Software installation history property list plugin. + + Extracts the history of installed applications and updates. + + References: + - https://forensics.wiki/mac_os_x_10.9_artifacts_location/#software-installation + """ PATH = "/Library/Receipts/InstallHistory.plist" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: - raise UnsupportedPluginError("No InstallHistory.plis file found") + raise UnsupportedPluginError("No InstallHistory.plist file found") @export(record=InstallationHistoryRecord) def installation_history(self) -> Iterator[InstallationHistoryRecord]: - """Yield installation history information.""" + """Return installation history information. + + Yields InstallationHistoryRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp of the installation. + display_name (string): Display name of the installed software. + display_version (string): Display version of the installed software. + process_name (string): Name of the installation process. + source (path): Path to the InstallHistory.plist file. + """ plist = plistlib.load(self.file.open()) data = plist[0] - display_name = data.get("displayName") - display_version = data.get("displayVersion") - process_name = data.get("processName") - date = data.get("date") - yield InstallationHistoryRecord( - ts=date, - display_name=display_name, - display_version=display_version, - process_name=process_name, + ts=data.get("date"), + display_name=data.get("displayName"), + display_version=data.get("displayVersion"), + process_name=data.get("processName"), source=self.file, _target=self.target, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py index 5350bc2a53..9b173cd9e5 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.py @@ -27,19 +27,16 @@ class KeyboardLayoutPlugin(Plugin): - """macOS keyboard layout plugin.""" + """macOS keyboard layout plugin. + + This plugin extracts information about the keyboard layouts of the system. + """ PATH = "/Library/Preferences/com.apple.HIToolbox.plist" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -47,7 +44,20 @@ def check_compatible(self) -> None: @export(record=KeyboardLayoutRecord) def keyboard_layout(self) -> Iterator[KeyboardLayoutRecord]: - """Yield macOS keyboard layout information.""" + """Return macOS keyboard layout information. + + Yields KeyboardLayoutRecord with the following fields: + + .. code-block:: text + + input_source_kind (string): Kind of the input source. + keyboard_layout_name (string): Name of the keyboard layout. + keyboard_layout_id (varint): ID of the keyboard layout. + enabled_layout (boolean): Whether the layout is enabled. + selected_layout (boolean): Whether the layout is selected. + current_layout (boolean): Whether it is the current layout. + source (path): Path to the com.apple.HIToolbox.plist file. + """ plist = plistlib.loads(self.file.read_bytes()) for source in plist.get("AppleEnabledInputSources", []): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py index e83eb12aa7..d4819d42b6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py @@ -13,27 +13,27 @@ from dissect.target import Target -KeychainRecord = TargetRecordDescriptor( - "macos/keychain", +GenpRecord = TargetRecordDescriptor( + "macos/keychain/genp", [ ("string", "table"), ("varint", "row_id"), - ("float", "cdat"), - ("float", "mdat"), - ("string", "desc"), - ("string", "icmt"), - ("string", "crtr"), - ("string", "type"), - ("string", "scrp"), - ("string", "labl"), - ("string", "alis"), + ("datetime", "cdat"), + ("datetime", "mdat"), + ("bytes", "desc"), + ("bytes", "icmt"), + ("varint", "crtr"), + ("varint", "keychain_type"), + ("varint", "scrp"), + ("bytes", "labl"), + ("bytes", "alis"), ("varint", "invi"), ("varint", "nega"), ("varint", "cusi"), - ("varint", "prot"), - ("string", "acct"), - ("string", "svce"), - ("string", "gena"), + ("bytes", "prot"), + ("bytes", "acct"), + ("bytes", "svce"), + ("bytes", "gena"), ("string", "data"), ("string", "agrp"), ("string", "pdmn"), @@ -45,10 +45,112 @@ ("string", "musr"), ("string", "UUID"), ("varint", "sysb"), - ("string", "pcss"), - ("string", "pcsk"), - ("string", "pcsi"), - ("string", "persistref"), + ("varint", "pcss"), + ("bytes", "pcsk"), + ("bytes", "pcsi"), + ("bytes", "persistref"), + ("varint", "clip"), + ("string", "ggrp"), + ("path", "source"), + ], +) + + +InetRecord = TargetRecordDescriptor( + "macos/keychain/inet", + [ + ("string", "table"), + ("varint", "row_id"), + ("datetime", "cdat"), + ("datetime", "mdat"), + ("bytes", "desc"), + ("bytes", "icmt"), + ("varint", "crtr"), + ("varint", "keychain_type"), + ("varint", "scrp"), + ("bytes", "labl"), + ("bytes", "alis"), + ("varint", "invi"), + ("varint", "nega"), + ("varint", "cusi"), + ("bytes", "prot"), + ("bytes", "acct"), + ("bytes", "sdmn"), + ("bytes", "srvr"), + ("string", "ptcl"), + ("bytes", "atyp"), + ("varint", "port"), + ("bytes", "path_binary"), + ("string", "data"), + ("string", "agrp"), + ("string", "pdmn"), + ("varint", "sync"), + ("varint", "tomb"), + ("string", "sha1"), + ("string", "vwht"), + ("string", "tkid"), + ("string", "musr"), + ("string", "UUID"), + ("varint", "sysb"), + ("varint", "pcss"), + ("bytes", "pcsk"), + ("bytes", "pcsi"), + ("bytes", "persistref"), + ("varint", "clip"), + ("string", "ggrp"), + ("path", "source"), + ], +) + +KeysRecord = TargetRecordDescriptor( + "macos/keychain/keys", + [ + ("string", "table"), + ("varint", "row_id"), + ("datetime", "cdat"), + ("datetime", "mdat"), + ("bytes", "kcls"), + ("bytes", "labl"), + ("bytes", "alis"), + ("varint", "perm"), + ("varint", "priv"), + ("varint", "modi"), + ("bytes", "klbl"), + ("bytes", "atag"), + ("varint", "crtr"), + ("varint", "keychain_type"), + ("varint", "bsiz"), + ("varint", "esiz"), + ("varint", "sdat"), + ("varint", "edat"), + ("varint", "sens"), + ("varint", "asen"), + ("varint", "extr"), + ("varint", "next"), + ("varint", "encr"), + ("varint", "decr"), + ("varint", "drve"), + ("varint", "sign"), + ("varint", "vrfy"), + ("varint", "snrc"), + ("varint", "vyrc"), + ("varint", "wrap"), + ("varint", "unwp"), + ("string", "data"), + ("string", "agrp"), + ("string", "pdmn"), + ("varint", "sync"), + ("varint", "tomb"), + ("string", "sha1"), + ("string", "vwht"), + ("string", "tkid"), + ("string", "musr"), + ("string", "UUID"), + ("varint", "sysb"), + ("varint", "pcss"), + ("bytes", "pcsk"), + ("bytes", "pcsi"), + ("bytes", "persistref"), ("varint", "clip"), ("string", "ggrp"), ("path", "source"), @@ -80,53 +182,71 @@ "macos/keychain/meta_data_keys", [ ("string", "table"), - ("string", "keyclass"), - ("string", "actual_key_class"), + ("varint", "keyclass"), + ("varint", "actual_keyclass"), ("string", "data"), ("path", "source"), ], ) KeychainRecords = ( - KeychainRecord, + GenpRecord, + InetRecord, + KeysRecord, SqliteSequenceRecord, TVersionRecord, MetaDataKeysRecord, ) FIELD_MAPPINGS = { - "actualKeyclass": "actual_key_class", + "actualKeyclass": "actual_keyclass", "rowid": "row_id", + "path": "path_binary", + "type": "keychain_type", +} + +CONVERT_TIMESTAMPS = { + "cdat": "2001", + "mdat": "2001", } class KeychainPlugin(Plugin): - """macOS keychain plugin.""" + """macOS keychain plugin. + + Parses Data Protection keychain databases (keychain-2.db). + These are stored in ``Library/Keychains/*/``, where ``*`` is the UUID that assigned to that Mac. + + References: + - https://eclecticlight.co/2023/08/07/an-introduction-to-keychains-and-how-theyve-changed/ + """ USER_PATH = ("Library/Keychains/*/keychain-2.db",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No keychain-2.db files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for _, path in _build_userdirs(self, self.USER_PATH): - self.files.add(path) + files.add(path) + return files @export(record=KeychainRecords) def keychain( self, ) -> Iterator[KeychainRecords]: - """Yield keychain information.""" - yield from build_sqlite_records(self, self.files, KeychainRecords, field_mappings=FIELD_MAPPINGS) + """Return macOS Keychain database entries.""" + yield from build_sqlite_records( + self, self.files, KeychainRecords, field_mappings=FIELD_MAPPINGS, convert_timestamps=CONVERT_TIMESTAMPS + ) - # Still missing cert, outgoingqueue, incomingqueue, synckeys, ckmirror, currentkeys, + # TODO: Add cert, outgoingqueue, incomingqueue, synckeys, ckmirror, currentkeys, # ckstate, item_backup, backup_keybag, ckmanifest, pending_manifest, ckmanifest_leaf, # backup_keyarchive, currentkeyarchives, archived_key_backup, pending_manifest_leaf, # currentitems, ckdevicestate, tlkshare, sharingIncomingQueue, diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py index d121bdd6af..dd297b3a2e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py @@ -81,7 +81,6 @@ ("varint", "sock_path_group"), ("string", "bonjour"), ("string", "multicast_group"), - ("boolean", "receive_packet_info"), ("string", "plist_path"), ("path", "source"), ] @@ -177,7 +176,6 @@ "SockPathGroup": "sock_path_group", "Bonjour": "bonjour", "MulticastGroup": "multicast_group", - "ReceivePacketInfo": "receive_packet_info", # UNRecord "UNSettingAlerts": "un_setting_alerts", "UNSettingAlwaysShowPreviews": "un_setting_always_show_previews", @@ -260,7 +258,17 @@ class LaunchersPlugin(Plugin): - """macOS launchers plugin.""" + """macOS launchers plugin. + + Parses LaunchAgent and LaunchDaemon files, which are configuration-based background services + managed by macOS that automatically run tasks or processes based on system or user-level triggers. + + References: + - https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html + - https://www.manpagez.com/man/5/launchd.plist/osx-10.13.1.php + - https://developer.apple.com/documentation/usernotifications/unusernotificationcenter + - https://medium.com/@durgaviswanadh/understanding-macos-launchagents-and-login-items-a-clear-practical-guide-5c0e39e3a6b3 + """ SYSTEM_LAUNCH_AGENT_PATHS = ( "/System/Library/LaunchAgents/*.plist", @@ -279,43 +287,206 @@ class LaunchersPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.launch_agent_files = set() - self.launch_daemon_files = set() - self._find_files() + self.launch_agent_files = self._find_agent_files() + self.launch_daemon_files = self._find_daemon_files() def check_compatible(self) -> None: if not (self.launch_agent_files or self.launch_daemon_files): raise UnsupportedPluginError("No Agent or Deamon files found") - def _find_files(self) -> None: - # --- System-wide LaunchAgents --- + def _find_agent_files(self) -> set: + launch_agent_files = set() for pattern in self.SYSTEM_LAUNCH_AGENT_PATHS: for path in self.target.fs.glob(pattern): - self.launch_agent_files.add(path) - - # --- Per-user LaunchAgents --- + launch_agent_files.add(path) for _, path in _build_userdirs(self, self.USER_LAUNCH_AGENT_PATHS): - self.launch_agent_files.add(path) + launch_agent_files.add(path) + return launch_agent_files - # --- System-wide LaunchDaemons --- + def _find_daemon_files(self) -> set: + launch_daemon_files = set() for pattern in self.SYSTEM_LAUNCH_DAEMON_PATHS: for path in self.target.fs.glob(pattern): - self.launch_daemon_files.add(path) - - # --- Per-user LaunchDaemons --- + launch_daemon_files.add(path) for _, path in _build_userdirs(self, self.USER_LAUNCH_DAEMON_PATHS): - self.launch_daemon_files.add(path) + launch_daemon_files.add(path) + return launch_daemon_files @export(record=LaunchAgentRecords) def launch_agents(self) -> Iterator[LaunchAgentRecords]: - """Yield macOS launch agent plist files.""" + """Return macOS LaunchAgent plist entries. + + Yields the following record types extracted from + LaunchAgent plist files: + + .. code-block:: text + + LauncherRecord: + label (string): Required key that uniquely identifies the job in launchd. + program (string): Absolute path to the executable mapped to execv(3). + program_arguments (string[]): Argument vector passed to execvp(3). + keep_alive (string): Controls whether the job remains running continuously + or is restarted based on configured conditions. + on_demand (boolean): Deprecated key; false is equivalent to KeepAlive=true. + disabled (string): Specifies if the job should be loaded by default; may be overridden externally. + run_at_load (boolean): Starts the job when it is loaded into launchd. + launch_only_once (boolean): Indicates the job must not be respawned after execution. + process_type (string): Declares the job classification (e.g. Background, + Adaptive, Interactive) used for resource management. + wait (boolean): inetd compatibility flag determining whether sockets are passed + directly or accepted on behalf of the job. + limit_load_to_developer_mode (boolean): Restricts execution depending on developer mode state. + limit_load_to_variant (string): Restricts loading to specific system variants. + limit_load_from_variant (string): Prevents loading on specific system variants. + limit_load_to_boot_mode (string[]): Restricts loading to specified boot modes. + limit_load_from_boot_mode (string): Prevents loading when in specified boot mode. + limit_load_to_hardware (string[]): Restricts loading to systems matching hardware values. + limit_load_from_hardware (string[]): Prevents loading on matching hardware values. + launch_events (string[]): Defines event-based triggers used to start the job. + mach_services (string[]): Mach services registered in the bootstrap namespace. + enable_pressured_exit (string): Enables lifecycle management under memory pressure. + enable_transactions (boolean): Indicates the job uses XPC transaction tracking for safe termination. + environment_variables (string[]): Environment variables to set before execution. + user_name (string): User identity the job runs as (for system domain jobs). + init_groups (boolean): Whether initgroups(3) is called to initialize group membership. + group_name (string): Group identity the job runs as. + start_interval (varint): Starts the job every specified number of seconds. + start_calendar_interval (string[]): Scheduling rules similar to cron-style timing. + throttle_interval (varint): Minimum interval between job invocations. + enable_globbing (boolean): Expands program arguments using glob(3) before execution. + standard_in_path (string): File mapped to stdin(4). + standard_out_path (string): File mapped to stdout(4). + standard_error_path (string): File mapped to stderr(4). + nice (varint): Scheduling priority applied with nice(3). + abandon_process_group (boolean): Prevents launchd from terminating the job's process group. + low_priority_io (boolean): Marks the job as low priority for filesystem I/O. + root_directory (string): Directory used as a chroot(2) environment. + working_directory (string): Directory set via chdir(2) before execution. + umask (varint): Value passed to umask(2) for file creation permissions. + time_out (varint): Idle timeout hint (deprecated and not implemented). + exit_time_out (varint): Time between SIGTERM and SIGKILL when stopping the job. + watch_paths (string[]): Triggers job when specified filesystem paths change. + queue_directories (string[]): Keeps job alive while directories are not empty. + start_on_mount (boolean): Starts the job when filesystems are mounted. + soft_resource_limits (string[]): Soft setrlimit(2)-based resource limits. + hard_resource_limits (string[]): Hard setrlimit(2)-based resource limits. + debug (boolean): Temporarily elevates logging to debug level for this job. + wait_for_debugger (boolean): Launches the process suspended until a debugger attaches. + + SocketRecord: + socket_key (string): Identifier used to associate socket configuration with the job. + sock_type (string): Socket type passed to socket(2) (e.g. stream, dgram). + sock_passive (boolean): Determines if listen(2) or connect(2) is used. + sock_node_name (string): Node name used for bind(2) or connect(2). + sock_service_name (string): Service name or port used for the socket. + sock_family (string): Address family (e.g. IPv4, IPv6, Unix). + sock_protocol (string): Protocol used by the socket (TCP or UDP). + sock_path_mode (varint): File mode for Unix domain socket. + sock_path_name (string): Filesystem path for Unix domain socket. + secure_socket_with_key (string): Environment variable key assigned to a generated socket path. + sock_path_owner (varint): User ID ownership of the socket file. + sock_path_group (varint): Group ID of the socket file. + bonjour (string): Registers the service with Bonjour if specified. + multicast_group (string): Multicast group joined by the socket. + + UNRecord: + un_setting_alerts (boolean): Indicates if notifications are allowed to show alerts. + un_setting_always_show_previews (boolean): Controls whether notification previews are always displayed. + un_setting_lock_screen (boolean): Determines if notifications are shown on the lock screen. + un_setting_modal_alert_style (boolean): Indicates if notifications use modal alert presentation. + un_automatically_show_settings (boolean): Indicates if notification settings are shown automatically. + un_setting_notification_center (boolean): Indicates if notifications appear in Notification Center. + un_daemon_should_receive_background_responses (boolean): Indicates background handling of + notification responses. + un_suppress_user_authorization_prompt (boolean): Indicates suppression of notification + permission prompts. + + UNNotificationRecord: + un_notification_icon_default (string): Default icon used for notifications. + un_notification_icon_settings (string): Icon used in notification settings. + """ yield from build_plist_records( self, self.launch_agent_files, LaunchAgentRecords, COLLAPSE_PATHS, FIELD_MAPPINGS ) @export(record=LaunchDaemonRecords) def launch_daemons(self) -> Iterator[LaunchDaemonRecords]: - """Yield macOS launch daemon plist files.""" + """Return macOS LaunchDaemon plist entries. + + Yields the following record types extracted from + LaunchDaemon plist files: + + .. code-block:: text + + LauncherRecord: + label (string): Unique identifier for the daemon in launchd. + program (string): Absolute executable path used with execv(3). + program_arguments (string[]): Argument vector passed with execvp(3). + keep_alive (string): Defines whether the daemon should remain running or + restart under specified conditions. + on_demand (boolean): Deprecated; false implies KeepAlive behavior. + disabled (string): Indicates whether the daemon is disabled by default. + run_at_load (boolean): Starts the daemon immediately when loaded. + launch_only_once (boolean): Ensures the daemon is only executed one time. + process_type (string): Declares job classification influencing system resource limits. + wait (boolean): inetd-style behavior for socket handling. + limit_load_to_developer_mode (boolean): Restricts loading based on developer mode. + limit_load_to_variant (string): Restricts loading to a system variant. + limit_load_from_variant (string): Prevents loading on a system variant. + limit_load_to_boot_mode (string[]): Restricts loading to specific boot modes. + limit_load_from_boot_mode (string): Prevents loading on specific boot modes. + limit_load_to_hardware (string[]): Restricts loading to specific hardware values. + limit_load_from_hardware (string[]): Prevents loading on specified hardware. + launch_events (string[]): Defines event sources that trigger daemon launch. + mach_services (string[]): Mach services advertised to the bootstrap namespace. + enable_pressured_exit (string): Enables system-managed termination under memory pressure. + enable_transactions (boolean): Enables XPC transaction tracking for controlled shutdown. + environment_variables (string[]): Environment variables set before execution. + user_name (string): User account used to run the daemon. + init_groups (boolean): Indicates if initgroups(3) is used for group setup. + group_name (string): Group under which the daemon runs. + start_interval (varint): Interval-based execution timing. + start_calendar_interval (string[]): Calendar-based scheduling rules. + throttle_interval (varint): Minimum delay between successive launches. + enable_globbing (boolean): Enables glob(3) expansion of arguments. + standard_in_path (string): File path mapped to stdin. + standard_out_path (string): File path mapped to stdout. + standard_error_path (string): File path mapped to stderr. + nice (varint): CPU scheduling priority. + abandon_process_group (boolean): Prevents launchd from terminating child processes. + low_priority_io (boolean): Applies low-priority I/O classification. + root_directory (string): Directory used as chroot environment. + working_directory (string): Working directory before execution. + umask (varint): File creation mask applied to the process. + time_out (varint): Deprecated idle timeout setting. + exit_time_out (varint): Delay before SIGKILL after SIGTERM. + watch_paths (string[]): Launch triggers based on filesystem changes. + queue_directories (string[]): Keeps daemon running while directories contain files. + start_on_mount (boolean): Starts daemon when filesystems are mounted. + soft_resource_limits (string[]): Soft resource limits applied via setrlimit(2). + hard_resource_limits (string[]): Hard resource limits applied via setrlimit(2). + debug (boolean): Enables debug logging in launchd for this job. + wait_for_debugger (boolean): Starts process suspended for debugger attachment. + + SocketRecord: + socket_key (string): Identifier for grouping socket definitions. + sock_type (string): Socket type used when creating the descriptor. + sock_passive (boolean): Indicates if the socket is listening or connecting. + sock_node_name (string): Address used when binding or connecting. + sock_service_name (string): Port or service name for the socket. + sock_family (string): Address family used for socket creation. + sock_protocol (string): Protocol associated with the socket. + sock_path_mode (varint): Permissions applied to socket file. + sock_path_name (string): Filesystem location for the socket. + secure_socket_with_key (string): Environment variable referencing generated socket path. + sock_path_owner (varint): Owner user ID of the socket file. + sock_path_group (varint): Group ID of the socket file. + bonjour (string): Optional Bonjour registration. + multicast_group (string): Multicast group subscription. + + ListenersRecord: + listeners (string[]): Identifiers referring to defined socket groups. + """ yield from build_plist_records( self, self.launch_daemon_files, LaunchDaemonRecords, COLLAPSE_PATHS, FIELD_MAPPINGS ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py index 0b9480d8f7..d8ae7c3103 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -15,7 +15,10 @@ class LocalePlugin(LocalePlugin): - """macOS locale plugin.""" + """macOS locale plugin. + + This plugin retrieves locale information from the system. + """ GLOBAL = "/Library/Preferences/.GlobalPreferences.plist" @@ -26,24 +29,41 @@ def check_compatible(self) -> None: if not self.target.os == "macos": raise UnsupportedPluginError("Unsupported Plugin") + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + @export(property=True) def timezone(self) -> str | None: - """Get the configured timezone of the system in IANA TZ standard format.""" + """Return the configured timezone of the system. + + .. code-block:: + + ["Europe", "Amsterdam"] -> Europe/Amsterdam + ["UTC"] -> UTC + """ preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) - tz_data = preferences["com.apple.TimeZonePref.Last_Selected_City"] - tz = None + tz_data = preferences.get("com.apple.TimeZonePref.Last_Selected_City") + + tz_candidate = "/".join(tz_data) + result = self.check_timezone(tz_candidate) + if result: + return result for entry in tz_data: - try: - tz = ZoneInfo(entry).key - except ZoneInfoNotFoundError: # noqa: PERF203 - continue + result = self.check_timezone(entry) + if result: + return result + + return None - return tz + def check_timezone(self, entry: str) -> str | None: + try: + return ZoneInfo(entry).key + except (ZoneInfoNotFoundError, PermissionError): + return None @export(property=True) def language(self) -> str | None: - """Get a list of installed languages on the system.""" + """Return a list of installed languages on the system.""" preferences = plistlib.load(self.target.fs.path(self.GLOBAL).open()) languages = preferences["AppleLanguages"] clean_languages = [] @@ -56,13 +76,13 @@ def language(self) -> str | None: @export(property=True) def install_date(self) -> str | None: - """Get the installation date of the system.""" + """Return the installation date of the system.""" mtime = self.target.fs.path("/private/var/db/.AppleSetupDone").lstat().st_mtime return datetime.fromtimestamp(mtime, timezone.utc) @export(property=True) def location_services_active(self) -> bool | None: - """Get the status of location services.""" + """Return whether location services are active.""" path = self.target.fs.path("/Library/Preferences/com.apple.timezone.auto.plist") if not path.exists(): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py index eb260007b1..475e9539ac 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py @@ -16,27 +16,28 @@ LoginItemsRecord = TargetRecordDescriptor( "macos/login_items", [ - ("string", "associatedBundleIdentifiers"), + ("string", "associated_bundle_identifiers"), ("bytes", "bookmark"), - ("string", "bundleIdentifier"), + ("string", "bundle_identifier"), ("string", "container"), - ("string", "designatedRequirement"), - ("string", "developerName"), + ("string", "designated_requirement"), + ("string", "developer_name"), ("varint", "login_item_disposition"), - ("datetime", "executableModificationDate"), - ("path", "executablePath"), + ("datetime", "executable_modification_date"), + ("path", "executable_path"), ("varint", "flags"), ("varint", "generation"), ("string", "identifier"), - ("bytes", "lightweightRequirement"), - ("datetime", "modificationDate"), + ("bytes", "lightweight_requirement"), + ("datetime", "modification_date"), ("string", "name"), - ("string", "programArguments"), + ("string", "program_arguments"), ("string", "sha256"), - ("string", "teamIdentifier"), + ("string", "team_identifier"), ("varint", "login_item_type"), ("string", "url"), ("string", "uuid"), + ("string[]", "items"), ("string", "plist_path"), ("path", "source"), ], @@ -57,16 +58,39 @@ LoginItemsRecords = (LoginItemsRecord, LoginItemsMetadataRecord) FIELD_MAPPINGS = { + "associatedBundleIdentifiers": "associated_bundle_identifiers", + "bundleIdentifier": "bundle_identifier", + "designatedRequirement": "designated_requirement", + "developerName": "developer_name", + "disposition": "login_item_disposition", + "executableModificationDate": "executable_modification_date", + "executablePath": "executable_path", + "lightweightRequirement": "lightweight_requirement", + "modificationDate": "modification_date", + "programArguments": "program_arguments", + "teamIdentifier": "team_identifier", + "type": "login_item_type", "backgroundAppRefreshLoadCount": "background_app_refresh_load_count", "launchServicesItemsImported": "launch_services_items_imported", "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", - "type": "login_item_type", - "disposition": "login_item_disposition", +} + +CONVERT_TIMESTAMPS = { + "modification_date": "2001", } class LoginItemsPlugin(Plugin): - """macOS login items plugin.""" + """macOS login items plugin. + + Parses macOS login items and background task entries from plist and BTM + files, which are used by the system to launch applications and services + at user login. + + References: + - https://www.swiftforensics.com/2025/01/macapt-update-to-btm-processing.html + - https://developer.apple.com/documentation/corefoundation/cfurl + """ SYSTEM_LOGIN_ITEMS_PATHS = ("/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v*.btm",) @@ -77,25 +101,84 @@ class LoginItemsPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - - self.login_items_files = set() - self._find_files() + self.login_items_files = self._find_files() def check_compatible(self) -> None: if not (self.login_items_files): raise UnsupportedPluginError("No Login Items files found") - def _find_files(self) -> None: - # --- System-wide --- + def _find_files(self) -> set: + login_items_files = set() for pattern in self.SYSTEM_LOGIN_ITEMS_PATHS: for path in self.target.fs.glob(pattern): - self.login_items_files.add(path) + login_items_files.add(path) - # --- Per-user --- for _, path in _build_userdirs(self, self.USER_LOGIN_ITEMS_PATHS): - self.login_items_files.add(path) + login_items_files.add(path) + + return login_items_files @export(record=LoginItemsRecord) def login_items(self) -> Iterator[LoginItemsRecord]: - """Yield macOS login items plist files.""" - yield from build_plist_records(self, self.login_items_files, LoginItemsRecords, field_mappings=FIELD_MAPPINGS) + """Return macOS login items and background task entries. + + Yields the following record types extracted from the + backgrounditems.btm and com.apple.loginitems.plist files: + + .. code-block:: text + + LoginItemsRecord: + associated_bundle_identifiers (string): Associated bundle identifiers. + bookmark (bytes): CFURL bookmark data referencing a file-system resource. + bundle_identifier (string): Bundle identifier of the item. + container (string): Containing app or bundle. + designated_requirement (string): Code signing designated requirement. + developer_name (string): Developer name. + login_item_disposition (varint): Numeric value describing state: + 1 = Enabled. + 2 = Allowed. + 4 = Hidden. + 8 = Notified. + executable_modification_date (datetime): Last modification time of the executable. + executable_path (path): Path to the executable. + flags (varint): Additional flags associated with the item. + generation (varint): Generation identifier of the record. + identifier (string): Unique identifier for the item. + lightweight_requirement (bytes): Lightweight code signing requirement data. + modification_date (datetime): Last modification timestamp of the record. + name (string): Name of the item. + program_arguments (string): Program arguments for execution. + sha256 (string): SHA256 hash of the executable. + team_identifier (string): Apple developer team identifier. + login_item_type (varint): Numeric value describing the item type: + 1 = user item. + 2 = app. + 4 = login item. + 8 = agent. + 16 = daemon. + 32 = developer. + 64 = spotlight. + 2048 = quicklook. + 65536 = legacy. + 524288 = curated. + url (string): URL associated with the item. + uuid (string): Universally unique identifier. + items (string[]): List of items. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the backgrounditems.btm or com.apple.loginitems.plist file. + + LoginItemsMetadataRecord: + generation (varint): Metadata generation value. + background_app_refresh_load_count (varint): Background app refresh load count. + launch_services_items_imported (boolean): Indicates LaunchServices import. + service_management_login_items_migrated (boolean): Indicates migration status. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the backgrounditems.btm or com.apple.loginitems.plist file. + """ + yield from build_plist_records( + self, + self.login_items_files, + LoginItemsRecords, + field_mappings=FIELD_MAPPINGS, + convert_timestamps=CONVERT_TIMESTAMPS, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index e0969b41a9..149437def2 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records @@ -13,9 +13,66 @@ from dissect.target.target import Target +LoginWindowRecord = TargetRecordDescriptor( + "macos/login_window", + [ + ("string", "build_version_as_string"), + ("varint", "build_version_stamp_as_number"), + ("string", "system_version_stamp_as_string"), + ("varint", "system_version_stamp_as_number"), + ("path", "source"), + ], +) + +# Found these fields on macOS Tahoe, but not on Ventura +MiniBuddyRecord = TargetRecordDescriptor( + "macos/login_window/mini_buddy", + [ + ("boolean", "mini_buddy_launch"), + ("path", "source"), + ], +) + +# Found these fields on macOS Ventura, but not on Tahoe +BundleRecord = TargetRecordDescriptor( + "macos/login_window/bundle", + [ + ("boolean", "hide"), + ("string", "bundle_id"), + ("path", "path"), + ("varint", "background_state"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +LoginWindowRecordRecords = (LoginWindowRecord, MiniBuddyRecord, BundleRecord) + +FIELD_MAPPINGS = { + "BuildVersionStampAsString": "build_version_as_string", + "SystemVersionStampAsNumber": "system_version_stamp_as_number", + "SystemVersionStampAsString": "system_version_stamp_as_string", + "BuildVersionStampAsNumber": "build_version_stamp_as_number", + "MiniBuddyLaunch": "mini_buddy_launch", + "Hide": "hide", + "BundleID": "bundle_id", + "Path": "path", + "BackgroundState": "background_state", +} + class LoginWindowPlugin(Plugin): - """macOS login window plugin.""" + """macOS login window plugin. + + Parses configuration settings related to macOS login window behavior. + Login window is the system component that handles actions during login and logout, + such as relaunching applications on login. + + References: + - https://developer.apple.com/documentation/devicemanagement/loginwindowscripts + - https://cocomelonc.github.io/macos/2026/03/29/mac-malware-persistence-7.html + - https://forums.macrumors.com/threads/keychain-minibuddyitem-what-this-is.2077558/ + """ SYSTEM_LOGIN_WINDOW_PATHS = ( "/Library/Preferences/com.apple.loginwindow.plist", @@ -31,25 +88,49 @@ class LoginWindowPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - - self.login_window_files = set() - self._find_files() + self.login_window_files = self._find_files() def check_compatible(self) -> None: if not (self.login_window_files): raise UnsupportedPluginError("No Login Window files found") - def _find_files(self) -> None: - # --- System-wide --- + def _find_files(self) -> set: + login_window_files = set() for pattern in self.SYSTEM_LOGIN_WINDOW_PATHS: for path in self.target.fs.glob(pattern): - self.login_window_files.add(path) - - # --- Per-user --- + login_window_files.add(path) for _, path in _build_userdirs(self, self.USER_LOGIN_WINDOW_PATHS): - self.login_window_files.add(path) + login_window_files.add(path) + return login_window_files + + @export(record=LoginWindowRecordRecords) + def login_window(self) -> Iterator[LoginWindowRecordRecords]: + """Return macOS login window configuration settings. + + Yields the following record types extracted from the + com.apple.loginwindow.plist files: + + .. code-block:: text + + LoginWindowRecord: + build_version_as_string (string): OS build version. + build_version_stamp_as_number (varint): Numeric build stamp. + system_version_stamp_as_string (string): OS version string. + system_version_stamp_as_number (varint): Numeric system version. + source (path): Path to the plist file. + + MiniBuddyRecord: + mini_buddy_launch (boolean): Whether MiniBuddy, the macOS Setup Assistant, should launch on login. + source (path): Path to the plist file. - @export(record=DynamicDescriptor(["string"])) - def login_window(self) -> Iterator[DynamicDescriptor]: - """Yield macOS login window plist files.""" - yield from build_plist_records(self, self.login_window_files, function_name="macos/login_window") + BundleRecord: + hide (boolean): Whether the app is hidden on relaunch. + bundle_id (string): Application bundle identifier. + path (path): Path to the application bundle. + background_state (varint): Relaunch background state, 2 = background process. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the plist file. + """ + yield from build_plist_records( + self, self.login_window_files, LoginWindowRecordRecords, field_mappings=FIELD_MAPPINGS + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py index 7f5c354b2e..1facc6c48f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py @@ -1,6 +1,5 @@ from __future__ import annotations -import struct from datetime import datetime, timezone from typing import TYPE_CHECKING @@ -46,11 +45,19 @@ }; """) +cs.load(""" +struct asl_string_header { + uint16 marker; + uint32 length; +}; +""") + + ASLRecord = TargetRecordDescriptor( "macos/logs/asl", [ ("datetime", "ts"), - ("varint", "severity_level"), + ("varint", "priority_level"), ("varint", "pid"), ("string", "asl_host"), ("string", "sender"), @@ -62,16 +69,24 @@ def _parse_asl_string_ref(data: bytes, ref: int) -> str | None: + # Resolve a string reference from the ASL file (inline or external) if ref == 0: return None + # Inline string packed into the reference itself if ref & 0x8000000000000000: - ref_bytes = struct.pack(">Q", ref & 0x7FFFFFFFFFFFFFFF) + ref_bytes = (ref & 0x7FFFFFFFFFFFFFFF).to_bytes(8, "big") slen = ref_bytes[0] return ref_bytes[1 : 1 + slen].decode("utf-8", errors="replace").rstrip("\x00") + # External string stored elsewhere in the file if ref + 6 < len(data) and data[ref : ref + 2] == b"\x00\x01": - slen = struct.unpack(">I", data[ref + 2 : ref + 6])[0] + try: + hdr = cs.asl_string_header(data[ref : ref + 6]) + slen = hdr.length + except Exception: + return None + if 0 < slen < 65536 and ref + 6 + slen <= len(data): return data[ref + 6 : ref + 6 + slen].decode("utf-8", errors="replace").rstrip("\x00") @@ -94,33 +109,35 @@ def _valid_ref(data: bytes, ref: int) -> bool: def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: - """Parse an ASL DB binary file using cstruct.""" + # Basic file validation (header + minimum size) if len(data) < 80 or data[:6] != b"ASL DB": return now = int(datetime.now(tz=timezone.utc).timestamp()) - pos = 0x80 + pos = 0x80 # Records typically start after header + # Iterate through file and try to locate valid ASL records while pos < len(data) - 60: rec_len = int.from_bytes(data[pos : pos + 2], "big") - # Sanity checks + # Skip invalid or unrealistic record sizes if rec_len < 120 or rec_len > 65535 or pos + rec_len + 2 > len(data): pos += 2 continue - # Parse struct safely try: + # Parse record structure using cstruct rec = cs.asl_record(data[pos + 2 : pos + 2 + rec_len]) except Exception: pos += 2 continue - # Timestamp validation + # Filter out invalid timestamps if not (946684800 < rec.time_s < now + 31536000): pos += 2 continue + # Validate that at least one string reference looks valid refs = [ rec.host_ref, rec.sender_ref, @@ -134,12 +151,13 @@ def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: pos += 2 continue - # Decode strings + # Decode referenced strings (host, sender, etc.) host = _parse_asl_string_ref(data, rec.host_ref) sender = _parse_asl_string_ref(data, rec.sender_ref) facility = _parse_asl_string_ref(data, rec.facility_ref) message = _parse_asl_string_ref(data, rec.message_ref) + # Drop non-printable strings if not _valid_value(host): host = None if not _valid_value(sender): @@ -149,10 +167,12 @@ def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: if not _valid_value(message): message = None + # Skip records without meaningful string content if not any([host, sender, facility, message]): pos += 2 continue + # Yield parsed log entry yield { "ts": datetime.fromtimestamp(rec.time_s, tz=timezone.utc), "level": rec.level, @@ -163,11 +183,21 @@ def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: "message": message, } + # Move to next record pos += rec_len + 2 class ASLPlugin(Plugin): - """Plugin to parse macOS ASL databases.""" + """Plugin to parse macOS Apple System Log (ASL) databases. + + The Apple System Log (ASL) system is a macOS logging mechanism designed with a similar goal + to the traditional Unix syslog API. ASL logs are stored in a proprietary binary format and + are typically located in /private/var/log/asl/. + + References: + - https://asl.readthedocs.io/en/latest/api.html#asl-messages + - https://www.cyberengage.org/post/making-sense-of-macos-logs-part1-a-user-friendly-guide + """ ASL_PATHS = ( "var/log/asl/*.asl", @@ -177,14 +207,17 @@ class ASLPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self._asl_files = set() - self._find_files() + self._asl_files = self._find_files() + + def _find_files(self) -> set: + files = set() - def _find_files(self) -> None: for pattern in self.ASL_PATHS: for path in self.target.fs.path("/").glob(pattern): if path.is_file(): - self._asl_files.add(path) + files.add(path) + + return files def check_compatible(self) -> None: if not self._asl_files: @@ -192,7 +225,29 @@ def check_compatible(self) -> None: @export(record=ASLRecord) def asl(self) -> Iterator[ASLRecord]: - """Return all apple system log messages.""" + """Return all macOS Apple System Log (ASL) messages. + + Yields ASLRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp (UTC). + priority_level (varint): ASL priority level: + 0 = Emergency. + 1 = Alert. + 2 = Critical. + 3 = Error. + 4 = Warning. + 5 = Notice. + 6 = Informational. + 7 = Debug. + pid (varint): Process ID. + asl_host (string): Hostname as stored in the ASL record. + sender (string): Sender process name. + facility (string): Logging facility. + message (string): Log message content. + source (path): Path to the ASL file. + """ for asl_path in self._asl_files: try: with asl_path.open("rb") as fh: @@ -205,7 +260,7 @@ def asl(self) -> Iterator[ASLRecord]: for rec in records: yield ASLRecord( ts=rec["ts"], - severity_level=rec["level"], + priority_level=rec["level"], pid=rec["pid"], asl_host=rec["host"], sender=rec["sender"], diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py index bc18ea7c7a..46d1885e1a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py @@ -31,7 +31,17 @@ class FsckAPFSLogPlugin(Plugin): - """Return information related to fsck_apfs log entries on macOS.""" + """Plugin to parse File System Consistency Check (FSCK) logs on macOS. + + The fsck_apfs.log file is a macOS log file associated with + filesystem checking activity. The fsck utility is used to check and + optionally repair filesystems. + + References: + - https://linux.die.net/man/8/fsck + - https://www.cyberengage.org/post/macos-incident-response-tactics-log-analysis-and-forensic-tools + - https://www.hackthelogs.com/MacLogs.html + """ FSCK_APFS_LOG_PATH = "/var/log/fsck_apfs.log" @@ -41,7 +51,19 @@ def check_compatible(self) -> None: @export(record=FsckAPFSLogRecord) def fsck_apfs_log(self) -> Iterator[FsckAPFSLogRecord]: - """Return all fsck_apfs log messages.""" + """Return all macOS fsck_apfs log messages. + + Yields FsckAPFSLogRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp (UTC), if present in the log line. + disk_path (string): Disk or APFS volume identifier from the log line. + message (string): Log message content. + source (path): Path to the fsck_apfs.log file. + + Lines without a recognizable timestamp will have ts set to None. + """ with self.target.fs.path(self.FSCK_APFS_LOG_PATH).open(mode="rt") as fh: for line in fh: if line != "\n": diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py index 8b02685c6a..2254ce80d1 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py @@ -33,10 +33,14 @@ class InstallLogPlugin(Plugin): - """Return information related software installations and updates on macOS. + """Plugin to parse install logs on macOS. + + Contains information on the software installation history. References: - https://sansorg.egnyte.com/dl/m9ftGF7heI + - https://www.cyberengage.org/post/macos-incident-response-tactics-log-analysis-and-forensic-tools + - https://www.hackthelogs.com/MacLogs.html """ INSTALL_LOG_PATH = "/var/log/install.log" @@ -76,18 +80,15 @@ def parse_log(self, current_ts: re.Match[str], current_buf: str) -> Iterator[Ins def install_log(self) -> Iterator[InstallLogRecord]: """Return all macOS install log messages. - Yields InstallLogRecord instances with fields: + Yields InstallLogRecord with the following fields: .. code-block:: text - ts (datetime): Timestamp of the log line. - host (str): Hostname. - component (str): Component name. - message (str): Log message. - source (path): Path to the log file. - - References: - - https://sansorg.egnyte.com/dl/m9ftGF7heI + ts (datetime): Timestamp (UTC). + host (string): Hostname parsed from the log line. + component (string): Component responsible for the log entry. + message (string): Log message content. + source (path): Path to the install.log file. """ current_ts: re.Match[str] | None = None current_buf = "" diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py index ac5284822a..144aea1d7b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -27,7 +27,14 @@ class SystemLogPlugin(Plugin): - """Return system logs on macOS.""" + """Plugin to parse system logs on macOS. + + Contains information on system diagnostics and general messages. + + References: + - https://www.cyberengage.org/post/macos-incident-response-tactics-log-analysis-and-forensic-tools + - https://www.hackthelogs.com/MacLogs.html + """ SYSTEM_LOG_GLOB = "/var/log/system.log*" @@ -39,12 +46,23 @@ def check_compatible(self) -> None: if not self.log_files: raise UnsupportedPluginError("No system log files found.") - def _resolve_files(self) -> None: + def _resolve_files(self) -> set: return set(self.target.fs.glob(self.SYSTEM_LOG_GLOB)) @export(record=SystemLogRecord) def system_log(self) -> Iterator[SystemLogRecord]: - """Return all macOS system log messages.""" + """Return all macOS system log messages. + + Yields SystemLogRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp (UTC) derived from the log line. + host (string): Hostname parsed from the log entry. + component (string): Component or process associated with the log entry. + message (string): Log message content. + source (path): Path to the system log file. + """ for file in self.log_files: filepath = self.target.fs.path(file) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py index f1ba8cd0bf..aefb6137b9 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/wifi_log.py @@ -44,19 +44,20 @@ class WifiLogPlugin(Plugin): - """macOS WiFi logs plugin.""" + """Plugin to parse WiFi logs on macOS. + + Contains information on WiFi connections and known hotspots + + References: + - https://www.cyberengage.org/post/macos-incident-response-tactics-log-analysis-and-forensic-tools + - https://www.hackthelogs.com/MacLogs.html + """ PATH = "/var/log/wifi.log" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -64,7 +65,17 @@ def check_compatible(self) -> None: @export(record=WifiLogRecord) def wifi_log(self) -> Iterator[WifiLogRecord]: - """Return all macOS WiFi log messages.""" + """Return all macOS Wi-Fi log messages. + + Yields WifiLogRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp (UTC) derived from the log line. + host (string): Hostname parsed from the log entry. + message (string): Log message content. + source (path): Path to the wifi.log file. + """ current_buf = "" for ts, line in year_rollover_helper( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py index 26d2b5eb1b..72bc1b1e5c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py @@ -185,7 +185,14 @@ class PeriodicPlugin(Plugin): - """macOS periodic plugin.""" + """macOS periodic plugin. + + Parses information on daily, weekly and monthly system maintenance jobs. + No longer in use since macOS Sequoia. + + References: + - https://man.freebsd.org/cgi/man.cgi?periodic.conf + """ PERIODIC_SCRIPTS_PATHS = ( "/etc/daily.local/*", @@ -227,7 +234,7 @@ def _find_files(self) -> None: @export(record=PeriodicScriptsRecord) def periodic_scripts(self) -> Iterator[PeriodicScriptsRecord]: - """Yield macOS periodic script paths.""" + """Return macOS periodic script paths.""" for file in self.periodic_scripts_files: yield PeriodicScriptsRecord( source=file, @@ -235,7 +242,152 @@ def periodic_scripts(self) -> Iterator[PeriodicScriptsRecord]: @export(record=PeriodicConfRecords) def periodic_conf(self) -> Iterator[PeriodicConfRecords]: - """Yield macOS periodic configuration information.""" + """Return macOS periodic configuration information. + + Yields the following record types extracted from the + periodic.conf files: + + .. code-block:: text + + PeriodicConfRecord: + local_periodic (string): List of directories to search for periodic scripts + when a non-absolute argument is passed to periodic(8). + dir_output (string): Specifies how script output is handled; an absolute path + writes to a file, otherwise treated as email recipients. + dir_show_success (boolean): Controls masking of output for scripts exiting with code 0. + dir_show_info (boolean): Controls masking of output for scripts exiting with code 1. + dir_show_badconfig (boolean): Controls masking of output for scripts exiting with code 2. + anticongestion_sleeptime (varint): Maximum number of seconds to randomly sleep to reduce load bursts. + source (path): Path to the periodic.conf file. + + PeriodicConfDailyRecord: + daily_clean_disks_enable (boolean): Enables removal of files matching configured patterns. + daily_clean_disks_files (string): List of filename patterns to match (wildcards allowed). + daily_clean_disks_days (varint): File age in days required before deletion. + daily_clean_disks_verbose (boolean): Reports removed files in output. + daily_clean_tmps_enable (boolean): Enables cleanup of temporary directories. + daily_clean_tmps_dirs (string): Directories to clean when enabled. + daily_clean_tmps_days (varint): File age threshold before deletion. + daily_clean_tmps_ignore (string): File patterns excluded from deletion. + daily_clean_tmps_verbose (boolean): Reports removed temporary files. + daily_clean_preserve_enable (boolean): Enables cleanup of /var/preserve. + daily_clean_preserve_days (varint): File age threshold before deletion. + daily_clean_preserve_verbose (boolean): Reports removed files. + daily_clean_msgs_enable (boolean): Enables purging of old system messages. + daily_clean_msgs_days (varint): Age threshold for message deletion. + daily_clean_rwho_enable (boolean): Enables purging of files in /var/who. + daily_clean_rwho_days (varint): File age threshold before deletion. + daily_clean_rwho_verbose (boolean): Reports removed files. + daily_clean_hoststat_enable (boolean): Runs sendmail host status cleanup. + daily_backup_efi_enable (boolean): Enables backup of EFI System Partition. + daily_backup_gmirror_enable (boolean): Enables backup of gmirror information. + daily_backup_gmirror_verbose (boolean): Reports differences between backups. + daily_backup_gpart_enable (boolean): Enables backup of partition tables and boot data. + daily_backup_gpart_verbose (boolean): Reports differences in partition backups. + daily_backup_passwd_enable (boolean): Enables backup and verification of passwd/group files. + daily_backup_aliases_enable (boolean): Enables backup and reporting of mail aliases. + daily_backup_zfs_enable (boolean): Enables backup of zfs-list and zpool-list output. + daily_backup_zfs_list_flags (string): Arguments passed to zfs-list(8). + daily_backup_zpool_list_flags (string): Arguments passed to zpool-list(8). + daily_backup_zfs_props_enable (boolean): Enables backup of zfs-get and zpool-get output. + daily_backup_zfs_get_flags (string): Arguments passed to zfs-get(8). + daily_backup_zpool_get_flags (string): Arguments passed to zpool-get(8). + daily_backup_zfs_verbose (boolean): Reports differences between ZFS backups. + daily_calendar_enable (boolean): Runs calendar(1) daily. + daily_accounting_enable (boolean): Enables rotation of process accounting files. + daily_accounting_compress (boolean): Compresses accounting files with gzip. + daily_accounting_save (varint): Number of accounting files retained. + daily_accounting_flags (string): Arguments passed to sa(8) utility. + daily_status_disks_enable (boolean): Enables disk status reporting (df and dump). + daily_status_disks_df_flags (string): Arguments passed to df(1). + daily_status_zfs_enable (boolean): Enables zpool status reporting. + daily_status_zfs_zpool_list_enable (boolean): Enables zpool list output. + daily_status_gmirror_enable (boolean): Enables gmirror status reporting. + daily_status_graid3_enable (boolean): Enables graid3 status reporting. + daily_status_gstripe_enable (boolean): Enables gstripe status reporting. + daily_status_gconcat_enable (boolean): Enables gconcat status reporting. + daily_status_mfi_enable (boolean): Enables mfi device status reporting. + daily_status_network_enable (boolean): Enables network interface reporting. + daily_status_network_netstat_flags (string): Arguments passed to netstat(1). + daily_status_network_usedns (boolean): Enables DNS lookups in netstat output. + daily_status_uptime_enable (boolean): Runs uptime(1) or ruptime(1). + daily_status_mailq_enable (boolean): Enables mail queue reporting. + daily_status_mailq_shorten (boolean): Produces abbreviated mail queue output. + daily_status_include_submit_mailq (boolean): Includes submit queue in output. + daily_status_security_enable (boolean): Enables execution of periodic security scripts. + daily_status_security_inline (boolean): Outputs security results inline. + daily_status_security_output (string): Destination for security output when not inline. + daily_status_mail_rejects_enable (boolean): Summarises rejected mail entries. + daily_status_mail_rejects_logs (varint): Number of maillog files inspected. + daily_status_ntpd_enable (boolean): Enables NTP status check. + daily_status_world_kernel (boolean): Checks kernel and userland consistency. + daily_queuerun_enable (boolean): Runs mail queue at least once daily. + daily_submit_queuerun (boolean): Runs submit mail queue when enabled. + daily_scrub_zfs_enable (boolean): Enables periodic ZFS scrub. + daily_scrub_zfs_pools (string): ZFS pools to scrub (defaults to all). + daily_scrub_zfs_default_threshold (varint): Default number of days between scrubs. + daily_trim_zfs_enable (boolean): Enables ZFS trim operation. + daily_trim_zfs_pools (string): ZFS pools to trim (defaults to all). + daily_local (string): Additional scripts executed after standard daily scripts. + daily_diff_flags (string): Arguments passed to diff(1) for comparisons. + source (path): Path to the periodic.conf file. + + PeriodicConfWeeklyRecord: + weekly_locate_enable (boolean): Runs locate.updatedb to rebuild locate database. + weekly_whatis_enable (boolean): Regenerates whatis database for apropos(1). + weekly_noid_enable (boolean): Searches for files with invalid ownership. + weekly_noid_dirs (string): Directories to scan for orphaned files. + weekly_status_security_enable (boolean): Enables weekly security checks. + weekly_status_security_inline (boolean): Outputs security results inline. + weekly_status_security_output (string): Destination for security output. + weekly_status_pkg_enable (boolean): Lists outdated installed packages. + pkg_version (string): Program used to determine outdated packages. + pkg_version_index (string): INDEX file used for package version comparison. + weekly_local (string): Additional scripts executed after standard weekly scripts. + source (path): Path to the periodic.conf file. + + PeriodicConfMonthlyRecord: + monthly_accounting_enable (boolean): Enables login accounting using ac(8). + monthly_status_security_enable (boolean): Enables monthly security checks. + monthly_status_security_inline (boolean): Outputs security results inline. + monthly_status_security_output (string): Destination for security output. + monthly_local (string): Additional scripts executed after standard monthly scripts. + source (path): Path to the periodic.conf file. + + PeriodicConfSecurityRecord: + security_status_diff_flags (string): Arguments passed to diff(1) for comparisons. + security_status_chksetuid_enable (boolean): Compares setuid file modes and timestamps. + security_status_chksetuid_period (string): Frequency of execution. + security_status_chkportsum_enable (boolean): Verifies installed package checksums. + security_status_chkportsum_period (string): Frequency of execution. + security_status_neggrpperm_enable (boolean): Checks for group permission inconsistencies. + security_status_neggrpperm_period (string): Frequency of execution. + security_status_chkmounts_enable (boolean): Compares mounted filesystem changes. + security_status_chkmounts_period (string): Frequency of execution. + security_status_noamd (boolean): Ignores amd mounts when comparing filesystems. + security_status_chkuid0_enable (boolean): Checks for accounts with UID 0. + security_status_chkuid0_period (string): Frequency of execution. + security_status_passwdless_enable (boolean): Checks for accounts without passwords. + security_status_passwdless_period (string): Frequency of execution. + security_status_logincheck_enable (boolean): Checks ownership of /etc/login.conf. + security_status_logincheck_period (string): Frequency of execution. + security_status_ipfwdenied_enable (boolean): Reports packets denied by ipfw. + security_status_ipfwdenied_period (string): Frequency of execution. + security_status_ipfdenied_enable (boolean): Reports packets denied by ipf. + security_status_ipfdenied_period (string): Frequency of execution. + security_status_pfdenied_enable (boolean): Reports packets denied by pf. + security_status_pfdenied_additionalanchors (string): Additional anchors to include. + security_status_pfdenied_period (string): Frequency of execution. + security_status_ipfwlimit_enable (boolean): Displays ipfw rules at limit. + security_status_ipfwlimit_period (string): Frequency of execution. + security_status_kernelmsg_enable (boolean): Shows new kernel messages (dmesg). + security_status_kernelmsg_period (string): Frequency of execution. + security_status_loginfail_enable (boolean): Reports failed login attempts. + security_status_loginfail_period (string): Frequency of execution. + security_status_tcpwrap_enable (boolean): Reports tcpwrapper denied connections. + security_status_tcpwrap_period (string): Frequency of execution. + source (path): Path to the periodic.conf file. + """ for file in self.periodic_conf_files: for record in PeriodicConfRecords: record_keys = set(record.fields.keys()) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 16399ba7d4..408ca1284a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import find_bundle_files from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records @@ -13,9 +13,53 @@ from dissect.target.target import Target +ResourcesInfoStringsRecord = TargetRecordDescriptor( + "macos/resources_info_strings", + [ + ("string", "cf_bundle_name"), + ("string", "cf_bundle_display_name"), + ("string", "cf_bundle_identifier"), + ("string", "cf_bundle_version"), + ("string", "cf_bundle_package_type"), + ("string", "cf_bundle_signature"), + ("string", "cf_bundle_executable"), + ("string[]", "cf_bundle_document_types"), + ("string", "cf_bundle_short_version_string"), + ("string", "ls_minimum_system_version"), + ("string", "ns_human_readable_copyright"), + ("string", "ns_main_nib_file"), + ("string", "ns_principal_class"), + ("path", "source"), + ], +) + +ResourcesInfoStringsRecords = (ResourcesInfoStringsRecord,) + +FIELD_MAPPINGS = { + "CFBundleName": "cf_bundle_name", + "CFBundleDisplayName": "cf_bundle_display_name", + "CFBundleIdentifier": "cf_bundle_identifier", + "CFBundleVersion": "cf_bundle_version", + "CFBundlePackageType": "cf_bundle_package_type", + "CFBundleSignature": "cf_bundle_signature", + "CFBundleExecutable": "cf_bundle_executable", + "CFBundleDocumentTypes": "cf_bundle_document_types", + "CFBundleShortVersionString ": "cf_bundle_short_version_string", + "LSMinimumSystemVersion": "ls_minimum_system_version", + "NSHumanReadableCopyright": "ns_human_readable_copyright", + "NSMainNibFile": "ns_main_nib_file", + "NSPrincipalClass": "ns_principal_class", +} + class ResourcesInfoStringsPlugin(Plugin): - """macOS Resources InfoPlist.strings plist file.""" + """macOS Resources InfoPlist.strings plugin. + + Parser localized bundle metadata for the Info.plist file. + + References: + - https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html + """ def __init__(self, target: Target): super().__init__(target) @@ -25,7 +69,34 @@ def check_compatible(self) -> None: if not self.files: raise UnsupportedPluginError("No Resources InfoPlist.strings files found") - @export(record=DynamicDescriptor(["string"])) - def resources_info_strings(self) -> Iterator[DynamicDescriptor]: - """Yield Resources InfoPlist.strings information.""" - yield from build_plist_records(self, self.files, function_name="macos/resources_info_strings") + @export(record=ResourcesInfoStringsRecords) + def resources_info_strings(self) -> Iterator[ResourcesInfoStringsRecords]: + """Return Resources InfoPlist.strings information. + + Yields ResourcesInfoStringsRecords with the following fields: + + .. code-block:: text + + cf_bundle_name (string): Short name for the bundle. + cf_bundle_display_name (string): Localized version of the application name. + cf_bundle_identifier (string): Identifies the application to the system. + cf_bundle_version (string): Specifies the build version number of the bundle. + cf_bundle_package_type (string): Type of bundle. + cf_bundle_signature (string): Creator code for the bundle. + cf_bundle_executable (string): Name of the main executable file. + cf_bundle_document_types (string[]): Document types supported by the application. + cf_bundle_short_version_string (string): Release version of the application. + ls_minimum_system_version (string): Minimum version of macOS required + for this application to run. + ns_human_readable_copyright (string): Copyright notice for the application. + ns_main_nib_file (string): The nib file to load when the application is launched + (without the .nib filename extension). + ns_principal_class (string): Entry point for dynamically loaded Objective-C code. + source (path): Path to the com.apple.airport.preferences.plist file. + """ + yield from build_plist_records( + self, + self.files, + ResourcesInfoStringsRecords, + field_mappings=FIELD_MAPPINGS, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py index e511758991..1c469fa95e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -15,7 +15,13 @@ class ResourcesLocalizableStringsPlugin(Plugin): - """macOS Resources Localizable.strings plist file.""" + """macOS Resources Localizable.strings plist file. + + Parses resource files used to store text that can be translated into different languages. + + References: + - https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html + """ def __init__(self, target: Target): super().__init__(target) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py index 9618f2df09..b86836d5c6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -26,26 +26,42 @@ class ShadowPlugin(Plugin): - """Unix shadow passwords plugin.""" + """macOS shadow plugin. + + Parses user password hashes plist files. + """ USER_FILE_GLOB = "/var/db/dslocal/nodes/Default/users/*.plist" def __init__(self, target: Target): super().__init__(target) - self.user_files = set() - self._resolve_files() + self.user_files = self._resolve_files() def check_compatible(self) -> None: if not self.user_files: raise UnsupportedPluginError("No shadow files found") - def _resolve_files(self) -> None: + def _resolve_files(self) -> set: + user_files = set() for file in self.target.fs.glob(self.USER_FILE_GLOB): - self.user_files.add(file) + user_files.add(file) + return user_files @export(record=ShadowRecord) def passwords(self) -> Iterator[ShadowRecord]: - """Yield shadow records from macOS user plist files.""" + """Return user password hashes from macOS user plist files. + + Yields ShadowRecords with the following fields: + + .. code-block:: text + + name (string): Username associated with the hash. + hash (string): Hex-encoded password hash. + salt (string): Hex-encoded salt used for key derivation. + iterations (varint): Number of iterations used by the hashing algorithm. + algorithm (string): Hashing algorithm identifier. + source (path): Path to the plist file. + """ for path in self.user_files: path = self.target.fs.path(path) user = plistlib.load(path.open()) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py index 23f3b517b0..c45ba98645 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py @@ -35,19 +35,17 @@ class SoftwareUpdatePreferencesPlugin(Plugin): - """macOS software update preferences plugin.""" + """macOS software update preferences plugin. + + References: + - https://eclecticlight.co/2022/11/14/how-does-ventura-update-faster-inside-the-macos-update-process/ + """ PATH = "/Library/Preferences/com.apple.SoftwareUpdate.plist" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -55,7 +53,28 @@ def check_compatible(self) -> None: @export(record=SoftwareUpdatePreferencesRecord) def software_update_preferences(self) -> Iterator[SoftwareUpdatePreferencesRecord]: - """Yield software update preference information.""" + """Return software update preference information. + + Yields SoftwareUpdatePreferencesRecords with the following fields: + + .. code-block:: text + + last_result_code (varint): Result code of the last update attempt. + last_attempt_system_version (string): macOS version targeted in the last update attempt. + last_attempt_build_version (string): Build version targeted in the last update attempt. + automatic_download (boolean): Whether automatic download of updates is enabled. + automatically_install_macos_updates (boolean): Whether macOS updates are installed automatically. + critical_update_install (boolean): Whether critical updates are installed automatically. + config_data_install (boolean): Whether configuration data updates are installed automatically. + recommended_updates (string[]): List of recommended updates. + splat_enabled (boolean): Whether the cryptex-based update subsystem (SPLAT) is enabled. + post_logout_notification (boolean): Whether to notify user after logout following update. + last_recommended_major_os_bundle_id (string): Bundle identifier of last recommended major OS upgrade. + primary_languages (string[]): Preferred system languages. + last_successful_date (datetime): Timestamp of last successful update. + last_full_successful_date (datetime): Timestamp of last full successful update. + source (path): Path to the com.apple.SoftwareUpdate.plist file. + """ plist = plistlib.load(self.file.open()) yield SoftwareUpdatePreferencesRecord( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py index ac39e7d628..a053c089f4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py @@ -20,13 +20,14 @@ class SystemPreferencesPlugin(Plugin): def __init__(self, target: Target): super().__init__(target) - self.files = set() - self._find_files() + self.files = self._find_files() - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for pattern in self.PATHS: for path in self.target.fs.glob(pattern): - self.files.add(path) + files.add(path) + return files def check_compatible(self) -> None: if not (self.files): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py index c9a5c00801..f07db84cad 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py @@ -23,11 +23,11 @@ ("varint", "auth_value"), ("varint", "auth_reason"), ("varint", "auth_version"), - ("string", "csreq"), + ("bytes", "csreq"), ("string", "policy_id"), - ("string", "indirect_object_identifier_type"), ("string", "indirect_object_identifier"), - ("string", "indirect_object_code_identity"), + ("string", "indirect_object_identifier_type"), + ("bytes", "indirect_object_code_identity"), ("varint", "flags"), ("datetime", "last_modified"), ("varint", "pid"), @@ -55,31 +55,92 @@ class TCCPlugin(Plugin): - """macOS transparency, consent, control (tcc) framework plugin.""" + """macOS transparency, consent, control (tcc) framework plugin. + + TCC is a mechanism in macOS to limit and control application access to certain features. This can include + things such as location services, contacts, photos, microphone, camera, accessibility, full disk access, etc. + + References: + - https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive + """ SYSTEM_PATH = "/Library/Application Support/com.apple.TCC/TCC.db" USER_PATH = ("Library/Application Support/com.apple.TCC/TCC.db",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No TCC.db files found") - def _find_files(self) -> None: - self.files.add(self.target.fs.path(self.SYSTEM_PATH)) + def _find_files(self) -> set: + files = set() + files.add(self.target.fs.path(self.SYSTEM_PATH)) for _, path in _build_userdirs(self, self.USER_PATH): - self.files.add(path) + files.add(path) + return files @export(record=TCCRecords) def tcc( self, ) -> Iterator[TCCRecords]: - """Yield transparency, consent, control (tcc) framework information.""" + """Return transparency, consent, control (tcc) framework information. + + Yields the following record types: + + .. code-block:: text + + AccessRecord: + table (string): Source table name (access). + service (string): What service access is being restricted to. + client (string): Bundle Identifier or absolute path to the program that wants to use the service. + client_type (varint): Whether client is a Bundle Identifier(0) or an absolute path(1) + auth_value (varint): Authorization decision: + 0 = denied. + 1 = unknown. + 2 = allowed + 3 = limited. + Observed auth_value = 5 in macOS Tahoe, but unsure about it's meaning. + auth_reason (varint): A code indicating how this auth_value was set: + 1 = Error. + 2 = User Consent. + 3 = User Set. + 4 = System Set. + 5 = Service Policy. + 6 = MDM Policy. + 7 = Override Policy. + 8 = Missing usage string. + 9 = Prompt Timeout. + 10 = Preflight Unknown. + 11 = Entitled. + 12 = App Type Policy. + auth_version (varint): Always 1 as of macOS Tahoe. + csreq (bytes): Binary code signing requirement blob that the client must + satisfy in order for access to be granted. + policy_id (string): Might be related to MDM(Mobile Device Management) policies, which can + be used by organizations to allow TCC access for some applications at a global level. + indirect_object_identifier (string): For some services this is what the client + is asking to interact with. This will be set to UNUSED if not applicable. + indirect_object_identifier_type (string): Whether indirect_object_identifier is a + Bundle Identifier(0) or an absolute path(1) + indirect_object_code_identity (bytes): Same as csreq, but for the + indirect_object_identifier instead of client. + flags (varint): Always 0 as of macOS Tahoe. + last_modified (datetime): The last time this entry was modified. + pid (varint): Process ID. + pid_version (string): Version of the process. + boot_uuid (string): System boot session UUID. + last_reminded (datetime): Last time user was reminded. + source (path): Path to the TCC.db database file. + + KeyValue: + table (string): Name of the source table. + key (string): Key name. + value (string): Value associated with the key. + source (path): Path to the TCC.db database file. + """ yield from build_sqlite_records(self, self.files, TCCRecords) - # Still missing policies, active_policy, access_overrides, expired tables + # TODO: Add policies, active_policy, access_overrides, expired tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index a4a58e4f63..a65907c874 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -23,7 +23,7 @@ ("varint", "z_opt"), ("varint", "z_was_deleted"), ("varint", "z_needs_save_to_cloud"), - ("string", "z_timestamp"), + ("datetime", "z_timestamp"), ("string", "z_phrase"), ("string", "z_shortcut"), ("string", "z_unique_name"), @@ -73,7 +73,7 @@ ("varint", "ns_persistence_maximum_framework_version"), ("string[]", "ns_store_model_version_identifiers"), ("string", "ns_store_type"), - ("string", "ns_auto_vacuum_level"), + ("varint", "ns_auto_vacuum_level"), ("string", "ns_store_model_version_hashes_digest"), ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), @@ -85,8 +85,8 @@ NSStoreModelVersionHashesRecord = TargetRecordDescriptor( "macos/text_replacements/ns_store_model_version_hashes", [ - ("string", "tr_cloud_kit_sync_state"), - ("string", "text_replacement_entry"), + ("bytes", "tr_cloud_kit_sync_state"), + ("bytes", "text_replacement_entry"), ("string", "plist_path"), ("path", "source"), ], @@ -96,7 +96,6 @@ "macos/text_replacements/z_model_cache", [ ("string", "table"), - ("bytes", "z_content"), ("path", "source"), ], ) @@ -129,7 +128,6 @@ "Z_MAX": "z_max", "Z_VERSION": "z_version", "Z_UUID": "z_uuid", - "Z_CONTENT": "z_content", "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", "NSStoreType": "ns_store_type", @@ -142,29 +140,110 @@ "TextReplacementEntry": "text_replacement_entry", } +CONVERT_TIMESTAMPS = { + "z_timestamp": "2001", +} + class TextReplacementsPlugin(Plugin): - """macOS text replacements plugin.""" + """macOS text replacements plugin. + + References: + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ PATH = ("Library/KeyboardServices/TextReplacements.db",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No TextReplacements.db files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for _, path in _build_userdirs(self, self.PATH): - self.files.add(path) + files.add(path) + return files @export(record=TextReplacementsRecords) def text_replacements( self, ) -> Iterator[TextReplacementsRecords]: - """Yield text replacements information.""" - yield from build_sqlite_records(self, self.files, TextReplacementsRecords, field_mappings=FIELD_MAPPINGS) + """Return text replacements information. + + Yields the following record types extracted from the + TextReplacements.db database: + + .. code-block:: text + + ZTextReplacementEntryRecord: + table (string): Name of the source table (ZTEXTREPLACEMENTENTRY). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_was_deleted (varint): Indicates if the record was deleted. + z_needs_save_to_cloud (varint): Indicates if the record needs to be saved to cloud. + z_timestamp (datetime): Timestamp associated with the record. + z_phrase (string): Full replacement text (what gets inserted). + z_shortcut (string): Shortcut text that triggers the replacement. + z_unique_name (string): Unique identifier for the replacement entry. + z_remote_record_info (string): Remote record info. + source (path): Path to the TextReplacements.db file. + + ZTrCloudKitSyncStateRecord: + table (string): Name of the source table (ZTRCLOUDKITSYNCSTATE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_did_pull_once (varint): Indicates if initial CloudKit sync has occurred. + z_fetch_change_token (string): CloudKit change token for sync state tracking. + source (path): Path to the TextReplacements.db file. + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): The ID of the table. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the TextReplacements.db file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the TextReplacements.db file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the hashes. + source (path): Path to the TextReplacements.db file. + + NSStoreModelVersionHashesRecord: + tr_cloud_kit_sync_state (bytes): Hash for ZTRCLOUDKITSYNCSTATE entity. + text_replacement_entry (bytes): Hash for ZTEXTREPLACEMENTENTRY entity. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the TextReplacements.db file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the TextReplacements.db file. + """ + yield from build_sqlite_records( + self, + self.files, + TextReplacementsRecords, + field_mappings=FIELD_MAPPINGS, + convert_timestamps=CONVERT_TIMESTAMPS, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py index 73fe008c6a..86742f4dad 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -22,19 +22,16 @@ class TimeMachinePlugin(Plugin): - """macOS time machine plugin.""" + """macOS Time Machine plugin. + + Parses Time Machine preferences. Time Machine is macOS's backup system. + """ PATH = "/Library/Preferences/com.apple.TimeMachine.plist" def __init__(self, target: Target): super().__init__(target) - self.file = None - self._resolve_file() - - def _resolve_file(self) -> None: - path = self.target.fs.path(self.PATH) - if path.exists(): - self.file = path + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None def check_compatible(self) -> None: if not self.file: @@ -42,7 +39,15 @@ def check_compatible(self) -> None: @export(record=TimeMachineRecord) def time_machine(self) -> Iterator[TimeMachineRecord]: - """Yield time machine information.""" + """Return macOS Time Machine preferences. + + Yields TimeMachineRecord with the following fields: + + .. code-block:: text + + preferences_version (varint): Version of the Time Machine preferences. + source (path): Path to the com.apple.TimeMachine.plist file. + """ plist = plistlib.load(self.file.open()) yield TimeMachineRecord( @@ -50,3 +55,7 @@ def time_machine(self) -> Iterator[TimeMachineRecord]: source=self.file, _target=self.target, ) + + +# I was only able to find a preferences_version field in the plist file on a fresh Tahoe system. +# It could be that more fields will show up in the plist file depending on user activity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 5346ea0a29..3775ab7998 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -53,8 +53,8 @@ ("varint", "z_warming_up"), ("varint", "z_account_type"), ("varint", "z_parent_account"), - ("float", "z_date"), - ("float", "z_last_credential_renewal_rejection_date"), + ("datetime", "z_date"), + ("datetime", "z_last_credential_renewal_rejection_date"), ("string", "z_account_description"), ("string", "z_authentication_type"), ("string", "z_credential_type"), @@ -131,19 +131,6 @@ ], ) -ZDataClassRecord = TargetRecordDescriptor( - "macos/user_accounts/z_dataclass", - [ - ("string", "table"), - ("varint", "z_pk"), - ("varint", "z_ent"), - ("varint", "z_opt"), - ("varint", "z_enum_value"), - ("string", "z_name"), - ("path", "source"), - ], -) - ZPrimaryKeyRecord = TargetRecordDescriptor( "macos/user_accounts/z_primary_key", [ @@ -169,8 +156,8 @@ ZPlistRecord = TargetRecordDescriptor( "macos/user_accounts/z_plist", [ - ("string", "ac_account_type_version"), - ("string", "ns_auto_vacuum_level"), + ("varint", "ac_account_type_version"), + ("varint", "ns_auto_vacuum_level"), ("varint", "ns_persistence_framework_version"), ("varint", "ns_persistence_maximum_framework_version"), ("string", "ns_store_model_version_checksum_key"), @@ -185,13 +172,13 @@ NSStoreModelVersionHashesRecord = TargetRecordDescriptor( "macos/user_accounts/ns_store_model_version_hashes", [ - ("string", "access_options_key"), - ("string", "account"), - ("string", "account_property"), - ("string", "account_type"), - ("string", "authorization"), - ("string", "credential_item"), - ("string", "dataclass"), + ("bytes", "access_options_key"), + ("bytes", "account_hash"), + ("bytes", "account_property"), + ("bytes", "account_type"), + ("bytes", "authorization"), + ("bytes", "credential_item"), + ("bytes", "dataclass"), ("string", "plist_path"), ("path", "source"), ], @@ -201,7 +188,6 @@ "macos/user_accounts/z_model_cache", [ ("string", "table"), - ("bytes", "z_content"), ("path", "source"), ], ) @@ -215,7 +201,6 @@ ZAccountTypeRecord, ZSupportedDataClassesRecord, ZSyncableDataClassesRecord, - ZDataClassRecord, ZPrimaryKeyRecord, ZMetadataRecord, ZPlistRecord, @@ -277,74 +262,196 @@ "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", "NSStoreType": "ns_store_type", "AccessOptionsKey": "access_options_key", - "Account": "account", + "Account": "account_hash", "AccountProperty": "account_property", "AccountType": "account_type", "Authorization": "authorization", "CredentialItem": "credential_item", "Dataclass": "dataclass", - "Z_CONTENT": "z_content", } class UserAccountsPlugin(Plugin): - """macOS user accounts plugin.""" + """macOS user accounts plugin. + + Parses macOS user account SQLite database files. + + References: + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ USER_PATH = ("Library/Accounts/Accounts*.sqlite",) def __init__(self, target: Target): super().__init__(target) - - self.files = set() - self._find_files() + self.files = self._find_files() def check_compatible(self) -> None: if not (self.files): raise UnsupportedPluginError("No user account database files found") - def _find_files(self) -> None: + def _find_files(self) -> set: + files = set() for _, path in _build_userdirs(self, self.USER_PATH): - self.files.add(path) - - @export( - record=[ - ZAccessOptionsKeyRecord, - ZOwningAccountTypesRecord, - ZAccountRecord, - ZEnabledDataClassesRecord, - ZAccountPropertyRecord, - ZAccountTypeRecord, - ZSupportedDataClassesRecord, - ZSyncableDataClassesRecord, - ZDataClassRecord, - ZPrimaryKeyRecord, - ZMetadataRecord, - ZPlistRecord, - NSStoreModelVersionHashesRecord, - ZModelCacheRecord, - ] - ) + files.add(path) + return files + + @export(record=UserAccountRecords) def user_accounts( self, - ) -> Iterator[ - [ - ZAccessOptionsKeyRecord, - ZOwningAccountTypesRecord, - ZAccountRecord, - ZEnabledDataClassesRecord, - ZAccountPropertyRecord, - ZAccountTypeRecord, - ZSupportedDataClassesRecord, - ZSyncableDataClassesRecord, - ZDataClassRecord, - ZPrimaryKeyRecord, - ZMetadataRecord, - ZPlistRecord, - NSStoreModelVersionHashesRecord, - ZModelCacheRecord, - ] - ]: - """Yield user accounts information.""" + ) -> Iterator[UserAccountRecords]: + """Return user accounts information. + + Yields the following record types extracted from the + Accounts*.sqlite databases: + + .. code-block:: text + + ZAccessOptionsKeyRecord: + table (string): Name of the source table (ZACCESSOPTIONSKEY). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_enum_value (varint): Enumeration value. + z_name (string): The name of the entity in the data model. + source (path): Path to the Accounts*.sqlite database file. + + ZOwningAccountTypesRecord: + table (string): Name of the source table (Z_1OWNINGACCOUNTTYPES). + z_1_access_keys (varint): Reference to z_pk in ZACCESSOPTIONSKEY. + z_4_owning_account_types (varint): Reference to z_pk in ZACCOUNTTYPE. + source (path): Path to the Accounts*.sqlite database file. + + ZAccountRecord: + table (string): Name of the source table (ZACCOUNT). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_active (varint): Indicates whether the account is active: + 0 = False. + 1 = True. + z_authenticated (varint): Indicates whether the account is authenticated: + 0 = False. + 1 = True. + z_supports_authentication (varint): Indicates if authentication is supported: + 0 = False. + 1 = True. + z_visible (varint): Indicates whether account is visible: + 0 = False. + 1 = True. + z_warming_up (varint): Indicates account initialization state: + 0 = False. + 1 = True. + z_account_type (varint): Reference to z_pk in ZACCOUNTTYPE. + z_parent_account (varint): Reference to z_pk of parent account. + z_date (datetime): Timestamp. + z_last_credential_renewal_rejection_date (datetime): Timestamp of last credential renewal rejection. + z_account_description (string): Account description. + z_authentication_type (string): Authentication type. + z_credential_type (string): Credential type. + z_identifier (string): Account identifier. + z_modification_id (string): Modification identifier. + z_owning_bundle_id (string): Bundle ID of owning service/app. + z_username (string): Username for the account. + z_dataclass_properties (bytes): Dataclass properties. + source (path): Path to the Accounts*.sqlite database file. + + ZEnabledDataClassesRecord: + table (string): Name of the source table (Z_2ENABLEDDATACLASSES). + z_2_enabled_accounts (varint): Reference to z_pk in ZACCOUNT. + z_7_enabled_dataclasses (varint): Reference to z_pk in ZDATACLASS. + source (path): Path to the Accounts*.sqlite database file. + + ZAccountPropertyRecord: + table (string): Name of the source table (ZACCOUNTPROPERTY). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_owner (varint): Reference to z_pk of owning ZACCOUNT. + z_key (string): Property key. + z_value (string): Property value. + source (path): Path to the Accounts*.sqlite database file. + + ZAccountTypeRecord: + table (string): Name of the source table (ZACCOUNTTYPE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_obsolete (varint): Indicates whether account type is obsolute: + 0 = False. + 1 = True. + z_supports_authentication (varint): Indicates whether authentication is supported: + 0 = False. + 1 = True. + z_supports_multiple_accounts (varint): Indicates whether multiple accounts are supported: + 0 = False. + 1 = True. + z_visibility (varint): Indicates visibility of the account type: + 0 = False. + 1 = True. + z_account_type_description (string): Description of account type. + z_credential_protection_policy (string): Credential protection policy. + z_credential_type (string): Credential type. + z_identifier (string): Account identifier. + z_owning_bundle_id (string): Owning bundle ID. + source (path): Path to the Accounts*.sqlite database file. + + ZSupportedDataClassesRecord: + table (string): Name of the source table (Z_4SUPPORTEDDATACLASSES). + z_4_supported_types (varint): Reference to z_pk in ZACCOUNTTYPE. + z_7_supported_dataclasses (varint): Reference to z_pk in ZDATACLASS. + source (path): Path to the Accounts*.sqlite database file. + + ZSyncableDataClassesRecord: + table (string): Name of the source table (Z_4SYNCABLEDATACLASSES). + z_4_syncable_types (varint): Reference to z_pk in ZACCOUNTTYPE. + z_7_syncable_dataclasses (varint): Reference to z_pk in ZDATACLASS. + source (path): Path to the Accounts*.sqlite database file. + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): The ID of the table. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the Accounts*.sqlite database file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the Accounts*.sqlite database file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ac_account_type_version (varint): AC account type version. + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the hashes. + source (path): Path to the Accounts*.sqlite database file. + + NSStoreModelVersionHashesRecord: + access_options_key (bytes): Hash for ZACCESSOPTIONSKEY entity. + account (bytes): Hash for ZACCOUNT entity. + account_property (bytes): Hash for ZACCOUNTPROPERTY entity. + account_type (bytes): Hash for ZACCOUNTTYPE entity. + authorization (bytes): Hash for ZAUTHORIZATION entity. + credential_item (bytes): Hash for ZCREDENTIALITEM entity. + dataclass (bytes): Hash for ZDATACLASS entity. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the Accounts*.sqlite database file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the Accounts*.sqlite database file. + """ yield from build_sqlite_records(self, self.files, UserAccountRecords, field_mappings=FIELD_MAPPINGS) - # Still missing Z_2PROVISIONEDDATACLASSES, ZAUTHORIZATION, ZCREDENTIALITEM tables + # TODO: Add Z_2PROVISIONEDDATACLASSES, ZAUTHORIZATION, ZCREDENTIALITEM tables diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/AudioDMAController_T8140 b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/AudioDMAController_T8140 new file mode 100644 index 0000000000..079543fb6a --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/AudioDMAController_T8140 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46089febb31ff8c9ace764838bd508b44a1e789395c807f9e8763d41d5e174fd +size 1029 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/EndpointSecurity b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/EndpointSecurity new file mode 100644 index 0000000000..0e93caedfd --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/EndpointSecurity @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af891ecfaa1a58cd1d17dc5c7b2b61bd862f1dc0c0b4833ff5ed9d7d1e560ca8 +size 2672 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/MobileDeviceUpdater b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/MobileDeviceUpdater new file mode 100644 index 0000000000..733732518c --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/MobileDeviceUpdater @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69137193ec18339b83e71a9816621fd25b4001144dc229eaa8fa0a0a81dfa647 +size 48542 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist new file mode 100644 index 0000000000..957816632a --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/login_window/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9603e183f310702e78a6c4025abe5dab3a5abf214bcd1227dc73cd800d2f169d +size 388 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/OSvKernDSPLib.strings similarity index 100% rename from tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/InfoPlist.strings rename to tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/OSvKernDSPLib.strings diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/WiFiAgent.strings b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/WiFiAgent.strings new file mode 100644 index 0000000000..0b51c765c5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/WiFiAgent.strings @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:265052bc23e88dab2558dfb7c101265fa215fb9df854c879bdd6b5c6c61c989f +size 101 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py index f055d6120c..ce3dd78759 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py @@ -44,7 +44,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert len(results) == 11 assert results[0].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[0].severity_level == 5 + assert results[0].priority_level == 5 assert results[0].pid == 121 assert results[0].asl_host == "localhost" assert results[0].sender == "syslogd" @@ -57,7 +57,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert results[0].source == "/var/log/asl/2026.05.06.G80.asl" assert results[1].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[1].severity_level == 5 + assert results[1].priority_level == 5 assert results[1].pid == 121 assert results[1].asl_host == "localhost" assert results[1].sender == "syslogd" @@ -70,7 +70,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert results[1].source == "/var/log/asl/2026.05.06.G80.asl" assert results[-1].ts == datetime(2026, 5, 6, 11, 50, 20, tzinfo=tz) - assert results[-1].severity_level == 5 + assert results[-1].priority_level == 5 assert results[-1].pid == 122 assert results[-1].asl_host == "localhost" assert results[-1].sender == "syslogd" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py index 06ad350a0e..e78e0ce4e6 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py @@ -36,6 +36,6 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: Virtua assert results[0].counter == 2 assert results[0].device_uuid == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" - assert results[0].version == "7200" - assert results[0].preferred_order == "[]" + assert results[0].version_number == 7200 + assert results[0].preferred_order == [] assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py index 06cbb7bce8..76a7d324dd 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py @@ -21,6 +21,8 @@ ], ) def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + # Test data was created by running: echo 'say "hello"' | at 09:00 tomorrow + # Command was executed on: 2026-05-11 (PDT time). tz = timezone.utc data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/usr/lib/cron/jobs/{test_file}", data_file) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py index 2cacb9c70e..f254a11859 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py @@ -53,8 +53,8 @@ def test_authorization_rules(test_file: str, target_unix: Target, fs_unix: Virtu assert results[175].rules_flags == 0 assert results[175].rules_tries is None assert results[175].rules_version == 0 - assert results[175].rules_created == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[175].rules_modified == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) assert results[175].rules_hash is None assert results[175].rules_identifier is None assert results[175].rules_requirement is None @@ -81,8 +81,8 @@ def test_authorization_rules(test_file: str, target_unix: Target, fs_unix: Virtu assert results[177].rules_flags == 1 assert results[177].rules_tries == "10000" assert results[177].rules_version == 0 - assert results[177].rules_created == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[177].rules_modified == datetime(1995, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) assert results[177].rules_hash is None assert results[177].rules_identifier is None assert results[177].rules_requirement is None @@ -98,6 +98,99 @@ def test_authorization_rules(test_file: str, target_unix: Target, fs_unix: Virtu assert results[177].rules_delegates_map == [] assert results[177].source == "/var/db/auth.db" + assert sorted(results[-9].tables) == ["rules", "rules_history"] + assert results[-9].rules_id == 199 + assert results[-9].rules_name == "system.preferences.security.remotepair" + assert results[-9].rules_type == 1 + assert results[-9].rules_class == 1 + assert results[-9].rules_group == "admin" + assert results[-9].rules_kofn is None + assert results[-9].rules_timeout == 30 + assert results[-9].rules_flags == 73 + assert results[-9].rules_tries == "10000" + assert results[-9].rules_version == 1 + assert results[-9].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-9].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-9].rules_hash is None + assert results[-9].rules_identifier is None + assert results[-9].rules_requirement is None + assert results[-9].rules_comment == "Used by Bezel Services to gate IR remote pairing." + assert results[-9].rules_delegates_map == [] + assert results[-9].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[-9].rules_history_source == "authd" + assert results[-9].rules_history_operation == 0 + assert results[-9].mechanisms_map_m_id is None + assert results[-9].mechanisms_map_ord is None + assert results[-9].mechanisms_plugin is None + assert results[-9].mechanisms_param is None + assert results[-9].mechanisms_privileged is None + assert results[-9].source == "/var/db/auth.db" + + assert sorted(results[-4].tables) == ["rules", "rules_history"] + assert results[-4].rules_id == 204 + assert results[-4].rules_name == "com.apple.Safari.allow-apple-events-to-run-javascript" + assert results[-4].rules_type == 1 + assert results[-4].rules_class == 1 + assert results[-4].rules_group is None + assert results[-4].rules_kofn is None + assert results[-4].rules_timeout == 2147483647 + assert results[-4].rules_flags == 12 + assert results[-4].rules_tries == "10000" + assert results[-4].rules_version == 0 + assert results[-4].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-4].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-4].rules_hash is None + assert results[-4].rules_identifier is None + assert results[-4].rules_requirement is None + assert ( + results[-4].rules_comment + == "This right is used by Safari to allow Apple Events to run JavaScript on web pages." + ) + assert results[-4].rules_delegates_map == [] + assert results[-4].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[-4].rules_history_source == "authd" + assert results[-4].rules_history_operation == 0 + assert results[-4].mechanisms_map_m_id is None + assert results[-4].mechanisms_map_ord is None + assert results[-4].mechanisms_plugin is None + assert results[-4].mechanisms_param is None + assert results[-4].mechanisms_privileged is None + assert results[-4].source == "/var/db/auth.db" + + assert sorted(results[-3].tables) == ["delegates_map", "rules", "rules_history"] + assert results[-3].rules_id == 205 + assert results[-3].rules_name == "com.apple.wifi" + assert results[-3].rules_type == 1 + assert results[-3].rules_class == 2 + assert results[-3].rules_group is None + assert results[-3].rules_kofn == 1 + assert results[-3].rules_timeout is None + assert results[-3].rules_flags == 0 + assert results[-3].rules_tries is None + assert results[-3].rules_version == 0 + assert results[-3].rules_created == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) + assert results[-3].rules_modified == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) + assert results[-3].rules_hash is None + assert results[-3].rules_identifier == "com.apple.airport.airportd" + assert results[-3].rules_requirement is not None + assert isinstance(results[-3].rules_requirement, (bytes, bytearray)) + assert results[-3].rules_comment == "For restricting WiFi control" + assert results[-3].rules_delegates_map == [ + "{'d_id': 22, 'ord': 0}", + "{'d_id': 26, 'ord': 1}", + "{'d_id': 25, 'ord': 2}", + "{'d_id': 35, 'ord': 3}", + ] + assert results[-3].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 2, tzinfo=tz) + assert results[-3].rules_history_source == "/usr/libexec/airportd" + assert results[-3].rules_history_operation == 0 + assert results[-3].mechanisms_map_m_id is None + assert results[-3].mechanisms_map_ord is None + assert results[-3].mechanisms_plugin is None + assert results[-3].mechanisms_param is None + assert results[-3].mechanisms_privileged is None + assert results[-3].source == "/var/db/auth.db" + assert results[-1].table == "config" assert results[-1].key == "data_ts" assert results[-1].value == "795677157.0" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py index fc1df28e93..c3a8064d97 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -23,11 +23,17 @@ "Host", "FileProvider", "Ethernet", + "EndpointSecurity", + "MobileDeviceUpdater", + "AudioDMAController_T8140", ), ( "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources", "/System/Library/CoreServices/FileProvider-Feedback.app/Contents/_CodeSignature/CodeResources", "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources", + "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/MobileDeviceUpdater.app/Contents/_CodeSignature/CodeResources", + "/System/Library/Extensions/AudioDMAController_T8140.kext/Contents/_CodeSignature/CodeResources", ), ) ], @@ -56,7 +62,7 @@ def test_code_signature_coderesources( results = list(target_unix.code_signature_coderesources()) results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 11 + assert len(results) == 294 assert results[0].omit assert results[0].weight == 20.0 @@ -81,3 +87,41 @@ def test_code_signature_coderesources( results[7].source == "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources" ) + + assert results[13].nested + assert results[13].weight == 0.0 + assert ( + results[13].plist_path + == "rules2/^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/" # noqa E501 + ) + assert ( + results[13].source + == "/System/Library/Extensions/AudioDMAController_T8140.kext/Contents/_CodeSignature/CodeResources" + ) + + assert ( + results[17].hash2 + == "yϑ\udcc9rl>h%\udcbek\udccf{\udca5E\udc90\udcdf-\x00R\udcac\udcd8\udcc3m.\udce6,(;\udce1v\x00" + ) + assert results[17].optional is None + assert results[17].plist_path == "files2/version.plist" + assert ( + results[17].source + == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[29].optional + assert results[29].weight == 1000.0 + assert results[29].plist_path == "rules2/^Resources/.*\\.lproj/" + assert ( + results[29].source + == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[153].hash == "\udcd7p\udcdfIJ\udcc2\x0e\udcc9\x161ř{\udca3\udc89\udce7M\udcd7\udcf3Q" + assert results[153].optional + assert results[153].plist_path == "files/Resources/zh_TW.lproj/MobileDeviceUpdateController.strings" + assert ( + results[153].source + == "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/MobileDeviceUpdater.app/Contents/_CodeSignature/CodeResources" # noqa E501 + ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py index 2e09d6c99b..2e2d6eebd2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -59,28 +59,28 @@ def test_contents_version( assert len(results) == 3 assert results[0].build_alias_of == "Ensemble" - assert results[0].build_version == "1983" + assert results[0].build_version == 1983 assert results[0].cf_bundle_short_version_string == "1.0" assert results[0].cf_bundle_version == "174.4.1" assert results[0].project_name == "Ensemble_executables" - assert results[0].source_version == "174004001000000" + assert results[0].source_version == 174004001000000 assert results[0].source == "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist" - assert results[1].build_version == "100" + assert results[1].build_version == 100 assert results[1].cf_bundle_short_version_string == "1.0" assert results[1].cf_bundle_version == "1" assert results[1].project_name == "MobileBluetooth" - assert results[1].source_version == "194026001000001" + assert results[1].source_version == 194026001000001 assert ( results[1].source == "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist" ) assert results[2].build_alias_of == "SwiftUI" - assert results[2].build_version == "12" + assert results[2].build_version == 12 assert results[2].cf_bundle_short_version_string == "7.4.27" assert results[2].cf_bundle_version == "7.4.27" assert results[2].project_name == "SwiftUICore" - assert results[2].source_version == "7004027000000" + assert results[2].source_version == 7004027000000 assert ( results[2].source == "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist" ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index f165d68df0..11768ace1e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -74,7 +74,7 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[51].ns_persistence_maximum_framework_version == 1526 assert results[51].ns_store_model_version_identifiers == [""] assert results[51].ns_store_type == "SQLite" - assert results[51].ns_auto_vacuum_level == "2" + assert results[51].ns_auto_vacuum_level == 2 assert ( results[51].ns_store_model_version_hashes_digest == "rvkNkhmOezVbzsczB2H+gkUsiGN7C2d7a9TtXbZPD0kn0MZYSVEGM64BycQewlVstp1ROUAOBjEmkbNTkiu6JA==" @@ -84,8 +84,12 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[51].ns_store_model_version_hashes_version == 3 assert results[51].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - assert results[52].tr_cloud_kit_sync_state is None - assert results[52].text_replacement_entry is None + assert results[52].activity is not None + assert isinstance(results[52].activity, (bytes, bytearray)) + assert results[52].group_binary is not None + assert isinstance(results[52].group_binary, (bytes, bytearray)) + assert results[52].trigger is not None + assert isinstance(results[52].trigger, (bytes, bytearray)) assert results[52].plist_path == "NSStoreModelVersionHashes" assert results[52].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" @@ -95,5 +99,4 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[53].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" assert results[54].table == "Z_MODELCACHE" - assert isinstance(results[54].z_content, (bytes, bytearray)) assert results[54].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py index 42764b0744..8532fc1428 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py @@ -36,11 +36,8 @@ def test_gkopaque(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste assert len(results) == 74247 assert results[0].table == "whitelist" - assert ( - results[0].current - == "\x00\x03'\udcec\udce1\udcfbZ'\udcb5\udcf5\udcc5\x1a\x00\udc99\x00\udcb1\udce4\udc85K\udcb7" - ) - assert results[0].opaque == "\udccb\udce5k\udc97\udc84\udc97N\n\x1c\x01Y\udcc4\x1f9+wB\x1bM#" + assert results[0].current == b"\x00\x03'\xec\xe1\xfbZ'\xb5\xf5\xc5\x1a\x00\x99\x00\xb1\xe4\x85K\xb7" + assert results[0].opaque == b"\xcb\xe5k\x97\x84\x97N\n\x1c\x01Y\xc4\x1f9+wB\x1bM#" assert results[0].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" assert results[-1].table == "conditions" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py index 031f16ded1..e9e856adb3 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -42,26 +42,26 @@ def test_groups(test_files: list[str], target_unix: Target, fs_unix: VirtualFile results.sort(key=lambda r: r.realname) assert len(results) == 3 - assert results[0].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEFFFFFFFFE" - assert results[0].members is None - assert results[0].smb_sid == "S-1-0-0" - assert results[0].gid == -2 - assert results[0].name == "['nobody', 'BUILTIN\\\\Nobody']" - assert results[0].realname == "Nobody" + assert results[0].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEFFFFFFFFE"] + assert results[0].members == [] + assert results[0].smb_sid == ["S-1-0-0"] + assert results[0].gid == [-2] + assert results[0].name == ["nobody", "BUILTIN\\Nobody"] + assert results[0].realname == ["Nobody"] assert results[0].source == "/var/db/dslocal/nodes/Default/groups/nobody.plist" - assert results[-1].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000104" - assert results[-1].members is None - assert results[-1].smb_sid is None - assert results[-1].gid == 260 - assert results[-1].name == "_applepay" - assert results[-1].realname == "applepay Daemon" - assert results[-1].source == "/var/db/dslocal/nodes/Default/groups/_applepay.plist" - - assert results[1].generateduid == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000129" - assert results[1].members == "_eligibilityd" - assert results[1].smb_sid is None - assert results[1].gid == 297 - assert results[1].name == "_eligibilityd" - assert results[1].realname == "OS Eligibility Daemon" + assert results[1].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000129"] + assert results[1].members == ["_eligibilityd"] + assert results[1].smb_sid == [] + assert results[1].gid == [297] + assert results[1].name == ["_eligibilityd"] + assert results[1].realname == ["OS Eligibility Daemon"] assert results[1].source == "/var/db/dslocal/nodes/Default/groups/_eligibilityd.plist" + + assert results[2].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000104"] + assert results[2].members == [] + assert results[2].smb_sid == [] + assert results[2].gid == [260] + assert results[2].name == ["_applepay"] + assert results[2].realname == ["applepay Daemon"] + assert results[2].source == "/var/db/dslocal/nodes/Default/groups/_applepay.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py index 44067b8459..43e4849f7f 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING from unittest.mock import patch @@ -49,10 +50,40 @@ def test_keychain(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste assert results[0].table == "genp" assert results[0].row_id == 1 + assert results[0].cdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) + assert results[0].mdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) + assert results[0].desc is None + assert results[0].icmt is None + assert results[0].crtr is None + assert results[0].keychain_type is None + assert results[0].scrp is None + assert results[0].labl is None + assert results[0].alis is None + assert results[0].invi is None + assert results[0].nega is None + assert results[0].cusi is None + assert results[0].prot is None + assert results[0].acct == b"T\xccQt\xcfq\x04qd\xc4\xd1\x94\xb9^\xc6S\x95\xa1\xc5\xdd" + assert results[0].svce == b"\x84f\xd7\x7f7Qw)\x16>\x8c>}\x38\\z\xa7\tQ\x04" + assert results[0].gena is None + assert results[0].data is not None assert results[0].agrp == "com.apple.security.indirect-unlock-key" assert results[0].pdmn == "ak" assert results[0].sync == 0 assert results[0].tomb == 0 + assert ( + results[0].sha1 + == "\udc9b\udce4|7],\udc88\udcfc\udcfa\udce7\x05\udcfe\udcb0\udcf2<\udce6\udca2\udcf2`\udc99" + ) + assert results[0].vwht == "" + assert results[0].tkid == "" + assert results[0].musr == "" + assert results[0].UUID == "FB64E7EF-96A7-4797-9883-E084735D7AC1" + assert results[0].sysb is None + assert results[0].pcss is None + assert results[0].pcsk is None + assert results[0].pcsi is None + assert results[0].persistref == b"\x9c\xf0v\x86\xd2\x93E\x1b\xb4G8\xa0V\xfc\xfd\xf5" assert results[0].clip == 0 assert results[0].ggrp == "" assert results[0].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" @@ -64,24 +95,91 @@ def test_keychain(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste assert results[88].table == "inet" assert results[88].row_id == 1 + assert results[88].cdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) + assert results[88].mdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) + assert results[88].desc == b"-\x9c\xd9\x85\x03\x1cG\x93\x81\x8e\xd6\xc9\xc6n\x91\xb7\xf9\x0b \xf6" + assert results[88].icmt is None + assert results[88].crtr is None + assert results[88].keychain_type is None + assert results[88].scrp is None + assert results[88].labl is None + assert results[88].alis is None assert results[88].invi == 1 + assert results[88].nega is None + assert results[88].cusi is None + assert results[88].prot is None + assert results[88].acct == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" + assert results[88].sdmn == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[88].srvr == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" + assert results[88].ptcl == "0" + assert results[88].atyp == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[88].port == 0 + assert results[88].path_binary == b"\xec\xa4\xb9~\x03\x05\r\x96\x7fzl\xc4\xe0\xbb)\xbb\xaeV,\xa3" + assert results[88].data is not None assert results[88].agrp == "com.apple.security.octagon" assert results[88].pdmn == "cku" assert results[88].sync == 0 assert results[88].tomb == 0 + assert results[88].sha1 == "\udc8bB\x1dn\udcb4B\udcf9*\udcfdJ\udcc5\udcee\udce6ؤQ\udcae\udc97\udcbf\udcc2" + assert results[88].vwht == "" + assert results[88].tkid == "" + assert results[88].musr == "" + assert results[88].UUID == "7A4DAD90-B283-47BC-9AE4-1D237C3493D8" assert results[88].sysb == 1 + assert results[88].pcss is None + assert results[88].pcsk is None + assert results[88].pcsi is None + assert results[88].persistref == b"TZ\x99|\xfd\xd2H\xd2\xad\xf3\x04L\xb3\xfcq\x9e" assert results[88].clip == 0 assert results[88].ggrp == "" assert results[88].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" assert results[89].table == "keys" assert results[89].row_id == 1 - assert results[89].crtr == "0" - assert results[89].type == "0" + assert results[89].cdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) + assert results[89].mdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) + assert results[89].kcls == b"\x90i\xcax\xe7E\n(QsC\x1b>R\xc5\xc2R\x99\xe4s" + assert results[89].labl is None + assert results[89].alis is None + assert results[89].perm is None + assert results[89].priv is None + assert results[89].modi is None + assert results[89].klbl == b"com.apple.routined.security.database" + assert results[89].atag == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[89].crtr == 0 + assert results[89].keychain_type == 0 + assert results[89].bsiz == 0 + assert results[89].esiz == 0 + assert results[89].sdat == 0.0 + assert results[89].edat == 0.0 + assert results[89].sens is None + assert results[89].asen is None + assert results[89].extr is None + assert results[89].next is None + assert results[89].encr is None + assert results[89].decr is None + assert results[89].drve is None + assert results[89].sign is None + assert results[89].vrfy is None + assert results[89].snrc is None + assert results[89].vyrc is None + assert results[89].wrap is None + assert results[89].unwp is None + assert results[89].data is not None assert results[89].agrp == "com.apple.routined" assert results[89].pdmn == "ck" assert results[89].sync == 1 assert results[89].tomb == 0 + assert results[89].sha1 == "\udc9cc\udc98\udcd8\udcea0\udcb12bFL\udca4ot87\udcc7\udcf4\x7f\udc82" + assert results[89].vwht == "" + assert results[89].tkid == "" + assert results[89].musr == "" + assert results[89].UUID == "22A9A2ED-79C9-4665-80F5-021793509144" + assert results[89].sysb is None + assert results[89].pcss is None + assert results[89].pcsk is None + assert results[89].pcsi is None + assert results[89].persistref == b"\xaf\x1e\xaaxqDG\xae\x81\xdb\xb1\xce\x9c\xe9C\xb9" assert results[89].clip == 0 assert results[89].ggrp == "" assert results[89].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" @@ -93,9 +191,10 @@ def test_keychain(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste assert results[94].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" assert results[95].table == "metadatakeys" - assert results[95].keyclass == "6" - assert results[95].actual_key_class == "6" - assert results[95].data == ( - "r\udcccߨ\udcfdeѽi\udcb5\x16\udcd5+\udcf7\udcfa\udca0\udce4\udce7\x13+\udc9d\udc8b\udcae\x06g\udcb4\udc9b\udca1ѡ\udca2\udca1aY摳K\udc83\udc80" # noqa RUF001 + assert results[95].keyclass == 6 + assert results[95].actual_keyclass == 6 + assert ( + results[95].data + == "r\udcccߨ\udcfdeѽi\udcb5\x16\udcd5+\udcf7\udcfa\udca0\udce4\udce7\x13+\udc9d\udc8b\udcae\x06g\udcb4\udc9b\udca1ѡ\udca2\udca1aY摳K\udc83\udc80" # noqa E501 ) assert results[95].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py index 166f046a50..1158d8bdc2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py @@ -72,10 +72,20 @@ def test_launch_agents( "/System/Library/PrivateFrameworks/Ecosystem.framework/Support/ecosystemagent" ] assert results[0].keep_alive is None + assert results[0].on_demand is None + assert results[0].disabled is None assert results[0].run_at_load is None + assert results[0].launch_only_once is None assert results[0].process_type == "Background" + assert results[0].wait is None assert results[0].limit_load_to_session_type is None + assert results[0].limit_load_to_developer_mode is None + assert results[0].limit_load_from_variant is None + assert results[0].limit_load_to_variant is None + assert results[0].limit_load_from_boot_mode is None + assert results[0].limit_load_to_boot_mode == [] assert results[0].limit_load_from_hardware == [] + assert results[0].limit_load_to_hardware == [] assert results[0].launch_events == [] assert results[0].mach_services == [ "('com.apple.ecosystem.agent.clear-notifications', True)", @@ -87,9 +97,30 @@ def test_launch_agents( assert results[0].enable_transactions assert results[0].environment_variables == [] assert results[0].user_name is None + assert results[0].init_groups is None + assert results[0].group_name is None + assert results[0].start_interval is None + assert results[0].start_calendar_interval == [] + assert results[0].throttle_interval is None + assert results[0].enable_globbing is None + assert results[0].standard_in_path is None + assert results[0].standard_out_path is None + assert results[0].standard_error_path is None + assert results[0].nice is None + assert results[0].abandon_process_group is None assert results[0].low_priority_io + assert results[0].root_directory is None + assert results[0].working_directory is None + assert results[0].umask is None + assert results[0].time_out is None + assert results[0].exit_time_out is None assert results[0].watch_paths == [] assert results[0].queue_directories == [] + assert results[0].start_on_mount is None + assert results[0].soft_resource_limits == [] + assert results[0].hard_resource_limits == [] + assert results[0].debug is None + assert results[0].wait_for_debugger is None assert results[0].plist_path is None assert results[0].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" @@ -117,21 +148,57 @@ def test_launch_agents( assert results[3].un_notification_icon_default == "notification-settings" assert results[3].un_notification_icon_settings == "notification-settings" - assert results[3].plist_path == "UNUserNotificationCenter/UNNotificationIcons" assert results[3].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" assert results[4].label == "com.openssh.ssh-agent" assert results[4].program is None assert results[4].program_arguments == ["/usr/bin/ssh-agent", "-l"] + assert results[4].keep_alive is None + assert results[4].on_demand is None + assert results[4].disabled is None + assert results[4].run_at_load is None + assert results[4].launch_only_once is None assert results[4].process_type is None + assert results[4].wait is None + assert results[4].limit_load_to_session_type is None + assert results[4].limit_load_to_developer_mode is None + assert results[4].limit_load_from_variant is None + assert results[4].limit_load_to_variant is None + assert results[4].limit_load_from_boot_mode is None + assert results[4].limit_load_to_boot_mode == [] + assert results[4].limit_load_from_hardware == [] + assert results[4].limit_load_to_hardware == [] + assert results[4].launch_events == [] assert results[4].mach_services == [] assert results[4].enable_pressured_exit is None assert results[4].enable_transactions assert results[4].environment_variables == [] assert results[4].user_name is None + assert results[4].init_groups is None + assert results[4].group_name is None + assert results[4].start_interval is None + assert results[4].start_calendar_interval == [] + assert results[4].throttle_interval is None + assert results[4].enable_globbing is None + assert results[4].standard_in_path is None + assert results[4].standard_out_path is None + assert results[4].standard_error_path is None + assert results[4].nice is None + assert results[4].abandon_process_group is None + assert results[4].low_priority_io is None + assert results[4].root_directory is None + assert results[4].working_directory is None + assert results[4].umask is None + assert results[4].time_out is None + assert results[4].exit_time_out is None assert results[4].watch_paths == [] assert results[4].queue_directories == [] + assert results[4].start_on_mount is None + assert results[4].soft_resource_limits == [] + assert results[4].hard_resource_limits == [] + assert results[4].debug is None + assert results[4].wait_for_debugger is None assert results[4].plist_path is None assert results[4].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" @@ -149,7 +216,6 @@ def test_launch_agents( assert results[5].sock_path_group is None assert results[5].bonjour is None assert results[5].multicast_group is None - assert results[5].receive_packet_info is None assert results[5].plist_path == "Sockets/Listeners" assert results[5].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" @@ -241,6 +307,11 @@ def test_launch_daemons( assert results[0].queue_directories == [] assert results[0].start_on_mount is None assert results[0].soft_resource_limits == [] + assert results[0].hard_resource_limits == [] + assert results[0].debug is None + assert results[0].wait_for_debugger is None + assert results[0].plist_path is None + assert results[0].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" assert results[1].socket_key is None assert results[1].sock_type is None @@ -256,6 +327,5 @@ def test_launch_daemons( assert results[1].sock_path_group is None assert results[1].bonjour is None assert results[1].multicast_group is None - assert results[1].receive_packet_info is None assert results[1].plist_path == "Sockets/Listeners[0]" assert results[1].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py index 4126c22e20..7b1a6c1cb4 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py @@ -61,30 +61,33 @@ def test_login_items( assert len(results) == 4 - assert results[0].associatedBundleIdentifiers is None + assert results[0].associated_bundle_identifiers is None + assert results[0].bookmark is not None assert isinstance(results[0].bookmark, (bytes, bytearray)) - assert results[0].bundleIdentifier == "com.microsoft.VSCode" + assert results[0].bundle_identifier == "com.microsoft.VSCode" assert results[0].container is None - assert results[0].designatedRequirement == ( + assert results[0].designated_requirement == ( 'identifier "com.microsoft.VSCode" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] ' # noqa E501 "/* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = UBF8T346G9" # noqa E501 ) - assert results[0].developerName is None + assert results[0].developer_name is None assert results[0].login_item_disposition == 3 - assert results[0].executableModificationDate == datetime(1970, 1, 1, 0, 0, tzinfo=tz) - assert results[0].executablePath is None + assert results[0].executable_modification_date == datetime(1970, 1, 1, 0, 0, tzinfo=tz) + assert results[0].executable_path is None assert results[0].flags == 0 assert results[0].generation == 0 assert results[0].identifier == "2.com.microsoft.VSCode" - assert isinstance(results[0].lightweightRequirement, (bytes, bytearray)) - assert results[0].modificationDate == datetime(1995, 4, 29, 15, 46, 38, tzinfo=tz) + assert results[0].lightweight_requirement is not None + assert isinstance(results[0].lightweight_requirement, (bytes, bytearray)) + assert results[0].modification_date == datetime(2026, 4, 29, 15, 46, 38, tzinfo=tz) assert results[0].name == "Visual Studio Code.app" - assert results[0].programArguments is None + assert results[0].program_arguments is None assert results[0].sha256 is None - assert results[0].teamIdentifier == "UBF8T346G9" + assert results[0].team_identifier == "UBF8T346G9" assert results[0].login_item_type == 2 assert results[0].url == "file:///Applications/Visual%20Studio%20Code.app/" assert results[0].uuid == "6f541698-5211-4cf2-95b7-e97534baee39" + assert results[0].items == [] assert results[0].plist_path == "itemsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90[0]" assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py index 8755ec08e1..84af77a3ed 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py @@ -21,10 +21,12 @@ ( "loginwindow.plist", "com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist", + "com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist", ), ( "/Users/user/Library/Preferences/loginwindow.plist", "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist", + "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist", ), ), ], @@ -58,22 +60,33 @@ def test_login_window( with ( patch.object(entries[0], "stat", return_value=stat_results[0]), patch.object(entries[1], "stat", return_value=stat_results[1]), + patch.object(entries[2], "stat", return_value=stat_results[2]), ): target_unix.add_plugin(LoginWindowPlugin) results = list(target_unix.login_window()) results.sort(key=lambda r: r.source) - assert len(results) == 2 + assert len(results) == 5 - assert not results[0].MiniBuddyLaunch + assert not results[0].hide + assert results[0].bundle_id == "com.apple.finder" + assert results[0].path == "/System/Library/CoreServices/Finder.app" + assert results[0].background_state == 2 + assert results[0].plist_path == "TALAppsToRelaunchAtLogin[0]" assert ( results[0].source + == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist" + ) + + assert not results[3].mini_buddy_launch + assert ( + results[3].source == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist" ) - assert results[-1].BuildVersionStampAsNumber == 52698816 - assert results[-1].BuildVersionStampAsString == "25E246" - assert results[-1].SystemVersionStampAsNumber == 436469760 - assert results[-1].SystemVersionStampAsString == "26.4" - assert results[-1].source == "/Users/user/Library/Preferences/loginwindow.plist" + assert results[4].build_version_as_string == "25E246" + assert results[4].build_version_stamp_as_number == 52698816 + assert results[4].system_version_stamp_as_string == "26.4" + assert results[4].system_version_stamp_as_number == 436469760 + assert results[4].source == "/Users/user/Library/Preferences/loginwindow.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py index e352b7427e..20c83d0e6e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py @@ -156,3 +156,5 @@ def test_periodic_conf( assert results[3].monthly_status_security_output is None assert results[3].monthly_local == "/etc/monthly.local" assert results[3].source == "/etc/defaults/periodic.conf" + + # TODO: Add test for PeriodicConfSecurityRecord diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py index 6ca775350b..8b74658731 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -17,8 +17,11 @@ ("names", "paths"), [ ( - ("InfoPlist.strings",), - ("/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings",), + ("WiFiAgent.strings", "OSvKernDSPLib.strings"), + ( + "/System/Library/CoreServices/WiFiAgent.app/Contents/Resources/InfoPlist.strings", + "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings", + ), ) ], ) @@ -38,12 +41,40 @@ def test_resources_info_strings( with ( patch.object(entries[0], "stat", return_value=stat_results[0]), + patch.object(entries[1], "stat", return_value=stat_results[1]), ): target_unix.add_plugin(ResourcesInfoStringsPlugin) results = list(target_unix.resources_info_strings()) results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 1 + assert len(results) == 2 - assert results[0].NSHumanReadableCopyright == "Copyright © 2004 Apple Inc. All rights reserved." - assert results[0].source == "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings" + assert results[0].cf_bundle_name == "WiFiAgent" + assert results[0].cf_bundle_display_name == "Wi-Fi" + assert results[0].cf_bundle_identifier is None + assert results[0].cf_bundle_version is None + assert results[0].cf_bundle_package_type is None + assert results[0].cf_bundle_signature is None + assert results[0].cf_bundle_executable is None + assert results[0].cf_bundle_document_types == [] + assert results[0].cf_bundle_short_version_string is None + assert results[0].ls_minimum_system_version is None + assert results[0].ns_human_readable_copyright is None + assert results[0].ns_main_nib_file is None + assert results[0].ns_principal_class is None + assert results[0].source == "/System/Library/CoreServices/WiFiAgent.app/Contents/Resources/InfoPlist.strings" + + assert results[1].cf_bundle_name is None + assert results[1].cf_bundle_display_name is None + assert results[1].cf_bundle_identifier is None + assert results[1].cf_bundle_version is None + assert results[1].cf_bundle_package_type is None + assert results[1].cf_bundle_signature is None + assert results[1].cf_bundle_executable is None + assert results[1].cf_bundle_document_types == [] + assert results[1].cf_bundle_short_version_string is None + assert results[1].ls_minimum_system_version is None + assert results[1].ns_human_readable_copyright == "Copyright © 2004 Apple Inc. All rights reserved." + assert results[1].ns_main_nib_file is None + assert results[1].ns_principal_class is None + assert results[1].source == "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py index 996be8c884..ad0900b285 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -76,26 +76,31 @@ def test_tcc( assert results[0].value == "32" assert results[0].source == "/Library/Application Support/com.apple.TCC/TCC.db" - assert results[-2].table == "access" - assert results[-2].service == "kTCCServiceLiverpool" - assert results[-2].client == "com.apple.homeenergyd" - assert results[-2].client_type == 0 - assert results[-2].auth_value == 2 - assert results[-2].auth_reason == 4 - assert results[-2].auth_version == 1 - assert results[-2].csreq is None - assert results[-2].policy_id is None - assert results[-2].indirect_object_identifier_type == "0" - assert results[-2].indirect_object_identifier == "UNUSED" - assert results[-2].indirect_object_code_identity is None - assert results[-2].indirect_object_identifier_type == "0" - assert results[-2].flags == 0 - assert results[-2].last_modified == datetime(2026, 3, 25, 14, 25, 29, tzinfo=tz) - assert results[-2].pid is None - assert results[-2].pid_version is None - assert results[-2].boot_uuid == "UNUSED" - assert results[-2].last_reminded == datetime(2026, 3, 25, 14, 25, 29, tzinfo=tz) - assert results[-2].source == "/Users/user/Library/Application Support/com.apple.TCC/TCC.db" + assert results[1].table == "access" + assert results[1].service == "kTCCServiceSystemPolicyAllFiles" + assert ( + results[1].client + == "/System/Library/PrivateFrameworks/VoiceShortcuts.framework/Versions/A/Support/siriactionsd" + ) + assert results[1].client_type == 1 + assert results[1].auth_value == 0 + assert results[1].auth_reason == 5 + assert results[1].auth_version == 1 + assert ( + results[1].csreq + == b"\xfa\xde\x0c\x00\x00\x00\x004\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x16com.apple.siriactionsd\x00\x00\x00\x00\x00\x03" # noqa E501 + ) + assert results[1].policy_id is None + assert results[1].indirect_object_identifier == "UNUSED" + assert results[1].indirect_object_identifier_type is None + assert results[1].indirect_object_code_identity is None + assert results[1].flags == 0 + assert results[1].last_modified == datetime(2026, 3, 25, 14, 13, 27, tzinfo=tz) + assert results[1].pid is None + assert results[1].pid_version is None + assert results[1].boot_uuid == "UNUSED" + assert results[1].last_reminded == datetime(1970, 1, 1, 0, 0, 0, tzinfo=tz) + assert results[1].source == "/Library/Application Support/com.apple.TCC/TCC.db" assert results[-1].table == "integrity_flag" assert results[-1].key == "integrity_flag" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py index 107d787e09..923962f1ab 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING from unittest.mock import patch @@ -62,7 +63,7 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[0].z_opt == 1 assert results[0].z_needs_save_to_cloud == 1 assert results[0].z_was_deleted == 0 - assert results[0].z_timestamp == "796140768.339994" + assert results[0].z_timestamp == datetime(2026, 3, 25, 14, 12, 48, 339994, tzinfo=timezone.utc) assert results[0].z_phrase == "On my way!" assert results[0].z_shortcut == "omw" assert results[0].z_unique_name == "CB36B9A8-B570-4972-BC6C-B12DAA7C0B97" @@ -93,7 +94,7 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[4].ns_persistence_maximum_framework_version == 1526 assert results[4].ns_store_type == "SQLite" - assert results[4].ns_auto_vacuum_level == "2" + assert results[4].ns_auto_vacuum_level == 2 assert results[4].ns_store_model_version_identifiers == [""] assert results[4].ns_store_model_version_hashes_version == 3 assert results[4].ns_store_model_version_hashes_digest == ( @@ -103,10 +104,10 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[4].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" assert results[5].tr_cloud_kit_sync_state == ( - "\udca2 \udc8cuo\udcd1V\udcd2p\udc89C#\udcbd\udcb1$\udcf1F\udc97X\udce9\x01\x1e\x02\udccb\udcf5\udce5y$\udc90\udcc4\udcee\udcdf" # noqa E501 + b"\xa2 \x8cuo\xd1V\xd2p\x89C#\xbd\xb1$\xf1F\x97X\xe9\x01\x1e\x02\xcb\xf5\xe5y$\x90\xc4\xee\xdf" ) assert results[5].text_replacement_entry == ( - "#C\t\udcd2l\udca7\udceaK##S\udcae\udcb7>\udc82\udcb5\udcb5ȯB\udcf4\t\udcc4]\udca2)\udcb0}+٫\x03" # noqa RUF001 + b"#C\t\xd2l\xa7\xeaK##S\xae\xb7>\x82\xb5\xb5\xc8\xafB\xf4\t\xc4]\xa2)\xb0}+\xd9\xab\x03" ) assert results[5].plist_path == "NSStoreModelVersionHashes" assert results[5].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" @@ -117,5 +118,4 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[6].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" assert results[7].table == "Z_MODELCACHE" - assert isinstance(results[7].z_content, bytes) assert results[7].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py index 4aaf957de1..21ad211c08 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -80,7 +80,16 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[19].z_warming_up == 0 assert results[19].z_account_type == 50 assert results[19].z_parent_account is None + assert results[19].z_date.isoformat() == "1995-03-25T14:11:32.168510+00:00" + assert results[19].z_last_credential_renewal_rejection_date is None + assert results[19].z_account_description is None + assert results[19].z_authentication_type is None + assert results[19].z_credential_type is None + assert results[19].z_identifier == "9DE5EA8C-7EE3-433C-9387-A1158923C75B" + assert results[19].z_modification_id == "581D5181-C65D-4598-80EE-7386D70A8799" + assert results[19].z_owning_bundle_id == "amsaccountsd" assert results[19].z_username == "local" + assert results[19].z_dataclass_properties is None assert results[19].source == "/Users/user/Library/Accounts/Accounts4.sqlite" assert results[21].table == "Z_2ENABLEDDATACLASSES" @@ -94,7 +103,7 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[22].z_opt == 1 assert results[22].z_owner == 1 assert results[22].z_key == "isLocalAccount" - assert results[22].z_value is not None + assert results[22].z_value == "True" assert results[22].source == "/Users/user/Library/Accounts/Accounts4.sqlite" assert results[36].table == "ZACCOUNTTYPE" @@ -106,6 +115,64 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[36].z_supports_multiple_accounts == 1 assert results[36].z_visibility == 0 assert results[36].z_account_type_description == "Gmail" + assert results[36].z_credential_protection_policy is None assert results[36].z_credential_type == "oauth2" assert results[36].z_identifier == "com.apple.account.Google" + assert results[36].z_owning_bundle_id == "com.apple.accountsd" assert results[36].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[92].table == "Z_4SUPPORTEDDATACLASSES" + assert results[92].z_4_supported_types == 55 + assert results[92].z_7_supported_dataclasses == 27 + assert results[92].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[197].table == "Z_4SYNCABLEDATACLASSES" + assert results[197].z_4_syncable_types == 55 + assert results[197].z_7_syncable_dataclasses == 27 + assert results[197].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[279].table == "Z_PRIMARYKEY" + assert results[279].z_ent == 1 + assert results[279].z_name == "AccessOptionsKey" + assert results[279].z_super == 0 + assert results[279].z_max == 7 + assert results[279].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[286].ac_account_type_version == 107 + assert results[286].ns_auto_vacuum_level == 2 + assert results[286].ns_persistence_framework_version == 1526 + assert results[286].ns_persistence_maximum_framework_version == 1526 + assert results[286].ns_store_model_version_checksum_key == "ZAPc9m2m45TI0HS9GepqhdVKmn8FObA8NpZcwc7UIT8=" + assert ( + results[286].ns_store_model_version_hashes_digest + == "6zOxtAC8CBiqUoJ8U+mC0mHIF9LkhkOGYval68MNS/V6S4tVudh/sDu45bnjvnLbb+I3Ouq7XXF3ZNolh1b4+A==" + ) + assert results[286].ns_store_model_version_hashes_version == 3 + assert results[286].ns_store_model_version_identifiers == "['30']" + assert results[286].ns_store_type == "SQLite" + assert results[286].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[287].access_options_key is not None + assert isinstance(results[287].access_options_key, (bytes, bytearray)) + assert results[287].account_hash is not None + assert isinstance(results[287].account_hash, (bytes, bytearray)) + assert results[287].account_property is not None + assert isinstance(results[287].account_property, (bytes, bytearray)) + assert results[287].account_type is not None + assert isinstance(results[287].account_type, (bytes, bytearray)) + assert results[287].authorization is not None + assert isinstance(results[287].authorization, (bytes, bytearray)) + assert results[287].credential_item is not None + assert isinstance(results[287].credential_item, (bytes, bytearray)) + assert results[287].dataclass is not None + assert isinstance(results[287].dataclass, (bytes, bytearray)) + assert results[287].plist_path == "NSStoreModelVersionHashes" + assert results[287].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[288].table == "Z_METADATA" + assert results[288].z_version == 1 + assert results[288].z_uuid == "9802D604-BFA7-4F9D-A39F-0CF8E6FD0FAC" + assert results[288].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[289].table == "Z_MODELCACHE" + assert results[289].source == "/Users/user/Library/Accounts/Accounts4.sqlite" From 3fa433a133a6755a26956792359b6ae9ae31e776 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:35:28 +0200 Subject: [PATCH 32/52] Add docstrings for find_bundle_files() and parse_timestamp() helpers --- .../bsd/darwin/macos/helpers/build_paths.py | 37 ++++++++++++++++--- .../darwin/macos/helpers/parse_timestamp.py | 11 ++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py index 32255be656..f485012ab0 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -55,19 +55,46 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta def find_bundle_files(target: Target, end_path: str) -> set: + """Search for files matching a given end path within known macOS bundle locations. + + Iterates over predefined base system paths and collects all matching files + found by recursively exploring bundle structures. + + Args: + target (Target): Object providing filesystem access. + end_path (str): Relative path to search for within bundles. + + Returns: + set: A set of matching file paths. + """ results = set() for base in START_PATHS: - results.update(find_end_paths(target, end_path, base)) + results.update(find_end_paths(target, end_path, target.fs.path(base))) return results -def find_end_paths(target: Target, end_path: str, base_path: str) -> set: - found = set() +def find_end_paths(target: Target, end_path: str, base_path: Path) -> set: + """Recursively search for files within macOS bundle directories that match a given end path. - if isinstance(base_path, str): - base_path = target.fs.path(base_path) + Looks for known bundle types (e.g., .app, .framework, .kext) based on the + EXTENSIONS mapping, which defines the internal subdirectories that should be traversed + for each bundle extension. Explores the relevant subpaths for every discovered bundle, + and checks whether the specified relative file path (end path) exists. + + Continues recursively into each valid subpath, in order to handle + nested bundle structures. + + Args: + target (Target): Object providing filesystem access. + end_path (str): Relative path to locate inside bundle directories. + base_path (Path): Base directory from which the search begins. + + Returns: + set: A set of matching file paths found within bundle hierarchies. + """ + found = set() for ext, subpaths in EXTENSIONS.items(): for bundle_str in target.fs.glob(f"{base_path}/{ext}"): diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py index 69bbca796c..88f06f5e01 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py @@ -8,6 +8,17 @@ def parse_timestamp(timestamp: re.Match) -> datetime: + """Parse a timestamp match into a datetime object. + + Attempts to parse the matched string as an ISO 8601 datetime. If that fails, + falls back to parsing a syslog-style timestamp ("%b %d %H:%M:%S") and assigns UTC timezone. + + Args: + timestamp (re.Match): A regex match object containing the timestamp string. + + Returns: + datetime: The parsed datetime object. + """ ts = None value = timestamp.group() try: From 2df67f5a36968a3b81615b3fc1cc38e5c848c471 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:47:21 +0200 Subject: [PATCH 33/52] Add docstrings for build_records helpers --- .../bsd/darwin/macos/helpers/build_records.py | 257 +++++++++++++++++- 1 file changed, 246 insertions(+), 11 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 7bdc626f60..fb547c694a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -34,6 +34,31 @@ def build_sqlite_records( field_mappings: dict | None = None, convert_timestamps: dict | None = None, ) -> Iterator[TargetRecordDescriptor]: + """Extract and normalize records from SQLite databases. + + Iterates over provided SQLite files, reads all tables and rows, and converts + them into dictionaries. Supports decoding of binary plist values and + NSKeyedArchiver blobs. The optional joins arg can be used to combine and + filter rows across related tables. + + Join behavior is controlled via the `joins` configuration: + - iterate: iterates over rows of table2 and merges them with + matching table1 entries. Table2 rows that do not match with any table1 + will be yielded separately. + - nested: adds a field to table1 rows containing all of the matching table2 rows. + - ignore: removes specific fields when values match, used to avoid duplicate fields on joins. + + Args: + plugin (Plugin): Plugin instance providing logging and target access. + files (set[str]): Paths to SQLite database files. + record_descriptors (tuple | None): Optional descriptors for record construction. + joins (tuple): Join configuration dictionary defining relationships between tables. + field_mappings (dict | None): Optional field name mappings. + convert_timestamps (dict | None): Optional timestamp conversion rules. + + Yields: + TargetRecordDescriptor: Normalized records constructed from database rows. + """ joins_by_table1 = defaultdict(list) joins_by_table2 = defaultdict(list) ignore_joins_map = defaultdict(list) @@ -152,6 +177,15 @@ def build_sqlite_records( def prefix_row(row_dict: dict, table: str) -> dict: + """Prefix all keys in a row dictionary with the table name. + + Args: + row_dict (dict): Row data as key-value pairs. + table (str): Table name to prepend to each key. + + Returns: + dict: A new dictionary with prefixed keys. + """ return {f"{table}_{k}": v for k, v in row_dict.items()} @@ -163,6 +197,25 @@ def handle_iterate_join( ignore_joins_map: defaultdict(list), tables: set[str], ) -> list[dict]: + """Process iterative joins between tables. + + Matches child rows to a parent row based on join keys and expands them + into separate result dictionaries. Recursively processes downstream joins, + allowing chained relationships across multiple tables. + + Applies "ignore" rules to remove matching fields where configured. + + Args: + database (SQLite3): Open SQLite database instance. + parent_dict (dict): Current parent row data. + current_join (dict): Join configuration describing the relationship. + joins_by_table1 (defaultdict): Joins indexed by source table. + ignore_joins_map (defaultdict): Mapping of ignore join rules. + tables (set[str]): Set of involved table names. + + Returns: + list[dict]: Expanded and prefixed row dictionaries. + """ results: list[dict] = [] ignore_joins = ignore_joins_map[(current_join["table1"], current_join["table2"])] @@ -214,6 +267,23 @@ def handle_nested_join( ignore_joins: list[dict], tables: set[str], ) -> None: + """Process nested joins between tables. + + Finds related rows in a secondary table and embeds them as a list of + dictionaries under a key named after the joined table. + + Applies ignore rules to remove matching fields where configured. + + Args: + database (SQLite3): Open SQLite database instance. + row_dict (dict): Current row being enriched. + current_join (dict): Join configuration describing the relationship. + ignore_joins (list[dict]): Ignore rules for this join. + tables (set[str]): Set of involved table names. + + Returns: + None: Modifies `row_dict` in place. + """ n_rows = [] for n_row in database.table(current_join["table2"]).rows(): if n_row[current_join["key2"]] == row_dict[current_join["key1"]]: @@ -228,6 +298,17 @@ def handle_nested_join( def is_nskeyedarchive_blob(value: (bytes, bytearray)) -> bool: + """Determine whether a binary blob is an NSKeyedArchiver-encoded plist. + + Attempts to parse the blob as a plist and checks for the structural + markers used by NSKeyedArchiver. + + Args: + value (bytes | bytearray): Binary data to inspect. + + Returns: + bool: True if the blob appears to be an NSKeyedArchiver archive. + """ try: plist_obj = plistlib.loads(value) except Exception: @@ -250,6 +331,27 @@ def build_plist_records( convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: + """Extract and normalize records from plist files. + + Iterates over provided file paths, parses each file as a plist, and emits + records from the resulting data structures. Supports both standard plist + formats and NSKeyedArchiver-encoded plists. + + Parsed data is passed to emit_dict_records for recursive traversal + and record construction. + + Args: + plugin (Plugin): Plugin instance providing logging and target access. + files (set[str]): Paths to plist files. + record_descriptors (tuple | None): Optional descriptors for record construction. + collapse_paths (set[tuple[str, bool]] | None): Plist paths to collapse during recursive traversal. + field_mappings (dict | None): Optional field name mappings. + convert_timestamps (dict | None): Optional timestamp conversion rules. + function_name (str | None): Optional name used for dynamic record creation. + + Yields: + Record: Normalized records constructed from plist contents. + """ for file in files: file = plugin.target.fs.path(file) try: @@ -289,6 +391,25 @@ def build_record_from_data( convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: + """Extract and normalize records from raw binary data containing plist content. + + Detects whether the input data is a binary plist and whether it contains + NSKeyedArchiver-encoded content. Parses the data accordingly, then passes + the resulting structure to emit_dict_records for recursive traversal + and record construction. + + Args: + plugin (Plugin): Plugin instance providing logging and target access. + file (str): File from which the raw_data was extracted. + raw_data (bytes): Raw binary data to parse. + record_descriptors (tuple | None): Optional descriptors for record construction. + field_mappings (dict | None): Optional field name mappings. + convert_timestamps (dict | None): Optional timestamp conversion rules. + function_name (str | None): Optional name used for dynamic record creation. + + Yields: + Record: Normalized records constructed from plist contents. + """ try: if not raw_data: return @@ -313,7 +434,22 @@ def build_record_from_data( plugin.target.log.exception("Failed to parse %s", file) -def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source: Path | None) -> Record: +def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source: Path) -> Record: + """Dynamically construct a record descriptor and corresponding record from a dictionary. + + Infers field types based on Python value types and builds a descriptor + accordingly. Supports lists, and includes the source + path as a field. + + Args: + plugin (Plugin): Plugin instance providing target context. + function_name (str): Name used for the generated record descriptor. + rdict (dict): Dictionary containing record data. + source (Path): Source path associated with the record. + + Returns: + Record: A record instance created from the dynamically generated descriptor. + """ record_fields = sorted(rdict.items()) record_values = { @@ -338,7 +474,7 @@ def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source record_fields.append(("path", "source")) - desc = create_event_descriptor(function_name, tuple(record_fields)) + desc = TargetRecordDescriptor(function_name, record_fields) return desc(**record_values) @@ -347,8 +483,25 @@ def select_descriptor( record_descriptors: tuple, rdict: dict, plugin: Plugin, - source: Path | None, + source: Path, ) -> TargetRecordDescriptor | None: + """Select the most appropriate record descriptor for a given data dictionary. + + Compares the keys in the dictionary against available record descriptors + and selects the one with the highest number of matching fields. In case of a tie, + the descriptor with fewer total fields is preferred. + + Logs a warning if the input contains fields not defined in the selected descriptor. + + Args: + record_descriptors (tuple): Available record descriptors. + rdict (dict): Dictionary containing record data. + plugin (Plugin): Plugin instance for logging. + source (Path): Source path associated with the record. + + Returns: + TargetRecordDescriptor | None: The selected descriptor, or None if no match is found. + """ formatted_rdict = {format_key(k): type(v).__name__ for k, v in rdict.items()} selected_record = None @@ -389,11 +542,30 @@ def select_descriptor( def build_record( plugin: Plugin, rdict: dict, - source: Path | None, - record_descriptors: tuple | None = None, + source: Path, + record_descriptors: tuple, field_mappings: dict | None = None, convert_timestamps: dict | None = None, ) -> Record: + """Construct a record from a dictionary using a matching record descriptor. + + Applies provided field mappings, selects an appropriate descriptor based on + the fields in the dictionary, filters unsupported fields, and performs provided + timestamp conversions before instantiating the record. + + If no matching descriptor is found, logs an error and returns None. + + Args: + plugin (Plugin): Plugin instance providing logging and target context. + rdict (dict): Dictionary containing record data. + source (Path): Source path associated with the record. + record_descriptors (tuple): Available record descriptors. + field_mappings (dict | None): Optional field name mappings. + convert_timestamps (dict | None): Optional timestamp conversion rules. + + Returns: + Record | None: Constructed record, or None if no descriptor matched. + """ if field_mappings: for key in list(rdict): for src, dst in field_mappings.items(): @@ -427,8 +599,21 @@ def build_record( return desc(**record_values) -def convert_timestamp(value: Any, mode: str) -> datetime | None: - if value is None or isinstance(value, datetime): +def convert_timestamp(value: Any, mode: str) -> datetime | Any: + """Convert a value to a datetime based on the specified timestamp format. + + Supports conversion from Apple's epoch (seconds since 2001-01-01 UTC). + If the value is None, it is returned unchanged. + Unsupported modes return the original value. + + Args: + value (Any): Input value to convert. + mode (str): Conversion mode (e.g., "2001" for Apple epoch). + + Returns: + datetime | Any: Converted datetime, or original value if no conversion applied. + """ + if value is None: return value if mode == "2001": @@ -437,11 +622,19 @@ def convert_timestamp(value: Any, mode: str) -> datetime | None: return value -def create_event_descriptor(function_name: str, record_fields: list[tuple[str, str]]) -> TargetRecordDescriptor: - return TargetRecordDescriptor(function_name, record_fields) +def format_key(key: str) -> str: + """Normalize a key name (string) to a valid format. + Replaces unsupported characters with underscores, collapses + repeated underscores, removes leading underscores, and ensures + the key does not start with a digit. -def format_key(key: str) -> str: + Args: + key (str): The key name to be formatted. + + Returns: + str: Normalized key name suitable for use as an identifier. + """ key = re_non_identifier.sub("_", key) key = re.sub(r"_+", "_", key) @@ -464,6 +657,18 @@ def format_key(key: str) -> str: def is_collapsed_path(child_path: str, collapse_paths: set[tuple[str, bool]]) -> bool: + """Determine whether a path should be collapsed during traversal. + + A path is considered collapsed if it matches a configured path exactly, + or if it is a descendant of a configured path when non-exact matching is allowed. + + Args: + child_path (str): Current traversal path. + collapse_paths (set[tuple[str, bool]]): Set of (path, exact) rules. + + Returns: + bool: True if the path should be collapsed, False otherwise. + """ for collapse_path, exact in collapse_paths: if child_path == collapse_path: return True @@ -475,7 +680,7 @@ def is_collapsed_path(child_path: str, collapse_paths: set[tuple[str, bool]]) -> def emit_dict_records( plugin: Plugin, node: dict, - source: Path | None, + source: Path, *, section: str | None = None, path: str | None = None, @@ -485,6 +690,35 @@ def emit_dict_records( convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: + """Recursively traverse a dictionary and emit records from its contents. + + Splits dictionary entries into attribute values and nested child dictionaries. + + Handles lists by separating scalar elements from nested dictionaries: + scalar values are retained as attributes + dictionary elements are treated as child nodes and processed recursively. + + Collapses specified paths into attribute values instead of recursing. + + Generates records for nodes containing attribute data, using either dynamic + descriptor construction or predefined record descriptors. Recursively processes + nested child dictionaries to emit additional records. + + Args: + plugin (Plugin): Plugin instance providing logging and target context. + node (dict): Dictionary node to process. + source (Path): Source path associated with the data. + section (str | None): Optional section label for records. + path (str | None): Current traversal path within the structure. + record_descriptors (tuple | None): Optional descriptors for record construction. + collapse_paths (set[tuple[str, bool]] | None): Paths to collapse instead of recurse. + field_mappings (dict | None): Optional field name mappings. + convert_timestamps (dict | None): Optional timestamp conversion rules. + function_name (str | None): Optional name for dynamic record creation. + + Yields: + Record: Records generated from dictionary contents. + """ if path and path.endswith("$class"): return @@ -575,6 +809,7 @@ def normalize_nsobj(obj: Any) -> Any: def load_plist_data(fh: BinaryIO) -> Any: + """Load and normalize data from an NSKeyedArchiver-encoded plist.""" ns = NSKeyedArchiver(fh) root = ns.get("store") return normalize_nsobj(root) From ecc58e56fdbb6f036a473b24d96ba6ff62ad9976 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:03:06 +0200 Subject: [PATCH 34/52] Fix typo in docstring --- .../plugins/os/unix/bsd/darwin/macos/resources_info_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py index 408ca1284a..1cf104c6c5 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -55,7 +55,7 @@ class ResourcesInfoStringsPlugin(Plugin): """macOS Resources InfoPlist.strings plugin. - Parser localized bundle metadata for the Info.plist file. + Parses localized bundle metadata for the Info.plist file. References: - https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html From 4930701283c88aa8ebc264e3433a75135be27700 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:21:07 +0200 Subject: [PATCH 35/52] Removed unused mocks --- .../darwin/macos/duet_activity_scheduler.py | 4 +- .../bsd/darwin/macos/helpers/build_records.py | 4 +- .../bsd/darwin/macos/text_replacements.py | 2 + .../os/unix/bsd/darwin/macos/user_accounts.py | 2 + .../os/unix/bsd/darwin/macos/logs/test_asl.py | 93 ++-- .../darwin/macos/logs/test_fsck_apfs_log.py | 38 +- .../bsd/darwin/macos/logs/test_install_log.py | 44 +- .../bsd/darwin/macos/logs/test_system_log.py | 1 - .../darwin/macos/test_airport_preferences.py | 23 +- .../os/unix/bsd/darwin/macos/test_at_jobs.py | 23 +- .../darwin/macos/test_authorization_rules.py | 337 ++++++------- .../test_code_signature_coderesources.py | 144 +++--- .../bsd/darwin/macos/test_contents_info.py | 128 +++-- .../bsd/darwin/macos/test_contents_version.py | 65 +-- .../test_directory_services_local_nodes.py | 45 +- .../macos/test_duet_activity_scheduler.py | 124 ++--- .../os/unix/bsd/darwin/macos/test_gkopaque.py | 37 +- .../macos/test_global_user_preferences.py | 77 ++- .../os/unix/bsd/darwin/macos/test_groups.py | 71 ++- .../darwin/macos/test_identity_services.py | 29 +- .../darwin/macos/test_installation_history.py | 23 +- .../bsd/darwin/macos/test_keyboard_layout.py | 49 +- .../os/unix/bsd/darwin/macos/test_keychain.py | 318 ++++++------ .../unix/bsd/darwin/macos/test_launchers.py | 476 +++++++++--------- .../unix/bsd/darwin/macos/test_login_items.py | 114 ++--- .../bsd/darwin/macos/test_login_window.py | 59 +-- .../os/unix/bsd/darwin/macos/test_periodic.py | 172 +++---- .../macos/test_resources_info_strings.py | 76 ++- .../os/unix/bsd/darwin/macos/test_shadow.py | 64 +-- .../macos/test_software_update_preferences.py | 49 +- .../darwin/macos/test_system_preferences.py | 23 +- .../os/unix/bsd/darwin/macos/test_tcc.py | 87 ++-- .../darwin/macos/test_text_replacements.py | 150 +++--- .../bsd/darwin/macos/test_time_machine.py | 17 +- .../bsd/darwin/macos/test_user_accounts.py | 267 +++++----- 35 files changed, 1442 insertions(+), 1793 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index 99962736ec..1cb076ad97 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -73,8 +73,10 @@ ], ) +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. ZModelCacheRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler/z_cache", + "macos/duet_activity_scheduler/z_model_cache", [ ("string", "table"), ("path", "source"), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index fb547c694a..acc5c3f4f3 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -70,7 +70,6 @@ def build_sqlite_records( if j["join"] == "ignore": key = (j["table1"], j["table2"]) ignore_joins_map[key].append(j) - for file in files: try: with SQLite3(file) as database: @@ -575,10 +574,11 @@ def build_record( desc = select_descriptor(record_descriptors, rdict, plugin, source) if desc is None: + typed_fields = ", ".join(f"{format_key(k)} ({type(v).__name__})" for k, v in sorted(rdict.items())) plugin.target.log.exception( "No matching record descriptor for %s with fields %s", source, - sorted(map(format_key, rdict)), + typed_fields, ) return None diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index a65907c874..0bdfc715b8 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -92,6 +92,8 @@ ], ) +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. ZModelCacheRecord = TargetRecordDescriptor( "macos/text_replacements/z_model_cache", [ diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 3775ab7998..62c7cb7fcd 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -184,6 +184,8 @@ ], ) +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. ZModelCacheRecord = TargetRecordDescriptor( "macos/user_accounts/z_model_cache", [ diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py index ce3dd78759..577cf02149 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,62 +21,50 @@ ) def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc - stat_results = [] - - entries = [] data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") fs_unix.map_file(f"/var/log/asl/{test_file}", data_file) - entry = fs_unix.get(f"/var/log/asl/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - entries.append(entry) - stat_results.append(stat_result) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(ASLPlugin) - results = list(target_unix.asl()) - results.sort(key=lambda r: r.source) + target_unix.add_plugin(ASLPlugin) + results = list(target_unix.asl()) + results.sort(key=lambda r: r.source) - assert len(results) == 11 + assert len(results) == 11 - assert results[0].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[0].priority_level == 5 - assert results[0].pid == 121 - assert results[0].asl_host == "localhost" - assert results[0].sender == "syslogd" - assert results[0].facility == "syslog" - assert results[0].message == ( - "Configuration Notice:\n" - 'ASL Module "com.apple.cdscheduler" claims selected messages.\n' - "Those messages may not appear in standard system log files or in the ASL database." - ) - assert results[0].source == "/var/log/asl/2026.05.06.G80.asl" + assert results[0].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) + assert results[0].priority_level == 5 + assert results[0].pid == 121 + assert results[0].asl_host == "localhost" + assert results[0].sender == "syslogd" + assert results[0].facility == "syslog" + assert results[0].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.cdscheduler" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[0].source == "/var/log/asl/2026.05.06.G80.asl" - assert results[1].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[1].priority_level == 5 - assert results[1].pid == 121 - assert results[1].asl_host == "localhost" - assert results[1].sender == "syslogd" - assert results[1].facility == "syslog" - assert results[1].message == ( - "Configuration Notice:\n" - 'ASL Module "com.apple.install" claims selected messages.\n' - "Those messages may not appear in standard system log files or in the ASL database." - ) - assert results[1].source == "/var/log/asl/2026.05.06.G80.asl" + assert results[1].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) + assert results[1].priority_level == 5 + assert results[1].pid == 121 + assert results[1].asl_host == "localhost" + assert results[1].sender == "syslogd" + assert results[1].facility == "syslog" + assert results[1].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.install" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[1].source == "/var/log/asl/2026.05.06.G80.asl" - assert results[-1].ts == datetime(2026, 5, 6, 11, 50, 20, tzinfo=tz) - assert results[-1].priority_level == 5 - assert results[-1].pid == 122 - assert results[-1].asl_host == "localhost" - assert results[-1].sender == "syslogd" - assert results[-1].facility == "syslog" - assert results[-1].message == ( - "Configuration Notice:\n" - 'ASL Module "com.apple.eventmonitor" claims selected messages.\n' - "Those messages may not appear in standard system log files or in the ASL database." - ) - assert results[-1].source == "/var/log/asl/2026.05.06.G80.asl" + assert results[-1].ts == datetime(2026, 5, 6, 11, 50, 20, tzinfo=tz) + assert results[-1].priority_level == 5 + assert results[-1].pid == 122 + assert results[-1].asl_host == "localhost" + assert results[-1].sender == "syslogd" + assert results[-1].facility == "syslog" + assert results[-1].message == ( + "Configuration Notice:\n" + 'ASL Module "com.apple.eventmonitor" claims selected messages.\n' + "Those messages may not appear in standard system log files or in the ASL database." + ) + assert results[-1].source == "/var/log/asl/2026.05.06.G80.asl" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py index 6841f5e049..dae81db10a 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -25,29 +24,22 @@ def test_fsck_apfs_log(test_file: str, target_unix: Target, fs_unix: VirtualFile data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") fs_unix.map_file(f"/var/log/{test_file}", data_file) - entry = fs_unix.get(f"/var/log/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 + target_unix.add_plugin(FsckAPFSLogPlugin) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + results = list(target_unix.fsck_apfs_log()) + assert len(results) == 3 - target_unix.add_plugin(FsckAPFSLogPlugin) + assert results[0].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) + assert results[0].disk_path == "/dev/rdisk1s1" + assert results[0].message == "fsck_apfs started at Mon May 4 04:23:21 2026" + assert results[0].source == "/var/log/fsck_apfs.log" - results = list(target_unix.fsck_apfs_log()) - assert len(results) == 3 + assert results[1].ts is None + assert results[1].disk_path == "/dev/rdisk1s1" + assert results[1].message == "error: container /dev/rdisk1 is mounted with write access; please re-run with -l." + assert results[1].source == "/var/log/fsck_apfs.log" - assert results[0].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) - assert results[0].disk_path == "/dev/rdisk1s1" - assert results[0].message == "fsck_apfs started at Mon May 4 04:23:21 2026" - assert results[0].source == "/var/log/fsck_apfs.log" - - assert results[1].ts is None - assert results[1].disk_path == "/dev/rdisk1s1" - assert results[1].message == "error: container /dev/rdisk1 is mounted with write access; please re-run with -l." - assert results[1].source == "/var/log/fsck_apfs.log" - - assert results[-1].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) - assert results[-1].disk_path == "/dev/rdisk1s1" - assert results[-1].message == "fsck_apfs completed at Mon May 4 04:23:21 2026" - assert results[-1].source == "/var/log/fsck_apfs.log" + assert results[-1].ts == datetime(2026, 5, 4, 4, 23, 21, tzinfo=tz) + assert results[-1].disk_path == "/dev/rdisk1s1" + assert results[-1].message == "fsck_apfs completed at Mon May 4 04:23:21 2026" + assert results[-1].source == "/var/log/fsck_apfs.log" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py index 35c1259d53..e30406e3b3 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -25,32 +24,25 @@ def test_install_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesy data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/{test_file}") fs_unix.map_file(f"/var/log/{test_file}", data_file) - entry = fs_unix.get(f"/var/log/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 + target_unix.add_plugin(InstallLogPlugin) - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + results = list(target_unix.install_log()) + assert len(results) == 3 - target_unix.add_plugin(InstallLogPlugin) + assert results[0].ts == datetime(2026, 3, 25, 14, 6, 59, tzinfo=tz) + assert results[0].host == "localhost" + assert results[0].component == "Installer" + assert results[0].message == "Progress[57]: Progress UI App Starting" + assert results[0].source == "/var/log/install.log" - results = list(target_unix.install_log()) - assert len(results) == 3 + assert results[1].ts == datetime(2026, 3, 25, 14, 7, tzinfo=tz) + assert results[1].host == "localhost" + assert results[1].component == "Installer" + assert results[1].message == "Progress[57]: Logging also using os_log, installerProgressLog = 0xc6501c080" + assert results[1].source == "/var/log/install.log" - assert results[0].ts == datetime(2026, 3, 25, 14, 6, 59, tzinfo=tz) - assert results[0].host == "localhost" - assert results[0].component == "Installer" - assert results[0].message == "Progress[57]: Progress UI App Starting" - assert results[0].source == "/var/log/install.log" - - assert results[1].ts == datetime(2026, 3, 25, 14, 7, tzinfo=tz) - assert results[1].host == "localhost" - assert results[1].component == "Installer" - assert results[1].message == "Progress[57]: Logging also using os_log, installerProgressLog = 0xc6501c080" - assert results[1].source == "/var/log/install.log" - - assert results[-1].ts == datetime(2026, 3, 25, 15, 18, 58, tzinfo=tz) - assert results[-1].host == "users-Virtual-Machine" - assert results[-1].component == "loginwindow[1042]:" - assert results[-1].message == "+[SUOSULoginCredentialPolicy currentLoginCredentialPolicy] = 0" - assert results[-1].source == "/var/log/install.log" + assert results[-1].ts == datetime(2026, 3, 25, 15, 18, 58, tzinfo=tz) + assert results[-1].host == "users-Virtual-Machine" + assert results[-1].component == "loginwindow[1042]:" + assert results[-1].message == "+[SUOSULoginCredentialPolicy currentLoginCredentialPolicy] = 0" + assert results[-1].source == "/var/log/install.log" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py index 493f004223..fca33742e8 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py @@ -23,7 +23,6 @@ def test_system_log(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/{test_file}") diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py index e78e0ce4e6..824f8fa03d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,20 +21,14 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Preferences/SystemConfiguration/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Preferences/SystemConfiguration/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(AirportPreferencesPlugin) - target_unix.add_plugin(AirportPreferencesPlugin) + results = list(target_unix.airport_preferences()) + assert len(results) == 1 - results = list(target_unix.airport_preferences()) - assert len(results) == 1 - - assert results[0].counter == 2 - assert results[0].device_uuid == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" - assert results[0].version_number == 7200 - assert results[0].preferred_order == [] - assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" + assert results[0].counter == 2 + assert results[0].device_uuid == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" + assert results[0].version_number == 7200 + assert results[0].preferred_order == [] + assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py index 76a7d324dd..e8f19560f9 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -26,20 +25,14 @@ def test_at_jobs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem tz = timezone.utc data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/usr/lib/cron/jobs/{test_file}", data_file) - entry = fs_unix.get(f"/usr/lib/cron/jobs/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(AtJobsPlugin) - target_unix.add_plugin(AtJobsPlugin) + results = list(target_unix.at_jobs()) + assert len(results) == 1 - results = list(target_unix.at_jobs()) - assert len(results) == 1 - - assert results[0].queue == "a" - assert results[0].seq == 9 - assert results[0].execution_time == datetime(2026, 5, 12, 16, 0, 0, tzinfo=tz) - assert results[0].command == 'say "hello"' - assert results[0].source == "/usr/lib/cron/jobs/a0000901c45260" + assert results[0].queue == "a" + assert results[0].seq == 9 + assert results[0].execution_time == datetime(2026, 5, 12, 16, 0, 0, tzinfo=tz) + assert results[0].command == 'say "hello"' + assert results[0].source == "/usr/lib/cron/jobs/a0000901c45260" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py index f254a11859..67c04eab79 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -24,174 +23,168 @@ def test_authorization_rules(test_file: str, target_unix: Target, fs_unix: Virtu tz = timezone.utc data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/var/db/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(AuthorizationRulesPlugin) - - results = list(target_unix.authorization_rules()) - - assert len(results) == 249 - - assert results[0].table == "sqlite_sequence" - assert results[0].name == "rules" - assert results[0].seq == 205 - assert results[0].source == "/var/db/auth.db" - - assert sorted(results[175].tables) == ["delegates_map", "rules", "rules_history"] - assert results[175].rules_id == 159 - assert results[175].rules_name == "com.apple.tcc.util.admin" - assert results[175].rules_type == 1 - assert results[175].rules_class == 2 - assert results[175].rules_group is None - assert results[175].rules_kofn is None - assert results[175].rules_timeout is None - assert results[175].rules_flags == 0 - assert results[175].rules_tries is None - assert results[175].rules_version == 0 - assert results[175].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[175].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[175].rules_hash is None - assert results[175].rules_identifier is None - assert results[175].rules_requirement is None - assert results[175].rules_comment == "For modification of TCC settings." - assert results[175].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) - assert results[175].rules_history_source == "authd" - assert results[175].rules_history_operation == 0 - assert results[175].mechanisms_map_m_id is None - assert results[175].mechanisms_map_ord is None - assert results[175].mechanisms_plugin is None - assert results[175].mechanisms_param is None - assert results[175].mechanisms_privileged is None - assert results[175].rules_delegates_map == ["{'d_id': 9, 'ord': 0}"] - assert results[175].source == "/var/db/auth.db" - - assert sorted(results[177].tables) == ["mechanisms", "mechanisms_map", "rules", "rules_history"] - assert results[177].rules_id == 161 - assert results[177].rules_name == "system.login.filevault" - assert results[177].rules_type == 1 - assert results[177].rules_class == 3 - assert results[177].rules_group is None - assert results[177].rules_kofn is None - assert results[177].rules_timeout is None - assert results[177].rules_flags == 1 - assert results[177].rules_tries == "10000" - assert results[177].rules_version == 0 - assert results[177].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[177].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[177].rules_hash is None - assert results[177].rules_identifier is None - assert results[177].rules_requirement is None - assert results[177].rules_comment == "Login mechanism based rule for Filevault." - assert results[177].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) - assert results[177].rules_history_source == "authd" - assert results[177].rules_history_operation == 0 - assert results[177].mechanisms_map_m_id == 24 - assert results[177].mechanisms_map_ord == 0 - assert results[177].mechanisms_plugin == "builtin" - assert results[177].mechanisms_param == "policy-banner" - assert results[177].mechanisms_privileged == 0 - assert results[177].rules_delegates_map == [] - assert results[177].source == "/var/db/auth.db" - - assert sorted(results[-9].tables) == ["rules", "rules_history"] - assert results[-9].rules_id == 199 - assert results[-9].rules_name == "system.preferences.security.remotepair" - assert results[-9].rules_type == 1 - assert results[-9].rules_class == 1 - assert results[-9].rules_group == "admin" - assert results[-9].rules_kofn is None - assert results[-9].rules_timeout == 30 - assert results[-9].rules_flags == 73 - assert results[-9].rules_tries == "10000" - assert results[-9].rules_version == 1 - assert results[-9].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[-9].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[-9].rules_hash is None - assert results[-9].rules_identifier is None - assert results[-9].rules_requirement is None - assert results[-9].rules_comment == "Used by Bezel Services to gate IR remote pairing." - assert results[-9].rules_delegates_map == [] - assert results[-9].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) - assert results[-9].rules_history_source == "authd" - assert results[-9].rules_history_operation == 0 - assert results[-9].mechanisms_map_m_id is None - assert results[-9].mechanisms_map_ord is None - assert results[-9].mechanisms_plugin is None - assert results[-9].mechanisms_param is None - assert results[-9].mechanisms_privileged is None - assert results[-9].source == "/var/db/auth.db" - - assert sorted(results[-4].tables) == ["rules", "rules_history"] - assert results[-4].rules_id == 204 - assert results[-4].rules_name == "com.apple.Safari.allow-apple-events-to-run-javascript" - assert results[-4].rules_type == 1 - assert results[-4].rules_class == 1 - assert results[-4].rules_group is None - assert results[-4].rules_kofn is None - assert results[-4].rules_timeout == 2147483647 - assert results[-4].rules_flags == 12 - assert results[-4].rules_tries == "10000" - assert results[-4].rules_version == 0 - assert results[-4].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[-4].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) - assert results[-4].rules_hash is None - assert results[-4].rules_identifier is None - assert results[-4].rules_requirement is None - assert ( - results[-4].rules_comment - == "This right is used by Safari to allow Apple Events to run JavaScript on web pages." - ) - assert results[-4].rules_delegates_map == [] - assert results[-4].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) - assert results[-4].rules_history_source == "authd" - assert results[-4].rules_history_operation == 0 - assert results[-4].mechanisms_map_m_id is None - assert results[-4].mechanisms_map_ord is None - assert results[-4].mechanisms_plugin is None - assert results[-4].mechanisms_param is None - assert results[-4].mechanisms_privileged is None - assert results[-4].source == "/var/db/auth.db" - - assert sorted(results[-3].tables) == ["delegates_map", "rules", "rules_history"] - assert results[-3].rules_id == 205 - assert results[-3].rules_name == "com.apple.wifi" - assert results[-3].rules_type == 1 - assert results[-3].rules_class == 2 - assert results[-3].rules_group is None - assert results[-3].rules_kofn == 1 - assert results[-3].rules_timeout is None - assert results[-3].rules_flags == 0 - assert results[-3].rules_tries is None - assert results[-3].rules_version == 0 - assert results[-3].rules_created == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) - assert results[-3].rules_modified == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) - assert results[-3].rules_hash is None - assert results[-3].rules_identifier == "com.apple.airport.airportd" - assert results[-3].rules_requirement is not None - assert isinstance(results[-3].rules_requirement, (bytes, bytearray)) - assert results[-3].rules_comment == "For restricting WiFi control" - assert results[-3].rules_delegates_map == [ - "{'d_id': 22, 'ord': 0}", - "{'d_id': 26, 'ord': 1}", - "{'d_id': 25, 'ord': 2}", - "{'d_id': 35, 'ord': 3}", - ] - assert results[-3].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 2, tzinfo=tz) - assert results[-3].rules_history_source == "/usr/libexec/airportd" - assert results[-3].rules_history_operation == 0 - assert results[-3].mechanisms_map_m_id is None - assert results[-3].mechanisms_map_ord is None - assert results[-3].mechanisms_plugin is None - assert results[-3].mechanisms_param is None - assert results[-3].mechanisms_privileged is None - assert results[-3].source == "/var/db/auth.db" - - assert results[-1].table == "config" - assert results[-1].key == "data_ts" - assert results[-1].value == "795677157.0" - assert results[-1].source == "/var/db/auth.db" + + target_unix.add_plugin(AuthorizationRulesPlugin) + + results = list(target_unix.authorization_rules()) + + assert len(results) == 249 + + assert results[0].table == "sqlite_sequence" + assert results[0].name == "rules" + assert results[0].seq == 205 + assert results[0].source == "/var/db/auth.db" + + assert sorted(results[175].tables) == ["delegates_map", "rules", "rules_history"] + assert results[175].rules_id == 159 + assert results[175].rules_name == "com.apple.tcc.util.admin" + assert results[175].rules_type == 1 + assert results[175].rules_class == 2 + assert results[175].rules_group is None + assert results[175].rules_kofn is None + assert results[175].rules_timeout is None + assert results[175].rules_flags == 0 + assert results[175].rules_tries is None + assert results[175].rules_version == 0 + assert results[175].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[175].rules_hash is None + assert results[175].rules_identifier is None + assert results[175].rules_requirement is None + assert results[175].rules_comment == "For modification of TCC settings." + assert results[175].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[175].rules_history_source == "authd" + assert results[175].rules_history_operation == 0 + assert results[175].mechanisms_map_m_id is None + assert results[175].mechanisms_map_ord is None + assert results[175].mechanisms_plugin is None + assert results[175].mechanisms_param is None + assert results[175].mechanisms_privileged is None + assert results[175].rules_delegates_map == ["{'d_id': 9, 'ord': 0}"] + assert results[175].source == "/var/db/auth.db" + + assert sorted(results[177].tables) == ["mechanisms", "mechanisms_map", "rules", "rules_history"] + assert results[177].rules_id == 161 + assert results[177].rules_name == "system.login.filevault" + assert results[177].rules_type == 1 + assert results[177].rules_class == 3 + assert results[177].rules_group is None + assert results[177].rules_kofn is None + assert results[177].rules_timeout is None + assert results[177].rules_flags == 1 + assert results[177].rules_tries == "10000" + assert results[177].rules_version == 0 + assert results[177].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[177].rules_hash is None + assert results[177].rules_identifier is None + assert results[177].rules_requirement is None + assert results[177].rules_comment == "Login mechanism based rule for Filevault." + assert results[177].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[177].rules_history_source == "authd" + assert results[177].rules_history_operation == 0 + assert results[177].mechanisms_map_m_id == 24 + assert results[177].mechanisms_map_ord == 0 + assert results[177].mechanisms_plugin == "builtin" + assert results[177].mechanisms_param == "policy-banner" + assert results[177].mechanisms_privileged == 0 + assert results[177].rules_delegates_map == [] + assert results[177].source == "/var/db/auth.db" + + assert sorted(results[-9].tables) == ["rules", "rules_history"] + assert results[-9].rules_id == 199 + assert results[-9].rules_name == "system.preferences.security.remotepair" + assert results[-9].rules_type == 1 + assert results[-9].rules_class == 1 + assert results[-9].rules_group == "admin" + assert results[-9].rules_kofn is None + assert results[-9].rules_timeout == 30 + assert results[-9].rules_flags == 73 + assert results[-9].rules_tries == "10000" + assert results[-9].rules_version == 1 + assert results[-9].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-9].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-9].rules_hash is None + assert results[-9].rules_identifier is None + assert results[-9].rules_requirement is None + assert results[-9].rules_comment == "Used by Bezel Services to gate IR remote pairing." + assert results[-9].rules_delegates_map == [] + assert results[-9].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[-9].rules_history_source == "authd" + assert results[-9].rules_history_operation == 0 + assert results[-9].mechanisms_map_m_id is None + assert results[-9].mechanisms_map_ord is None + assert results[-9].mechanisms_plugin is None + assert results[-9].mechanisms_param is None + assert results[-9].mechanisms_privileged is None + assert results[-9].source == "/var/db/auth.db" + + assert sorted(results[-4].tables) == ["rules", "rules_history"] + assert results[-4].rules_id == 204 + assert results[-4].rules_name == "com.apple.Safari.allow-apple-events-to-run-javascript" + assert results[-4].rules_type == 1 + assert results[-4].rules_class == 1 + assert results[-4].rules_group is None + assert results[-4].rules_kofn is None + assert results[-4].rules_timeout == 2147483647 + assert results[-4].rules_flags == 12 + assert results[-4].rules_tries == "10000" + assert results[-4].rules_version == 0 + assert results[-4].rules_created == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-4].rules_modified == datetime(2026, 3, 25, 14, 7, 1, 144442, tzinfo=tz) + assert results[-4].rules_hash is None + assert results[-4].rules_identifier is None + assert results[-4].rules_requirement is None + assert ( + results[-4].rules_comment + == "This right is used by Safari to allow Apple Events to run JavaScript on web pages." + ) + assert results[-4].rules_delegates_map == [] + assert results[-4].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 1, tzinfo=tz) + assert results[-4].rules_history_source == "authd" + assert results[-4].rules_history_operation == 0 + assert results[-4].mechanisms_map_m_id is None + assert results[-4].mechanisms_map_ord is None + assert results[-4].mechanisms_plugin is None + assert results[-4].mechanisms_param is None + assert results[-4].mechanisms_privileged is None + assert results[-4].source == "/var/db/auth.db" + + assert sorted(results[-3].tables) == ["delegates_map", "rules", "rules_history"] + assert results[-3].rules_id == 205 + assert results[-3].rules_name == "com.apple.wifi" + assert results[-3].rules_type == 1 + assert results[-3].rules_class == 2 + assert results[-3].rules_group is None + assert results[-3].rules_kofn == 1 + assert results[-3].rules_timeout is None + assert results[-3].rules_flags == 0 + assert results[-3].rules_tries is None + assert results[-3].rules_version == 0 + assert results[-3].rules_created == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) + assert results[-3].rules_modified == datetime(2026, 3, 25, 14, 7, 2, 383632, tzinfo=tz) + assert results[-3].rules_hash is None + assert results[-3].rules_identifier == "com.apple.airport.airportd" + assert results[-3].rules_requirement is not None + assert isinstance(results[-3].rules_requirement, (bytes, bytearray)) + assert results[-3].rules_comment == "For restricting WiFi control" + assert results[-3].rules_delegates_map == [ + "{'d_id': 22, 'ord': 0}", + "{'d_id': 26, 'ord': 1}", + "{'d_id': 25, 'ord': 2}", + "{'d_id': 35, 'ord': 3}", + ] + assert results[-3].rules_history_timestamp == datetime(2026, 3, 25, 14, 7, 2, tzinfo=tz) + assert results[-3].rules_history_source == "/usr/libexec/airportd" + assert results[-3].rules_history_operation == 0 + assert results[-3].mechanisms_map_m_id is None + assert results[-3].mechanisms_map_ord is None + assert results[-3].mechanisms_plugin is None + assert results[-3].mechanisms_param is None + assert results[-3].mechanisms_privileged is None + assert results[-3].source == "/var/db/auth.db" + + assert results[-1].table == "config" + assert results[-1].key == "data_ts" + assert results[-1].value == "795677157.0" + assert results[-1].source == "/var/db/auth.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py index c3a8064d97..62360686cf 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -41,87 +40,70 @@ def test_code_signature_coderesources( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/code_signature/{name}") fs_unix.map_file(f"{path}", data_file) - entry = fs_unix.get(f"{path}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(CodeSignatureCodeResourcesPlugin) - - results = list(target_unix.code_signature_coderesources()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - - assert len(results) == 294 - - assert results[0].omit - assert results[0].weight == 20.0 - assert results[0].plist_path == "rules/^.*" - assert ( - results[0].source - == "/System/Library/CoreServices/FileProvider-Feedback.app/Contents/_CodeSignature/CodeResources" - ) - - assert results[3].cdhash == "\x18\udc8f\x03\x1f\udcb2f(\udcafD.F\udcdbK\udcc05\udc91B\x1f\x06\udca9" - assert results[3].requirement == 'identifier "com.apple.AppleUSBEthernet_kasan" and anchor apple' - assert results[3].plist_path == "files2/macOS/AppleUSBEthernet_kasan" - assert ( - results[3].source - == "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources" - ) - - assert results[7].cdhash == "B\udcd5uȈ\udcf1K\x03qd\udce6\udcf2T4L\udc95\udcc4\udce0\x06\udc9f" - assert results[7].requirement == 'identifier "com.apple.AppleUSBHostS5L8930X_kasan" and anchor apple' - assert results[7].plist_path == "files2/macOS/AppleUSBHostS5L8930X_kasan" - assert ( - results[7].source - == "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources" - ) - - assert results[13].nested - assert results[13].weight == 0.0 - assert ( - results[13].plist_path - == "rules2/^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/" # noqa E501 - ) - assert ( - results[13].source - == "/System/Library/Extensions/AudioDMAController_T8140.kext/Contents/_CodeSignature/CodeResources" - ) - - assert ( - results[17].hash2 - == "yϑ\udcc9rl>h%\udcbek\udccf{\udca5E\udc90\udcdf-\x00R\udcac\udcd8\udcc3m.\udce6,(;\udce1v\x00" - ) - assert results[17].optional is None - assert results[17].plist_path == "files2/version.plist" - assert ( - results[17].source - == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" - ) - assert results[29].optional - assert results[29].weight == 1000.0 - assert results[29].plist_path == "rules2/^Resources/.*\\.lproj/" - assert ( - results[29].source - == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" - ) - - assert results[153].hash == "\udcd7p\udcdfIJ\udcc2\x0e\udcc9\x161ř{\udca3\udc89\udce7M\udcd7\udcf3Q" - assert results[153].optional - assert results[153].plist_path == "files/Resources/zh_TW.lproj/MobileDeviceUpdateController.strings" - assert ( - results[153].source - == "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/MobileDeviceUpdater.app/Contents/_CodeSignature/CodeResources" # noqa E501 - ) + target_unix.add_plugin(CodeSignatureCodeResourcesPlugin) + + results = list(target_unix.code_signature_coderesources()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + + assert len(results) == 294 + + assert results[0].omit + assert results[0].weight == 20.0 + assert results[0].plist_path == "rules/^.*" + assert ( + results[0].source + == "/System/Library/CoreServices/FileProvider-Feedback.app/Contents/_CodeSignature/CodeResources" + ) + + assert results[3].cdhash == "\x18\udc8f\x03\x1f\udcb2f(\udcafD.F\udcdbK\udcc05\udc91B\x1f\x06\udca9" + assert results[3].requirement == 'identifier "com.apple.AppleUSBEthernet_kasan" and anchor apple' + assert results[3].plist_path == "files2/macOS/AppleUSBEthernet_kasan" + assert results[3].source == "/System/Library/Extensions/AppleUSBEthernet.kext/Contents/_CodeSignature/CodeResources" + + assert results[7].cdhash == "B\udcd5uȈ\udcf1K\x03qd\udce6\udcf2T4L\udc95\udcc4\udce0\x06\udc9f" + assert results[7].requirement == 'identifier "com.apple.AppleUSBHostS5L8930X_kasan" and anchor apple' + assert results[7].plist_path == "files2/macOS/AppleUSBHostS5L8930X_kasan" + assert ( + results[7].source + == "/System/Library/Extensions/AppleUSBHostS5L8930X.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[13].nested + assert results[13].weight == 0.0 + assert ( + results[13].plist_path + == "rules2/^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/" # noqa E501 + ) + assert ( + results[13].source + == "/System/Library/Extensions/AudioDMAController_T8140.kext/Contents/_CodeSignature/CodeResources" + ) + + assert ( + results[17].hash2 + == "yϑ\udcc9rl>h%\udcbek\udccf{\udca5E\udc90\udcdf-\x00R\udcac\udcd8\udcc3m.\udce6,(;\udce1v\x00" + ) + assert results[17].optional is None + assert results[17].plist_path == "files2/version.plist" + assert ( + results[17].source == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[29].optional + assert results[29].weight == 1000.0 + assert results[29].plist_path == "rules2/^Resources/.*\\.lproj/" + assert ( + results[29].source == "/System/Library/Extensions/EndpointSecurity.kext/Contents/_CodeSignature/CodeResources" + ) + + assert results[153].hash == "\udcd7p\udcdfIJ\udcc2\x0e\udcc9\x161ř{\udca3\udc89\udce7M\udcd7\udcf3Q" + assert results[153].optional + assert results[153].plist_path == "files/Resources/zh_TW.lproj/MobileDeviceUpdateController.strings" + assert ( + results[153].source + == "/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/MobileDeviceUpdater.app/Contents/_CodeSignature/CodeResources" # noqa E501 + ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py index 7cdb85f31d..110fca8bac 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -33,82 +32,69 @@ def test_contents_info( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: - stat_results = [] - - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_info/{name}") fs_unix.map_file(f"{path}", data_file) - entry = fs_unix.get(f"{path}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(ContentsInfoPlugin) + target_unix.add_plugin(ContentsInfoPlugin) - results = list(target_unix.contents_info()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + results = list(target_unix.contents_info()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 7 + assert len(results) == 7 - assert results[0].BuildMachineOSBuild == "23A344017" - assert results[0].CFBundleDevelopmentRegion == "English" - assert results[0].CFBundleExecutable == "UnmountAssistantAgent" - assert results[0].CFBundleIdentifier == "com.apple.UnmountAssistantAgent" - assert results[0].CFBundleInfoDictionaryVersion == "6.0" - assert results[0].CFBundleName == "UnmountAssistantAgent" - assert results[0].CFBundlePackageType == "APPL" - assert results[0].CFBundleShortVersionString == "5.0" - assert results[0].CFBundleSignature == "????" - assert results[0].CFBundleSupportedPlatforms == ["macOSX"] - assert results[0].CFBundleVersion == "5.0" - assert results[0].DTCompiler == "com.apple.compilers.llvm.clang.1_0" - assert results[0].DTPlatformBuild == "" - assert results[0].DTPlatformName == "macosx" - assert results[0].DTPlatformVersion == "26.4" - assert results[0].DTSDKBuild == "25E222" - assert results[0].DTSDKName == "macosx26.4.internal" - assert results[0].DTXcode == "2630" - assert results[0].DTXcodeBuild == "17E6107" - assert results[0].LSMinimumSystemVersion == "26.4" - assert results[0].LSUIElement - assert results[0].NSPrincipalClass == "NSApplication" - assert results[0].source == "/System/Library/CoreServices/UnmountAssistantAgent.app/Contents/Info.plist" + assert results[0].BuildMachineOSBuild == "23A344017" + assert results[0].CFBundleDevelopmentRegion == "English" + assert results[0].CFBundleExecutable == "UnmountAssistantAgent" + assert results[0].CFBundleIdentifier == "com.apple.UnmountAssistantAgent" + assert results[0].CFBundleInfoDictionaryVersion == "6.0" + assert results[0].CFBundleName == "UnmountAssistantAgent" + assert results[0].CFBundlePackageType == "APPL" + assert results[0].CFBundleShortVersionString == "5.0" + assert results[0].CFBundleSignature == "????" + assert results[0].CFBundleSupportedPlatforms == ["macOSX"] + assert results[0].CFBundleVersion == "5.0" + assert results[0].DTCompiler == "com.apple.compilers.llvm.clang.1_0" + assert results[0].DTPlatformBuild == "" + assert results[0].DTPlatformName == "macosx" + assert results[0].DTPlatformVersion == "26.4" + assert results[0].DTSDKBuild == "25E222" + assert results[0].DTSDKName == "macosx26.4.internal" + assert results[0].DTXcode == "2630" + assert results[0].DTXcodeBuild == "17E6107" + assert results[0].LSMinimumSystemVersion == "26.4" + assert results[0].LSUIElement + assert results[0].NSPrincipalClass == "NSApplication" + assert results[0].source == "/System/Library/CoreServices/UnmountAssistantAgent.app/Contents/Info.plist" - assert results[4].BuildMachineOSBuild == "23A344017" - assert results[4].CFBundleDevelopmentRegion == "English" - assert results[4].CFBundleExecutable == "AppleHPET" - assert results[4].CFBundleIdentifier == "com.apple.driver.AppleHPET" - assert results[4].CFBundleInfoDictionaryVersion == "6.0" - assert results[4].CFBundleName == "High Precision Event Timer Driver" - assert results[4].CFBundlePackageType == "KEXT" - assert results[4].CFBundleShortVersionString == "1.8" - assert results[4].CFBundleSignature == "????" - assert results[4].CFBundleSupportedPlatforms == ["macOSX"] - assert results[4].CFBundleVersion == "1.8" - assert results[4].DTCompiler == "com.apple.compilers.llvm.clang.1_0" - assert results[4].DTPlatformBuild == "25E245" - assert results[4].DTPlatformName == "macosx" - assert results[4].DTPlatformVersion == "26.4" - assert results[4].DTSDKBuild == "25E245" - assert results[4].DTSDKName == "macosx26.4.internal" - assert results[4].DTXcode == "2630" - assert results[4].DTXcodeBuild == "17E6107" - assert results[4].LSMinimumSystemVersion == "26.4" - assert results[4].NSHumanReadableCopyright == ("Copyright © 2005-2012 Apple Inc. All rights reserved.") - assert results[4].OSBundleRequired == "Root" - assert results[4].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" + assert results[4].BuildMachineOSBuild == "23A344017" + assert results[4].CFBundleDevelopmentRegion == "English" + assert results[4].CFBundleExecutable == "AppleHPET" + assert results[4].CFBundleIdentifier == "com.apple.driver.AppleHPET" + assert results[4].CFBundleInfoDictionaryVersion == "6.0" + assert results[4].CFBundleName == "High Precision Event Timer Driver" + assert results[4].CFBundlePackageType == "KEXT" + assert results[4].CFBundleShortVersionString == "1.8" + assert results[4].CFBundleSignature == "????" + assert results[4].CFBundleSupportedPlatforms == ["macOSX"] + assert results[4].CFBundleVersion == "1.8" + assert results[4].DTCompiler == "com.apple.compilers.llvm.clang.1_0" + assert results[4].DTPlatformBuild == "25E245" + assert results[4].DTPlatformName == "macosx" + assert results[4].DTPlatformVersion == "26.4" + assert results[4].DTSDKBuild == "25E245" + assert results[4].DTSDKName == "macosx26.4.internal" + assert results[4].DTXcode == "2630" + assert results[4].DTXcodeBuild == "17E6107" + assert results[4].LSMinimumSystemVersion == "26.4" + assert results[4].NSHumanReadableCopyright == ("Copyright © 2005-2012 Apple Inc. All rights reserved.") + assert results[4].OSBundleRequired == "Root" + assert results[4].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" - assert results[6].com_apple_iokit_IOACPIFamily == "1.1.0" - assert results[6].com_apple_kpi_iokit == "9.0.0" - assert results[6].com_apple_kpi_libkern == "9.0.0" - assert results[6].com_apple_kpi_mach == "9.0.0" - assert results[6].com_apple_kpi_unsupported == "9.0.0" - assert results[6].plist_path == "OSBundleLibraries" - assert results[6].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" + assert results[6].com_apple_iokit_IOACPIFamily == "1.1.0" + assert results[6].com_apple_kpi_iokit == "9.0.0" + assert results[6].com_apple_kpi_libkern == "9.0.0" + assert results[6].com_apple_kpi_mach == "9.0.0" + assert results[6].com_apple_kpi_unsupported == "9.0.0" + assert results[6].plist_path == "OSBundleLibraries" + assert results[6].source == "/System/Library/Extensions/AppleHPET.kext/Contents/Info.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py index 2e2d6eebd2..e6dbba0f04 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -35,52 +34,36 @@ def test_contents_version( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/contents_version/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(ContentsVersionPlugin) + target_unix.add_plugin(ContentsVersionPlugin) - results = list(target_unix.contents_version()) - results.sort(key=lambda r: r.source) + results = list(target_unix.contents_version()) + results.sort(key=lambda r: r.source) - assert len(results) == 3 + assert len(results) == 3 - assert results[0].build_alias_of == "Ensemble" - assert results[0].build_version == 1983 - assert results[0].cf_bundle_short_version_string == "1.0" - assert results[0].cf_bundle_version == "174.4.1" - assert results[0].project_name == "Ensemble_executables" - assert results[0].source_version == 174004001000000 - assert results[0].source == "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist" + assert results[0].build_alias_of == "Ensemble" + assert results[0].build_version == 1983 + assert results[0].cf_bundle_short_version_string == "1.0" + assert results[0].cf_bundle_version == "174.4.1" + assert results[0].project_name == "Ensemble_executables" + assert results[0].source_version == 174004001000000 + assert results[0].source == "/System/Library/CoreServices/UniversalControl.app/Contents/version.plist" - assert results[1].build_version == 100 - assert results[1].cf_bundle_short_version_string == "1.0" - assert results[1].cf_bundle_version == "1" - assert results[1].project_name == "MobileBluetooth" - assert results[1].source_version == 194026001000001 - assert ( - results[1].source == "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist" - ) + assert results[1].build_version == 100 + assert results[1].cf_bundle_short_version_string == "1.0" + assert results[1].cf_bundle_version == "1" + assert results[1].project_name == "MobileBluetooth" + assert results[1].source_version == 194026001000001 + assert results[1].source == "/System/Library/Frameworks/IOBluetoothUI.framework/Versions/A/Resources/version.plist" - assert results[2].build_alias_of == "SwiftUI" - assert results[2].build_version == 12 - assert results[2].cf_bundle_short_version_string == "7.4.27" - assert results[2].cf_bundle_version == "7.4.27" - assert results[2].project_name == "SwiftUICore" - assert results[2].source_version == 7004027000000 - assert ( - results[2].source == "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist" - ) + assert results[2].build_alias_of == "SwiftUI" + assert results[2].build_version == 12 + assert results[2].cf_bundle_short_version_string == "7.4.27" + assert results[2].cf_bundle_version == "7.4.27" + assert results[2].project_name == "SwiftUICore" + assert results[2].source_version == 7004027000000 + assert results[2].source == "/System/Library/Frameworks/SwiftUICore.framework/Versions/A/Resources/version.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py index 5bab000939..5326e66f0c 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -27,40 +26,28 @@ ) def test_directory_services_local_nodes(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: tz = timezone.utc - stat_results = [] - entries = [] - for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(DirectoryServicesLocalNodesPlugin) + target_unix.add_plugin(DirectoryServicesLocalNodesPlugin) - results = list(target_unix.directory_services_local_nodes()) + results = list(target_unix.directory_services_local_nodes()) - assert len(results) == 1452 + assert len(results) == 1452 - results.sort(key=lambda r: r.tables) + results.sort(key=lambda r: r.tables) - assert results[0].tables == ["generateduid", "rec:groups"] - assert results[0].filetime == datetime(2026, 3, 20, 4, 25, 57, tzinfo=tz) - assert results[0].filename == "_appowner.plist" - assert results[0].recordtype == "groups" - assert results[0].value == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000057" - assert results[0].source == "/var/db/dslocal/nodes/Default/sqlindex" + assert results[0].tables == ["generateduid", "rec:groups"] + assert results[0].filetime == datetime(2026, 3, 20, 4, 25, 57, tzinfo=tz) + assert results[0].filename == "_appowner.plist" + assert results[0].recordtype == "groups" + assert results[0].value == "ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000057" + assert results[0].source == "/var/db/dslocal/nodes/Default/sqlindex" - assert results[-69].tables == ["users"] - assert results[-69].filetime is None - assert results[-69].filename == "_appserveradm.plist" - assert results[-69].recordtype == "groups" - assert results[-69].value == "_mbsetupuser" - assert results[-69].source == "/var/db/dslocal/nodes/Default/sqlindex" + assert results[-69].tables == ["users"] + assert results[-69].filetime is None + assert results[-69].filename == "_appserveradm.plist" + assert results[-69].recordtype == "groups" + assert results[-69].value == "_mbsetupuser" + assert results[-69].source == "/var/db/dslocal/nodes/Default/sqlindex" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index 11768ace1e..650ea885c3 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -1,11 +1,9 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest -from dissect.target.helpers.record import UnixUserRecord from dissect.target.plugins.os.unix.bsd.darwin.macos.duet_activity_scheduler import DuetActivitySchedulerPlugin from tests._utils import absolute_path @@ -24,79 +22,57 @@ ], ) def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: - user = UnixUserRecord( - name="user", - uid=501, - gid=20, - home="/Users/user", - shell="/bin/zsh", - ) - target_unix.users = lambda: [ - user, - ] - - stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler/{test_file}") fs_unix.map_file(f"/var/db/DuetActivityScheduler/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/DuetActivityScheduler/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - entries.append(entry) - stat_results.append(stat_result) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(DuetActivitySchedulerPlugin) - - results = list(target_unix.duet_activity_scheduler()) - - assert len(results) == 55 - - assert results[0].table == "ZGROUP" - assert results[0].z_pk == 1 - assert results[0].z_ent == 2 - assert results[0].z_opt == 1 - assert results[0].z_max_concurrent == 6 - assert results[0].z_name == "com.apple.dasd.defaultNetwork" - assert results[0].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - assert results[48].table == "Z_PRIMARYKEY" - assert results[48].z_ent == 1 - assert results[48].z_name == "Activity" - assert results[48].z_super == 0 - assert results[48].z_max == 0 - assert results[48].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - - assert results[51].ns_persistence_maximum_framework_version == 1526 - assert results[51].ns_store_model_version_identifiers == [""] - assert results[51].ns_store_type == "SQLite" - assert results[51].ns_auto_vacuum_level == 2 - assert ( - results[51].ns_store_model_version_hashes_digest - == "rvkNkhmOezVbzsczB2H+gkUsiGN7C2d7a9TtXbZPD0kn0MZYSVEGM64BycQewlVstp1ROUAOBjEmkbNTkiu6JA==" - ) - assert results[51].ns_store_model_version_checksum_key == "rXdwmenydb+cl65S3tSy9rIL6lkwSXqL7UvaJVK21Lc=" - assert results[51].ns_persistence_framework_version == 1526 - assert results[51].ns_store_model_version_hashes_version == 3 - assert results[51].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - - assert results[52].activity is not None - assert isinstance(results[52].activity, (bytes, bytearray)) - assert results[52].group_binary is not None - assert isinstance(results[52].group_binary, (bytes, bytearray)) - assert results[52].trigger is not None - assert isinstance(results[52].trigger, (bytes, bytearray)) - assert results[52].plist_path == "NSStoreModelVersionHashes" - assert results[52].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - - assert results[53].table == "Z_METADATA" - assert results[53].z_version == 1 - assert results[53].z_uuid == "514A8E5F-DE48-4C3E-9129-3AF14DEAD0E1" - assert results[53].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" - - assert results[54].table == "Z_MODELCACHE" - assert results[54].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + target_unix.add_plugin(DuetActivitySchedulerPlugin) + + results = list(target_unix.duet_activity_scheduler()) + + assert len(results) == 55 + + assert results[0].table == "ZGROUP" + assert results[0].z_pk == 1 + assert results[0].z_ent == 2 + assert results[0].z_opt == 1 + assert results[0].z_max_concurrent == 6 + assert results[0].z_name == "com.apple.dasd.defaultNetwork" + assert results[0].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[48].table == "Z_PRIMARYKEY" + assert results[48].z_ent == 1 + assert results[48].z_name == "Activity" + assert results[48].z_super == 0 + assert results[48].z_max == 0 + assert results[48].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[51].ns_persistence_maximum_framework_version == 1526 + assert results[51].ns_store_model_version_identifiers == [""] + assert results[51].ns_store_type == "SQLite" + assert results[51].ns_auto_vacuum_level == 2 + assert ( + results[51].ns_store_model_version_hashes_digest + == "rvkNkhmOezVbzsczB2H+gkUsiGN7C2d7a9TtXbZPD0kn0MZYSVEGM64BycQewlVstp1ROUAOBjEmkbNTkiu6JA==" + ) + assert results[51].ns_store_model_version_checksum_key == "rXdwmenydb+cl65S3tSy9rIL6lkwSXqL7UvaJVK21Lc=" + assert results[51].ns_persistence_framework_version == 1526 + assert results[51].ns_store_model_version_hashes_version == 3 + assert results[51].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[52].activity is not None + assert isinstance(results[52].activity, (bytes, bytearray)) + assert results[52].group_binary is not None + assert isinstance(results[52].group_binary, (bytes, bytearray)) + assert results[52].trigger is not None + assert isinstance(results[52].trigger, (bytes, bytearray)) + assert results[52].plist_path == "NSStoreModelVersionHashes" + assert results[52].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[53].table == "Z_METADATA" + assert results[53].z_version == 1 + assert results[53].z_uuid == "514A8E5F-DE48-4C3E-9129-3AF14DEAD0E1" + assert results[53].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" + + assert results[54].table == "Z_MODELCACHE" + assert results[54].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py index 8532fc1428..61fe98690d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,29 +21,23 @@ def test_gkopaque(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/var/db/gkopaque.bundle/Contents/Resources/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/gkopaque.bundle/Contents/Resources/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(GatekeeperOpaqueConfigurationPlugin) - target_unix.add_plugin(GatekeeperOpaqueConfigurationPlugin) + results = list(target_unix.gkopaque()) - results = list(target_unix.gkopaque()) + assert len(results) == 74247 - assert len(results) == 74247 + assert results[0].table == "whitelist" + assert results[0].current == b"\x00\x03'\xec\xe1\xfbZ'\xb5\xf5\xc5\x1a\x00\x99\x00\xb1\xe4\x85K\xb7" + assert results[0].opaque == b"\xcb\xe5k\x97\x84\x97N\n\x1c\x01Y\xc4\x1f9+wB\x1bM#" + assert results[0].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" - assert results[0].table == "whitelist" - assert results[0].current == b"\x00\x03'\xec\xe1\xfbZ'\xb5\xf5\xc5\x1a\x00\x99\x00\xb1\xe4\x85K\xb7" - assert results[0].opaque == b"\xcb\xe5k\x97\x84\x97N\n\x1c\x01Y\xc4\x1f9+wB\x1bM#" - assert results[0].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" - - assert results[-1].table == "conditions" - assert results[-1].label == "google chrome (canary)" - assert results[-1].weight == 300 - assert results[-1].conditions_source == "EQHXZ8M8AV" - assert results[-1].identifier == "com.google.Chrome.canary" - assert results[-1].version is None - assert results[-1].conditions == "{errors=[-67013]}" - assert results[-1].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" + assert results[-1].table == "conditions" + assert results[-1].label == "google chrome (canary)" + assert results[-1].weight == 300 + assert results[-1].conditions_source == "EQHXZ8M8AV" + assert results[-1].identifier == "com.google.Chrome.canary" + assert results[-1].version is None + assert results[-1].conditions == "{errors=[-67013]}" + assert results[-1].source == "/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py index 059827fd71..2705f6730c 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -63,57 +62,45 @@ def test_global_user_preferences( securityagent, root, ] - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/global_user_preferences/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(GlobalUserPreferencesPlugin) + target_unix.add_plugin(GlobalUserPreferencesPlugin) - results = list(target_unix.global_user_preferences()) - results.sort(key=lambda r: r.source) + results = list(target_unix.global_user_preferences()) + results.sort(key=lambda r: r.source) - assert len(results) == 4 + assert len(results) == 4 - assert results[0].AKLastLocale == "en_US@rg=nlzzzz" - assert results[0].com_apple_sound_beep_flash == 0 - assert not results[0].AppleMiniaturizeOnDoubleClick - assert results[0].NSAutomaticPeriodSubstitutionEnabled - assert results[0].NSSpellCheckerDictionaryContainerTransitionComplete - assert results[0].com_apple_springing_delay == "0.5" - assert results[0].ACDMonthlyAnalyticsLastPosted == "796140692.59226" - assert results[0].AKLastIDMSEnvironment == 0 - assert results[0].NSAutomaticCapitalizationEnabled - assert results[0].NSLinguisticDataAssetsRequestTime == "2026-03-25 14:12:53.295950" - assert results[0].AppleAntiAliasingThreshold == 4 - assert results[0].com_apple_springing_enabled - assert results[0].AppleLocale == "en_US@rg=nlzzzz" - assert results[0].com_apple_trackpad_forceClick - assert results[0].NSLinguisticDataAssetsRequestLastInterval == "86400.0" - assert results[0].AppleLanguagesSchemaVersion == 5400 - assert results[0].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" + assert results[0].AKLastLocale == "en_US@rg=nlzzzz" + assert results[0].com_apple_sound_beep_flash == 0 + assert not results[0].AppleMiniaturizeOnDoubleClick + assert results[0].NSAutomaticPeriodSubstitutionEnabled + assert results[0].NSSpellCheckerDictionaryContainerTransitionComplete + assert results[0].com_apple_springing_delay == "0.5" + assert results[0].ACDMonthlyAnalyticsLastPosted == "796140692.59226" + assert results[0].AKLastIDMSEnvironment == 0 + assert results[0].NSAutomaticCapitalizationEnabled + assert results[0].NSLinguisticDataAssetsRequestTime == "2026-03-25 14:12:53.295950" + assert results[0].AppleAntiAliasingThreshold == 4 + assert results[0].com_apple_springing_enabled + assert results[0].AppleLocale == "en_US@rg=nlzzzz" + assert results[0].com_apple_trackpad_forceClick + assert results[0].NSLinguisticDataAssetsRequestLastInterval == "86400.0" + assert results[0].AppleLanguagesSchemaVersion == 5400 + assert results[0].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" - assert results[1].replace == "omw" - assert results[1].on == 1 - assert getattr(results[1], "with") == "On my way!" - assert results[1].plist_path == "NSUserDictionaryReplacementItems[0]" - assert results[1].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" + assert results[1].replace == "omw" + assert results[1].on == 1 + assert getattr(results[1], "with") == "On my way!" + assert results[1].plist_path == "NSUserDictionaryReplacementItems[0]" + assert results[1].source == "/Users/user/Library/Preferences/.GlobalPreferences.plist" - assert results[2].AppleKeyboardUIMode == 2 - assert results[2].source == "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist" + assert results[2].AppleKeyboardUIMode == 2 + assert results[2].source == "/private/var/db/securityagent/Library/Preferences/.GlobalPreferences.plist" - assert results[3].AppleLocale == "en_US" - assert results[3].AppleKeyboardUIMode == 3 - assert results[3].com_apple_sound_beep_flash == 0 - assert results[3].source == "/private/var/root/Library/Preferences/.GlobalPreferences.plist" + assert results[3].AppleLocale == "en_US" + assert results[3].AppleKeyboardUIMode == 3 + assert results[3].com_apple_sound_beep_flash == 0 + assert results[3].source == "/private/var/root/Library/Preferences/.GlobalPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py index e9e856adb3..9ddadfa42d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -20,48 +19,36 @@ ], ) def test_groups(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: - stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/groups/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/groups/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/groups/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - entries.append(entry) - stat_results.append(stat_result) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(GroupPlugin) - - results = list(target_unix.groups()) - results.sort(key=lambda r: r.realname) - assert len(results) == 3 - - assert results[0].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEFFFFFFFFE"] - assert results[0].members == [] - assert results[0].smb_sid == ["S-1-0-0"] - assert results[0].gid == [-2] - assert results[0].name == ["nobody", "BUILTIN\\Nobody"] - assert results[0].realname == ["Nobody"] - assert results[0].source == "/var/db/dslocal/nodes/Default/groups/nobody.plist" - - assert results[1].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000129"] - assert results[1].members == ["_eligibilityd"] - assert results[1].smb_sid == [] - assert results[1].gid == [297] - assert results[1].name == ["_eligibilityd"] - assert results[1].realname == ["OS Eligibility Daemon"] - assert results[1].source == "/var/db/dslocal/nodes/Default/groups/_eligibilityd.plist" - - assert results[2].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000104"] - assert results[2].members == [] - assert results[2].smb_sid == [] - assert results[2].gid == [260] - assert results[2].name == ["_applepay"] - assert results[2].realname == ["applepay Daemon"] - assert results[2].source == "/var/db/dslocal/nodes/Default/groups/_applepay.plist" + target_unix.add_plugin(GroupPlugin) + + results = list(target_unix.groups()) + results.sort(key=lambda r: r.realname) + assert len(results) == 3 + + assert results[0].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEFFFFFFFFE"] + assert results[0].members == [] + assert results[0].smb_sid == ["S-1-0-0"] + assert results[0].gid == [-2] + assert results[0].name == ["nobody", "BUILTIN\\Nobody"] + assert results[0].realname == ["Nobody"] + assert results[0].source == "/var/db/dslocal/nodes/Default/groups/nobody.plist" + + assert results[1].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000129"] + assert results[1].members == ["_eligibilityd"] + assert results[1].smb_sid == [] + assert results[1].gid == [297] + assert results[1].name == ["_eligibilityd"] + assert results[1].realname == ["OS Eligibility Daemon"] + assert results[1].source == "/var/db/dslocal/nodes/Default/groups/_eligibilityd.plist" + + assert results[2].generateduid == ["ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000104"] + assert results[2].members == [] + assert results[2].smb_sid == [] + assert results[2].gid == [260] + assert results[2].name == ["_applepay"] + assert results[2].realname == ["applepay Daemon"] + assert results[2].source == "/var/db/dslocal/nodes/Default/groups/_applepay.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py b/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py index 88956ad68a..0de0c04320 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -34,25 +33,19 @@ def test_identity_services(test_file: str, target_unix: Target, fs_unix: Virtual data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"Users/user/Library/IdentityServices/{test_file}", data_file) - entry = fs_unix.get(f"Users/user/Library/IdentityServices/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(IdentityServicesPlugin) - target_unix.add_plugin(IdentityServicesPlugin) + results = list(target_unix.identity_services()) - results = list(target_unix.identity_services()) + assert len(results) == 2 - assert len(results) == 2 + assert results[0].table == "_SqliteDatabaseProperties" + assert results[0].key == "_ClientVersion" + assert results[0].value == "10027" + assert results[0].source == "/Users/user/Library/IdentityServices/ids.db" - assert results[0].table == "_SqliteDatabaseProperties" - assert results[0].key == "_ClientVersion" - assert results[0].value == "10027" - assert results[0].source == "/Users/user/Library/IdentityServices/ids.db" - - assert results[1].table == "_SqliteDatabaseProperties" - assert results[1].key == "InternalMigration" - assert results[1].value == "100" - assert results[1].source == "/Users/user/Library/IdentityServices/ids.db" + assert results[1].table == "_SqliteDatabaseProperties" + assert results[1].key == "InternalMigration" + assert results[1].value == "100" + assert results[1].source == "/Users/user/Library/IdentityServices/ids.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py index d290bcaedb..2dad7ca294 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -24,20 +23,14 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: Virtua tz = timezone.utc data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Receipts/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Receipts/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(InstallationHistoryPlugin) - target_unix.add_plugin(InstallationHistoryPlugin) + results = list(target_unix.installation_history()) + assert len(results) == 1 - results = list(target_unix.installation_history()) - assert len(results) == 1 - - assert results[0].ts == datetime(2026, 3, 25, 14, 7, 11, tzinfo=tz) - assert results[0].display_name == "macOS 26.4" - assert results[0].display_version == "26.4" - assert results[0].process_name == "softwareupdated" - assert results[0].source == "/Library/Receipts/InstallHistory.plist" + assert results[0].ts == datetime(2026, 3, 25, 14, 7, 11, tzinfo=tz) + assert results[0].display_name == "macOS 26.4" + assert results[0].display_version == "26.4" + assert results[0].process_name == "softwareupdated" + assert results[0].source == "/Library/Receipts/InstallHistory.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py index d8c5097673..fd9380cb16 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,30 +21,24 @@ def test_keyboard_layout(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Preferences/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(KeyboardLayoutPlugin) - - results = list(target_unix.keyboard_layout()) - assert len(results) == 2 - - assert results[0].input_source_kind == "Keyboard Layout" - assert results[0].keyboard_layout_name == "U.S." - assert results[0].keyboard_layout_id == 0 - assert results[0].enabled_layout - assert results[0].selected_layout - assert not results[0].current_layout - assert results[0].source == "/Library/Preferences/com.apple.HIToolbox.plist" - - assert results[1].input_source_kind == "Keyboard Layout" - assert results[1].keyboard_layout_name == "Dutch" - assert results[1].keyboard_layout_id == 26 - assert results[1].enabled_layout - assert not results[1].selected_layout - assert not results[1].current_layout - assert results[1].source == "/Library/Preferences/com.apple.HIToolbox.plist" + + target_unix.add_plugin(KeyboardLayoutPlugin) + + results = list(target_unix.keyboard_layout()) + assert len(results) == 2 + + assert results[0].input_source_kind == "Keyboard Layout" + assert results[0].keyboard_layout_name == "U.S." + assert results[0].keyboard_layout_id == 0 + assert results[0].enabled_layout + assert results[0].selected_layout + assert not results[0].current_layout + assert results[0].source == "/Library/Preferences/com.apple.HIToolbox.plist" + + assert results[1].input_source_kind == "Keyboard Layout" + assert results[1].keyboard_layout_name == "Dutch" + assert results[1].keyboard_layout_id == 26 + assert results[1].enabled_layout + assert not results[1].selected_layout + assert not results[1].current_layout + assert results[1].source == "/Library/Preferences/com.apple.HIToolbox.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py index 43e4849f7f..a52edc9074 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -35,166 +34,157 @@ def test_keychain(test_file: str, target_unix: Target, fs_unix: VirtualFilesyste data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/{test_file}", data_file) - entry = fs_unix.get(f"/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(KeychainPlugin) - - results = list(target_unix.keychain()) - - assert len(results) == 100 - - assert results[0].table == "genp" - assert results[0].row_id == 1 - assert results[0].cdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) - assert results[0].mdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) - assert results[0].desc is None - assert results[0].icmt is None - assert results[0].crtr is None - assert results[0].keychain_type is None - assert results[0].scrp is None - assert results[0].labl is None - assert results[0].alis is None - assert results[0].invi is None - assert results[0].nega is None - assert results[0].cusi is None - assert results[0].prot is None - assert results[0].acct == b"T\xccQt\xcfq\x04qd\xc4\xd1\x94\xb9^\xc6S\x95\xa1\xc5\xdd" - assert results[0].svce == b"\x84f\xd7\x7f7Qw)\x16>\x8c>}\x38\\z\xa7\tQ\x04" - assert results[0].gena is None - assert results[0].data is not None - assert results[0].agrp == "com.apple.security.indirect-unlock-key" - assert results[0].pdmn == "ak" - assert results[0].sync == 0 - assert results[0].tomb == 0 - assert ( - results[0].sha1 - == "\udc9b\udce4|7],\udc88\udcfc\udcfa\udce7\x05\udcfe\udcb0\udcf2<\udce6\udca2\udcf2`\udc99" - ) - assert results[0].vwht == "" - assert results[0].tkid == "" - assert results[0].musr == "" - assert results[0].UUID == "FB64E7EF-96A7-4797-9883-E084735D7AC1" - assert results[0].sysb is None - assert results[0].pcss is None - assert results[0].pcsk is None - assert results[0].pcsi is None - assert results[0].persistref == b"\x9c\xf0v\x86\xd2\x93E\x1b\xb4G8\xa0V\xfc\xfd\xf5" - assert results[0].clip == 0 - assert results[0].ggrp == "" - assert results[0].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" - - assert results[84].table == "sqlite_sequence" - assert results[84].name == "tversion" - assert results[84].seq == 1 - assert results[84].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" - - assert results[88].table == "inet" - assert results[88].row_id == 1 - assert results[88].cdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) - assert results[88].mdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) - assert results[88].desc == b"-\x9c\xd9\x85\x03\x1cG\x93\x81\x8e\xd6\xc9\xc6n\x91\xb7\xf9\x0b \xf6" - assert results[88].icmt is None - assert results[88].crtr is None - assert results[88].keychain_type is None - assert results[88].scrp is None - assert results[88].labl is None - assert results[88].alis is None - assert results[88].invi == 1 - assert results[88].nega is None - assert results[88].cusi is None - assert results[88].prot is None - assert results[88].acct == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" - assert results[88].sdmn == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" - assert results[88].srvr == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" - assert results[88].ptcl == "0" - assert results[88].atyp == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" - assert results[88].port == 0 - assert results[88].path_binary == b"\xec\xa4\xb9~\x03\x05\r\x96\x7fzl\xc4\xe0\xbb)\xbb\xaeV,\xa3" - assert results[88].data is not None - assert results[88].agrp == "com.apple.security.octagon" - assert results[88].pdmn == "cku" - assert results[88].sync == 0 - assert results[88].tomb == 0 - assert results[88].sha1 == "\udc8bB\x1dn\udcb4B\udcf9*\udcfdJ\udcc5\udcee\udce6ؤQ\udcae\udc97\udcbf\udcc2" - assert results[88].vwht == "" - assert results[88].tkid == "" - assert results[88].musr == "" - assert results[88].UUID == "7A4DAD90-B283-47BC-9AE4-1D237C3493D8" - assert results[88].sysb == 1 - assert results[88].pcss is None - assert results[88].pcsk is None - assert results[88].pcsi is None - assert results[88].persistref == b"TZ\x99|\xfd\xd2H\xd2\xad\xf3\x04L\xb3\xfcq\x9e" - assert results[88].clip == 0 - assert results[88].ggrp == "" - assert results[88].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" - - assert results[89].table == "keys" - assert results[89].row_id == 1 - assert results[89].cdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) - assert results[89].mdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) - assert results[89].kcls == b"\x90i\xcax\xe7E\n(QsC\x1b>R\xc5\xc2R\x99\xe4s" - assert results[89].labl is None - assert results[89].alis is None - assert results[89].perm is None - assert results[89].priv is None - assert results[89].modi is None - assert results[89].klbl == b"com.apple.routined.security.database" - assert results[89].atag == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" - assert results[89].crtr == 0 - assert results[89].keychain_type == 0 - assert results[89].bsiz == 0 - assert results[89].esiz == 0 - assert results[89].sdat == 0.0 - assert results[89].edat == 0.0 - assert results[89].sens is None - assert results[89].asen is None - assert results[89].extr is None - assert results[89].next is None - assert results[89].encr is None - assert results[89].decr is None - assert results[89].drve is None - assert results[89].sign is None - assert results[89].vrfy is None - assert results[89].snrc is None - assert results[89].vyrc is None - assert results[89].wrap is None - assert results[89].unwp is None - assert results[89].data is not None - assert results[89].agrp == "com.apple.routined" - assert results[89].pdmn == "ck" - assert results[89].sync == 1 - assert results[89].tomb == 0 - assert results[89].sha1 == "\udc9cc\udc98\udcd8\udcea0\udcb12bFL\udca4ot87\udcc7\udcf4\x7f\udc82" - assert results[89].vwht == "" - assert results[89].tkid == "" - assert results[89].musr == "" - assert results[89].UUID == "22A9A2ED-79C9-4665-80F5-021793509144" - assert results[89].sysb is None - assert results[89].pcss is None - assert results[89].pcsk is None - assert results[89].pcsi is None - assert results[89].persistref == b"\xaf\x1e\xaaxqDG\xae\x81\xdb\xb1\xce\x9c\xe9C\xb9" - assert results[89].clip == 0 - assert results[89].ggrp == "" - assert results[89].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" - - assert results[94].table == "tversion" - assert results[94].row_id == 1 - assert results[94].version == "12" - assert results[94].minor == 12 - assert results[94].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" - - assert results[95].table == "metadatakeys" - assert results[95].keyclass == 6 - assert results[95].actual_keyclass == 6 - assert ( - results[95].data - == "r\udcccߨ\udcfdeѽi\udcb5\x16\udcd5+\udcf7\udcfa\udca0\udce4\udce7\x13+\udc9d\udc8b\udcae\x06g\udcb4\udc9b\udca1ѡ\udca2\udca1aY摳K\udc83\udc80" # noqa E501 - ) - assert results[95].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + target_unix.add_plugin(KeychainPlugin) + + results = list(target_unix.keychain()) + + assert len(results) == 100 + + assert results[0].table == "genp" + assert results[0].row_id == 1 + assert results[0].cdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) + assert results[0].mdat == datetime(2026, 3, 25, 14, 11, 26, 182470, tzinfo=timezone.utc) + assert results[0].desc is None + assert results[0].icmt is None + assert results[0].crtr is None + assert results[0].keychain_type is None + assert results[0].scrp is None + assert results[0].labl is None + assert results[0].alis is None + assert results[0].invi is None + assert results[0].nega is None + assert results[0].cusi is None + assert results[0].prot is None + assert results[0].acct == b"T\xccQt\xcfq\x04qd\xc4\xd1\x94\xb9^\xc6S\x95\xa1\xc5\xdd" + assert results[0].svce == b"\x84f\xd7\x7f7Qw)\x16>\x8c>}\x38\\z\xa7\tQ\x04" + assert results[0].gena is None + assert results[0].data is not None + assert results[0].agrp == "com.apple.security.indirect-unlock-key" + assert results[0].pdmn == "ak" + assert results[0].sync == 0 + assert results[0].tomb == 0 + assert results[0].sha1 == "\udc9b\udce4|7],\udc88\udcfc\udcfa\udce7\x05\udcfe\udcb0\udcf2<\udce6\udca2\udcf2`\udc99" + assert results[0].vwht == "" + assert results[0].tkid == "" + assert results[0].musr == "" + assert results[0].UUID == "FB64E7EF-96A7-4797-9883-E084735D7AC1" + assert results[0].sysb is None + assert results[0].pcss is None + assert results[0].pcsk is None + assert results[0].pcsi is None + assert results[0].persistref == b"\x9c\xf0v\x86\xd2\x93E\x1b\xb4G8\xa0V\xfc\xfd\xf5" + assert results[0].clip == 0 + assert results[0].ggrp == "" + assert results[0].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[84].table == "sqlite_sequence" + assert results[84].name == "tversion" + assert results[84].seq == 1 + assert results[84].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[88].table == "inet" + assert results[88].row_id == 1 + assert results[88].cdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) + assert results[88].mdat == datetime(2026, 3, 25, 14, 11, 26, 218230, tzinfo=timezone.utc) + assert results[88].desc == b"-\x9c\xd9\x85\x03\x1cG\x93\x81\x8e\xd6\xc9\xc6n\x91\xb7\xf9\x0b \xf6" + assert results[88].icmt is None + assert results[88].crtr is None + assert results[88].keychain_type is None + assert results[88].scrp is None + assert results[88].labl is None + assert results[88].alis is None + assert results[88].invi == 1 + assert results[88].nega is None + assert results[88].cusi is None + assert results[88].prot is None + assert results[88].acct == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" + assert results[88].sdmn == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[88].srvr == b"\xd1*:s\x143|\xe6\x15nz\xe6\x06\xce\x1bs@\xe2\x87\x01" + assert results[88].ptcl == "0" + assert results[88].atyp == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[88].port == 0 + assert results[88].path_binary == b"\xec\xa4\xb9~\x03\x05\r\x96\x7fzl\xc4\xe0\xbb)\xbb\xaeV,\xa3" + assert results[88].data is not None + assert results[88].agrp == "com.apple.security.octagon" + assert results[88].pdmn == "cku" + assert results[88].sync == 0 + assert results[88].tomb == 0 + assert results[88].sha1 == "\udc8bB\x1dn\udcb4B\udcf9*\udcfdJ\udcc5\udcee\udce6ؤQ\udcae\udc97\udcbf\udcc2" + assert results[88].vwht == "" + assert results[88].tkid == "" + assert results[88].musr == "" + assert results[88].UUID == "7A4DAD90-B283-47BC-9AE4-1D237C3493D8" + assert results[88].sysb == 1 + assert results[88].pcss is None + assert results[88].pcsk is None + assert results[88].pcsi is None + assert results[88].persistref == b"TZ\x99|\xfd\xd2H\xd2\xad\xf3\x04L\xb3\xfcq\x9e" + assert results[88].clip == 0 + assert results[88].ggrp == "" + assert results[88].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[89].table == "keys" + assert results[89].row_id == 1 + assert results[89].cdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) + assert results[89].mdat == datetime(2026, 3, 25, 14, 11, 27, 254737, tzinfo=timezone.utc) + assert results[89].kcls == b"\x90i\xcax\xe7E\n(QsC\x1b>R\xc5\xc2R\x99\xe4s" + assert results[89].labl is None + assert results[89].alis is None + assert results[89].perm is None + assert results[89].priv is None + assert results[89].modi is None + assert results[89].klbl == b"com.apple.routined.security.database" + assert results[89].atag == b"\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t" + assert results[89].crtr == 0 + assert results[89].keychain_type == 0 + assert results[89].bsiz == 0 + assert results[89].esiz == 0 + assert results[89].sdat == 0.0 + assert results[89].edat == 0.0 + assert results[89].sens is None + assert results[89].asen is None + assert results[89].extr is None + assert results[89].next is None + assert results[89].encr is None + assert results[89].decr is None + assert results[89].drve is None + assert results[89].sign is None + assert results[89].vrfy is None + assert results[89].snrc is None + assert results[89].vyrc is None + assert results[89].wrap is None + assert results[89].unwp is None + assert results[89].data is not None + assert results[89].agrp == "com.apple.routined" + assert results[89].pdmn == "ck" + assert results[89].sync == 1 + assert results[89].tomb == 0 + assert results[89].sha1 == "\udc9cc\udc98\udcd8\udcea0\udcb12bFL\udca4ot87\udcc7\udcf4\x7f\udc82" + assert results[89].vwht == "" + assert results[89].tkid == "" + assert results[89].musr == "" + assert results[89].UUID == "22A9A2ED-79C9-4665-80F5-021793509144" + assert results[89].sysb is None + assert results[89].pcss is None + assert results[89].pcsk is None + assert results[89].pcsi is None + assert results[89].persistref == b"\xaf\x1e\xaaxqDG\xae\x81\xdb\xb1\xce\x9c\xe9C\xb9" + assert results[89].clip == 0 + assert results[89].ggrp == "" + assert results[89].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[94].table == "tversion" + assert results[94].row_id == 1 + assert results[94].version == "12" + assert results[94].minor == 12 + assert results[94].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" + + assert results[95].table == "metadatakeys" + assert results[95].keyclass == 6 + assert results[95].actual_keyclass == 6 + assert ( + results[95].data + == "r\udcccߨ\udcfdeѽi\udcb5\x16\udcd5+\udcf7\udcfa\udca0\udce4\udce7\x13+\udc9d\udc8b\udcae\x06g\udcb4\udc9b\udca1ѡ\udca2\udca1aY摳K\udc83\udc80" # noqa E501 + ) + assert results[95].source == "/Users/user/Library/Keychains/E253F552-3A40-5010-9ACE-98662C9CFE20/keychain-2.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py index 1158d8bdc2..f8af95f9f2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -43,181 +42,169 @@ def test_launch_agents( shell="/bin/zsh", ) target_unix.users = lambda: [user] - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/launchers/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(LaunchersPlugin) + target_unix.add_plugin(LaunchersPlugin) + results = list(target_unix.launch_agents()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) - results = list(target_unix.launch_agents()) + assert len(results) == 6 - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) + assert results[0].label == "com.apple.ecosystemagent" + assert results[0].program is None + assert results[0].program_arguments == [ + "/System/Library/PrivateFrameworks/Ecosystem.framework/Support/ecosystemagent" + ] + assert results[0].keep_alive is None + assert results[0].on_demand is None + assert results[0].disabled is None + assert results[0].run_at_load is None + assert results[0].launch_only_once is None + assert results[0].process_type == "Background" + assert results[0].wait is None + assert results[0].limit_load_to_session_type is None + assert results[0].limit_load_to_developer_mode is None + assert results[0].limit_load_from_variant is None + assert results[0].limit_load_to_variant is None + assert results[0].limit_load_from_boot_mode is None + assert results[0].limit_load_to_boot_mode == [] + assert results[0].limit_load_from_hardware == [] + assert results[0].limit_load_to_hardware == [] + assert results[0].launch_events == [] + assert results[0].mach_services == [ + "('com.apple.ecosystem.agent.clear-notifications', True)", + "('com.apple.ecosystem.agent.notifications', True)", + "('com.apple.ecosystem.unsupportedapplicationlist', True)", + "('com.apple.usernotifications.delegate.com.apple.ecosystem.notifications', True)", + ] + assert results[0].enable_pressured_exit == "True" + assert results[0].enable_transactions + assert results[0].environment_variables == [] + assert results[0].user_name is None + assert results[0].init_groups is None + assert results[0].group_name is None + assert results[0].start_interval is None + assert results[0].start_calendar_interval == [] + assert results[0].throttle_interval is None + assert results[0].enable_globbing is None + assert results[0].standard_in_path is None + assert results[0].standard_out_path is None + assert results[0].standard_error_path is None + assert results[0].nice is None + assert results[0].abandon_process_group is None + assert results[0].low_priority_io + assert results[0].root_directory is None + assert results[0].working_directory is None + assert results[0].umask is None + assert results[0].time_out is None + assert results[0].exit_time_out is None + assert results[0].watch_paths == [] + assert results[0].queue_directories == [] + assert results[0].start_on_mount is None + assert results[0].soft_resource_limits == [] + assert results[0].hard_resource_limits == [] + assert results[0].debug is None + assert results[0].wait_for_debugger is None + assert results[0].plist_path is None + assert results[0].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" - assert len(results) == 6 + assert results[1].un_setting_alerts is None + assert results[1].un_setting_always_show_previews is None + assert results[1].un_setting_lock_screen is None + assert results[1].un_setting_modal_alert_style is None + assert results[1].un_automatically_show_settings + assert results[1].un_setting_notification_center is None + assert results[1].un_daemon_should_receive_background_responses + assert results[1].un_suppress_user_authorization_prompt + assert results[1].plist_path == "UNUserNotificationCenter" + assert results[1].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" - assert results[0].label == "com.apple.ecosystemagent" - assert results[0].program is None - assert results[0].program_arguments == [ - "/System/Library/PrivateFrameworks/Ecosystem.framework/Support/ecosystemagent" - ] - assert results[0].keep_alive is None - assert results[0].on_demand is None - assert results[0].disabled is None - assert results[0].run_at_load is None - assert results[0].launch_only_once is None - assert results[0].process_type == "Background" - assert results[0].wait is None - assert results[0].limit_load_to_session_type is None - assert results[0].limit_load_to_developer_mode is None - assert results[0].limit_load_from_variant is None - assert results[0].limit_load_to_variant is None - assert results[0].limit_load_from_boot_mode is None - assert results[0].limit_load_to_boot_mode == [] - assert results[0].limit_load_from_hardware == [] - assert results[0].limit_load_to_hardware == [] - assert results[0].launch_events == [] - assert results[0].mach_services == [ - "('com.apple.ecosystem.agent.clear-notifications', True)", - "('com.apple.ecosystem.agent.notifications', True)", - "('com.apple.ecosystem.unsupportedapplicationlist', True)", - "('com.apple.usernotifications.delegate.com.apple.ecosystem.notifications', True)", - ] - assert results[0].enable_pressured_exit == "True" - assert results[0].enable_transactions - assert results[0].environment_variables == [] - assert results[0].user_name is None - assert results[0].init_groups is None - assert results[0].group_name is None - assert results[0].start_interval is None - assert results[0].start_calendar_interval == [] - assert results[0].throttle_interval is None - assert results[0].enable_globbing is None - assert results[0].standard_in_path is None - assert results[0].standard_out_path is None - assert results[0].standard_error_path is None - assert results[0].nice is None - assert results[0].abandon_process_group is None - assert results[0].low_priority_io - assert results[0].root_directory is None - assert results[0].working_directory is None - assert results[0].umask is None - assert results[0].time_out is None - assert results[0].exit_time_out is None - assert results[0].watch_paths == [] - assert results[0].queue_directories == [] - assert results[0].start_on_mount is None - assert results[0].soft_resource_limits == [] - assert results[0].hard_resource_limits == [] - assert results[0].debug is None - assert results[0].wait_for_debugger is None - assert results[0].plist_path is None - assert results[0].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + assert results[2].un_setting_alerts + assert not results[2].un_setting_always_show_previews + assert results[2].un_setting_lock_screen + assert results[2].un_setting_modal_alert_style + assert results[2].un_automatically_show_settings is None + assert results[2].un_setting_notification_center + assert results[2].un_daemon_should_receive_background_responses is None + assert results[2].un_suppress_user_authorization_prompt is None + assert results[2].plist_path == "UNUserNotificationCenter/UNDefaultSettings" + assert results[2].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" - assert results[1].un_setting_alerts is None - assert results[1].un_setting_always_show_previews is None - assert results[1].un_setting_lock_screen is None - assert results[1].un_setting_modal_alert_style is None - assert results[1].un_automatically_show_settings - assert results[1].un_setting_notification_center is None - assert results[1].un_daemon_should_receive_background_responses - assert results[1].un_suppress_user_authorization_prompt - assert results[1].plist_path == "UNUserNotificationCenter" - assert results[1].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + assert results[3].un_notification_icon_default == "notification-settings" + assert results[3].un_notification_icon_settings == "notification-settings" + assert results[3].plist_path == "UNUserNotificationCenter/UNNotificationIcons" + assert results[3].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" - assert results[2].un_setting_alerts - assert not results[2].un_setting_always_show_previews - assert results[2].un_setting_lock_screen - assert results[2].un_setting_modal_alert_style - assert results[2].un_automatically_show_settings is None - assert results[2].un_setting_notification_center - assert results[2].un_daemon_should_receive_background_responses is None - assert results[2].un_suppress_user_authorization_prompt is None - assert results[2].plist_path == "UNUserNotificationCenter/UNDefaultSettings" - assert results[2].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" + assert results[4].label == "com.openssh.ssh-agent" + assert results[4].program is None + assert results[4].program_arguments == ["/usr/bin/ssh-agent", "-l"] + assert results[4].keep_alive is None + assert results[4].on_demand is None + assert results[4].disabled is None + assert results[4].run_at_load is None + assert results[4].launch_only_once is None + assert results[4].process_type is None + assert results[4].wait is None + assert results[4].limit_load_to_session_type is None + assert results[4].limit_load_to_developer_mode is None + assert results[4].limit_load_from_variant is None + assert results[4].limit_load_to_variant is None + assert results[4].limit_load_from_boot_mode is None + assert results[4].limit_load_to_boot_mode == [] + assert results[4].limit_load_from_hardware == [] + assert results[4].limit_load_to_hardware == [] + assert results[4].launch_events == [] + assert results[4].mach_services == [] + assert results[4].enable_pressured_exit is None + assert results[4].enable_transactions + assert results[4].environment_variables == [] + assert results[4].user_name is None + assert results[4].init_groups is None + assert results[4].group_name is None + assert results[4].start_interval is None + assert results[4].start_calendar_interval == [] + assert results[4].throttle_interval is None + assert results[4].enable_globbing is None + assert results[4].standard_in_path is None + assert results[4].standard_out_path is None + assert results[4].standard_error_path is None + assert results[4].nice is None + assert results[4].abandon_process_group is None + assert results[4].low_priority_io is None + assert results[4].root_directory is None + assert results[4].working_directory is None + assert results[4].umask is None + assert results[4].time_out is None + assert results[4].exit_time_out is None + assert results[4].watch_paths == [] + assert results[4].queue_directories == [] + assert results[4].start_on_mount is None + assert results[4].soft_resource_limits == [] + assert results[4].hard_resource_limits == [] + assert results[4].debug is None + assert results[4].wait_for_debugger is None + assert results[4].plist_path is None + assert results[4].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" - assert results[3].un_notification_icon_default == "notification-settings" - assert results[3].un_notification_icon_settings == "notification-settings" - assert results[3].plist_path == "UNUserNotificationCenter/UNNotificationIcons" - assert results[3].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" - - assert results[4].label == "com.openssh.ssh-agent" - assert results[4].program is None - assert results[4].program_arguments == ["/usr/bin/ssh-agent", "-l"] - assert results[4].keep_alive is None - assert results[4].on_demand is None - assert results[4].disabled is None - assert results[4].run_at_load is None - assert results[4].launch_only_once is None - assert results[4].process_type is None - assert results[4].wait is None - assert results[4].limit_load_to_session_type is None - assert results[4].limit_load_to_developer_mode is None - assert results[4].limit_load_from_variant is None - assert results[4].limit_load_to_variant is None - assert results[4].limit_load_from_boot_mode is None - assert results[4].limit_load_to_boot_mode == [] - assert results[4].limit_load_from_hardware == [] - assert results[4].limit_load_to_hardware == [] - assert results[4].launch_events == [] - assert results[4].mach_services == [] - assert results[4].enable_pressured_exit is None - assert results[4].enable_transactions - assert results[4].environment_variables == [] - assert results[4].user_name is None - assert results[4].init_groups is None - assert results[4].group_name is None - assert results[4].start_interval is None - assert results[4].start_calendar_interval == [] - assert results[4].throttle_interval is None - assert results[4].enable_globbing is None - assert results[4].standard_in_path is None - assert results[4].standard_out_path is None - assert results[4].standard_error_path is None - assert results[4].nice is None - assert results[4].abandon_process_group is None - assert results[4].low_priority_io is None - assert results[4].root_directory is None - assert results[4].working_directory is None - assert results[4].umask is None - assert results[4].time_out is None - assert results[4].exit_time_out is None - assert results[4].watch_paths == [] - assert results[4].queue_directories == [] - assert results[4].start_on_mount is None - assert results[4].soft_resource_limits == [] - assert results[4].hard_resource_limits == [] - assert results[4].debug is None - assert results[4].wait_for_debugger is None - assert results[4].plist_path is None - assert results[4].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" - - assert results[5].socket_key is None - assert results[5].sock_type is None - assert results[5].sock_passive is None - assert results[5].sock_node_name is None - assert results[5].sock_service_name is None - assert results[5].sock_family is None - assert results[5].sock_protocol is None - assert results[5].sock_path_mode is None - assert results[5].sock_path_name is None - assert results[5].secure_socket_with_key == "SSH_AUTH_SOCK" - assert results[5].sock_path_owner is None - assert results[5].sock_path_group is None - assert results[5].bonjour is None - assert results[5].multicast_group is None - assert results[5].plist_path == "Sockets/Listeners" - assert results[5].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" + assert results[5].socket_key is None + assert results[5].sock_type is None + assert results[5].sock_passive is None + assert results[5].sock_node_name is None + assert results[5].sock_service_name is None + assert results[5].sock_family is None + assert results[5].sock_protocol is None + assert results[5].sock_path_mode is None + assert results[5].sock_path_name is None + assert results[5].secure_socket_with_key == "SSH_AUTH_SOCK" + assert results[5].sock_path_owner is None + assert results[5].sock_path_group is None + assert results[5].bonjour is None + assert results[5].multicast_group is None + assert results[5].plist_path == "Sockets/Listeners" + assert results[5].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" @pytest.mark.parametrize( @@ -235,97 +222,84 @@ def test_launch_daemons( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: - stat_results = [] - - entries = [] - for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/launchers/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(LaunchersPlugin) - results = list(target_unix.launch_daemons()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) + target_unix.add_plugin(LaunchersPlugin) + results = list(target_unix.launch_daemons()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) - assert len(results) == 2 + assert len(results) == 2 - assert results[0].label == "org.cups.cupsd" - assert results[0].program is None - assert results[0].program_arguments == ["/usr/sbin/cupsd", "-l"] - assert results[0].keep_alive == "[('PathState', {'/private/var/spool/cups/cache/org.cups.cupsd': True})]" - assert results[0].on_demand is None - assert results[0].disabled is None - assert results[0].run_at_load is None - assert results[0].launch_only_once is None - assert results[0].process_type == "Interactive" - assert results[0].wait is None - assert results[0].limit_load_to_session_type is None - assert results[0].limit_load_to_developer_mode is None - assert results[0].limit_load_from_variant is None - assert results[0].limit_load_to_variant is None - assert results[0].limit_load_from_boot_mode is None - assert results[0].limit_load_to_boot_mode == [] - assert results[0].limit_load_from_hardware == [] - assert results[0].limit_load_to_hardware == [] - assert results[0].launch_events == [] - assert results[0].mach_services == [] - assert results[0].enable_pressured_exit is None - assert results[0].enable_transactions - assert results[0].environment_variables == [ - "('CUPS_DEBUG_LOG', '/var/log/cups/debug_log')", - "('CUPS_DEBUG_LEVEL', '3')", - "('CUPS_DEBUG_FILTER', '^(cupsDo|cupsGet|cupsMake|cupsSet|http|_http|ipp|_ipp|mime).*')", - ] - assert results[0].user_name is None - assert results[0].init_groups is None - assert results[0].group_name is None - assert results[0].start_interval is None - assert results[0].start_calendar_interval == [] - assert results[0].throttle_interval is None - assert results[0].enable_globbing is None - assert results[0].standard_in_path is None - assert results[0].standard_out_path is None - assert results[0].standard_error_path is None - assert results[0].nice is None - assert results[0].abandon_process_group is None - assert results[0].low_priority_io is None - assert results[0].root_directory is None - assert results[0].working_directory is None - assert results[0].umask is None - assert results[0].time_out is None - assert results[0].exit_time_out == 60 - assert results[0].watch_paths == [] - assert results[0].queue_directories == [] - assert results[0].start_on_mount is None - assert results[0].soft_resource_limits == [] - assert results[0].hard_resource_limits == [] - assert results[0].debug is None - assert results[0].wait_for_debugger is None - assert results[0].plist_path is None - assert results[0].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" + assert results[0].label == "org.cups.cupsd" + assert results[0].program is None + assert results[0].program_arguments == ["/usr/sbin/cupsd", "-l"] + assert results[0].keep_alive == "[('PathState', {'/private/var/spool/cups/cache/org.cups.cupsd': True})]" + assert results[0].on_demand is None + assert results[0].disabled is None + assert results[0].run_at_load is None + assert results[0].launch_only_once is None + assert results[0].process_type == "Interactive" + assert results[0].wait is None + assert results[0].limit_load_to_session_type is None + assert results[0].limit_load_to_developer_mode is None + assert results[0].limit_load_from_variant is None + assert results[0].limit_load_to_variant is None + assert results[0].limit_load_from_boot_mode is None + assert results[0].limit_load_to_boot_mode == [] + assert results[0].limit_load_from_hardware == [] + assert results[0].limit_load_to_hardware == [] + assert results[0].launch_events == [] + assert results[0].mach_services == [] + assert results[0].enable_pressured_exit is None + assert results[0].enable_transactions + assert results[0].environment_variables == [ + "('CUPS_DEBUG_LOG', '/var/log/cups/debug_log')", + "('CUPS_DEBUG_LEVEL', '3')", + "('CUPS_DEBUG_FILTER', '^(cupsDo|cupsGet|cupsMake|cupsSet|http|_http|ipp|_ipp|mime).*')", + ] + assert results[0].user_name is None + assert results[0].init_groups is None + assert results[0].group_name is None + assert results[0].start_interval is None + assert results[0].start_calendar_interval == [] + assert results[0].throttle_interval is None + assert results[0].enable_globbing is None + assert results[0].standard_in_path is None + assert results[0].standard_out_path is None + assert results[0].standard_error_path is None + assert results[0].nice is None + assert results[0].abandon_process_group is None + assert results[0].low_priority_io is None + assert results[0].root_directory is None + assert results[0].working_directory is None + assert results[0].umask is None + assert results[0].time_out is None + assert results[0].exit_time_out == 60 + assert results[0].watch_paths == [] + assert results[0].queue_directories == [] + assert results[0].start_on_mount is None + assert results[0].soft_resource_limits == [] + assert results[0].hard_resource_limits == [] + assert results[0].debug is None + assert results[0].wait_for_debugger is None + assert results[0].plist_path is None + assert results[0].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" - assert results[1].socket_key is None - assert results[1].sock_type is None - assert results[1].sock_passive is None - assert results[1].sock_node_name is None - assert results[1].sock_service_name is None - assert results[1].sock_family is None - assert results[1].sock_protocol is None - assert results[1].sock_path_mode == 49663 - assert results[1].sock_path_name == "/private/var/run/cupsd" - assert results[1].secure_socket_with_key is None - assert results[1].sock_path_owner is None - assert results[1].sock_path_group is None - assert results[1].bonjour is None - assert results[1].multicast_group is None - assert results[1].plist_path == "Sockets/Listeners[0]" - assert results[1].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" + assert results[1].socket_key is None + assert results[1].sock_type is None + assert results[1].sock_passive is None + assert results[1].sock_node_name is None + assert results[1].sock_service_name is None + assert results[1].sock_family is None + assert results[1].sock_protocol is None + assert results[1].sock_path_mode == 49663 + assert results[1].sock_path_name == "/private/var/run/cupsd" + assert results[1].secure_socket_with_key is None + assert results[1].sock_path_owner is None + assert results[1].sock_path_group is None + assert results[1].bonjour is None + assert results[1].multicast_group is None + assert results[1].plist_path == "Sockets/Listeners[0]" + assert results[1].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py index 7b1a6c1cb4..49a270357d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -39,75 +38,64 @@ def test_login_items( shell="/bin/zsh", ) target_unix.users = lambda: [user] - stat_results = [] - entries = [] - for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(LoginItemsPlugin) + target_unix.add_plugin(LoginItemsPlugin) - results = list(target_unix.login_items()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + results = list(target_unix.login_items()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 4 + assert len(results) == 4 - assert results[0].associated_bundle_identifiers is None - assert results[0].bookmark is not None - assert isinstance(results[0].bookmark, (bytes, bytearray)) - assert results[0].bundle_identifier == "com.microsoft.VSCode" - assert results[0].container is None - assert results[0].designated_requirement == ( - 'identifier "com.microsoft.VSCode" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] ' # noqa E501 - "/* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = UBF8T346G9" # noqa E501 - ) - assert results[0].developer_name is None - assert results[0].login_item_disposition == 3 - assert results[0].executable_modification_date == datetime(1970, 1, 1, 0, 0, tzinfo=tz) - assert results[0].executable_path is None - assert results[0].flags == 0 - assert results[0].generation == 0 - assert results[0].identifier == "2.com.microsoft.VSCode" - assert results[0].lightweight_requirement is not None - assert isinstance(results[0].lightweight_requirement, (bytes, bytearray)) - assert results[0].modification_date == datetime(2026, 4, 29, 15, 46, 38, tzinfo=tz) - assert results[0].name == "Visual Studio Code.app" - assert results[0].program_arguments is None - assert results[0].sha256 is None - assert results[0].team_identifier == "UBF8T346G9" - assert results[0].login_item_type == 2 - assert results[0].url == "file:///Applications/Visual%20Studio%20Code.app/" - assert results[0].uuid == "6f541698-5211-4cf2-95b7-e97534baee39" - assert results[0].items == [] - assert results[0].plist_path == "itemsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90[0]" - assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + assert results[0].associated_bundle_identifiers is None + assert results[0].bookmark is not None + assert isinstance(results[0].bookmark, (bytes, bytearray)) + assert results[0].bundle_identifier == "com.microsoft.VSCode" + assert results[0].container is None + assert results[0].designated_requirement == ( + 'identifier "com.microsoft.VSCode" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] ' + "/* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = UBF8T346G9" # noqa E501 + ) + assert results[0].developer_name is None + assert results[0].login_item_disposition == 3 + assert results[0].executable_modification_date == datetime(1970, 1, 1, 0, 0, tzinfo=tz) + assert results[0].executable_path is None + assert results[0].flags == 0 + assert results[0].generation == 0 + assert results[0].identifier == "2.com.microsoft.VSCode" + assert results[0].lightweight_requirement is not None + assert isinstance(results[0].lightweight_requirement, (bytes, bytearray)) + assert results[0].modification_date == datetime(2026, 4, 29, 15, 46, 38, tzinfo=tz) + assert results[0].name == "Visual Studio Code.app" + assert results[0].program_arguments is None + assert results[0].sha256 is None + assert results[0].team_identifier == "UBF8T346G9" + assert results[0].login_item_type == 2 + assert results[0].url == "file:///Applications/Visual%20Studio%20Code.app/" + assert results[0].uuid == "6f541698-5211-4cf2-95b7-e97534baee39" + assert results[0].items == [] + assert results[0].plist_path == "itemsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90[0]" + assert results[0].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[1].generation == 2 - assert results[1].background_app_refresh_load_count == 17 - assert results[1].launch_services_items_imported - assert results[1].service_management_login_items_migrated - assert results[1].plist_path == "userSettingsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90" - assert results[1].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + assert results[1].generation == 2 + assert results[1].background_app_refresh_load_count == 17 + assert results[1].launch_services_items_imported + assert results[1].service_management_login_items_migrated + assert results[1].plist_path == "userSettingsByUserIdentifier/5C6F7FDD-02B2-498E-97B6-DE77293A8E90" + assert results[1].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[2].generation == 0 - assert results[2].background_app_refresh_load_count == 13 - assert not results[2].launch_services_items_imported - assert not results[2].service_management_login_items_migrated - assert results[2].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000" - assert results[2].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + assert results[2].generation == 0 + assert results[2].background_app_refresh_load_count == 13 + assert not results[2].launch_services_items_imported + assert not results[2].service_management_login_items_migrated + assert results[2].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000" + assert results[2].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" - assert results[3].generation == 14 - assert results[3].background_app_refresh_load_count == 41 - assert not results[3].launch_services_items_imported - assert results[3].service_management_login_items_migrated - assert results[3].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE" - assert results[3].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" + assert results[3].generation == 14 + assert results[3].background_app_refresh_load_count == 41 + assert not results[3].launch_services_items_imported + assert results[3].service_management_login_items_migrated + assert results[3].plist_path == "userSettingsByUserIdentifier/FFFFEEEE-DDDD-CCCC-BBBB-AAAAFFFFFFFE" + assert results[3].source == "/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-v16.btm" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py index 84af77a3ed..dbcfbcadfa 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -37,8 +36,6 @@ def test_login_window( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: - stat_results = [] - entries = [] user = UnixUserRecord( name="user", uid=501, @@ -51,42 +48,32 @@ def test_login_window( for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/login_window/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - patch.object(entries[2], "stat", return_value=stat_results[2]), - ): - target_unix.add_plugin(LoginWindowPlugin) + target_unix.add_plugin(LoginWindowPlugin) - results = list(target_unix.login_window()) - results.sort(key=lambda r: r.source) + results = list(target_unix.login_window()) + results.sort(key=lambda r: r.source) - assert len(results) == 5 + assert len(results) == 5 - assert not results[0].hide - assert results[0].bundle_id == "com.apple.finder" - assert results[0].path == "/System/Library/CoreServices/Finder.app" - assert results[0].background_state == 2 - assert results[0].plist_path == "TALAppsToRelaunchAtLogin[0]" - assert ( - results[0].source - == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist" - ) + assert not results[0].hide + assert results[0].bundle_id == "com.apple.finder" + assert results[0].path == "/System/Library/CoreServices/Finder.app" + assert results[0].background_state == 2 + assert results[0].plist_path == "TALAppsToRelaunchAtLogin[0]" + assert ( + results[0].source + == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.16130786-970B-53D1-A07B-005E50471D95.plist" + ) - assert not results[3].mini_buddy_launch - assert ( - results[3].source - == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist" - ) + assert not results[3].mini_buddy_launch + assert ( + results[3].source + == "/Users/user/Library/Preferences/ByHost/com.apple.loginwindow.E253F552-3A40-5010-9ACE-98662C9CFE20.plist" + ) - assert results[4].build_version_as_string == "25E246" - assert results[4].build_version_stamp_as_number == 52698816 - assert results[4].system_version_stamp_as_string == "26.4" - assert results[4].system_version_stamp_as_number == 436469760 - assert results[4].source == "/Users/user/Library/Preferences/loginwindow.plist" + assert results[4].build_version_as_string == "25E246" + assert results[4].build_version_stamp_as_number == 52698816 + assert results[4].system_version_stamp_as_string == "26.4" + assert results[4].system_version_stamp_as_number == 436469760 + assert results[4].source == "/Users/user/Library/Preferences/loginwindow.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py index 20c83d0e6e..dfdf702758 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -34,32 +33,20 @@ def test_periodic_scripts( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: - stat_results = [] - - entries = [] - for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/periodic/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(PeriodicPlugin) + target_unix.add_plugin(PeriodicPlugin) - results = list(target_unix.periodic_scripts()) - results.sort(key=lambda r: r.source) + results = list(target_unix.periodic_scripts()) + results.sort(key=lambda r: r.source) - assert len(results) == 2 + assert len(results) == 2 - assert results[0].source == "/etc/periodic/daily/110.clean-tmps" + assert results[0].source == "/etc/periodic/daily/110.clean-tmps" - assert results[1].source == "/etc/periodic/monthly/199.rotate-fax" + assert results[1].source == "/etc/periodic/monthly/199.rotate-fax" @pytest.mark.parametrize( @@ -77,84 +64,75 @@ def test_periodic_conf( target_unix: Target, fs_unix: VirtualFilesystem, ) -> None: - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/periodic/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - ): - target_unix.add_plugin(PeriodicPlugin) - - results = list(target_unix.periodic_conf()) - results.sort(key=lambda r: r.source) - - assert len(results) == 4 - - assert results[0].local_periodic == "/usr/local/etc/periodic" - assert results[0].dir_output is None - assert results[0].dir_show_success is None - assert results[0].dir_show_info is None - assert results[0].dir_show_badconfig is None - assert results[0].anticongestion_sleeptime is None - assert results[0].source == "/etc/defaults/periodic.conf" - - assert results[1].daily_clean_disks_enable is None - assert results[1].daily_clean_disks_files is None - assert results[1].daily_clean_disks_days is None - assert results[1].daily_clean_disks_verbose is None - assert results[1].daily_clean_tmps_enable - assert results[1].daily_clean_tmps_dirs == "/tmp" - assert results[1].daily_clean_tmps_days == 3 - assert results[1].daily_clean_tmps_ignore == "$daily_clean_tmps_ignore quota.user quota.group" - assert results[1].daily_clean_tmps_verbose - assert results[1].daily_clean_preserve_enable is None - assert results[1].daily_clean_preserve_days is None - assert results[1].daily_clean_preserve_verbose is None - assert results[1].daily_clean_msgs_enable - assert results[1].daily_clean_msgs_days is None - assert results[1].daily_clean_rwho_enable - assert results[1].daily_clean_rwho_days == 7 - assert results[1].daily_clean_rwho_verbose - assert results[1].daily_clean_hoststat_enable is None - assert results[1].daily_accounting_enable - assert not results[1].daily_accounting_compress - assert results[1].daily_accounting_save == 3 - assert results[1].daily_accounting_flags == "-q" - assert results[1].daily_status_disks_enable - assert results[1].daily_status_disks_df_flags == "-l -h" - assert results[1].daily_status_network_enable - assert results[1].daily_status_network_usedns - assert results[1].daily_status_mailq_enable - assert not results[1].daily_status_mailq_shorten - assert results[1].daily_status_include_submit_mailq - assert results[1].daily_local == "/etc/daily.local" - assert results[1].source == "/etc/defaults/periodic.conf" - - assert results[2].weekly_locate_enable is None - assert results[2].weekly_whatis_enable is None - assert results[2].weekly_noid_enable is None - assert results[2].weekly_noid_dirs is None - assert results[2].weekly_status_security_enable is None - assert results[2].weekly_status_security_inline is None - assert results[2].weekly_status_security_output is None - assert results[2].weekly_status_pkg_enable is None - assert results[2].pkg_version is None - assert results[2].pkg_version_index is None - assert results[2].weekly_local == "/etc/weekly.local" - assert results[2].source == "/etc/defaults/periodic.conf" - - assert results[3].monthly_accounting_enable - assert results[3].monthly_status_security_enable is None - assert results[3].monthly_status_security_inline is None - assert results[3].monthly_status_security_output is None - assert results[3].monthly_local == "/etc/monthly.local" - assert results[3].source == "/etc/defaults/periodic.conf" - - # TODO: Add test for PeriodicConfSecurityRecord + + target_unix.add_plugin(PeriodicPlugin) + + results = list(target_unix.periodic_conf()) + results.sort(key=lambda r: r.source) + + assert len(results) == 4 + + assert results[0].local_periodic == "/usr/local/etc/periodic" + assert results[0].dir_output is None + assert results[0].dir_show_success is None + assert results[0].dir_show_info is None + assert results[0].dir_show_badconfig is None + assert results[0].anticongestion_sleeptime is None + assert results[0].source == "/etc/defaults/periodic.conf" + + assert results[1].daily_clean_disks_enable is None + assert results[1].daily_clean_disks_files is None + assert results[1].daily_clean_disks_days is None + assert results[1].daily_clean_disks_verbose is None + assert results[1].daily_clean_tmps_enable + assert results[1].daily_clean_tmps_dirs == "/tmp" + assert results[1].daily_clean_tmps_days == 3 + assert results[1].daily_clean_tmps_ignore == "$daily_clean_tmps_ignore quota.user quota.group" + assert results[1].daily_clean_tmps_verbose + assert results[1].daily_clean_preserve_enable is None + assert results[1].daily_clean_preserve_days is None + assert results[1].daily_clean_preserve_verbose is None + assert results[1].daily_clean_msgs_enable + assert results[1].daily_clean_msgs_days is None + assert results[1].daily_clean_rwho_enable + assert results[1].daily_clean_rwho_days == 7 + assert results[1].daily_clean_rwho_verbose + assert results[1].daily_clean_hoststat_enable is None + assert results[1].daily_accounting_enable + assert not results[1].daily_accounting_compress + assert results[1].daily_accounting_save == 3 + assert results[1].daily_accounting_flags == "-q" + assert results[1].daily_status_disks_enable + assert results[1].daily_status_disks_df_flags == "-l -h" + assert results[1].daily_status_network_enable + assert results[1].daily_status_network_usedns + assert results[1].daily_status_mailq_enable + assert not results[1].daily_status_mailq_shorten + assert results[1].daily_status_include_submit_mailq + assert results[1].daily_local == "/etc/daily.local" + assert results[1].source == "/etc/defaults/periodic.conf" + + assert results[2].weekly_locate_enable is None + assert results[2].weekly_whatis_enable is None + assert results[2].weekly_noid_enable is None + assert results[2].weekly_noid_dirs is None + assert results[2].weekly_status_security_enable is None + assert results[2].weekly_status_security_inline is None + assert results[2].weekly_status_security_output is None + assert results[2].weekly_status_pkg_enable is None + assert results[2].pkg_version is None + assert results[2].pkg_version_index is None + assert results[2].weekly_local == "/etc/weekly.local" + assert results[2].source == "/etc/defaults/periodic.conf" + + assert results[3].monthly_accounting_enable + assert results[3].monthly_status_security_enable is None + assert results[3].monthly_status_security_inline is None + assert results[3].monthly_status_security_output is None + assert results[3].monthly_local == "/etc/monthly.local" + assert results[3].source == "/etc/defaults/periodic.conf" + + # TODO: Add test for PeriodicConfSecurityRecord diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py index 8b74658731..71262b533c 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -28,53 +27,42 @@ def test_resources_info_strings( names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem ) -> None: - stat_results = [] - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/{name}") fs_unix.map_file(f"{path}", data_file) - entry = fs_unix.get(f"{path}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(ResourcesInfoStringsPlugin) + target_unix.add_plugin(ResourcesInfoStringsPlugin) - results = list(target_unix.resources_info_strings()) - results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) - assert len(results) == 2 + results = list(target_unix.resources_info_strings()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", ""))) + assert len(results) == 2 - assert results[0].cf_bundle_name == "WiFiAgent" - assert results[0].cf_bundle_display_name == "Wi-Fi" - assert results[0].cf_bundle_identifier is None - assert results[0].cf_bundle_version is None - assert results[0].cf_bundle_package_type is None - assert results[0].cf_bundle_signature is None - assert results[0].cf_bundle_executable is None - assert results[0].cf_bundle_document_types == [] - assert results[0].cf_bundle_short_version_string is None - assert results[0].ls_minimum_system_version is None - assert results[0].ns_human_readable_copyright is None - assert results[0].ns_main_nib_file is None - assert results[0].ns_principal_class is None - assert results[0].source == "/System/Library/CoreServices/WiFiAgent.app/Contents/Resources/InfoPlist.strings" + assert results[0].cf_bundle_name == "WiFiAgent" + assert results[0].cf_bundle_display_name == "Wi-Fi" + assert results[0].cf_bundle_identifier is None + assert results[0].cf_bundle_version is None + assert results[0].cf_bundle_package_type is None + assert results[0].cf_bundle_signature is None + assert results[0].cf_bundle_executable is None + assert results[0].cf_bundle_document_types == [] + assert results[0].cf_bundle_short_version_string is None + assert results[0].ls_minimum_system_version is None + assert results[0].ns_human_readable_copyright is None + assert results[0].ns_main_nib_file is None + assert results[0].ns_principal_class is None + assert results[0].source == "/System/Library/CoreServices/WiFiAgent.app/Contents/Resources/InfoPlist.strings" - assert results[1].cf_bundle_name is None - assert results[1].cf_bundle_display_name is None - assert results[1].cf_bundle_identifier is None - assert results[1].cf_bundle_version is None - assert results[1].cf_bundle_package_type is None - assert results[1].cf_bundle_signature is None - assert results[1].cf_bundle_executable is None - assert results[1].cf_bundle_document_types == [] - assert results[1].cf_bundle_short_version_string is None - assert results[1].ls_minimum_system_version is None - assert results[1].ns_human_readable_copyright == "Copyright © 2004 Apple Inc. All rights reserved." - assert results[1].ns_main_nib_file is None - assert results[1].ns_principal_class is None - assert results[1].source == "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings" + assert results[1].cf_bundle_name is None + assert results[1].cf_bundle_display_name is None + assert results[1].cf_bundle_identifier is None + assert results[1].cf_bundle_version is None + assert results[1].cf_bundle_package_type is None + assert results[1].cf_bundle_signature is None + assert results[1].cf_bundle_executable is None + assert results[1].cf_bundle_document_types == [] + assert results[1].cf_bundle_short_version_string is None + assert results[1].ls_minimum_system_version is None + assert results[1].ns_human_readable_copyright == "Copyright © 2004 Apple Inc. All rights reserved." + assert results[1].ns_main_nib_file is None + assert results[1].ns_principal_class is None + assert results[1].source == "/System/Library/Extensions/OSvKernDSPLib.kext/Contents/Resources/InfoPlist.strings" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py index 57d1519af2..b98b5cb759 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -20,43 +19,32 @@ ], ) def test_passwords(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: - stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/shadow/{test_file}") fs_unix.map_file(f"/var/db/dslocal/nodes/Default/users/{test_file}", data_file) - entry = fs_unix.get(f"/var/db/dslocal/nodes/Default/users/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - entries.append(entry) - stat_results.append(stat_result) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(ShadowPlugin) - - results = list(target_unix.passwords()) - results.sort(key=lambda r: r.name) - assert len(results) == 2 - - assert results[0].name == "_mbsetupuser" - assert ( - results[0].hash - == "46e8c10ca67d3d8a3998fa83b9dbb845d3a5c8b257d60272a22912a1a0fd6bff5d315b6f5b50577e465a8ae57f5a0f0fd52ea708d614141c428911d3273b67125d9599198590376ad38f43d6ed3a1173fd86c378b9879c5a026809c802df5ae5bd72e53bb033f9aa9e0e24600c03d0ec287e466f5c79eb1be42fac7afeee2b7e" # noqa: E501 - ) - assert results[0].salt == "225ea6a940b08c6985c792a7195ef008527b5c83829ca0850bdcae77e517c9b5" - assert results[0].iterations == 78125 - assert results[0].algorithm == "SALTED-SHA512-PBKDF2" - assert results[0].source == "/var/db/dslocal/nodes/Default/users/_mbsetupuser.plist" - - assert results[-1].name == "user" - assert ( - results[-1].hash - == "f6e502079b9eb8b2f49b099c235aed2debb0b80084dca99f9a23db41dbfca88be0266bf62dfca5923ccd20d4c8f81140e2cb09b7951cce001ccb37b9fa3cee46b68a0edfc8e055e7f523feaec444f775eaddcf7d4e91e5e918a0dd715a4a749fd92974b023db4cb8851f4fdee3bd091755686be92a3f8ff906c6552907a8b0dc" # noqa: E501 - ) - assert results[-1].salt == "64e7869eef2d9bf35bc9b72fddcf90055be833eed2c6c7d58ca91f0c0754f5f6" - assert results[-1].iterations == 128205 - assert results[-1].algorithm == "SALTED-SHA512-PBKDF2" - assert results[-1].source == "/var/db/dslocal/nodes/Default/users/user.plist" + + target_unix.add_plugin(ShadowPlugin) + + results = list(target_unix.passwords()) + results.sort(key=lambda r: r.name) + assert len(results) == 2 + + assert results[0].name == "_mbsetupuser" + assert ( + results[0].hash + == "46e8c10ca67d3d8a3998fa83b9dbb845d3a5c8b257d60272a22912a1a0fd6bff5d315b6f5b50577e465a8ae57f5a0f0fd52ea708d614141c428911d3273b67125d9599198590376ad38f43d6ed3a1173fd86c378b9879c5a026809c802df5ae5bd72e53bb033f9aa9e0e24600c03d0ec287e466f5c79eb1be42fac7afeee2b7e" # noqa: E501 + ) + assert results[0].salt == "225ea6a940b08c6985c792a7195ef008527b5c83829ca0850bdcae77e517c9b5" + assert results[0].iterations == 78125 + assert results[0].algorithm == "SALTED-SHA512-PBKDF2" + assert results[0].source == "/var/db/dslocal/nodes/Default/users/_mbsetupuser.plist" + + assert results[-1].name == "user" + assert ( + results[-1].hash + == "f6e502079b9eb8b2f49b099c235aed2debb0b80084dca99f9a23db41dbfca88be0266bf62dfca5923ccd20d4c8f81140e2cb09b7951cce001ccb37b9fa3cee46b68a0edfc8e055e7f523feaec444f775eaddcf7d4e91e5e918a0dd715a4a749fd92974b023db4cb8851f4fdee3bd091755686be92a3f8ff906c6552907a8b0dc" # noqa: E501 + ) + assert results[-1].salt == "64e7869eef2d9bf35bc9b72fddcf90055be833eed2c6c7d58ca91f0c0754f5f6" + assert results[-1].iterations == 128205 + assert results[-1].algorithm == "SALTED-SHA512-PBKDF2" + assert results[-1].source == "/var/db/dslocal/nodes/Default/users/user.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py index 52cd236dd8..d1d0552498 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -24,30 +23,24 @@ def test_software_update_preferences(test_file: str, target_unix: Target, fs_uni tz = timezone.utc data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Preferences/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result - - target_unix.add_plugin(SoftwareUpdatePreferencesPlugin) - - results = list(target_unix.software_update_preferences()) - assert len(results) == 1 - - assert results[0].last_result_code == 2 - assert results[0].last_attempt_system_version == "26.4 (25E246)" - assert results[0].last_attempt_build_version == "26.4 (25E246)" - assert results[0].automatic_download - assert results[0].automatically_install_macos_updates - assert results[0].critical_update_install - assert results[0].config_data_install - assert results[0].recommended_updates == [] - assert results[0].splat_enabled - assert not results[0].post_logout_notification - assert results[0].last_recommended_major_os_bundle_id == "" - assert results[0].primary_languages == ["en", "en-US"] - assert results[0].last_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) - assert results[0].last_full_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) - assert results[0].source == "/Library/Preferences/com.apple.SoftwareUpdate.plist" + + target_unix.add_plugin(SoftwareUpdatePreferencesPlugin) + + results = list(target_unix.software_update_preferences()) + assert len(results) == 1 + + assert results[0].last_result_code == 2 + assert results[0].last_attempt_system_version == "26.4 (25E246)" + assert results[0].last_attempt_build_version == "26.4 (25E246)" + assert results[0].automatic_download + assert results[0].automatically_install_macos_updates + assert results[0].critical_update_install + assert results[0].config_data_install + assert results[0].recommended_updates == [] + assert results[0].splat_enabled + assert not results[0].post_logout_notification + assert results[0].last_recommended_major_os_bundle_id == "" + assert results[0].primary_languages == ["en", "en-US"] + assert results[0].last_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) + assert results[0].last_full_successful_date == datetime(2026, 3, 25, 14, 11, 47, tzinfo=tz) + assert results[0].source == "/Library/Preferences/com.apple.SoftwareUpdate.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py index 3528b10243..84f613e9a8 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,20 +21,14 @@ def test_system_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Preferences/SystemConfiguration/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Preferences/SystemConfiguration/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(SystemPreferencesPlugin) - target_unix.add_plugin(SystemPreferencesPlugin) + results = list(target_unix.system_preferences()) + assert len(results) == 1 - results = list(target_unix.system_preferences()) - assert len(results) == 1 - - assert results[0].Counter == 2 - assert results[0].DeviceUUID == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" - assert results[0].Version == 7200 - assert results[0].PreferredOrder == [] - assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" + assert results[0].Counter == 2 + assert results[0].DeviceUUID == "0527924E-C5F8-4703-BDDC-9283B6E9FDAE" + assert results[0].Version == 7200 + assert results[0].PreferredOrder == [] + assert results[0].source == "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py index ad0900b285..70ee71c709 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -48,61 +47,49 @@ def test_tcc( target_unix.users = lambda: [ user, ] - stat_results = [] - - entries = [] for name, path in zip(names, paths, strict=True): data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/tcc/{name}") fs_unix.map_file(path, data_file) - entry = fs_unix.get(path) - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - stat_results.append(stat_result) - entries.append(entry) - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(TCCPlugin) + target_unix.add_plugin(TCCPlugin) - results = list(target_unix.tcc()) - results.sort(key=lambda r: r.source) + results = list(target_unix.tcc()) + results.sort(key=lambda r: r.source) - assert len(results) == 51 + assert len(results) == 51 - assert results[0].table == "admin" - assert results[0].key == "version" - assert results[0].value == "32" - assert results[0].source == "/Library/Application Support/com.apple.TCC/TCC.db" + assert results[0].table == "admin" + assert results[0].key == "version" + assert results[0].value == "32" + assert results[0].source == "/Library/Application Support/com.apple.TCC/TCC.db" - assert results[1].table == "access" - assert results[1].service == "kTCCServiceSystemPolicyAllFiles" - assert ( - results[1].client - == "/System/Library/PrivateFrameworks/VoiceShortcuts.framework/Versions/A/Support/siriactionsd" - ) - assert results[1].client_type == 1 - assert results[1].auth_value == 0 - assert results[1].auth_reason == 5 - assert results[1].auth_version == 1 - assert ( - results[1].csreq - == b"\xfa\xde\x0c\x00\x00\x00\x004\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x16com.apple.siriactionsd\x00\x00\x00\x00\x00\x03" # noqa E501 - ) - assert results[1].policy_id is None - assert results[1].indirect_object_identifier == "UNUSED" - assert results[1].indirect_object_identifier_type is None - assert results[1].indirect_object_code_identity is None - assert results[1].flags == 0 - assert results[1].last_modified == datetime(2026, 3, 25, 14, 13, 27, tzinfo=tz) - assert results[1].pid is None - assert results[1].pid_version is None - assert results[1].boot_uuid == "UNUSED" - assert results[1].last_reminded == datetime(1970, 1, 1, 0, 0, 0, tzinfo=tz) - assert results[1].source == "/Library/Application Support/com.apple.TCC/TCC.db" + assert results[1].table == "access" + assert results[1].service == "kTCCServiceSystemPolicyAllFiles" + assert ( + results[1].client + == "/System/Library/PrivateFrameworks/VoiceShortcuts.framework/Versions/A/Support/siriactionsd" + ) + assert results[1].client_type == 1 + assert results[1].auth_value == 0 + assert results[1].auth_reason == 5 + assert results[1].auth_version == 1 + assert ( + results[1].csreq + == b"\xfa\xde\x0c\x00\x00\x00\x004\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x02\x00\x00\x00\x16com.apple.siriactionsd\x00\x00\x00\x00\x00\x03" # noqa E501 + ) + assert results[1].policy_id is None + assert results[1].indirect_object_identifier == "UNUSED" + assert results[1].indirect_object_identifier_type is None + assert results[1].indirect_object_code_identity is None + assert results[1].flags == 0 + assert results[1].last_modified == datetime(2026, 3, 25, 14, 13, 27, tzinfo=tz) + assert results[1].pid is None + assert results[1].pid_version is None + assert results[1].boot_uuid == "UNUSED" + assert results[1].last_reminded == datetime(1970, 1, 1, 0, 0, 0, tzinfo=tz) + assert results[1].source == "/Library/Application Support/com.apple.TCC/TCC.db" - assert results[-1].table == "integrity_flag" - assert results[-1].key == "integrity_flag" - assert results[-1].value == "0" - assert results[-1].source == "/Users/user/Library/Application Support/com.apple.TCC/TCC.db" + assert results[-1].table == "integrity_flag" + assert results[-1].key == "integrity_flag" + assert results[-1].value == "0" + assert results[-1].source == "/Users/user/Library/Application Support/com.apple.TCC/TCC.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py index 923962f1ab..9f9b9b5e76 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -2,7 +2,6 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -36,86 +35,75 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: user, ] - stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/text_replacements/{test_file}") fs_unix.map_file(f"Users/user/Library/KeyboardServices/{test_file}", data_file) - entry = fs_unix.get(f"Users/user/Library/KeyboardServices/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - entries.append(entry) - stat_results.append(stat_result) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(TextReplacementsPlugin) - - results = list(target_unix.text_replacements()) - - assert len(results) == 8 - - assert results[0].table == "ZTEXTREPLACEMENTENTRY" - assert results[0].z_pk == 1 - assert results[0].z_ent == 1 - assert results[0].z_opt == 1 - assert results[0].z_needs_save_to_cloud == 1 - assert results[0].z_was_deleted == 0 - assert results[0].z_timestamp == datetime(2026, 3, 25, 14, 12, 48, 339994, tzinfo=timezone.utc) - assert results[0].z_phrase == "On my way!" - assert results[0].z_shortcut == "omw" - assert results[0].z_unique_name == "CB36B9A8-B570-4972-BC6C-B12DAA7C0B97" - assert results[0].z_remote_record_info is None - assert results[0].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[1].table == "ZTRCLOUDKITSYNCSTATE" - assert results[1].z_pk == 1 - assert results[1].z_ent == 2 - assert results[1].z_opt == 1 - assert results[1].z_did_pull_once == 0 - assert results[1].z_fetch_change_token is None - assert results[1].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[2].table == "Z_PRIMARYKEY" - assert results[2].z_ent == 1 - assert results[2].z_name == "TextReplacementEntry" - assert results[2].z_super == 0 - assert results[2].z_max == 1 - assert results[2].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[3].table == "Z_PRIMARYKEY" - assert results[3].z_ent == 2 - assert results[3].z_name == "TRCloudKitSyncState" - assert results[3].z_super == 0 - assert results[3].z_max == 1 - assert results[3].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[4].ns_persistence_maximum_framework_version == 1526 - assert results[4].ns_store_type == "SQLite" - assert results[4].ns_auto_vacuum_level == 2 - assert results[4].ns_store_model_version_identifiers == [""] - assert results[4].ns_store_model_version_hashes_version == 3 - assert results[4].ns_store_model_version_hashes_digest == ( - "nb/qJ+hB9auf83oGYKFndhE+Etk/7JNbNosAbVi5Zu0biqTNK/UfC4ofTqRhou6nHBT1ci00ct9E+U9vnbhfPw==" - ) - assert results[4].ns_store_model_version_checksum_key == ("c4ljuOCxf+SvLXrpw0Xcnwe7kkFsMpkUuv43N2r16m4=") - assert results[4].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[5].tr_cloud_kit_sync_state == ( - b"\xa2 \x8cuo\xd1V\xd2p\x89C#\xbd\xb1$\xf1F\x97X\xe9\x01\x1e\x02\xcb\xf5\xe5y$\x90\xc4\xee\xdf" - ) - assert results[5].text_replacement_entry == ( - b"#C\t\xd2l\xa7\xeaK##S\xae\xb7>\x82\xb5\xb5\xc8\xafB\xf4\t\xc4]\xa2)\xb0}+\xd9\xab\x03" - ) - assert results[5].plist_path == "NSStoreModelVersionHashes" - assert results[5].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[6].table == "Z_METADATA" - assert results[6].z_version == 1 - assert results[6].z_uuid == "555547C2-D9F5-4B51-8BEE-EEE6158CDDED" - assert results[6].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" - - assert results[7].table == "Z_MODELCACHE" - assert results[7].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + target_unix.add_plugin(TextReplacementsPlugin) + + results = list(target_unix.text_replacements()) + + assert len(results) == 8 + + assert results[0].table == "ZTEXTREPLACEMENTENTRY" + assert results[0].z_pk == 1 + assert results[0].z_ent == 1 + assert results[0].z_opt == 1 + assert results[0].z_needs_save_to_cloud == 1 + assert results[0].z_was_deleted == 0 + assert results[0].z_timestamp == datetime(2026, 3, 25, 14, 12, 48, 339994, tzinfo=timezone.utc) + assert results[0].z_phrase == "On my way!" + assert results[0].z_shortcut == "omw" + assert results[0].z_unique_name == "CB36B9A8-B570-4972-BC6C-B12DAA7C0B97" + assert results[0].z_remote_record_info is None + assert results[0].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[1].table == "ZTRCLOUDKITSYNCSTATE" + assert results[1].z_pk == 1 + assert results[1].z_ent == 2 + assert results[1].z_opt == 1 + assert results[1].z_did_pull_once == 0 + assert results[1].z_fetch_change_token is None + assert results[1].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[2].table == "Z_PRIMARYKEY" + assert results[2].z_ent == 1 + assert results[2].z_name == "TextReplacementEntry" + assert results[2].z_super == 0 + assert results[2].z_max == 1 + assert results[2].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[3].table == "Z_PRIMARYKEY" + assert results[3].z_ent == 2 + assert results[3].z_name == "TRCloudKitSyncState" + assert results[3].z_super == 0 + assert results[3].z_max == 1 + assert results[3].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[4].ns_persistence_maximum_framework_version == 1526 + assert results[4].ns_store_type == "SQLite" + assert results[4].ns_auto_vacuum_level == 2 + assert results[4].ns_store_model_version_identifiers == [""] + assert results[4].ns_store_model_version_hashes_version == 3 + assert results[4].ns_store_model_version_hashes_digest == ( + "nb/qJ+hB9auf83oGYKFndhE+Etk/7JNbNosAbVi5Zu0biqTNK/UfC4ofTqRhou6nHBT1ci00ct9E+U9vnbhfPw==" + ) + assert results[4].ns_store_model_version_checksum_key == ("c4ljuOCxf+SvLXrpw0Xcnwe7kkFsMpkUuv43N2r16m4=") + assert results[4].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[5].tr_cloud_kit_sync_state == ( + b"\xa2 \x8cuo\xd1V\xd2p\x89C#\xbd\xb1$\xf1F\x97X\xe9\x01\x1e\x02\xcb\xf5\xe5y$\x90\xc4\xee\xdf" + ) + assert results[5].text_replacement_entry == ( + b"#C\t\xd2l\xa7\xeaK##S\xae\xb7>\x82\xb5\xb5\xc8\xafB\xf4\t\xc4]\xa2)\xb0}+\xd9\xab\x03" + ) + assert results[5].plist_path == "NSStoreModelVersionHashes" + assert results[5].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[6].table == "Z_METADATA" + assert results[6].z_version == 1 + assert results[6].z_uuid == "555547C2-D9F5-4B51-8BEE-EEE6158CDDED" + assert results[6].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" + + assert results[7].table == "Z_MODELCACHE" + assert results[7].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py index 23f00fbfae..6ccb8700e0 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -22,17 +21,11 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") fs_unix.map_file(f"/Library/Preferences/{test_file}", data_file) - entry = fs_unix.get(f"/Library/Preferences/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - with patch.object(entry, "stat") as mock_stat: - mock_stat.return_value = stat_result + target_unix.add_plugin(TimeMachinePlugin) - target_unix.add_plugin(TimeMachinePlugin) + results = list(target_unix.time_machine()) + assert len(results) == 1 - results = list(target_unix.time_machine()) - assert len(results) == 1 - - assert results[0].preferences_version == 6 - assert results[0].source == "/Library/Preferences/com.apple.TimeMachine.plist" + assert results[0].preferences_version == 6 + assert results[0].source == "/Library/Preferences/com.apple.TimeMachine.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py index 21ad211c08..7d3462808b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from unittest.mock import patch import pytest @@ -34,145 +33,133 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt target_unix.users = lambda: [ user, ] - - stat_results = [] - entries = [] for test_file in test_files: data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/user_accounts/{test_file}") fs_unix.map_file(f"Users/user/Library/Accounts/{test_file}", data_file) - entry = fs_unix.get(f"Users/user/Library/Accounts/{test_file}") - stat_result = entry.stat() - stat_result.st_mtime = 1704067199 - entries.append(entry) - stat_results.append(stat_result) - - with ( - patch.object(entries[0], "stat", return_value=stat_results[0]), - patch.object(entries[1], "stat", return_value=stat_results[1]), - ): - target_unix.add_plugin(UserAccountsPlugin) - - results = list(target_unix.user_accounts()) - - assert len(results) == 290 - - assert results[0].table == "ZACCESSOPTIONSKEY" - assert results[0].z_pk == 1 - assert results[0].z_ent == 1 - assert results[0].z_opt == 1 - assert results[0].z_enum_value == 0 - assert results[0].z_name == "ACTencentWeiboAppIdKey" - assert results[0].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[7].table == "Z_1OWNINGACCOUNTTYPES" - assert results[7].z_1_access_keys == 7 - assert results[7].z_4_owning_account_types == 43 - assert results[7].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[19].table == "ZACCOUNT" - assert results[19].z_pk == 1 - assert results[19].z_ent == 2 - assert results[19].z_opt == 7 - assert results[19].z_active == 0 - assert results[19].z_authenticated == 0 - assert results[19].z_supports_authentication == 1 - assert results[19].z_visible == 1 - assert results[19].z_warming_up == 0 - assert results[19].z_account_type == 50 - assert results[19].z_parent_account is None - assert results[19].z_date.isoformat() == "1995-03-25T14:11:32.168510+00:00" - assert results[19].z_last_credential_renewal_rejection_date is None - assert results[19].z_account_description is None - assert results[19].z_authentication_type is None - assert results[19].z_credential_type is None - assert results[19].z_identifier == "9DE5EA8C-7EE3-433C-9387-A1158923C75B" - assert results[19].z_modification_id == "581D5181-C65D-4598-80EE-7386D70A8799" - assert results[19].z_owning_bundle_id == "amsaccountsd" - assert results[19].z_username == "local" - assert results[19].z_dataclass_properties is None - assert results[19].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[21].table == "Z_2ENABLEDDATACLASSES" - assert results[21].z_2_enabled_accounts == 2 - assert results[21].z_7_enabled_dataclasses == 20 - assert results[21].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[22].table == "ZACCOUNTPROPERTY" - assert results[22].z_pk == 1 - assert results[22].z_ent == 3 - assert results[22].z_opt == 1 - assert results[22].z_owner == 1 - assert results[22].z_key == "isLocalAccount" - assert results[22].z_value == "True" - assert results[22].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[36].table == "ZACCOUNTTYPE" - assert results[36].z_pk == 1 - assert results[36].z_ent == 4 - assert results[36].z_opt == 1 - assert results[36].z_obsolete == 0 - assert results[36].z_supports_authentication == 1 - assert results[36].z_supports_multiple_accounts == 1 - assert results[36].z_visibility == 0 - assert results[36].z_account_type_description == "Gmail" - assert results[36].z_credential_protection_policy is None - assert results[36].z_credential_type == "oauth2" - assert results[36].z_identifier == "com.apple.account.Google" - assert results[36].z_owning_bundle_id == "com.apple.accountsd" - assert results[36].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[92].table == "Z_4SUPPORTEDDATACLASSES" - assert results[92].z_4_supported_types == 55 - assert results[92].z_7_supported_dataclasses == 27 - assert results[92].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[197].table == "Z_4SYNCABLEDATACLASSES" - assert results[197].z_4_syncable_types == 55 - assert results[197].z_7_syncable_dataclasses == 27 - assert results[197].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[279].table == "Z_PRIMARYKEY" - assert results[279].z_ent == 1 - assert results[279].z_name == "AccessOptionsKey" - assert results[279].z_super == 0 - assert results[279].z_max == 7 - assert results[279].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[286].ac_account_type_version == 107 - assert results[286].ns_auto_vacuum_level == 2 - assert results[286].ns_persistence_framework_version == 1526 - assert results[286].ns_persistence_maximum_framework_version == 1526 - assert results[286].ns_store_model_version_checksum_key == "ZAPc9m2m45TI0HS9GepqhdVKmn8FObA8NpZcwc7UIT8=" - assert ( - results[286].ns_store_model_version_hashes_digest - == "6zOxtAC8CBiqUoJ8U+mC0mHIF9LkhkOGYval68MNS/V6S4tVudh/sDu45bnjvnLbb+I3Ouq7XXF3ZNolh1b4+A==" - ) - assert results[286].ns_store_model_version_hashes_version == 3 - assert results[286].ns_store_model_version_identifiers == "['30']" - assert results[286].ns_store_type == "SQLite" - assert results[286].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[287].access_options_key is not None - assert isinstance(results[287].access_options_key, (bytes, bytearray)) - assert results[287].account_hash is not None - assert isinstance(results[287].account_hash, (bytes, bytearray)) - assert results[287].account_property is not None - assert isinstance(results[287].account_property, (bytes, bytearray)) - assert results[287].account_type is not None - assert isinstance(results[287].account_type, (bytes, bytearray)) - assert results[287].authorization is not None - assert isinstance(results[287].authorization, (bytes, bytearray)) - assert results[287].credential_item is not None - assert isinstance(results[287].credential_item, (bytes, bytearray)) - assert results[287].dataclass is not None - assert isinstance(results[287].dataclass, (bytes, bytearray)) - assert results[287].plist_path == "NSStoreModelVersionHashes" - assert results[287].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[288].table == "Z_METADATA" - assert results[288].z_version == 1 - assert results[288].z_uuid == "9802D604-BFA7-4F9D-A39F-0CF8E6FD0FAC" - assert results[288].source == "/Users/user/Library/Accounts/Accounts4.sqlite" - - assert results[289].table == "Z_MODELCACHE" - assert results[289].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + target_unix.add_plugin(UserAccountsPlugin) + + results = list(target_unix.user_accounts()) + + assert len(results) == 290 + + assert results[0].table == "ZACCESSOPTIONSKEY" + assert results[0].z_pk == 1 + assert results[0].z_ent == 1 + assert results[0].z_opt == 1 + assert results[0].z_enum_value == 0 + assert results[0].z_name == "ACTencentWeiboAppIdKey" + assert results[0].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[7].table == "Z_1OWNINGACCOUNTTYPES" + assert results[7].z_1_access_keys == 7 + assert results[7].z_4_owning_account_types == 43 + assert results[7].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[19].table == "ZACCOUNT" + assert results[19].z_pk == 1 + assert results[19].z_ent == 2 + assert results[19].z_opt == 7 + assert results[19].z_active == 0 + assert results[19].z_authenticated == 0 + assert results[19].z_supports_authentication == 1 + assert results[19].z_visible == 1 + assert results[19].z_warming_up == 0 + assert results[19].z_account_type == 50 + assert results[19].z_parent_account is None + assert results[19].z_date.isoformat() == "1995-03-25T14:11:32.168510+00:00" + assert results[19].z_last_credential_renewal_rejection_date is None + assert results[19].z_account_description is None + assert results[19].z_authentication_type is None + assert results[19].z_credential_type is None + assert results[19].z_identifier == "9DE5EA8C-7EE3-433C-9387-A1158923C75B" + assert results[19].z_modification_id == "581D5181-C65D-4598-80EE-7386D70A8799" + assert results[19].z_owning_bundle_id == "amsaccountsd" + assert results[19].z_username == "local" + assert results[19].z_dataclass_properties is None + assert results[19].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[21].table == "Z_2ENABLEDDATACLASSES" + assert results[21].z_2_enabled_accounts == 2 + assert results[21].z_7_enabled_dataclasses == 20 + assert results[21].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[22].table == "ZACCOUNTPROPERTY" + assert results[22].z_pk == 1 + assert results[22].z_ent == 3 + assert results[22].z_opt == 1 + assert results[22].z_owner == 1 + assert results[22].z_key == "isLocalAccount" + assert results[22].z_value == "True" + assert results[22].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[36].table == "ZACCOUNTTYPE" + assert results[36].z_pk == 1 + assert results[36].z_ent == 4 + assert results[36].z_opt == 1 + assert results[36].z_obsolete == 0 + assert results[36].z_supports_authentication == 1 + assert results[36].z_supports_multiple_accounts == 1 + assert results[36].z_visibility == 0 + assert results[36].z_account_type_description == "Gmail" + assert results[36].z_credential_protection_policy is None + assert results[36].z_credential_type == "oauth2" + assert results[36].z_identifier == "com.apple.account.Google" + assert results[36].z_owning_bundle_id == "com.apple.accountsd" + assert results[36].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[92].table == "Z_4SUPPORTEDDATACLASSES" + assert results[92].z_4_supported_types == 55 + assert results[92].z_7_supported_dataclasses == 27 + assert results[92].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[197].table == "Z_4SYNCABLEDATACLASSES" + assert results[197].z_4_syncable_types == 55 + assert results[197].z_7_syncable_dataclasses == 27 + assert results[197].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[279].table == "Z_PRIMARYKEY" + assert results[279].z_ent == 1 + assert results[279].z_name == "AccessOptionsKey" + assert results[279].z_super == 0 + assert results[279].z_max == 7 + assert results[279].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[286].ac_account_type_version == 107 + assert results[286].ns_auto_vacuum_level == 2 + assert results[286].ns_persistence_framework_version == 1526 + assert results[286].ns_persistence_maximum_framework_version == 1526 + assert results[286].ns_store_model_version_checksum_key == "ZAPc9m2m45TI0HS9GepqhdVKmn8FObA8NpZcwc7UIT8=" + assert ( + results[286].ns_store_model_version_hashes_digest + == "6zOxtAC8CBiqUoJ8U+mC0mHIF9LkhkOGYval68MNS/V6S4tVudh/sDu45bnjvnLbb+I3Ouq7XXF3ZNolh1b4+A==" + ) + assert results[286].ns_store_model_version_hashes_version == 3 + assert results[286].ns_store_model_version_identifiers == "['30']" + assert results[286].ns_store_type == "SQLite" + assert results[286].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[287].access_options_key is not None + assert isinstance(results[287].access_options_key, (bytes, bytearray)) + assert results[287].account_hash is not None + assert isinstance(results[287].account_hash, (bytes, bytearray)) + assert results[287].account_property is not None + assert isinstance(results[287].account_property, (bytes, bytearray)) + assert results[287].account_type is not None + assert isinstance(results[287].account_type, (bytes, bytearray)) + assert results[287].authorization is not None + assert isinstance(results[287].authorization, (bytes, bytearray)) + assert results[287].credential_item is not None + assert isinstance(results[287].credential_item, (bytes, bytearray)) + assert results[287].dataclass is not None + assert isinstance(results[287].dataclass, (bytes, bytearray)) + assert results[287].plist_path == "NSStoreModelVersionHashes" + assert results[287].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[288].table == "Z_METADATA" + assert results[288].z_version == 1 + assert results[288].z_uuid == "9802D604-BFA7-4F9D-A39F-0CF8E6FD0FAC" + assert results[288].source == "/Users/user/Library/Accounts/Accounts4.sqlite" + + assert results[289].table == "Z_MODELCACHE" + assert results[289].source == "/Users/user/Library/Accounts/Accounts4.sqlite" From f9a0fe2ef64f7cb85559c5912daf79189f3fa02b Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:23:29 +0200 Subject: [PATCH 36/52] Add Duet KnowledgeC plugin --- .../unix/bsd/darwin/macos/duet_knowledge_c.py | 491 ++++++++++++++++++ .../bsd/darwin/macos/duet_knowledge_c/Main.db | 3 + .../darwin/macos/duet_knowledge_c/Main.db-wal | 3 + .../bsd/darwin/macos/duet_knowledge_c/User.db | 3 + .../darwin/macos/duet_knowledge_c/User.db-wal | 3 + .../bsd/darwin/macos/test_duet_knowledge_c.py | 241 +++++++++ 6 files changed, 744 insertions(+) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db-wal create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py new file mode 100644 index 0000000000..444e23a3df --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -0,0 +1,491 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +ZKeyValueRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_key_value", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("string", "z_domain"), + ("string", "z_key"), + ("string", "z_value"), + ("path", "source"), + ], +) + +ZContextualChangeRegistrationRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_contextual_change_registration", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_is_active"), + ("varint", "z_is_multi_device_registration"), + ("datetime", "z_creation_date"), + ("string", "z_identifier"), + ("string", "z_properties"), + ("path", "source"), + ], +) + +ZObjectRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_object", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_uuid_hash"), + ("string", "z_event"), + ("varint", "z_source_fk"), + ("string", "z_category_type"), + ("varint", "z_integer_value"), + ("varint", "z_compatibility_version"), + ("varint", "z_end_day_of_week"), + ("varint", "z_end_second_of_day"), + ("varint", "z_has_custom_metadata"), + ("varint", "z_has_structured_metadata"), + ("varint", "z_seconds_from_gmt"), + ("varint", "z_should_sync"), + ("varint", "z_start_day_of_week"), + ("varint", "z_start_second_of_day"), + ("varint", "z_value_class"), + ("varint", "z_value_integer"), + ("varint", "z_value_type_code"), + ("varint", "z_structured_metadata"), + ("string", "z_value"), + ("string", "z_9_value"), + ("string", "z_identifier_type"), + ("string", "z_quantity_type"), + ("datetime", "z_creation_date"), + ("datetime", "z_local_creation_date"), + ("varint", "z_confidence"), + ("datetime", "z_end_date"), + ("datetime", "z_start_date"), + ("varint", "z_value_double"), + ("varint", "z_double_value"), + ("string", "z_uuid"), + ("string", "z_stream_name"), + ("string", "z_value_string"), + ("string", "z_string"), + ("string", "z_metadata"), + ("path", "source"), + ], +) + +ZSourceRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_source", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("string", "z_user_id"), + ("string", "z_bundle_id"), + ("string", "z_device_id"), + ("string", "z_group_id"), + ("string", "z_intent_id"), + ("string", "z_item_id"), + ("string", "z_source_id"), + ("path", "source"), + ], +) + +# ZSTRUCTUREDMETADATA table ~200+ more columns, most of which are None in the majority of rows. +# Reduced record descriptor to core fields, other fields will be included in a warning. +ZStructuredMetadataRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_structured_metadata", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("path", "source"), + ], +) + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/z_plist", + [ + ("varint", "ns_persistence_maximum_framework_version"), + ("string[]", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("varint", "ns_auto_vacuum_level"), + ("string", "ns_store_model_version_hashes_digest"), + ("string", "ns_store_model_version_checksum_key"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_store_model_version_hashes_version"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/ns_store_model_version_hashes", + [ + ("bytes", "addition_change_set"), + ("bytes", "category_hash"), + ("bytes", "contextual_change_registration"), + ("bytes", "contextual_key_path"), + ("bytes", "custom_metadata"), + ("bytes", "deletion_change_set"), + ("bytes", "event"), + ("bytes", "histogram"), + ("bytes", "histogram_value"), + ("bytes", "identifier_hash"), + ("bytes", "key_value"), + ("bytes", "z_object"), + ("bytes", "quantity"), + ("bytes", "z_source"), + ("bytes", "structured_metadata"), + ("bytes", "sync_peer"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. +ZModelCacheRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/z_model_cache", + [ + ("string", "table"), + ("path", "source"), + ], +) + +DuetKnowledgeCRecords = ( + ZKeyValueRecord, + ZContextualChangeRegistrationRecord, + ZObjectRecord, + ZSourceRecord, + ZStructuredMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZModelCacheRecord, +) +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "ZISACTIVE": "z_is_active", + "ZISMULTIDEVICEREGISTRATION": "z_is_multi_device_registration", + "ZCREATIONDATE": "z_creation_date", + "ZIDENTIFIER": "z_identifier", + "ZPROPERTIES": "z_properties", + "ZUUIDHASH": "z_uuid_hash", + "ZEVENT": "z_event", + "ZSOURCE": "z_source_fk", + "ZCATEGORYTYPE": "z_category_type", + "ZINTEGERVALUE": "z_integer_value", + "ZCOMPATIBILITYVERSION": "z_compatibility_version", + "ZENDDAYOFWEEK": "z_end_day_of_week", + "ZENDSECONDOFDAY": "z_end_second_of_day", + "ZHASCUSTOMMETADATA": "z_has_custom_metadata", + "ZHASSTRUCTUREDMETADATA": "z_has_structured_metadata", + "ZSECONDSFROMGMT": "z_seconds_from_gmt", + "ZSHOULDSYNC": "z_should_sync", + "ZSTARTDAYOFWEEK": "z_start_day_of_week", + "ZSTARTSECONDOFDAY": "z_start_second_of_day", + "ZVALUECLASS": "z_value_class", + "ZVALUEINTEGER": "z_value_integer", + "ZVALUETYPECODE": "z_value_type_code", + "ZSTRUCTUREDMETADATA": "z_structured_metadata", + "ZKEY": "z_key", + "ZVALUE": "z_value", + "Z9_VALUE": "z_9_value", + "ZIDENTIFIERTYPE": "z_identifier_type", + "ZQUANTITYTYPE": "z_quantity_type", + "ZLOCALCREATIONDATE": "z_local_creation_date", + "ZCONFIDENCE": "z_confidence", + "ZENDDATE": "z_end_date", + "ZSTARTDATE": "z_start_date", + "ZVALUEDOUBLE": "z_value_double", + "ZDOUBLEVALUE": "z_double_value", + "ZUUID": "z_uuid", + "Z_UUID": "z_uuid", + "ZSTREAMNAME": "z_stream_name", + "ZVALUESTRING": "z_value_string", + "ZSTRING": "z_string", + "ZMETADATA": "z_metadata", + "ZDOMAIN": "z_domain", + "ZUSERID": "z_user_id", + "ZBUNDLEID": "z_bundle_id", + "ZDEVICEID": "z_device_id", + "ZGROUPID": "z_group_id", + "ZINTENTID": "z_intent_id", + "ZITEMID": "z_item_id", + "ZSOURCEID": "z_source_id", + "Z_NAME": "z_name", + "ZNAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "Z_VERSION": "z_version", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "AdditionChangeSet": "addition_change_set", + "Category": "category_hash", + "ContextualChangeRegistration": "contextual_change_registration", + "ContextualKeyPath": "contextual_key_path", + "CustomMetadata": "custom_metadata", + "DeletionChangeSet": "deletion_change_set", + "Event": "event", + "Histogram": "histogram", + "HistogramValue": "histogram_value", + "Identifier": "identifier_hash", + "KeyValue": "key_value", + "Object": "z_object", + "Quantity": "quantity", + "Source": "z_source", + "StructuredMetadata": "structured_metadata", + "SyncPeer": "sync_peer", +} + +CONVERT_TIMESTAMPS = { + "z_creation_date": "2001", + "z_local_creation_date": "2001", + "z_end_date": "2001", + "z_start_date": "2001", +} + + +class DuetKnowledgeCPlugin(Plugin): + """macOS Duet KnowledgeC Plugin. + + The KnowledgeC database is known for storing a wealth of data, including but not limited to: Application Activity, + Application Focus, Device Battery Percentage, Device Lock State, and Device Orientation. + Some of this data is being migrated to the Biome framework in newer macOS versions. + + References: + - https://belkasoft.com/lagging-for-win + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ + + PATH = "/var/db/CoreDuet/Knowledge/knowledgeC.db" + + USER_PATH = ("Library/Application Support/Knowledge/knowledgeC.db",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def _find_files(self) -> set: + files = set() + files.add(self.target.fs.path(self.PATH)) + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No knowledgeC.db files found") + + @export(record=DuetKnowledgeCRecords) + def duet_knowledge_c( + self, + ) -> Iterator[DuetKnowledgeCRecords]: + """Return macOS KnowledgeC database entries. + + Yields multiple record types extracted from the knowledgeC.db databases. + + .. code-block:: text + + ZKeyValueRecord: + table (string): Name of the source table (Z_KEYVALUE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_domain (string): Domain of the key/value pair. + z_key (string): Property key. + z_value (string): Property value. + source (path): Path to the knowledgeC.db database file. + + ZContextualChangeRegistrationRecord: + table (string): Name of the source table (ZCONTEXTUALCHANGEREGISTRATION). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_is_active (varint): Whether the registration is active: + 0 = False. + 1 = True. + z_is_multi_device_registration (varint): Whether the registration is a multi-device registration: + 0 = False. + 1 = True. + z_creation_date (datetime): Creation timestamp. + z_identifier (string): Identifier of the registration. + z_properties (string): Properties of the registration. + source (path): Path to the knowledgeC.db database file. + + ZObjectRecord: + table (string): Name of the source table (ZOBJECT). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_uuid_hash (varint): Hash of the UUID. + z_event (string): Associated event. + z_source_fk (varint): Reference to z_pk in ZSOURCE table. + z_category_type (string): Category type. + z_integer_value (varint): Integer value. + z_compatibility_version (varint): Compatibility version. + z_end_day_of_week (varint): End day of week. + z_end_second_of_day (varint): End second of day. + z_has_custom_metadata (varint): Whether entry has custom metadata: + 0 = False. + 1 = True. + z_has_structured_metadata (varint): Whether entry has structured metadata: + 0 = False. + 1 = True. + z_seconds_from_gmt (varint): Timezone offset from gmt. + z_should_sync (varint): Sync flag. + z_start_day_of_week (varint): Start day of week. + z_start_second_of_day (varint): Start second of day. + z_value_class (varint): Value class identifier. + z_value_integer (varint): Integer value. + z_value_type_code (varint): Value type code. + z_structured_metadata (varint): Foreign key to z_pk in structured metadata. + z_value (string): Property value. + z_9_value (string): Secondary property value. + z_identifier_type (string): Identifier type. + z_quantity_type (string): Quantity type. + z_creation_date (datetime): Creation timestamp. + z_local_creation_date (datetime): Local creation timestamp. + z_confidence (varint): Confidence score. + z_end_date (datetime): End timestamp. + z_start_date (datetime): Start timestamp. + z_value_double (varint): Double value. + z_double_value (varint): Double value? (Usually None) + z_uuid (string): The ID identifier (UUID type) of the current database file. + z_stream_name (string): Name of associated stream. + z_value_string (string): String value. + z_string (string): Additional string field. + z_metadata (string): Metadata. + source (path): Path to the knowledgeC.db database file. + + ZSourceRecord: + table (string): Name of the source table (ZSOURCE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_user_id (string): User identifier. + z_bundle_id (string): Application bundle ID. + z_device_id (string): Device identifier. + z_group_id (string): Group identifier. + z_intent_id (string): Intent identifier. + z_item_id (string): Item identifier. + z_source_id (string): Unique source identifier. + source (path): Path to the knowledgeC.db database file. + + ZStructuredMetadataRecord (table contains ~200+ more columns): + table (string): Name of the source table (ZSTRUCTUREDMETADATA). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + source (path): Path to the knowledgeC.db database file. + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): The ID of the table. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the knowledgeC.db database file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the knowledgeC.db database file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the hashes. + source (path): Path to the knowledgeC.db database file. + + NSStoreModelVersionHashesRecord: + addition_change_set (bytes): Hash for ZADDITIONCHANGESET entity. + category_hash (bytes): Category hash. + contextual_change_registration (bytes): Hash for ZCONTEXTUALCHANGEREGISTRATION entity. + contextual_key_path (bytes): Hash for ZCONTEXTUALKEYPATH entity. + custom_metadata (bytes): Hash for ZCUSTOMMETADATA entity. + deletion_change_set (bytes): Hash for ZDELETIONCHANGESET entity. + event (bytes): Hash for Z_4EVENT entity. + histogram (bytes): Hash for ZHISTOGRAM entity. + histogram_value (bytes): Hash for ZHISTOGRAMVALUE entity. + identifier_hash (bytes): Identifier hash. + key_value (bytes): Hash for ZKEYVALUE entity. + z_object (bytes): Hash for ZOBJECT entity. + quantity (bytes): Hash for ZQUANTITY entity. + z_source (bytes): Hash for ZSOURCE entity. + structured_metadata (bytes): Hash for ZSTRUCTUREDMETADATA entity. + sync_peer (bytes): Hash for ZSYNCPEER entity. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the knowledgeC.db database file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the knowledgeC.db database file. + """ + yield from build_sqlite_records( + self, + self.files, + DuetKnowledgeCRecords, + field_mappings=FIELD_MAPPINGS, + convert_timestamps=CONVERT_TIMESTAMPS, + ) + + +# TODO: Add ZADDITIONCHANGESET, ZCONTEXTUALKEYPATH, ZCUSTOMMETADATA, Z_4EVENT, +# ZDELETIONCHANGESET, ZHISTOGRAM, ZHISTOGRAMVALUE, ZKEYVALUE, ZSYNCPEER tables diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db new file mode 100644 index 0000000000..6eac43a25e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:507abf8563ce3029e054e1a59ebab2ffb79b8c65d83ef928bb3ae7bc9bab2642 +size 753664 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db-wal new file mode 100644 index 0000000000..3091de45dd --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/Main.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb1de9dde337f7f1e7dc0b55e191bb47251e010a8d00f75ef6fa698736c0fc2 +size 2101400 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db new file mode 100644 index 0000000000..ae46074350 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40ed13a5f6b61d125dc22edf316234f48322b02d2f9255706c51d61ec1a73ede +size 679936 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db-wal new file mode 100644 index 0000000000..f858cc7f64 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/User.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d056066d4a456fff6fab4d083e597dda74e477eec632459f8173a94f3e60e219 +size 1240152 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py new file mode 100644 index 0000000000..4e56f0dbb7 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.duet_knowledge_c import DuetKnowledgeCPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + ("names", "paths"), + [ + ( + ( + "Main.db", + "Main.db-wal", + "User.db", + "User.db-wal", + ), + ( + "/var/db/CoreDuet/Knowledge/knowledgeC.db", + "/var/db/CoreDuet/Knowledge/knowledgeC.db-wal", + "Users/user/Library/Application Support/Knowledge/knowledgeC.db", + "Users/user/Library/Application Support/Knowledge/knowledgeC.db-wal", + ), + ), + ], +) +def test_duet_knowledge_c( + names: tuple[str, ...], + paths: tuple[str, ...], + target_unix: Target, + fs_unix: VirtualFilesystem, +) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c/{name}") + fs_unix.map_file(path, data_file) + + target_unix.add_plugin(DuetKnowledgeCPlugin) + + results = list(target_unix.duet_knowledge_c()) + results.sort(key=lambda r: r.source) + + assert len(results) == 1519 + + assert results[0].table == "ZKEYVALUE" + assert results[0].z_pk == 1 + assert results[0].z_ent == 8 + assert results[0].z_opt == 1 + assert results[0].z_domain == "_DKKnowledgeStorage" + assert results[0].z_key == "_DKDeviceIdentifier" + assert results[0].z_value == "0CDFEFCE-422B-5A80-ABA8-FF214731A207" + assert results[0].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[4].table == "ZOBJECT" + assert results[4].z_pk == 1 + assert results[4].z_ent == 11 + assert results[4].z_opt == 1 + assert results[4].z_uuid_hash == -8894672422482144995 + assert results[4].z_event is None + assert results[4].z_source_fk is None + assert results[4].z_category_type is None + assert results[4].z_integer_value is None + assert results[4].z_compatibility_version == 0 + assert results[4].z_end_day_of_week == 2 + assert results[4].z_end_second_of_day == 41368 + assert results[4].z_has_custom_metadata == 0 + assert results[4].z_has_structured_metadata == 1 + assert results[4].z_seconds_from_gmt == -25200 + assert results[4].z_should_sync == 0 + assert results[4].z_start_day_of_week == 2 + assert results[4].z_start_second_of_day == 41357 + assert results[4].z_value_class == 1 + assert results[4].z_value_integer == 8601942810060236944 + assert results[4].z_value_type_code == 6584185901589580638 + assert results[4].z_structured_metadata == 1 + assert results[4].z_value is None + assert results[4].z_9_value is None + assert results[4].z_identifier_type is None + assert results[4].z_quantity_type is None + assert results[4].z_creation_date == datetime(2026, 5, 4, 11, 30, 14, 945838, tzinfo=timezone.utc) + assert results[4].z_local_creation_date == datetime(2026, 5, 4, 11, 30, 14, 945838, tzinfo=timezone.utc) + assert results[4].z_confidence == 1 + assert results[4].z_end_date == datetime(2026, 5, 4, 11, 29, 28, tzinfo=timezone.utc) + assert results[4].z_start_date == datetime(2026, 5, 4, 11, 29, 17, tzinfo=timezone.utc) + assert results[4].z_value_double == 8601942810060236800 + assert results[4].z_double_value is None + assert results[4].z_uuid == "69EE812C-4254-4E54-BFB7-3ED3B5BEFB4C" + assert results[4].z_stream_name == "/app/usage" + assert results[4].z_value_string == "com.apple.SetupAssistant" + assert results[4].z_string is None + assert results[4].z_metadata is None + assert results[4].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[701].table == "ZSOURCE" + assert results[701].z_pk == 1 + assert results[701].z_ent == 14 + assert results[701].z_opt == 1 + assert results[701].z_user_id is None + assert results[701].z_bundle_id == "com.apple.Spotlight" + assert results[701].z_device_id is None + assert results[701].z_group_id is None + assert results[701].z_intent_id is None + assert results[701].z_item_id is None + assert results[701].z_source_id is None + assert results[701].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[714].table == "ZSTRUCTUREDMETADATA" + assert results[714].z_pk == 1 + assert results[714].z_ent == 15 + assert results[714].z_opt == 262 + assert results[714].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[875].ns_persistence_maximum_framework_version == 1526 + assert results[875].ns_store_model_version_identifiers == ["34"] + assert results[875].ns_store_type == "SQLite" + assert results[875].ns_auto_vacuum_level == 2 + assert ( + results[875].ns_store_model_version_hashes_digest + == "3qw03JLBtryClpLQRtPhA43k8KZaw9qHvu+RVzuPBYSEm8++KjboFwDAjFByeXrvryJrBSPo/o4LL6G9pmuo8Q==" + ) + assert results[875].ns_store_model_version_checksum_key == "1nRfv9qJjn86Sz4iC1FpuA6z3NAw1YNPMMABL4ISiEA=" + assert results[875].ns_persistence_framework_version == 1526 + assert results[875].ns_store_model_version_hashes_version == 3 + assert results[875].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[875].ns_persistence_maximum_framework_version == 1526 + assert results[875].ns_store_model_version_identifiers == ["34"] + assert results[875].ns_store_type == "SQLite" + assert results[875].ns_auto_vacuum_level == 2 + assert ( + results[875].ns_store_model_version_hashes_digest + == "3qw03JLBtryClpLQRtPhA43k8KZaw9qHvu+RVzuPBYSEm8++KjboFwDAjFByeXrvryJrBSPo/o4LL6G9pmuo8Q==" + ) + assert results[875].ns_store_model_version_checksum_key == "1nRfv9qJjn86Sz4iC1FpuA6z3NAw1YNPMMABL4ISiEA=" + assert results[875].ns_persistence_framework_version == 1526 + assert results[875].ns_store_model_version_hashes_version == 3 + assert results[875].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert ( + results[876].addition_change_set + == b"\x01\r!\xe0\xf4\xeb\x12VS\xa7\x9f\xa8\xaaF]m\xaa&*\xf4\x9f\x99\x1a%\x16y{\x98\x9b\x81\x80c" + ) + assert ( + results[876].category_hash + == b"\xfdXs \xdaJ\x1eo\t\x08\x10y\xc5\xa8\x80\xb3\x85$\x9c\xc2\x08\xef\r\x1d?\x96\xad5\xc0\xf3B\xa1" + ) + assert ( + results[876].contextual_change_registration + == b'\x8dg\x9b}\xb0\x7f\xe6sQ\xc9"\xc3>yd;n\xaf.,\xfd\xc8\x8fw\x06M\xb85+\xcd\xc0\xd7' + ) + assert ( + results[876].contextual_key_path + == b"\xd9\x16K\xa993y\xd6\x9bP\x03\xbew\x175\x8b\xa7\x16g@Up@9C\x85'*\x81\xf8\xa9M" + ) + assert ( + results[876].custom_metadata + == b"\xdd\xc4\x92x\xc5#.8\xfa4\x8b\xfe\xc6j\xa9P\xc8\xcbt\xb1\xca[\x06ze\xff\x12P\x14\r\x822" + ) + assert ( + results[876].deletion_change_set + == b"5\xf7X\xdd\xfdw\xaf\xef\x0f\xca\x15\x04\x85\xec\x17\x1d6\xc1H\x8c\x8b\x8e\x92\xa1\x18\xd8\xd2p\xec\xf3cH" + ) + assert ( + results[876].event + == b"\x92\x19\x0f7\xc3\x05q\xbb\x07#[\x18'\xde\x9e\xaf\x8e\x84h'[\xb2\x1bq\xcf\xfe\x88z\x01N\xf0\xf8" + ) + assert ( + results[876].histogram + == b"\xdby\x06D2\xade\x84z:\x8c+!\xb4\xdc\x1d\x02\xb9\x007\x866\x00\xad\x8d\xc4fz\xba\x05\xc7\xdf" + ) + assert ( + results[876].histogram_value + == b"N\x92&F\xacK\xce\xbc\xa1\x9d\xe1\xdf\xba\xa1X|\xe6DO\xebQ\x07\x8c2\xb0C7\xc66\x95}O" + ) + assert ( + results[876].identifier_hash + == b"\xa5\x0b\xd3\x1aJ7W\x05\xacp\xb7\x03\xdd\x85\x84\x04\xc2\xc6bOvy\x1ayZ\xb3Qm5^\x8a\xf8" + ) + assert ( + results[876].key_value + == b"\xf5\xe0z;\xbe\xdf~\xed\x10T4-^\x88\x95\x99\xd4e\xa8AZc\xdf\x0c?\x90 \xfc\x88\x06\x7fE" + ) + assert ( + results[876].z_object + == b"{\xe0\xb9\x12<\xb6\xe9\xfd\x03\x81'P\xce\xdd\x15c\xf7\xbc\x0bI\xb4\xd4\x98\xc2\xfa\x01TK\x8f\x81&u" + ) + assert ( + results[876].quantity + == b'l-&\xc2\xa4\xc6\xc4w\xeb"EC\x8a\xc6\x84\x91\x8fQ\x81\xaf\xfc\x7f\x137\x98\x04o\x89\xf0\x93\xf4\x94' + ) + assert ( + results[876].z_source + == b":`?\xb5\xfa\x1b\xfa\x1dJo\x05h\xf2\x83\x91(D3#\x9c.\xfa\x1d\xd6\xca\xd6\xca'\xa7\xf1\x8b\xb7" + ) + assert ( + results[876].structured_metadata + == b"+$\x00D\x94A\x91\xe8#\x96=\xe6B\x9e6\x8bY\xb4U\x81AD\x85Y\xff\x7fZ\xef\xde2\xa0<" + ) + assert ( + results[876].sync_peer + == b"\xb4q\xdc\xac\x9f\x14\x0c\xc6X\x00\x18i\xce\x80\x11\xfc\x8b\x1c\x85\x95\xf44\x17\x8a\xe1MC\xc3\x05\x809l" + ) + assert results[876].plist_path == "NSStoreModelVersionHashes" + assert results[876].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[877].table == "Z_METADATA" + assert results[877].z_version == 1 + assert results[877].z_uuid == "D66E2FEA-1161-43F5-B00C-B877BBDE1A2D" + assert results[877].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[878].table == "Z_MODELCACHE" + assert results[878].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" + + assert results[879].table == "ZCONTEXTUALCHANGEREGISTRATION" + assert results[879].z_pk == 1 + assert results[879].z_ent == 2 + assert results[879].z_opt == 22 + assert results[879].z_is_active == 1 + assert results[879].z_is_multi_device_registration == 0 + assert results[879].z_creation_date == datetime(2026, 5, 11, 9, 1, 51, 201296, tzinfo=timezone.utc) + assert results[879].z_identifier == "com.apple.das.apppolicy.appchanged" + assert results[879].z_properties == "<_CDContextualChangeRegistration>" + assert results[879].source == "/var/db/CoreDuet/Knowledge/knowledgeC.db" From 0c0890d4cdfbebb4925346ab4562b7b3aa390ee1 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:43:34 +0200 Subject: [PATCH 37/52] Add Duet InteractionC Plugin --- .../darwin/macos/duet_activity_scheduler.py | 2 +- .../bsd/darwin/macos/duet_interaction_c.py | 247 ++++++++++++++++++ .../unix/bsd/darwin/macos/duet_knowledge_c.py | 18 +- .../bsd/darwin/macos/helpers/build_records.py | 84 ++---- .../bsd/darwin/macos/text_replacements.py | 2 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 2 +- .../macos/duet_interaction_c/interactionC.db | 3 + .../duet_interaction_c/interactionC.db-wal | 3 + .../darwin/macos/test_duet_interaction_c.py | 85 ++++++ 9 files changed, 377 insertions(+), 69 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db-wal create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index 1cb076ad97..9faa114941 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -183,7 +183,7 @@ def duet_activity_scheduler( ns_store_model_version_hashes_digest (string): Digest of model version hashes. ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. - ns_store_model_version_hashes_version (varint): Version of the hashes. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. source (path): Path to the DuetActivitySchedulerClassC.db file. ActivityRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py new file mode 100644 index 0000000000..e1ae2837f4 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_plist", + [ + ("varint", "ns_persistence_maximum_framework_version"), + ("string[]", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("varint", "ns_auto_vacuum_level"), + ("string", "ns_store_model_version_hashes_digest"), + ("string", "ns_store_model_version_checksum_key"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_store_model_version_hashes_version"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/ns_store_model_version_hashes", + [ + ("bytes", "attachment"), + ("bytes", "contacts"), + ("bytes", "interactions"), + ("bytes", "keywords"), + ("bytes", "metadata"), + ("bytes", "version"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +ZKeyValueMetadataRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_key_value_metadata", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("string", "z_key"), + ("string", "z_value"), + ("path", "source"), + ], +) + +ZVersionRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_key_value_metadata", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_number"), + ("datetime", "z_creation_date"), + ("string", "z_key"), + ("path", "source"), + ], +) + +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. +ZModelCacheRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/z_model_cache", + [ + ("string", "table"), + ("path", "source"), + ], +) + +DuetInteractionCRecords = ( + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, + ZKeyValueMetadataRecord, + ZVersionRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "ZNAME": "z_name", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "ZKEY": "z_key", + "ZVALUE": "z_value", + "ZNUMBER": "z_number", + "ZCREATIONDATE": "z_creation_date", + "Attachment": "attachment", + "Contacts": "contacts", + "Interactions": "interactions", + "Keywords": "keywords", + "Metadata": "metadata", + "Version": "version", +} + +CONVERT_TIMESTAMPS = { + "z_creation_date": "2001", +} + + +class DuetInteractionCPlugin(Plugin): + """macOS Duet InteractionC plugin. + + Parses basic information about recent app activity. + + References: + - https://www.msab.com/blog/hidden-gems-in-apple-ios-digital-forensics/ + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ + + PATH = "/var/db/CoreDuet/People/interactionC.db" + + def __init__(self, target: Target): + super().__init__(target) + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No interactionC.db file found") + + @export(record=DuetInteractionCRecords) + def duet_interaction_c( + self, + ) -> Iterator[DuetInteractionCRecords]: + """Return macOS Duet InteractionC database entries. + + Yields the following record types extracted from the + interactionC.db database: + + .. code-block:: text + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): The ID of the table. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the interactionC.db file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the interactionC.db file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + source (path): Path to the interactionC.db file. + + NSStoreModelVersionHashesRecord: + attachment (bytes): Hash for ZATTACHMENT entity. + contacts (bytes): Hash for ZCONTACTS entity. + interactions (bytes): Hash for ZINTERACTIONS entity. + keywords (bytes): Hash for ZKEYWORDS entity. + metadata (bytes): Hash for ZMETADATA entity. + version (bytes): Hash for ZVERSION entity. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the knowledgeC.db database file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the interactionC.db file. + + ZKeyValueMetadataRecord: + table (string): Name of the source table (ZMETADATA). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_key (string): Property key. + z_value (string): Property value. + source (path): Path to the knowledgeC.db database file. + + ZVersionRecord: + table (string): Name of the source table (ZVERSION). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): The ID of the table. + z_opt (varint): The version number of the data record. + z_creation_date (datetime): Creation timestamp. + z_key (string): Property key. + """ + yield from build_sqlite_records( + self, + (self.file,), + DuetInteractionCRecords, + field_mappings=FIELD_MAPPINGS, + convert_timestamps=CONVERT_TIMESTAMPS, + ) + + # TODO: Add ZATTACHMENT, Z_1INTERACTIONS, ZCONTACTS, Z_2INTERACTIONRECIPIENT, + # ZINTERACTIONS, `Z_3KEYWORDS, ZKEYWORDS tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index 444e23a3df..6130bd03fa 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -106,7 +106,7 @@ ], ) -# ZSTRUCTUREDMETADATA table ~200+ more columns, most of which are None in the majority of rows. +# ZSTRUCTUREDMETADATA table contains 200+ more columns, most of which are None in the majority of rows. # Reduced record descriptor to core fields, other fields will be included in a warning. ZStructuredMetadataRecord = TargetRecordDescriptor( "macos/duet_knowledge_c/z_structured_metadata", @@ -142,7 +142,7 @@ ) ZPlistRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler/z_plist", + "macos/duet_knowledge_c/z_plist", [ ("varint", "ns_persistence_maximum_framework_version"), ("string[]", "ns_store_model_version_identifiers"), @@ -157,7 +157,7 @@ ) NSStoreModelVersionHashesRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler/ns_store_model_version_hashes", + "macos/duet_knowledge_c/ns_store_model_version_hashes", [ ("bytes", "addition_change_set"), ("bytes", "category_hash"), @@ -296,12 +296,10 @@ class DuetKnowledgeCPlugin(Plugin): """macOS Duet KnowledgeC Plugin. - The KnowledgeC database is known for storing a wealth of data, including but not limited to: Application Activity, - Application Focus, Device Battery Percentage, Device Lock State, and Device Orientation. - Some of this data is being migrated to the Biome framework in newer macOS versions. + Parses information about app and system activities. References: - - https://belkasoft.com/lagging-for-win + - https://www.msab.com/blog/hidden-gems-in-apple-ios-digital-forensics/ - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey """ @@ -400,7 +398,7 @@ def duet_knowledge_c( z_value_double (varint): Double value. z_double_value (varint): Double value? (Usually None) z_uuid (string): The ID identifier (UUID type) of the current database file. - z_stream_name (string): Name of associated stream. + z_stream_name (string): Name of associated stream, identifies the activity. z_value_string (string): String value. z_string (string): Additional string field. z_metadata (string): Metadata. @@ -420,7 +418,7 @@ def duet_knowledge_c( z_source_id (string): Unique source identifier. source (path): Path to the knowledgeC.db database file. - ZStructuredMetadataRecord (table contains ~200+ more columns): + ZStructuredMetadataRecord (table contains 200+ more columns): table (string): Name of the source table (ZSTRUCTUREDMETADATA). z_pk (varint): The autoincrement primary key of the table. z_ent (varint): The ID of the table. @@ -451,7 +449,7 @@ def duet_knowledge_c( ns_store_model_version_hashes_digest (string): Digest of model version hashes. ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. - ns_store_model_version_hashes_version (varint): Version of the hashes. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. source (path): Path to the knowledgeC.db database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index acc5c3f4f3..114c5c84ca 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -91,10 +91,34 @@ def build_sqlite_records( key, ) else: - yield from build_record_from_data( - plugin, file, value, record_descriptors, field_mappings, convert_timestamps - ) - row_dict.pop(key) + try: + plist_data = ( + load_plist_data(value) + if b"$archiver" in value[:128] + else plistlib.loads(value) + ) + + if isinstance(plist_data, dict): + yield from emit_dict_records( + plugin, + plist_data, + file, + record_descriptors=record_descriptors, + field_mappings=field_mappings, + convert_timestamps=convert_timestamps, + ) + row_dict.pop(key) + else: + row_dict[key] = plist_data + + except Exception: + plugin.target.log.exception( + "Failed to parse plist in %s (table=%s, key=%s)", + file, + table.name, + key, + ) + row_dict.pop(key) if table.name in joins_by_table2: for j2 in joins_by_table2[table.name]: @@ -381,58 +405,6 @@ def build_plist_records( plugin.target.log.exception("Failed to parse %s", file) -def build_record_from_data( - plugin: Plugin, - file: str, - raw_data: bytes, - record_descriptors: tuple | None = None, - field_mappings: dict | None = None, - convert_timestamps: dict | None = None, - function_name: str | None = None, -) -> Iterator[Record]: - """Extract and normalize records from raw binary data containing plist content. - - Detects whether the input data is a binary plist and whether it contains - NSKeyedArchiver-encoded content. Parses the data accordingly, then passes - the resulting structure to emit_dict_records for recursive traversal - and record construction. - - Args: - plugin (Plugin): Plugin instance providing logging and target access. - file (str): File from which the raw_data was extracted. - raw_data (bytes): Raw binary data to parse. - record_descriptors (tuple | None): Optional descriptors for record construction. - field_mappings (dict | None): Optional field name mappings. - convert_timestamps (dict | None): Optional timestamp conversion rules. - function_name (str | None): Optional name used for dynamic record creation. - - Yields: - Record: Normalized records constructed from plist contents. - """ - try: - if not raw_data: - return - - if raw_data.startswith(b"bplist00"): - data = load_plist_data(raw_data) if b"$archiver" in raw_data[:128] else plistlib.loads(raw_data) - - else: - data = plistlib.load(io.BytesIO(raw_data)) - - yield from emit_dict_records( - plugin, - data, - file, - record_descriptors=record_descriptors, - field_mappings=field_mappings, - convert_timestamps=convert_timestamps, - function_name=function_name, - ) - - except Exception: - plugin.target.log.exception("Failed to parse %s", file) - - def dynamic_build_record(plugin: Plugin, function_name: str, rdict: dict, source: Path) -> Record: """Dynamically construct a record descriptor and corresponding record from a dictionary. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index 0bdfc715b8..5b6c6cb4ae 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -229,7 +229,7 @@ def text_replacements( ns_store_model_version_hashes_digest (string): Digest of model version hashes. ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. - ns_store_model_version_hashes_version (varint): Version of the hashes. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. source (path): Path to the TextReplacements.db file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 62c7cb7fcd..c7760128a1 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -436,7 +436,7 @@ def user_accounts( ns_store_model_version_hashes_digest (string): Digest of model version hashes. ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. - ns_store_model_version_hashes_version (varint): Version of the hashes. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. source (path): Path to the Accounts*.sqlite database file. NSStoreModelVersionHashesRecord: diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db new file mode 100644 index 0000000000..2c34b7bba1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8996d53c6b25ba17e98b0c6ca72b4ce8d27798dba14e2ef2f857f25deedc585a +size 4096 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db-wal new file mode 100644 index 0000000000..9888c43401 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/interactionC.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da93fe7cd70cca60b38d57212a15914e6841c6e2313503df1ed1db3e8374a539 +size 296672 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py new file mode 100644 index 0000000000..d05cdfa16c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.duet_interaction_c import DuetInteractionCPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "interactionC.db", + "interactionC.db-wal", + ] + ], +) +def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/duet_interaction_c/{test_file}") + fs_unix.map_file(f"/var/db/CoreDuet/People/{test_file}", data_file) + + target_unix.add_plugin(DuetInteractionCPlugin) + + results = list(target_unix.duet_interaction_c()) + + assert len(results) == 12 + + assert results[0].table == "ZMETADATA" + assert results[0].z_pk == 1 + assert results[0].z_ent == 5 + assert results[0].z_opt == 1 + assert results[0].z_key == "migrateIMessageDomainIdentifiers" + assert results[0].z_value == "True" + assert results[0].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[1].table == "ZVERSION" + assert results[1].z_pk == 1 + assert results[1].z_ent == 6 + assert results[1].z_opt == 1 + assert results[1].z_number == 1 + assert results[1].z_creation_date == datetime(2026, 5, 4, 11, 23, 21, 944464, tzinfo=timezone.utc) + assert results[1].z_key == "store_version" + assert results[1].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[2].table == "Z_PRIMARYKEY" + assert results[2].z_ent == 1 + assert results[2].z_name == "Attachment" + assert results[2].z_super == 0 + assert results[2].z_max == 0 + assert results[2].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[8].ns_persistence_maximum_framework_version == 1526 + assert results[8].ns_store_model_version_identifiers == ["15"] + assert results[8].ns_store_type == "SQLite" + assert results[8].ns_auto_vacuum_level == 2 + assert results[8].ns_store_model_version_hashes_digest == "xtIOAHN/XyhlY4uahAH4diGw8uqBPUzu3nHg0qTa308d9X2+IXWH5fIYFrZ8DkWCDbXQ46RPaENynVIpxse4Jw==" + assert results[8].ns_store_model_version_checksum_key == "yBhxwKvskbIdxbJOzzLgxhbLYTjrWz9otOnAd9BgKA0=" + assert results[8].ns_persistence_framework_version == 1526 + assert results[8].ns_store_model_version_hashes_version == 3 + assert results[8].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[9].attachment == b'%6\xe0\xe5\x85u\x91Wf\xc7\xe8\xb8y\xe6\xfdf\x1a\xb9\x17t\x81h&e\xdc\xa9\xc3\xe1\xd3\x15\x9e\xe9' + assert results[9].contacts == b'\x83\x87\x97\xd4\x08\xc9\xadN\xbc\xfa\xa8\xf6\x9e\xb0\x0e\xdf\x12\x96K\xb25\x1af\xef\xaaQ\xf3\x13I\x82\xfa\x94' + assert results[9].interactions == b'\x92m\xbe\x9d\x12\xf0M\xf3\xa7\xf9(\xbd9\x96Y\xa7e\xff\x1f\x9fE\xa2{\xe1\x03\xd2|\xdb\x12s%\x86' + assert results[9].keywords == b'\xff/$fi\xef\xb0\x03\x9d\xfd\xc8%>D\x9f\xbd3"\xd39\x84\xff\xe0\xf4\xa1\xe7\xd1\xf8\xcc253' + assert results[9].metadata == b'\x93\\\xfe\x85\x91lG\xd1\x83lc\xde\xdeCO\x92G\xd0/\x8c\xa1t\xe0)y\x1d\xd1\x86M0\xdf\xc4' + assert results[9].version == b'\x94\x07\xac\x82%\x9f22\x9c\x162\xe9\xc5\xdb7\xb9\x1e\xf8\x8c(\x8e\xb1\xd7JT\xfd*\xe0\xad\x7f5\x01' + assert results[9].plist_path == "NSStoreModelVersionHashes" + assert results[9].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[10].table == "Z_METADATA" + assert results[10].z_version == 1 + assert results[10].z_uuid == "DD35E9BD-94E0-4016-887B-B91BFA9FCF84" + assert results[10].source == "/var/db/CoreDuet/People/interactionC.db" + + assert results[11].table == "Z_MODELCACHE" + assert results[11].source == "/var/db/CoreDuet/People/interactionC.db" From aa8757cd0d5bc46eaf0eadf548a62d54b40ab9a7 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:11:07 +0200 Subject: [PATCH 38/52] Minor fixes & additional comments for build_sqlite_records --- .../bsd/darwin/macos/duet_interaction_c.py | 20 +++++------ .../unix/bsd/darwin/macos/duet_knowledge_c.py | 2 +- .../bsd/darwin/macos/helpers/build_records.py | 27 ++++++++++---- .../darwin/macos/test_duet_interaction_c.py | 35 +++++++++++++++---- 4 files changed, 60 insertions(+), 24 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py index e1ae2837f4..ef8999f36c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -52,12 +52,12 @@ NSStoreModelVersionHashesRecord = TargetRecordDescriptor( "macos/duet_interaction_c/ns_store_model_version_hashes", [ - ("bytes", "attachment"), + ("bytes", "attachment_hash"), ("bytes", "contacts"), ("bytes", "interactions"), - ("bytes", "keywords"), + ("bytes", "keywords_hash"), ("bytes", "metadata"), - ("bytes", "version"), + ("bytes", "version_hash"), ("string", "plist_path"), ("path", "source"), ], @@ -77,7 +77,7 @@ ) ZVersionRecord = TargetRecordDescriptor( - "macos/duet_interaction_c/z_key_value_metadata", + "macos/duet_interaction_c/z_version", [ ("string", "table"), ("varint", "z_pk"), @@ -132,12 +132,12 @@ "ZVALUE": "z_value", "ZNUMBER": "z_number", "ZCREATIONDATE": "z_creation_date", - "Attachment": "attachment", + "Attachment": "attachment_hash", "Contacts": "contacts", "Interactions": "interactions", - "Keywords": "keywords", + "Keywords": "keywords_hash", "Metadata": "metadata", - "Version": "version", + "Version": "version_hash", } CONVERT_TIMESTAMPS = { @@ -205,12 +205,12 @@ def duet_interaction_c( source (path): Path to the interactionC.db file. NSStoreModelVersionHashesRecord: - attachment (bytes): Hash for ZATTACHMENT entity. + attachment_hash (bytes): Hash for ZATTACHMENT entity. contacts (bytes): Hash for ZCONTACTS entity. interactions (bytes): Hash for ZINTERACTIONS entity. - keywords (bytes): Hash for ZKEYWORDS entity. + keywords_hash (bytes): Hash for ZKEYWORDS entity. metadata (bytes): Hash for ZMETADATA entity. - version (bytes): Hash for ZVERSION entity. + version_hash (bytes): Hash for ZVERSION entity. plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the knowledgeC.db database file. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index 6130bd03fa..bb2764d7db 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -296,7 +296,7 @@ class DuetKnowledgeCPlugin(Plugin): """macOS Duet KnowledgeC Plugin. - Parses information about app and system activities. + Parses information about app and system activities. References: - https://www.msab.com/blog/hidden-gems-in-apple-ios-digital-forensics/ diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 114c5c84ca..5d4eaca785 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -1,6 +1,5 @@ from __future__ import annotations -import io import plistlib import re import uuid @@ -42,17 +41,26 @@ def build_sqlite_records( filter rows across related tables. Join behavior is controlled via the `joins` configuration: - - iterate: iterates over rows of table2 and merges them with + iterate: iterates over rows of table2 and merges them with matching table1 entries. Table2 rows that do not match with any table1 will be yielded separately. - - nested: adds a field to table1 rows containing all of the matching table2 rows. - - ignore: removes specific fields when values match, used to avoid duplicate fields on joins. + nested: adds a field to table1 rows containing all of the matching table2 rows. + ignore: removes specific fields when values match, used to avoid duplicate fields on joins. Args: plugin (Plugin): Plugin instance providing logging and target access. files (set[str]): Paths to SQLite database files. record_descriptors (tuple | None): Optional descriptors for record construction. - joins (tuple): Join configuration dictionary defining relationships between tables. + joins (tuple): Collection of join configuration dictionaries defining relationships between tables. + Each item is a dict describing a join rule between two tables: + table1 (str): Name of the primary table. + key1 (str): Column in table1 used for joining. + table2 (str): Name of the secondary table. + key2 (str): Column in table2 used for joining. + join (str): Join type to apply: + iterate = Create separate records for each matching row in table2. + nested = Attach matching table2 rows as a nested dict in table1 records. + ignore = Used to omit duplicate or redundant fields during joins. field_mappings (dict | None): Optional field name mappings. convert_timestamps (dict | None): Optional timestamp conversion rules. @@ -78,6 +86,7 @@ def build_sqlite_records( row_dict = {k: v for k, v in row} # noqa C416 for key, value in list(row_dict.items()): + # Decode binary plist values (including NSKeyedArchiver blobs) if isinstance(value, (bytes, bytearray)) and value.startswith(b"bplist00"): if is_nskeyedarchive_blob(value): try: @@ -109,6 +118,8 @@ def build_sqlite_records( ) row_dict.pop(key) else: + # If binary plist is an attribute and not a dict + # replace binary plist with extracted value row_dict[key] = plist_data except Exception: @@ -121,6 +132,7 @@ def build_sqlite_records( row_dict.pop(key) if table.name in joins_by_table2: + # Yield table2 rows that don't have a matching table1 for j2 in joins_by_table2[table.name]: if row_dict.get(j2["key2"]) is None: row_dict["table"] = table.name @@ -153,6 +165,7 @@ def build_sqlite_records( iterate_rows = defaultdict(list) tables.add(table.name) + # Handle joins for j in joins_by_table1[table.name]: ignore_joins = ignore_joins_map[(j["table1"], j["table2"])] @@ -165,6 +178,7 @@ def build_sqlite_records( elif j["join"] == "nested": handle_nested_join(database, row_dict, j, ignore_joins, tables) + # If there were iterate joins, yield records for every combination if len(iterate_rows) > 0: row_dict = prefix_row(row_dict, table.name) keys = list(iterate_rows.keys()) @@ -186,10 +200,11 @@ def build_sqlite_records( convert_timestamps=convert_timestamps, ) else: + # Yield row as record without iterate joins yield build_record( plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps ) - + # Yield row as record without joins else: row_dict["table"] = table.name yield build_record( diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py index d05cdfa16c..4338332552 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py @@ -61,18 +61,39 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[8].ns_store_model_version_identifiers == ["15"] assert results[8].ns_store_type == "SQLite" assert results[8].ns_auto_vacuum_level == 2 - assert results[8].ns_store_model_version_hashes_digest == "xtIOAHN/XyhlY4uahAH4diGw8uqBPUzu3nHg0qTa308d9X2+IXWH5fIYFrZ8DkWCDbXQ46RPaENynVIpxse4Jw==" + assert ( + results[8].ns_store_model_version_hashes_digest + == "xtIOAHN/XyhlY4uahAH4diGw8uqBPUzu3nHg0qTa308d9X2+IXWH5fIYFrZ8DkWCDbXQ46RPaENynVIpxse4Jw==" + ) assert results[8].ns_store_model_version_checksum_key == "yBhxwKvskbIdxbJOzzLgxhbLYTjrWz9otOnAd9BgKA0=" assert results[8].ns_persistence_framework_version == 1526 assert results[8].ns_store_model_version_hashes_version == 3 assert results[8].source == "/var/db/CoreDuet/People/interactionC.db" - assert results[9].attachment == b'%6\xe0\xe5\x85u\x91Wf\xc7\xe8\xb8y\xe6\xfdf\x1a\xb9\x17t\x81h&e\xdc\xa9\xc3\xe1\xd3\x15\x9e\xe9' - assert results[9].contacts == b'\x83\x87\x97\xd4\x08\xc9\xadN\xbc\xfa\xa8\xf6\x9e\xb0\x0e\xdf\x12\x96K\xb25\x1af\xef\xaaQ\xf3\x13I\x82\xfa\x94' - assert results[9].interactions == b'\x92m\xbe\x9d\x12\xf0M\xf3\xa7\xf9(\xbd9\x96Y\xa7e\xff\x1f\x9fE\xa2{\xe1\x03\xd2|\xdb\x12s%\x86' - assert results[9].keywords == b'\xff/$fi\xef\xb0\x03\x9d\xfd\xc8%>D\x9f\xbd3"\xd39\x84\xff\xe0\xf4\xa1\xe7\xd1\xf8\xcc253' - assert results[9].metadata == b'\x93\\\xfe\x85\x91lG\xd1\x83lc\xde\xdeCO\x92G\xd0/\x8c\xa1t\xe0)y\x1d\xd1\x86M0\xdf\xc4' - assert results[9].version == b'\x94\x07\xac\x82%\x9f22\x9c\x162\xe9\xc5\xdb7\xb9\x1e\xf8\x8c(\x8e\xb1\xd7JT\xfd*\xe0\xad\x7f5\x01' + assert ( + results[9].attachment_hash + == b"%6\xe0\xe5\x85u\x91Wf\xc7\xe8\xb8y\xe6\xfdf\x1a\xb9\x17t\x81h&e\xdc\xa9\xc3\xe1\xd3\x15\x9e\xe9" + ) + assert ( + results[9].contacts + == b"\x83\x87\x97\xd4\x08\xc9\xadN\xbc\xfa\xa8\xf6\x9e\xb0\x0e\xdf\x12\x96K\xb25\x1af\xef\xaaQ\xf3\x13I\x82\xfa\x94" # noqa E501 + ) + assert ( + results[9].interactions + == b"\x92m\xbe\x9d\x12\xf0M\xf3\xa7\xf9(\xbd9\x96Y\xa7e\xff\x1f\x9fE\xa2{\xe1\x03\xd2|\xdb\x12s%\x86" + ) + assert ( + results[9].keywords_hash + == b'\xff/$fi\xef\xb0\x03\x9d\xfd\xc8%>D\x9f\xbd3"\xd39\x84\xff\xe0\xf4\xa1\xe7\xd1\xf8\xcc253' + ) + assert ( + results[9].metadata + == b"\x93\\\xfe\x85\x91lG\xd1\x83lc\xde\xdeCO\x92G\xd0/\x8c\xa1t\xe0)y\x1d\xd1\x86M0\xdf\xc4" + ) + assert ( + results[9].version_hash + == b"\x94\x07\xac\x82%\x9f22\x9c\x162\xe9\xc5\xdb7\xb9\x1e\xf8\x8c(\x8e\xb1\xd7JT\xfd*\xe0\xad\x7f5\x01" + ) assert results[9].plist_path == "NSStoreModelVersionHashes" assert results[9].source == "/var/db/CoreDuet/People/interactionC.db" From e607bc6ac2bd268265428490d2ae8292cc7a4d1b Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:30:22 +0200 Subject: [PATCH 39/52] add notes & call_history plugins --- .../os/unix/bsd/darwin/macos/call_history.py | 222 ++++++++ .../darwin/macos/duet_activity_scheduler.py | 4 +- .../bsd/darwin/macos/duet_interaction_c.py | 6 +- .../unix/bsd/darwin/macos/duet_knowledge_c.py | 12 +- .../bsd/darwin/macos/helpers/build_paths.py | 11 +- .../bsd/darwin/macos/helpers/build_records.py | 6 + .../plugins/os/unix/bsd/darwin/macos/notes.py | 518 ++++++++++++++++++ .../bsd/darwin/macos/text_replacements.py | 6 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 10 +- .../macos/call_history/CallHistory.storedata | 3 + .../call_history/CallHistory.storedata-wal | 3 + .../bsd/darwin/macos/notes/NotesV7.storedata | 3 + .../darwin/macos/notes/NotesV7.storedata-wal | 3 + .../bsd/darwin/macos/test_call_history.py | 96 ++++ .../os/unix/bsd/darwin/macos/test_notes.py | 207 +++++++ 15 files changed, 1087 insertions(+), 23 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata-wal create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_notes.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py new file mode 100644 index 0000000000..b471d4f629 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import ( + build_sqlite_records, +) + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +ZCallBPropertiesRecord = TargetRecordDescriptor( + "macos/call_history/call_db_properties_record", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_timer_all"), + ("varint", "z_timer_incoming"), + ("varint", "z_timer_last"), + ("varint", "z_timer_lifetime"), + ("varint", "z_timer_outgoing"), + ("path", "source"), + ], +) + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/call_history/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/call_history/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/call_history/z_plist", + [ + ("varint", "ac_account_type_version"), + ("varint", "ns_auto_vacuum_level"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_persistence_maximum_framework_version"), + ("string", "ns_store_model_version_checksum_key"), + ("string", "ns_store_model_version_hashes_digest"), + ("varint", "ns_store_model_version_hashes_version"), + ("string", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/call_history/ns_store_model_version_hashes", + [ + ("bytes", "call_db_properties"), + ("bytes", "call_record"), + ("bytes", "emergency_media_item"), + ("bytes", "handle"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. +ZModelCacheRecord = TargetRecordDescriptor( + "macos/call_history/z_model_cache", + [ + ("string", "table"), + ("path", "source"), + ], +) + +CallHistoryRecords = ( + ZCallBPropertiesRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "ZTIMER_ALL": "z_timer_all", + "ZTIMER_INCOMING": "z_timer_incoming", + "ZTIMER_LAST": "z_timer_last", + "ZTIMER_LIFETIME": "z_timer_lifetime", + "ZTIMER_OUTGOING": "z_timer_outgoing", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "CallDBProperties": "call_db_properties", + "CallRecord": "call_record", + "EmergencyMediaItem": "emergency_media_item", + "Handle": "handle", +} + + +class CallHistoryPlugin(Plugin): + """macOS call history plugin. + + Parses macOS call history SQLite database file. + + References: + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ + + USER_PATH = ("Library/Application Support/CallHistoryDB/CallHistory.storedata",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No CallHistory.storedata file found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=CallHistoryRecords) + def call_history( + self, + ) -> Iterator[CallHistoryRecords]: + """Return call history information. + + Yields the following record types extracted from the + CallHistory.storedata database: + + .. code-block:: text + + ZCallBPropertiesRecord: + table (string): Name of the source table (ZCALLDBPROPERTIES). + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_timer_all (varint): Timer for all calls. + z_timer_incoming (varint): Timer for incoming calls. + z_timer_last (varint): Timer for last call. + z_timer_lifetime (varint): Timer of lifetime. + z_timer_outgoing (varint): Timer for outgoing calls. + source (path): Path to the CallHistory.storedata database file. + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): Entity identifier. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the CallHistory.storedata database file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the CallHistory.storedata database file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ac_account_type_version (varint): AC account type version. + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + source (path): Path to the CallHistory.storedata database file. + + NSStoreModelVersionHashesRecord: + call_db_properties (bytes): Hash for ZCALLDBPROPERTIES entity. + call_record (bytes): Hash for ZCALLRECORD entity. + emergency_media_item (bytes): Hash for ZEMERGENCYMEDIAITEM entity. + handle (bytes): Hash for ZHANDLE entity. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the CallHistory.storedata database file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the CallHistory.storedata database file. + """ + yield from build_sqlite_records(self, self.files, CallHistoryRecords, field_mappings=FIELD_MAPPINGS) + + # TODO: Add ZCALLRECORD, Z_2REMOTEPARTICIPANTHANDLES, ZEMERGENCYMEDIAITEM, ZHANDLE tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index 9faa114941..c7c091d7ae 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -152,7 +152,7 @@ def duet_activity_scheduler( ZPrimaryKeyRecord: table (string): Name of the source table (Z_PRIMARYKEY). - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_name (string): The name of the entity in the data model. z_super (varint): This value corresponds to the Z_ENT of the parent entity. 0 indicates that the entity has no parent entity. @@ -163,7 +163,7 @@ def duet_activity_scheduler( table (string): Name of the source table (ZGROUP). z_max_concurrent (varint): Maximum number of concurrent activities allowed. z_name (string): The name of the entity in the data model. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_pk (varint): The autoincrement primary key of the table. source (path): Path to the DuetActivitySchedulerClassC.db file. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py index ef8999f36c..34334a28ed 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -179,7 +179,7 @@ def duet_interaction_c( ZPrimaryKeyRecord: table (string): Name of the source table (Z_PRIMARYKEY). - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_name (string): The name of the entity in the data model. z_super (varint): This value corresponds to the Z_ENT of the parent entity. 0 indicates that the entity has no parent entity. @@ -221,7 +221,7 @@ def duet_interaction_c( ZKeyValueMetadataRecord: table (string): Name of the source table (ZMETADATA). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_key (string): Property key. z_value (string): Property value. @@ -230,7 +230,7 @@ def duet_interaction_c( ZVersionRecord: table (string): Name of the source table (ZVERSION). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_creation_date (datetime): Creation timestamp. z_key (string): Property key. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index bb2764d7db..e127ba153c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -336,7 +336,7 @@ def duet_knowledge_c( ZKeyValueRecord: table (string): Name of the source table (Z_KEYVALUE). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_domain (string): Domain of the key/value pair. z_key (string): Property key. @@ -346,7 +346,7 @@ def duet_knowledge_c( ZContextualChangeRegistrationRecord: table (string): Name of the source table (ZCONTEXTUALCHANGEREGISTRATION). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_is_active (varint): Whether the registration is active: 0 = False. @@ -362,7 +362,7 @@ def duet_knowledge_c( ZObjectRecord: table (string): Name of the source table (ZOBJECT). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_uuid_hash (varint): Hash of the UUID. z_event (string): Associated event. @@ -407,7 +407,7 @@ def duet_knowledge_c( ZSourceRecord: table (string): Name of the source table (ZSOURCE). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_user_id (string): User identifier. z_bundle_id (string): Application bundle ID. @@ -421,13 +421,13 @@ def duet_knowledge_c( ZStructuredMetadataRecord (table contains 200+ more columns): table (string): Name of the source table (ZSTRUCTUREDMETADATA). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. source (path): Path to the knowledgeC.db database file. ZPrimaryKeyRecord: table (string): Name of the source table (Z_PRIMARYKEY). - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_name (string): The name of the entity in the data model. z_super (varint): This value corresponds to the Z_ENT of the parent entity. 0 indicates that the entity has no parent entity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py index f485012ab0..cc19fb4a67 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -23,10 +23,13 @@ def _build_userdirs(plugin: Plugin, hist_paths: list[str]) -> set[tuple[UserDeta for user_details in plugin.target.user_details.all_with_home(): for d in hist_paths: home_dir: Path = user_details.home_path - for cur_dir in home_dir.glob(d): - cur_dir = cur_dir.resolve() - if cur_dir.exists(): - users_dirs.add((user_details, cur_dir)) + try: + for cur_dir in home_dir.glob(d): + cur_dir = cur_dir.resolve() + if cur_dir.exists(): + users_dirs.add((user_details, cur_dir)) + except Exception: + pass return users_dirs diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 5d4eaca785..87caa7783e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -81,6 +81,12 @@ def build_sqlite_records( for file in files: try: with SQLite3(file) as database: + # for table in database.tables(): + # print(table.name) + # for row in table.rows(): + # print(row) + # break + # print() for table in database.tables(): for row in table.rows(): row_dict = {k: v for k, v in row} # noqa C416 diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py new file mode 100644 index 0000000000..a6275e866e --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -0,0 +1,518 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import ( + build_sqlite_records, +) + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +ZAccountRecord = TargetRecordDescriptor( + "macos/notes/z_account", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_allow_insecure_authentication"), + ("varint", "z_did_choose_to_migrate"), + ("varint", "z_enabled"), + ("varint", "z_root_folder"), + ("varint", "z6_root_folder"), + ("varint", "z_trash_folder"), + ("string", "z_gmail_capabilities_support"), + ("string", "z_port"), + ("string", "z_security_layer_type"), + ("varint", "z_migration_offered"), + ("string", "z_account_description"), + ("string", "z_email_address"), + ("string", "z_full_name"), + ("string", "z_parent_account_identifier"), + ("string", "z_user_name"), + ("string", "z_folder_hierarchy_sync_state"), + ("string", "z_authentication"), + ("string", "z_host_name"), + ("string", "z_server_path_prefix"), + ("string", "z_external_url"), + ("string", "z_internal_url"), + ("string", "z_last_used_autodiscover_url"), + ("string", "z_tls_certificate"), + ("path", "source"), + ], +) + +ZFolderRecord = TargetRecordDescriptor( + "macos/notes/z_folder", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_account"), + ("varint", "z1_account"), + ("varint", "z_parent"), + ("varint", "z6_parent"), + ("string", "z_is_distinguished"), + ("string", "z_alleged_highest_modification_sequence"), + ("string", "z_computed_highest_modification_sequence"), + ("string", "z_uid_next"), + ("string", "z_uid_validity"), + ("varint", "z_trash_account"), + ("varint", "z1_trash_account"), + ("string", "z_name"), + ("string", "z_change_key"), + ("string", "z_user_name"), + ("varint", "z_folder_id"), + ("string", "z_sync_state"), + ("string", "z_server_name"), + ("path", "source"), + ], +) + +ZPrimaryKeyRecord = TargetRecordDescriptor( + "macos/notes/z_primary_key", + [ + ("string", "table"), + ("varint", "z_ent"), + ("string", "z_name"), + ("varint", "z_super"), + ("varint", "z_max"), + ("path", "source"), + ], +) + +ZMetadataRecord = TargetRecordDescriptor( + "macos/notes/z_metadata", + [ + ("string", "table"), + ("varint", "z_version"), + ("string", "z_uuid"), + ("path", "source"), + ], +) + +ZPlistRecord = TargetRecordDescriptor( + "macos/notes/z_plist", + [ + ("varint", "ac_account_type_version"), + ("varint", "ns_auto_vacuum_level"), + ("varint", "ns_persistence_framework_version"), + ("varint", "ns_persistence_maximum_framework_version"), + ("string", "ns_store_model_version_checksum_key"), + ("string", "ns_store_model_version_hashes_digest"), + ("varint", "ns_store_model_version_hashes_version"), + ("string", "ns_store_model_version_identifiers"), + ("string", "ns_store_type"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/notes/ns_store_model_version_hashes", + [ + ("bytes", "account_hash"), + ("bytes", "attachment_hash"), + ("bytes", "ews_account"), + ("bytes", "ews_folder"), + ("bytes", "ews_note"), + ("bytes", "folder"), + ("bytes", "folder_action"), + ("bytes", "imap_account"), + ("bytes", "imap_folder"), + ("bytes", "imap_note"), + ("bytes", "insert_folder_action"), + ("bytes", "insert_note_action"), + ("bytes", "local_account"), + ("bytes", "move_folder_action"), + ("bytes", "move_note_action"), + ("bytes", "note"), + ("bytes", "note_action"), + ("bytes", "note_body"), + ("bytes", "offline_action"), + ("bytes", "trash_folder"), + ("bytes", "update_folder_action"), + ("bytes", "update_note_action"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +# Contains additional Z_CONTENT field which is a binary blob. This field been removed +# from the record descriptor. The field's presence will still be mentioned in a warning. +ZModelCacheRecord = TargetRecordDescriptor( + "macos/notes/z_model_cache", + [ + ("string", "table"), + ("path", "source"), + ], +) + +AChangeRecord = TargetRecordDescriptor( + "macos/notes/a_change", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_change_type"), + ("varint", "z_entity"), + ("varint", "z_entity_pk"), + ("varint", "z_transaction_id"), + ("bytes", "z_columns"), + ("path", "source"), + ], +) + +ATransactionRecord = TargetRecordDescriptor( + "macos/notes/a_transaction", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_author_ts"), + ("varint", "z_bundle_id_ts"), + ("varint", "z_context_name_ts"), + ("varint", "z_process_id_ts"), + ("datetime", "z_timestamp"), + ("string", "z_author"), + ("string", "z_bundle_id"), + ("string", "z_context_name"), + ("varint", "z_process_id"), + ("string", "z_query_gen"), + ("path", "source"), + ], +) + +ATransactionStringRecord = TargetRecordDescriptor( + "macos/notes/a_transaction_string", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("string", "z_name"), + ("path", "source"), + ], +) + +NotesRecords = ( + ZAccountRecord, + ZFolderRecord, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + ZModelCacheRecord, + AChangeRecord, + ATransactionRecord, + ATransactionStringRecord, +) + +FIELD_MAPPINGS = { + "Z_PK": "z_pk", + "Z_ENT": "z_ent", + "Z_OPT": "z_opt", + "Z_NAME": "z_name", + "Z_SUPER": "z_super", + "Z_MAX": "z_max", + "Z_VERSION": "z_version", + "Z_UUID": "z_uuid", + "NSAutoVacuumLevel": "ns_auto_vacuum_level", + "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", + "NSPersistenceMaximumFrameworkVersion": "ns_persistence_maximum_framework_version", + "NSStoreModelVersionChecksumKey": "ns_store_model_version_checksum_key", + "NSStoreModelVersionHashesDigest": "ns_store_model_version_hashes_digest", + "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", + "NSStoreModelVersionIdentifiers": "ns_store_model_version_identifiers", + "NSStoreType": "ns_store_type", + "ZALLOWINSECUREAUTHENTICATION": "z_allow_insecure_authentication", + "ZDIDCHOOSETOMIGRATE": "z_did_choose_to_migrate", + "ZENABLED": "z_enabled", + "ZROOTFOLDER": "z_root_folder", + "Z6_ROOTFOLDER": "z6_root_folder", + "ZTRASHFOLDER": "z_trash_folder", + "ZGMAILCAPABILITIESSUPPORT": "z_gmail_capabilities_support", + "ZPORT": "z_port", + "ZMIGRATIONOFFERED": "z_migration_offered", + "ZACCOUNTDESCRIPTION": "z_account_description", + "ZEMAILADDRESS": "z_email_address", + "ZFULLNAME": "z_full_name", + "ZUSERNAME": "z_user_name", + "ZFOLDERHIERARCHYSYNCSTATE": "z_folder_hierarchy_sync_state", + "ZAUTHENTICATION": "z_authentication", + "ZHOSTNAME": "z_host_name", + "ZSERVERPATHPREFIX": "z_server_path_prefix", + "ZEXTERNALURL": "z_external_url", + "ZINTERNALURL": "z_internal_url", + "ZLASTUSEDAUTODISCOVERURL": "z_last_used_autodiscover_url", + "ZTLSCERTIFICATE": "z_tls_certificate", + "ZACCOUNT": "z_account", + "Z1_ACCOUNT": "z1_account", + "ZPARENT": "z_parent", + "Z6_PARENT": "z6_parent", + "ZISDISTINGUISHED": "z_is_distinguished", + "ZALLEGEDHIGHESTMODIFICATIONSEQUENCE": "z_alleged_highest_modification_sequence", + "ZCOMPUTEDHIGHESTMODIFICATIONSEQUENCE": "z_computed_highest_modification_sequence", + "ZUIDNEXT": "z_uid_next", + "ZTRASHACCOUNT": "z_trash_account", + "Z1_TRASHACCOUNT": "z1_trash_account", + "ZNAME": "z_name", + "ZCHANGEKEY": "z_change_key", + "ZFOLDERID": "z_folder_id", + "ZSYNCSTATE": "z_sync_state", + "ZSERVERNAME": "z_server_name", + "ZCHANGETYPE": "z_change_type", + "ZENTITY": "z_entity", + "ZENTITYPK": "z_entity_pk", + "ZTRANSACTIONID": "z_transaction_id", + "ZCOLUMNS": "z_columns", + "ZAUTHORTS": "z_author_ts", + "ZBUNDLEIDTS": "z_bundle_id_ts", + "ZCONTEXTNAMETS": "z_context_name_ts", + "ZPROCESSIDTS": "z_process_id_ts", + "ZTIMESTAMP": "z_timestamp", + "ZAUTHOR": "z_author", + "ZBUNDLEID": "z_bundle_id", + "ZCONTEXTNAME": "z_context_name", + "ZPROCESSID": "z_process_id", + "ZQUERYGEN": "z_query_gen", + "ZSECURITYLAYERTYPE": "z_security_layer_type", + "ZPARENTACACCOUNTIDENTIFIER": "z_parent_account_identifier", + "ZUIDVALIDITY": "z_uid_validity", + "Account": "account_hash", + "Attachment": "attachment_hash", + "EWSAccount": "ews_account", + "EWSFolder": "ews_folder", + "EWSNote": "ews_note", + "Folder": "folder", + "FolderAction": "folder_action", + "IMAPAccount": "imap_account", + "IMAPFolder": "imap_folder", + "IMAPNote": "imap_note", + "InsertFolderAction": "insert_folder_action", + "InsertNoteAction": "insert_note_action", + "LocalAccount": "local_account", + "MoveFolderAction": "move_folder_action", + "MoveNoteAction": "move_note_action", + "Note": "note", + "NoteAction": "note_action", + "NoteBody": "note_body", + "OfflineAction": "offline_action", + "TrashFolder": "trash_folder", + "UpdateFolderAction": "update_folder_action", + "UpdateNoteAction": "update_note_action", +} + +CONVERT_TIMESTAMPS = { + "z_timestamp": "2001", +} + + +class NotesPlugin(Plugin): + """macOS notes plugin. + + Parses macOS notes SQLite database file. + + References: + - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ + - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + """ + + USER_PATH = ("Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV*.storedata",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No NotesV*.storedata files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=NotesRecords) + def notes( + self, + ) -> Iterator[NotesRecords]: + """Return notes information. + + Yields the following record types extracted from the + NotesV*.storedata databases: + + .. code-block:: text + + ZAccountRecord: + table (string): Name of the source table (ZACCOUNT). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_allow_insecure_authentication (varint): Indicates whether insecure authentication is allowed: + 0 = False. + 1 = True. + z_did_choose_to_migrate (varint): Indicates if migration was selected: + 0 = False. + 1 = True. + z_enabled (varint): Indicates whether the account is enabled. + z_root_folder (varint): Reference to the root folder. + z6_root_folder (varint): Alternate root folder reference. + z_trash_folder (varint): Reference to the trash folder. + z_gmail_capabilities_support (string): Gmail capability support flag. + z_port (string): Port value. + z_security_layer_type (string): Security layer type. + z_migration_offered (varint): Indicates if migration was offered: + 0 = False. + 1 = True. + z_account_description (string): Account description: + z_email_address (string): Associated email address. + z_full_name (string): Full name of the account. + z_parent_account_identifier (string): Parent account identifier. + z_user_name (string): Username. + z_folder_hierarchy_sync_state (string): Folder sync state. + z_authentication (string): Authentication method. + z_host_name (string): Hostname. + z_server_path_prefix (string): Server path prefix. + z_external_url (string): External URL. + z_internal_url (string): Internal URL. + z_last_used_autodiscover_url (string): Last used autodiscover URL. + z_tls_certificate (string): TLS certificate data. + source (path): Path to the database file. + + ZFolderRecord: + table (string): Name of the source table (ZFOLDER). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_account (varint): Reference to Z_PK in ZACCOUNT. + z1_account (varint): Alternate reference to Z_PK in ZACCOUNT. + z_parent (varint): Parent folder reference. + z6_parent (varint): Alternate parent reference. + z_is_distinguished (varint): Whether entry is distinguished: + 0 = False. + 1 = True. + z_alleged_highest_modification_sequence (string): Alleged highest modification sequence. + z_computed_highest_modification_sequence (string): Computed highest modification sequence. + z_uid_next (string): Next UID value. + z_uid_validity: UID validity. + z_trash_account (varint): Trash account reference. + z1_trash_account (varint): Alternate trash account reference. + z_name (string): Entry name. + z_change_key (string): Change key. + z_user_name (string): Username. + z_folder_id (varint): Folder identifier. + z_sync_state (string): Synchronization state. + z_server_name (string): Server name. + source (path): Path to the database file. + + ZPrimaryKeyRecord: + table (string): Name of the source table (Z_PRIMARYKEY). + z_ent (varint): Entity identifier. + z_name (string): The name of the entity in the data model. + z_super (varint): This value corresponds to the Z_ENT of the parent entity. + 0 indicates that the entity has no parent entity. + z_max (varint): Marks the last used z_pk value for each registry table. + source (path): Path to the database file. + + ZMetadataRecord: + table (string): Name of the source table (Z_METADATA). + z_version (varint): The specific purpose is unknown, value is always 1. + z_uuid (string): The ID identifier (UUID type) of the current database file. + source (path): Path to the database file. + + ZPlistRecord (Plist extracted from Z_METADATA's Z_PLIST field): + ac_account_type_version (varint): AC account type version. + ns_persistence_maximum_framework_version (varint): Maximum supported persistence framework version. + ns_store_model_version_identifiers (string[]): Version identifiers for the model, + used to create the store. + ns_store_type (string): Store type. + ns_auto_vacuum_level (varint): Auto-vacuum level. + ns_store_model_version_hashes_digest (string): Digest of model version hashes. + ns_store_model_version_checksum_key (string): Model version checksum key. + ns_persistence_framework_version (varint): Persistence framework version. + ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + source (path): Path to the database file. + + NSStoreModelVersionHashesRecord: + account_hash (bytes): Hash for ZACCOUNT entity. + attachment_hash (bytes): Hash for ZATTACHMENT entity. + ews_account (bytes): Hash for EWS account. + ews_folder (bytes): Hash for EWS folder. + ews_note (bytes): Hash for EWS note. + folder (bytes): Hash for ZFOLDER entity. + folder_action (bytes): Hash for folder action. + imap_account (bytes): Hash for IMAP account. + imap_folder (bytes): Hash for IMAP folder. + imap_note (bytes): Hash for IMAP note. + insert_folder_action (bytes): Hash for insert folder action. + insert_note_action (bytes): Hash for insert note action. + local_account (bytes): Hash for local account. + move_folder_action (bytes): Hash for move folder action. + move_note_action (bytes): Hash for move note action. + note (bytes): Hash for ZNOTE entity. + note_action (bytes): Hash for note action. + note_body (bytes): Hash for ZNOTEBODY entity. + offline_action (bytes): Hash for ZOFFLINEACTION entity. + trash_folder (bytes): Hash for trash folder. + update_folder_action (bytes): Hash for update folder action. + update_note_action (bytes): Hash for update note action. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the database file. + + ZModelCacheRecord (contains Z_CONTENT field with binary data): + table (string): Name of the source table (Z_MODELCACHE). + source (path): Path to the database file. + + AChangeRecord: + table (string): Name of the source table (ACHANGE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_change_type (varint): Type of change. + z_entity (varint): Entity type affected. + z_entity_pk (varint): Primary key of affected entity. + z_transaction_id (varint): Transaction identifier. + z_columns (bytes): Columns affected by the change. + source (path): Path to the database file. + + ATransactionRecord: + table (string): Name of the source table (ATRANSACTION). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_author_ts (varint): Author timestamp reference. + z_bundle_id_ts (varint): Bundle ID timestamp reference. + z_context_name_ts (varint): Context name timestamp reference. + z_process_id_ts (varint): Process ID timestamp reference. + z_timestamp (datetime): Transaction timestamp. + z_author (string): Author of the transaction. + z_bundle_id (string): Bundle identifier. + z_context_name (string): Context name. + z_process_id (varint): Process ID. + z_query_gen (string): Query generation. + source (path): Path to the database file. + + ATransactionStringRecord: + table (string): Name of the source table (ATRANSACTIONSTRING). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_name (string): The name of the entity in the data model. + source (path): Path to the database file. + """ + yield from build_sqlite_records( + self, self.files, NotesRecords, field_mappings=FIELD_MAPPINGS, convert_timestamps=CONVERT_TIMESTAMPS + ) + + # TODO: Add ZNOTE, ZNOTEBODY, ZOFFLINEACTION, ZATTACHMENT, tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index 5b6c6cb4ae..ed5b3e4730 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -185,7 +185,7 @@ def text_replacements( ZTextReplacementEntryRecord: table (string): Name of the source table (ZTEXTREPLACEMENTENTRY). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_was_deleted (varint): Indicates if the record was deleted. z_needs_save_to_cloud (varint): Indicates if the record needs to be saved to cloud. @@ -199,7 +199,7 @@ def text_replacements( ZTrCloudKitSyncStateRecord: table (string): Name of the source table (ZTRCLOUDKITSYNCSTATE). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_did_pull_once (varint): Indicates if initial CloudKit sync has occurred. z_fetch_change_token (string): CloudKit change token for sync state tracking. @@ -207,7 +207,7 @@ def text_replacements( ZPrimaryKeyRecord: table (string): Name of the source table (Z_PRIMARYKEY). - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_name (string): The name of the entity in the data model. z_super (varint): This value corresponds to the Z_ENT of the parent entity. 0 indicates that the entity has no parent entity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index c7760128a1..170971be35 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -313,7 +313,7 @@ def user_accounts( ZAccessOptionsKeyRecord: table (string): Name of the source table (ZACCESSOPTIONSKEY). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_enum_value (varint): Enumeration value. z_name (string): The name of the entity in the data model. @@ -328,7 +328,7 @@ def user_accounts( ZAccountRecord: table (string): Name of the source table (ZACCOUNT). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_active (varint): Indicates whether the account is active: 0 = False. @@ -368,7 +368,7 @@ def user_accounts( ZAccountPropertyRecord: table (string): Name of the source table (ZACCOUNTPROPERTY). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_owner (varint): Reference to z_pk of owning ZACCOUNT. z_key (string): Property key. @@ -378,7 +378,7 @@ def user_accounts( ZAccountTypeRecord: table (string): Name of the source table (ZACCOUNTTYPE). z_pk (varint): The autoincrement primary key of the table. - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. z_obsolete (varint): Indicates whether account type is obsolute: 0 = False. @@ -413,7 +413,7 @@ def user_accounts( ZPrimaryKeyRecord: table (string): Name of the source table (Z_PRIMARYKEY). - z_ent (varint): The ID of the table. + z_ent (varint): Entity identifier. z_name (string): The name of the entity in the data model. z_super (varint): This value corresponds to the Z_ENT of the parent entity. 0 indicates that the entity has no parent entity. diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata b/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata new file mode 100644 index 0000000000..bd599f21c5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23bfebba8f86e5ae5079e51fdb5d6987c0eefadfa1a23587bedd1c5edabee275 +size 90112 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata-wal new file mode 100644 index 0000000000..cacf47f3e6 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/call_history/CallHistory.storedata-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74ed776922490b94b7220b85d72df3f633f5606e3114c6822d71ba6c6ff43d1d +size 8272 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata new file mode 100644 index 0000000000..a8df074d5d --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2da33e73a278aa0ac12f1e6fe20c35f24bdc7b752661eb5e6a7de4823a630d03 +size 200704 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata-wal new file mode 100644 index 0000000000..85917cedea --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NotesV7.storedata-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:961e9430a6fd08aeaae68fff4c65548a9f7f67ddb0e05075c650297d3250ab47 +size 148352 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py new file mode 100644 index 0000000000..6362b4aebb --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.call_history import CallHistoryPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "CallHistory.storedata", + "CallHistory.storedata-wal", + ] + ], +) +def test_call_history(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/call_history/{test_file}") + fs_unix.map_file(f"Users/user/Library/Application Support/CallHistoryDB/{test_file}", data_file) + + target_unix.add_plugin(CallHistoryPlugin) + + results = list(target_unix.call_history()) + + assert len(results) == 9 + + assert results[0].table == "ZCALLDBPROPERTIES" + assert results[0].z_pk == 1 + assert results[0].z_ent == 1 + assert results[0].z_opt == 1 + assert results[0].z_timer_all == 0 + assert results[0].z_timer_incoming == 0 + assert results[0].z_timer_last == 0 + assert results[0].z_timer_lifetime == 0 + assert results[0].z_timer_outgoing == 0 + assert results[0].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + assert results[1].table == "Z_PRIMARYKEY" + assert results[1].z_ent == 1 + assert results[1].z_name == "CallDBProperties" + assert results[1].z_super == 0 + assert results[1].z_max == 1 + assert results[1].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + assert results[5].ac_account_type_version is None + assert results[5].ns_auto_vacuum_level == 2 + assert results[5].ns_persistence_framework_version == 1526 + assert results[5].ns_persistence_maximum_framework_version == 1526 + assert results[5].ns_store_model_version_checksum_key == "6DaLHrl7O3U+MTsaWar2wVaVZaX9wGEPdMNvdZH8pQo=" + assert ( + results[5].ns_store_model_version_hashes_digest + == "LEcn8D9uwY2SJgHgh77aZm8/vqfyybIcJvNNEfArEjU5Jsk+HqJ26e3bbK00b2Msn7RuITsNT8uEJbQejQdctA==" + ) + assert results[5].ns_store_model_version_hashes_version == 3 + assert results[5].ns_store_model_version_identifiers == "['43']" + assert results[5].ns_store_type == "SQLite" + assert results[5].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + assert results[6].plist_path == "NSStoreModelVersionHashes" + + assert results[6].call_db_properties is not None + assert isinstance(results[6].call_db_properties, (bytes, bytearray)) + assert results[6].call_record is not None + assert isinstance(results[6].call_record, (bytes, bytearray)) + assert results[6].emergency_media_item is not None + assert isinstance(results[6].emergency_media_item, (bytes, bytearray)) + assert results[6].handle is not None + assert isinstance(results[6].handle, (bytes, bytearray)) + assert results[6].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + assert results[7].table == "Z_METADATA" + assert results[7].z_version == 1 + assert results[7].z_uuid == "A3ED16E2-9E8F-45FC-A3EA-1ADAEF54C44A" + assert results[7].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + assert results[8].table == "Z_MODELCACHE" + assert results[8].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py new file mode 100644 index 0000000000..2ed92dc354 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.notes import NotesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "NotesV7.storedata", + "NotesV7.storedata-wal", + ] + ], +) +def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/notes/{test_file}") + fs_unix.map_file(f"Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/{test_file}", data_file) + + target_unix.add_plugin(NotesPlugin) + + results = list(target_unix.notes()) + + assert len(results) == 42 + + assert results[0].table == "ZACCOUNT" + assert results[0].z_pk == 1 + assert results[0].z_ent == 4 + assert results[0].z_opt == 2 + assert results[0].z_allow_insecure_authentication == 0 + assert results[0].z_did_choose_to_migrate == 1 + assert results[0].z_enabled == 1 + assert results[0].z_root_folder == 2 + assert results[0].z6_root_folder == 6 + assert results[0].z_trash_folder == 1 + assert results[0].z_gmail_capabilities_support is None + assert results[0].z_port is None + assert results[0].z_security_layer_type is None + assert results[0].z_migration_offered == 0 + assert results[0].z_account_description == "On My Mac" + assert results[0].z_email_address is None + assert results[0].z_full_name is None + assert results[0].z_parent_account_identifier is None + assert results[0].z_user_name is None + assert results[0].z_folder_hierarchy_sync_state is None + assert results[0].z_authentication is None + assert results[0].z_host_name is None + assert results[0].z_server_path_prefix is None + assert results[0].z_external_url is None + assert results[0].z_internal_url is None + assert results[0].z_last_used_autodiscover_url is None + assert results[0].z_tls_certificate is None + assert results[0].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[1].table == "ZFOLDER" + assert results[1].z_pk == 1 + assert results[1].z_ent == 9 + assert results[1].z_opt == 2 + assert results[1].z_account == 1 + assert results[1].z1_account == 4 + assert results[1].z_parent is None + assert results[1].z6_parent is None + assert results[1].z_is_distinguished is None + assert results[1].z_alleged_highest_modification_sequence is None + assert results[1].z_computed_highest_modification_sequence is None + assert results[1].z_uid_next is None + assert results[1].z_uid_validity is None + assert results[1].z_trash_account == 1 + assert results[1].z1_trash_account == 4 + assert results[1].z_name == "Trash" + assert results[1].z_change_key is None + assert results[1].z_user_name is None + assert results[1].z_folder_id is None + assert results[1].z_sync_state is None + assert results[1].z_server_name is None + assert results[1].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[4].table == "Z_PRIMARYKEY" + assert results[4].z_ent == 1 + assert results[4].z_name == "Account" + assert results[4].z_super == 0 + assert results[4].z_max == 1 + assert results[4].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[29].ac_account_type_version is None + assert results[29].ns_auto_vacuum_level == 2 + assert results[29].ns_persistence_framework_version == 1526 + assert results[29].ns_persistence_maximum_framework_version == 1526 + assert results[29].ns_store_model_version_checksum_key == "kEYcIlyOEwm45cPXDVhZk/RhZ1zBhdqNxj73uVM6ANM=" + assert ( + results[29].ns_store_model_version_hashes_digest + == "oKU9BJZ8XlLnCDx32ddVJ7zUewwSZxitb1jZ2XSfZEqW7ZTynDfDUJKGaDF4E//G64SKKXQU253iSERK2dTzdA==" + ) + assert results[29].ns_store_model_version_hashes_version == 3 + assert results[29].ns_store_model_version_identifiers == "['']" + assert results[29].ns_store_type == "SQLite" + assert results[29].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[30].plist_path == "NSStoreModelVersionHashes" + assert results[30].account_hash is not None + assert isinstance(results[30].account_hash, (bytes, bytearray)) + assert results[30].attachment_hash is not None + assert isinstance(results[30].attachment_hash, (bytes, bytearray)) + assert results[30].ews_account is not None + assert isinstance(results[30].ews_account, (bytes, bytearray)) + assert results[30].ews_folder is not None + assert isinstance(results[30].ews_folder, (bytes, bytearray)) + assert results[30].ews_note is not None + assert isinstance(results[30].ews_note, (bytes, bytearray)) + assert results[30].folder is not None + assert isinstance(results[30].folder, (bytes, bytearray)) + assert results[30].folder_action is not None + assert isinstance(results[30].folder_action, (bytes, bytearray)) + assert results[30].imap_account is not None + assert isinstance(results[30].imap_account, (bytes, bytearray)) + assert results[30].imap_folder is not None + assert isinstance(results[30].imap_folder, (bytes, bytearray)) + assert results[30].imap_note is not None + assert isinstance(results[30].imap_note, (bytes, bytearray)) + assert results[30].insert_folder_action is not None + assert isinstance(results[30].insert_folder_action, (bytes, bytearray)) + assert results[30].insert_note_action is not None + assert isinstance(results[30].insert_note_action, (bytes, bytearray)) + assert results[30].local_account is not None + assert isinstance(results[30].local_account, (bytes, bytearray)) + assert results[30].move_folder_action is not None + assert isinstance(results[30].move_folder_action, (bytes, bytearray)) + assert results[30].move_note_action is not None + assert isinstance(results[30].move_note_action, (bytes, bytearray)) + assert results[30].note is not None + assert isinstance(results[30].note, (bytes, bytearray)) + assert results[30].note_action is not None + assert isinstance(results[30].note_action, (bytes, bytearray)) + assert results[30].note_body is not None + assert isinstance(results[30].note_body, (bytes, bytearray)) + assert results[30].offline_action is not None + assert isinstance(results[30].offline_action, (bytes, bytearray)) + assert results[30].trash_folder is not None + assert isinstance(results[30].trash_folder, (bytes, bytearray)) + assert results[30].update_folder_action is not None + assert isinstance(results[30].update_folder_action, (bytes, bytearray)) + assert results[30].update_note_action is not None + assert isinstance(results[30].update_note_action, (bytes, bytearray)) + assert results[30].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[31].table == "Z_METADATA" + assert results[31].z_version == 1 + assert results[31].z_uuid == "FCCEFDB3-D6A9-4BC1-A57A-9BFCE8592C33" + assert results[31].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[32].table == "Z_MODELCACHE" + assert results[32].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[33].table == "ACHANGE" + assert results[33].z_pk == 1 + assert results[33].z_ent == 16001 + assert results[33].z_opt is None + assert results[33].z_change_type == 0 + assert results[33].z_entity == 6 + assert results[33].z_entity_pk == 2 + assert results[33].z_transaction_id == 1 + assert results[33].z_columns is None + assert results[33].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[38].table == "ATRANSACTION" + assert results[38].z_pk == 1 + assert results[38].z_ent == 16002 + assert results[38].z_opt is None + assert results[38].z_author_ts is None + assert results[38].z_bundle_id_ts == 1 + assert results[38].z_context_name_ts is None + assert results[38].z_process_id_ts == 2 + assert results[38].z_timestamp == datetime(2026, 5, 4, 11, 35, 17, 303456, tzinfo=timezone.utc) + assert results[38].z_author is None + assert results[38].z_bundle_id is None + assert results[38].z_context_name is None + assert results[38].z_process_id is None + assert results[38].z_query_gen is None + assert results[38].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[40].table == "ATRANSACTIONSTRING" + assert results[40].z_pk == 1 + assert results[40].z_ent == 16003 + assert results[40].z_opt is None + assert results[40].z_name == "com.apple.Notes" + assert results[40].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" From 7704d03e324c40667a289f97af4d282fcfb7df14 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:34:16 +0200 Subject: [PATCH 40/52] Add Safari favicons and per site preferences plugins --- .../bsd/darwin/macos/helpers/build_records.py | 6 - .../unix/bsd/darwin/macos/safari/__init__.py | 0 .../macos/safari/airport_preferences.py | 69 +++++++++++ .../darwin/macos/safari/safari_favicons.py | 115 ++++++++++++++++++ .../safari/safari_per_site_preferences.py | 111 +++++++++++++++++ .../macos/safari/safari_favicons/favicons.db | 3 + .../safari/safari_favicons/favicons.db-wal | 3 + .../PerSitePreferences.db | 3 + .../PerSitePreferences.db-wal | 0 .../unix/bsd/darwin/macos/safari/__init__.py | 0 .../macos/safari/test_safari_favicons.py | 70 +++++++++++ .../test_safari_per_site_preferences.py | 63 ++++++++++ 12 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/__init__.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db-wal create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db-wal create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/__init__.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_preferences.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 87caa7783e..5d4eaca785 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -81,12 +81,6 @@ def build_sqlite_records( for file in files: try: with SQLite3(file) as database: - # for table in database.tables(): - # print(table.name) - # for row in table.rows(): - # print(row) - # break - # print() for table in database.tables(): for row in table.rows(): row_dict = {k: v for k, v in row} # noqa C416 diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/__init__.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py new file mode 100644 index 0000000000..d7ae42ae4b --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +AirportPreferencesRecord = TargetRecordDescriptor( + "macos/airport_preferences", + [ + ("varint", "counter"), + ("string", "device_uuid"), + ("string[]", "preferred_order"), + ("varint", "version_number"), + ("path", "source"), + ], +) + + +class AirportPreferencesPlugin(Plugin): + """macOS AirPort (WiFi) preferences plugin. + + Contains WiFi network information. + + References: + - https://apple.stackexchange.com/questions/301346/how-can-i-better-sort-and-set-wifi-network-preferences-on-mac + """ + + PATH = "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" + + def __init__(self, target: Target): + super().__init__(target) + self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None + + def check_compatible(self) -> None: + if not self.file: + raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found") + + @export(record=AirportPreferencesRecord) + def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: + """Return macOS AirPort (Wi-Fi) preferences. + + Yields AirportPreferencesRecord with the following fields: + + .. code-block:: text + + counter (varint): The Counter key of the plist. + device_uuid (string): UUID of the device. + preferred_order (string[]): Ordered list of known Wi-Fi network SSIDs. + version_number (varint): The version number of the plist. + source (path): Path to the com.apple.airport.preferences.plist file. + """ + plist = plistlib.load(self.file.open()) + + yield AirportPreferencesRecord( + counter=plist.get("Counter"), + device_uuid=plist.get("DeviceUUID"), + preferred_order=plist.get("PreferredOrder"), + version_number=plist.get("Version"), + source=self.file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py new file mode 100644 index 0000000000..839c4568b9 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +PageURLRecord = TargetRecordDescriptor( + "macos/safari/favicon/page_url", + [ + ("string", "table"), + ("string", "url"), + ("string", "uuid"), + ("path", "source"), + ], +) + +IconInfoRecord = TargetRecordDescriptor( + "macos/safari/favicon/icon_info", + [ + ("string", "table"), + ("string", "uuid"), + ("string", "url"), + ("datetime", "timestamp"), + ("varint", "width"), + ("varint", "height"), + ("varint", "has_generated_representations"), + ("path", "source"), + ], +) + +RejectedResourcesRecord = TargetRecordDescriptor( + "macos/safari/favicon/rejected_resources", + [ + ("string", "table"), + ("string", "page_url"), + ("string", "icon_url"), + ("datetime", "timestamp"), + ("path", "source"), + ], +) + + +SafariFaviconRecords = (PageURLRecord, IconInfoRecord, RejectedResourcesRecord) + +CONVERT_TIMESTAMPS = { + "timestamp": "2001", +} + + +class SafariFaviconsPlugin(Plugin): + """macOS Safari favicons SQLite database plugin.""" + + USER_PATH = ("Library/Safari/Favicon Cache/favicons.db",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No favicons.db files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariFaviconRecords) + def safari_favicons( + self, + ) -> Iterator[SafariFaviconRecords]: + """Return Safari favicon information. + + Yields the following record types: + + .. code-block:: text + + PageURLRecord: + table (string): Name of the source table (page_url). + url (string): URL of the webpage. + uuid (string): Unique identifier. + source (path): Path to the favicons.db database file. + + IconInfoRecord: + table (string): Name of the source table (icon_info). + uuid (string): Unique identifier. + url (string): URL of the favicon image. + timestamp (datetime): Timestamp. + width (varint): Width of the favicon. + height (varint): Height of the favicon. + has_generated_representations (varint): Indicates whether the favicon has generated representations: + 0 = False. + 1 = True. + source (path): Path to the favicons.db database file. + + RejectedResourcesRecord: + table (string): Name of the source table (rejected_resources). + page_url (string): URL of the webpage associated with the rejected favicon. + icon_url (string): URL of the rejected favicon resource. + timestamp (datetime): Timestamp. + source (path): Path to the favicons.db database file. + """ + yield from build_sqlite_records(self, self.files, SafariFaviconRecords, convert_timestamps=CONVERT_TIMESTAMPS) + + # TODO: Add database_info table diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py new file mode 100644 index 0000000000..1145d2cfe6 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + + +SQLiteSequenceRecord = TargetRecordDescriptor( + "macos/per_site_preferences/sqlite_sequence", + [ + ("string", "table"), + ("string", "name"), + ("varint", "seq"), + ("path", "source"), + ], +) + +PreferencesValuesRecord = TargetRecordDescriptor( + "macos/safari/per_site_preferences/preferences_values", + [ + ("string", "table"), + ("varint", "id"), + ("string", "preference_domain"), + ("string", "preference"), + ("varint", "preference_value"), + ("datetime", "timestamp"), + ("string", "sync_data"), + ("string", "record_name"), + ("path", "source"), + ], +) + + +SafariPerSitePreferencesRecords = ( + SQLiteSequenceRecord, + PreferencesValuesRecord, +) + +FIELD_MAPPINGS = { + "domain": "preference_domain", +} + +CONVERT_TIMESTAMPS = { + "timestamp": "2001", +} + + +class SafariPerSitePreferencesPlugin(Plugin): + """macOS Safari per site preferences SQLite database plugin.""" + + USER_PATH = ("Library/Safari/PerSitePreferences.db",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No PerSitePreferences.db files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariPerSitePreferencesRecords) + def safari_per_site_preferences( + self, + ) -> Iterator[SafariPerSitePreferencesRecords]: + """Return Safari per site preferences information. + + Yields the following record types: + + .. code-block:: text + + PreferencesValuesRecord: + table (string): Name of the source table (preference_values). + id (varint): Primary key of the row. + preference_domain (string): Domain name the preference applies to. + preference (string): Name of the preference. + preference_value (varint): Value assigned to the preference. + timestamp (datetime): Timestamp indicating when the preference was set or updated. + sync_data (string): Synchronization metadata associated with the preference. + record_name (string): Record name. + source (path): Path to the PerSitePreferences.db database file. + + SQLiteSequenceTableRecord: + table (string): Name of the source table (sqlite_sequence). + name (string): Name of the table for which the sequence applies. + seq (varint): Current autoincrement value for the table. + source (path): Path to the PerSitePreferences.db database file. + """ + yield from build_sqlite_records( + self, + self.files, + SafariPerSitePreferencesRecords, + field_mappings=FIELD_MAPPINGS, + convert_timestamps=CONVERT_TIMESTAMPS, + ) + + # TODO: Add default_preferences table diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db new file mode 100644 index 0000000000..4c64a1e39a --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e5a602f1f78bab0b4f4191cd539d8e036cc462f2be8baefa98e2d7ea974e714 +size 176128 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db-wal new file mode 100644 index 0000000000..e6be8524d0 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/favicons.db-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12dcdd53f6259e493d53c6d086671cd488e1da5dd4ca210f817afffe55ba1cdf +size 148352 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db new file mode 100644 index 0000000000..217c0f93b5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:225e1ef981fff32ba42b1255530ad65b26eb5489e6650c2aa8cdb2bc61ac5fe9 +size 32768 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/PerSitePreferences.db-wal new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/__init__.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py new file mode 100644 index 0000000000..d3bea8a782 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_favicons import SafariFaviconsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "favicons.db", + "favicons.db-wal", + ] + ], +) +def test_safari_favicons(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + for test_file in test_files: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons/{test_file}") + fs_unix.map_file(f"Users/user/Library/Safari/Favicon Cache/{test_file}", data_file) + + target_unix.add_plugin(SafariFaviconsPlugin) + + results = list(target_unix.safari_favicons()) + + assert len(results) == 142 + + assert results[0].table == "page_url" + assert ( + results[0].url + == "https://www.google.com/search?client=safari&rls=en&q=digital+forensic+artifacts+repositroy&ie=UTF-8&oe=UTF-8&sei=jYX4abCaDamK9u8PoYyHyAU" + ) + assert results[0].uuid == "7273D3EF-09B6-498C-8385-47AEFBA2E3F0" + assert results[0].source == "/Users/user/Library/Safari/Favicon Cache/favicons.db" + + assert results[113].table == "icon_info" + assert results[113].uuid == "7273D3EF-09B6-498C-8385-47AEFBA2E3F0" + assert results[113].url == "https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico" + assert results[113].timestamp == datetime(2026, 5, 4, 11, 39, 58, 200725, tzinfo=timezone.utc) + assert results[113].width == 32 + assert results[113].height == 32 + assert results[113].has_generated_representations == 1 + assert results[113].source == "/Users/user/Library/Safari/Favicon Cache/favicons.db" + + assert results[137].table == "rejected_resources" + assert ( + results[137].page_url + == "https://www.google.com/search?client=safari&rls=en&q=digital+forensic+artifacts+repositroy&ie=UTF-8&oe=UTF-8" + ) + assert results[137].icon_url == "https://www.google.com/favicon.ico" + assert results[137].source == "/Users/user/Library/Safari/Favicon Cache/favicons.db" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_preferences.py new file mode 100644 index 0000000000..ced5a11298 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_preferences.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_per_site_preferences import ( + SafariPerSitePreferencesPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_files", + [ + [ + "PerSitePreferences.db", + "PerSitePreferences.db-wal", + ] + ], +) +def test_safari_per_site_preferences(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + for test_file in test_files: + data_file = absolute_path( + f"_data/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences/{test_file}" + ) + fs_unix.map_file(f"Users/user/Library/Safari/{test_file}", data_file) + + target_unix.add_plugin(SafariPerSitePreferencesPlugin) + + results = list(target_unix.safari_per_site_preferences()) + + assert len(results) == 3 + + assert results[0].table == "sqlite_sequence" + assert results[0].name == "preference_values" + assert results[0].seq == 2 + assert results[0].source == "/Users/user/Library/Safari/PerSitePreferences.db" + + assert results[1].table == "preference_values" + assert results[1].id == 1 + assert results[1].preference_domain == "code.visualstudio.com" + assert results[1].preference == "PerSitePreferencesDownloads" + assert results[1].preference_value == 0 + assert results[1].timestamp is None + assert results[1].sync_data is None + assert results[1].record_name is None + assert results[1].source == "/Users/user/Library/Safari/PerSitePreferences.db" From de3d9d35dc62e730aee967c03fc4665f57a1634d Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:51:11 +0200 Subject: [PATCH 41/52] Add safari_preferences plugin --- .../macos/safari/airport_preferences.py | 69 ------------------ .../darwin/macos/safari/safari_preferences.py | 72 +++++++++++++++++++ .../macos/safari/com.apple.Safari.plist | 3 + .../macos/safari/test_safari_preferences.py | 44 ++++++++++++ 4 files changed, 119 insertions(+), 69 deletions(-) delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py deleted file mode 100644 index d7ae42ae4b..0000000000 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/airport_preferences.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -import plistlib -from typing import TYPE_CHECKING - -from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import TargetRecordDescriptor -from dissect.target.plugin import Plugin, export - -if TYPE_CHECKING: - from collections.abc import Iterator - - from dissect.target import Target - -AirportPreferencesRecord = TargetRecordDescriptor( - "macos/airport_preferences", - [ - ("varint", "counter"), - ("string", "device_uuid"), - ("string[]", "preferred_order"), - ("varint", "version_number"), - ("path", "source"), - ], -) - - -class AirportPreferencesPlugin(Plugin): - """macOS AirPort (WiFi) preferences plugin. - - Contains WiFi network information. - - References: - - https://apple.stackexchange.com/questions/301346/how-can-i-better-sort-and-set-wifi-network-preferences-on-mac - """ - - PATH = "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist" - - def __init__(self, target: Target): - super().__init__(target) - self.file = self.target.fs.path(self.PATH) if self.target.fs.path(self.PATH).exists() else None - - def check_compatible(self) -> None: - if not self.file: - raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found") - - @export(record=AirportPreferencesRecord) - def airport_preferences(self) -> Iterator[AirportPreferencesRecord]: - """Return macOS AirPort (Wi-Fi) preferences. - - Yields AirportPreferencesRecord with the following fields: - - .. code-block:: text - - counter (varint): The Counter key of the plist. - device_uuid (string): UUID of the device. - preferred_order (string[]): Ordered list of known Wi-Fi network SSIDs. - version_number (varint): The version number of the plist. - source (path): Path to the com.apple.airport.preferences.plist file. - """ - plist = plistlib.load(self.file.open()) - - yield AirportPreferencesRecord( - counter=plist.get("Counter"), - device_uuid=plist.get("DeviceUUID"), - preferred_order=plist.get("PreferredOrder"), - version_number=plist.get("Version"), - source=self.file, - _target=self.target, - ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py new file mode 100644 index 0000000000..5374282f20 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +SafariPreferencesRecord = TargetRecordDescriptor( + "macos/safari_preferences", + [ + ("varint", "iio_launch_info"), + ("path", "source"), + ], +) + + +class SafariPreferencesPlugin(Plugin): + """macOS Safari favicons SQLite database plugin. + + Parses Safari's configuration settings and a list of recent searches performed by the user. + + References: + - https://medium.com/@cyberengage.org/p13-analyzing-safari-browser-apple-mail-data-and-recents-database-artifacts-on-macos-9b58848d70ec + """ + + USER_PATH = ("Library/Preferences/com.apple.Safari.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No com.apple.Safari.plist files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariPreferencesRecord) + def safari_preferences(self) -> Iterator[SafariPreferencesRecord]: + """Return macOS Safari preferences. + + Yields SafariPreferencesRecords with the following fields: + + .. code-block:: text + + iio_launch_info (varint): Image I/O launch info. + source (path): Path to the com.apple.Safari.plist file. + """ + for file in self.files: + plist = plistlib.load(file.open()) + + yield SafariPreferencesRecord( + iio_launch_info=plist.get("IIO_LaunchInfo"), + source=file, + _target=self.target, + ) + + +# I was only able to find a IIO_LaunchInfo field in the plist file on a fresh Tahoe system. +# It could be that more fields will show up in the plist file depending on user activity. diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist new file mode 100644 index 0000000000..fff22d7c58 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d2ec8b8f2a4c2c7973ca0a0c3e5eb3fc11dc76661e9347a21291b44cd4d2581 +size 70 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py new file mode 100644 index 0000000000..c7516f8b20 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_preferences import SafariPreferencesPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "com.apple.Safari.plist", + ], +) +def test_safari_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") + fs_unix.map_file(f"Users/user/Library/Preferences/{test_file}", data_file) + + target_unix.add_plugin(SafariPreferencesPlugin) + + results = list(target_unix.safari_preferences()) + + assert len(results) == 1 + + assert results[0].iio_launch_info == -2 + assert results[0].source == "/Users/user/Library/Preferences/com.apple.Safari.plist" From 52ff082103f7a65806cdd861f8e3cb1509494b91 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:20:59 +0200 Subject: [PATCH 42/52] Add some more fields to LauncherRecord from the launchd manual --- .../target/plugins/os/unix/bsd/darwin/macos/launchers.py | 6 ++++++ .../unix/bsd/darwin/macos/safari/safari_preferences.py | 3 +-- .../plugins/os/unix/bsd/darwin/macos/time_machine.py | 3 +-- tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py | 9 +++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py index dd297b3a2e..713781a944 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py @@ -62,6 +62,9 @@ ("string[]", "hard_resource_limits"), ("boolean", "debug"), ("boolean", "wait_for_debugger"), + ("string[]", "path_state"), + ("string[]", "other_job_enabled"), + ("boolean", "low_priority_background_io"), ("string", "plist_path"), ("path", "source"), ] @@ -161,6 +164,9 @@ "HardResourceLimits": "hard_resource_limits", "Debug": "debug", "WaitForDebugger": "wait_for_debugger", + "path_state": "PathState", + "other_job_enabled": "OtherJobEnabled", + "low_priority_background_io": "LowPriorityBackgroundIO", # SocketRecord "SocketKey": "socket_key", "SockType": "sock_type", diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py index 5374282f20..366a72fd16 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py @@ -67,6 +67,5 @@ def safari_preferences(self) -> Iterator[SafariPreferencesRecord]: _target=self.target, ) - # I was only able to find a IIO_LaunchInfo field in the plist file on a fresh Tahoe system. -# It could be that more fields will show up in the plist file depending on user activity. +# TODO: Check if more fields show up in the plist file depending on user activity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py index 86742f4dad..26008bc29e 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -56,6 +56,5 @@ def time_machine(self) -> Iterator[TimeMachineRecord]: _target=self.target, ) - # I was only able to find a preferences_version field in the plist file on a fresh Tahoe system. -# It could be that more fields will show up in the plist file depending on user activity. +# TODO: Check if more fields show up in the plist file depending on user activity. diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py index f8af95f9f2..02c74db059 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py @@ -108,6 +108,9 @@ def test_launch_agents( assert results[0].hard_resource_limits == [] assert results[0].debug is None assert results[0].wait_for_debugger is None + assert results[0].path_state == [] + assert results[0].other_job_enabled == [] + assert results[0].low_priority_background_io is None assert results[0].plist_path is None assert results[0].source == "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist" @@ -186,6 +189,9 @@ def test_launch_agents( assert results[4].hard_resource_limits == [] assert results[4].debug is None assert results[4].wait_for_debugger is None + assert results[4].path_state == [] + assert results[4].other_job_enabled == [] + assert results[4].low_priority_background_io is None assert results[4].plist_path is None assert results[4].source == "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.plist" @@ -284,6 +290,9 @@ def test_launch_daemons( assert results[0].hard_resource_limits == [] assert results[0].debug is None assert results[0].wait_for_debugger is None + assert results[0].path_state == [] + assert results[0].other_job_enabled == [] + assert results[0].low_priority_background_io is None assert results[0].plist_path is None assert results[0].source == "/System/Library/LaunchDaemons/org.cups.cupsd.plist" From 3658cb6162b711cc5b2d4ae1ba467ae3c097dcb5 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:52:02 +0200 Subject: [PATCH 43/52] Replace true/false varints with booleans --- .../unix/bsd/darwin/macos/duet_knowledge_c.py | 24 +++------ .../plugins/os/unix/bsd/darwin/macos/notes.py | 28 ++++------ .../darwin/macos/safari/safari_favicons.py | 6 +-- .../os/unix/bsd/darwin/macos/user_accounts.py | 54 +++++++------------ .../macos/safari/test_safari_favicons.py | 2 +- .../bsd/darwin/macos/test_duet_knowledge_c.py | 4 +- .../os/unix/bsd/darwin/macos/test_notes.py | 8 +-- .../bsd/darwin/macos/test_user_accounts.py | 18 +++---- 8 files changed, 54 insertions(+), 90 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index e127ba153c..3cc29f0590 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -34,8 +34,8 @@ ("varint", "z_pk"), ("varint", "z_ent"), ("varint", "z_opt"), - ("varint", "z_is_active"), - ("varint", "z_is_multi_device_registration"), + ("boolean", "z_is_active"), + ("boolean", "z_is_multi_device_registration"), ("datetime", "z_creation_date"), ("string", "z_identifier"), ("string", "z_properties"), @@ -58,8 +58,8 @@ ("varint", "z_compatibility_version"), ("varint", "z_end_day_of_week"), ("varint", "z_end_second_of_day"), - ("varint", "z_has_custom_metadata"), - ("varint", "z_has_structured_metadata"), + ("boolean", "z_has_custom_metadata"), + ("boolean", "z_has_structured_metadata"), ("varint", "z_seconds_from_gmt"), ("varint", "z_should_sync"), ("varint", "z_start_day_of_week"), @@ -348,12 +348,8 @@ def duet_knowledge_c( z_pk (varint): The autoincrement primary key of the table. z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. - z_is_active (varint): Whether the registration is active: - 0 = False. - 1 = True. - z_is_multi_device_registration (varint): Whether the registration is a multi-device registration: - 0 = False. - 1 = True. + z_is_active (boolean): Whether the registration is active. + z_is_multi_device_registration (boolean): Whether the registration is a multi-device registration. z_creation_date (datetime): Creation timestamp. z_identifier (string): Identifier of the registration. z_properties (string): Properties of the registration. @@ -372,12 +368,8 @@ def duet_knowledge_c( z_compatibility_version (varint): Compatibility version. z_end_day_of_week (varint): End day of week. z_end_second_of_day (varint): End second of day. - z_has_custom_metadata (varint): Whether entry has custom metadata: - 0 = False. - 1 = True. - z_has_structured_metadata (varint): Whether entry has structured metadata: - 0 = False. - 1 = True. + z_has_custom_metadata (boolean): Whether entry has custom metadata. + z_has_structured_metadata (boolean): Whether entry has structured metadata. z_seconds_from_gmt (varint): Timezone offset from gmt. z_should_sync (varint): Sync flag. z_start_day_of_week (varint): Start day of week. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py index a6275e866e..d9066799df 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -22,16 +22,16 @@ ("varint", "z_pk"), ("varint", "z_ent"), ("varint", "z_opt"), - ("varint", "z_allow_insecure_authentication"), - ("varint", "z_did_choose_to_migrate"), - ("varint", "z_enabled"), + ("boolean", "z_allow_insecure_authentication"), + ("boolean", "z_did_choose_to_migrate"), + ("boolean", "z_enabled"), ("varint", "z_root_folder"), ("varint", "z6_root_folder"), ("varint", "z_trash_folder"), ("string", "z_gmail_capabilities_support"), ("string", "z_port"), ("string", "z_security_layer_type"), - ("varint", "z_migration_offered"), + ("boolean", "z_migration_offered"), ("string", "z_account_description"), ("string", "z_email_address"), ("string", "z_full_name"), @@ -60,7 +60,7 @@ ("varint", "z1_account"), ("varint", "z_parent"), ("varint", "z6_parent"), - ("string", "z_is_distinguished"), + ("boolean", "z_is_distinguished"), ("string", "z_alleged_highest_modification_sequence"), ("string", "z_computed_highest_modification_sequence"), ("string", "z_uid_next"), @@ -359,22 +359,16 @@ def notes( z_pk (varint): The autoincrement primary key of the table. z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. - z_allow_insecure_authentication (varint): Indicates whether insecure authentication is allowed: - 0 = False. - 1 = True. - z_did_choose_to_migrate (varint): Indicates if migration was selected: - 0 = False. - 1 = True. - z_enabled (varint): Indicates whether the account is enabled. + z_allow_insecure_authentication (boolean): Indicates whether insecure authentication is allowed. + z_did_choose_to_migrate (boolean): Indicates if migration was selected. + z_enabled (boolean): Indicates whether the account is enabled. z_root_folder (varint): Reference to the root folder. z6_root_folder (varint): Alternate root folder reference. z_trash_folder (varint): Reference to the trash folder. z_gmail_capabilities_support (string): Gmail capability support flag. z_port (string): Port value. z_security_layer_type (string): Security layer type. - z_migration_offered (varint): Indicates if migration was offered: - 0 = False. - 1 = True. + z_migration_offered (boolean): Indicates if migration was offered. z_account_description (string): Account description: z_email_address (string): Associated email address. z_full_name (string): Full name of the account. @@ -399,9 +393,7 @@ def notes( z1_account (varint): Alternate reference to Z_PK in ZACCOUNT. z_parent (varint): Parent folder reference. z6_parent (varint): Alternate parent reference. - z_is_distinguished (varint): Whether entry is distinguished: - 0 = False. - 1 = True. + z_is_distinguished (boolean): Whether entry is distinguished. z_alleged_highest_modification_sequence (string): Alleged highest modification sequence. z_computed_highest_modification_sequence (string): Computed highest modification sequence. z_uid_next (string): Next UID value. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py index 839c4568b9..f2e9cbc269 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py @@ -32,7 +32,7 @@ ("datetime", "timestamp"), ("varint", "width"), ("varint", "height"), - ("varint", "has_generated_representations"), + ("boolean", "has_generated_representations"), ("path", "source"), ], ) @@ -98,9 +98,7 @@ def safari_favicons( timestamp (datetime): Timestamp. width (varint): Width of the favicon. height (varint): Height of the favicon. - has_generated_representations (varint): Indicates whether the favicon has generated representations: - 0 = False. - 1 = True. + has_generated_representations (boolean): Indicates whether the favicon has generated representations. source (path): Path to the favicons.db database file. RejectedResourcesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 170971be35..88402b99b6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -46,11 +46,11 @@ ("varint", "z_pk"), ("varint", "z_ent"), ("varint", "z_opt"), - ("varint", "z_active"), - ("varint", "z_authenticated"), - ("varint", "z_supports_authentication"), - ("varint", "z_visible"), - ("varint", "z_warming_up"), + ("boolean", "z_active"), + ("boolean", "z_authenticated"), + ("boolean", "z_supports_authentication"), + ("boolean", "z_visible"), + ("boolean", "z_warming_up"), ("varint", "z_account_type"), ("varint", "z_parent_account"), ("datetime", "z_date"), @@ -98,10 +98,10 @@ ("varint", "z_pk"), ("varint", "z_ent"), ("varint", "z_opt"), - ("varint", "z_obsolete"), - ("varint", "z_supports_authentication"), - ("varint", "z_supports_multiple_accounts"), - ("varint", "z_visibility"), + ("boolean", "z_obsolete"), + ("boolean", "z_supports_authentication"), + ("boolean", "z_supports_multiple_accounts"), + ("boolean", "z_visibility"), ("string", "z_account_type_description"), ("string", "z_credential_protection_policy"), ("string", "z_credential_type"), @@ -330,21 +330,11 @@ def user_accounts( z_pk (varint): The autoincrement primary key of the table. z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. - z_active (varint): Indicates whether the account is active: - 0 = False. - 1 = True. - z_authenticated (varint): Indicates whether the account is authenticated: - 0 = False. - 1 = True. - z_supports_authentication (varint): Indicates if authentication is supported: - 0 = False. - 1 = True. - z_visible (varint): Indicates whether account is visible: - 0 = False. - 1 = True. - z_warming_up (varint): Indicates account initialization state: - 0 = False. - 1 = True. + z_active (boolean): Indicates whether the account is active. + z_authenticated (boolean): Indicates whether the account is authenticated. + z_supports_authentication (boolean): Indicates if authentication is supported. + z_visible (boolean): Indicates whether account is visible. + z_warming_up (boolean): Indicates account initialization state. z_account_type (varint): Reference to z_pk in ZACCOUNTTYPE. z_parent_account (varint): Reference to z_pk of parent account. z_date (datetime): Timestamp. @@ -380,18 +370,10 @@ def user_accounts( z_pk (varint): The autoincrement primary key of the table. z_ent (varint): Entity identifier. z_opt (varint): The version number of the data record. - z_obsolete (varint): Indicates whether account type is obsolute: - 0 = False. - 1 = True. - z_supports_authentication (varint): Indicates whether authentication is supported: - 0 = False. - 1 = True. - z_supports_multiple_accounts (varint): Indicates whether multiple accounts are supported: - 0 = False. - 1 = True. - z_visibility (varint): Indicates visibility of the account type: - 0 = False. - 1 = True. + z_obsolete (boolean): Indicates whether account type is obsolute. + z_supports_authentication (boolean): Indicates whether authentication is supported. + z_supports_multiple_accounts (boolean): Indicates whether multiple accounts are supported. + z_visibility (boolean): Indicates visibility of the account type. z_account_type_description (string): Description of account type. z_credential_protection_policy (string): Credential protection policy. z_credential_type (string): Credential type. diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py index d3bea8a782..949b775477 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py @@ -58,7 +58,7 @@ def test_safari_favicons(test_files: list[str], target_unix: Target, fs_unix: Vi assert results[113].timestamp == datetime(2026, 5, 4, 11, 39, 58, 200725, tzinfo=timezone.utc) assert results[113].width == 32 assert results[113].height == 32 - assert results[113].has_generated_representations == 1 + assert results[113].has_generated_representations assert results[113].source == "/Users/user/Library/Safari/Favicon Cache/favicons.db" assert results[137].table == "rejected_resources" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py index 4e56f0dbb7..00f0d6c9b9 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py @@ -81,8 +81,8 @@ def test_duet_knowledge_c( assert results[4].z_compatibility_version == 0 assert results[4].z_end_day_of_week == 2 assert results[4].z_end_second_of_day == 41368 - assert results[4].z_has_custom_metadata == 0 - assert results[4].z_has_structured_metadata == 1 + assert not results[4].z_has_custom_metadata + assert results[4].z_has_structured_metadata assert results[4].z_seconds_from_gmt == -25200 assert results[4].z_should_sync == 0 assert results[4].z_start_day_of_week == 2 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py index 2ed92dc354..74aa866664 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py @@ -48,16 +48,16 @@ def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFiles assert results[0].z_pk == 1 assert results[0].z_ent == 4 assert results[0].z_opt == 2 - assert results[0].z_allow_insecure_authentication == 0 - assert results[0].z_did_choose_to_migrate == 1 - assert results[0].z_enabled == 1 + assert not results[0].z_allow_insecure_authentication + assert results[0].z_did_choose_to_migrate + assert results[0].z_enabled assert results[0].z_root_folder == 2 assert results[0].z6_root_folder == 6 assert results[0].z_trash_folder == 1 assert results[0].z_gmail_capabilities_support is None assert results[0].z_port is None assert results[0].z_security_layer_type is None - assert results[0].z_migration_offered == 0 + assert not results[0].z_migration_offered assert results[0].z_account_description == "On My Mac" assert results[0].z_email_address is None assert results[0].z_full_name is None diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py index 7d3462808b..e3373ddd0d 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -60,11 +60,11 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[19].z_pk == 1 assert results[19].z_ent == 2 assert results[19].z_opt == 7 - assert results[19].z_active == 0 - assert results[19].z_authenticated == 0 - assert results[19].z_supports_authentication == 1 - assert results[19].z_visible == 1 - assert results[19].z_warming_up == 0 + assert not results[19].z_active + assert not results[19].z_authenticated + assert results[19].z_supports_authentication + assert results[19].z_visible + assert not results[19].z_warming_up assert results[19].z_account_type == 50 assert results[19].z_parent_account is None assert results[19].z_date.isoformat() == "1995-03-25T14:11:32.168510+00:00" @@ -97,10 +97,10 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[36].z_pk == 1 assert results[36].z_ent == 4 assert results[36].z_opt == 1 - assert results[36].z_obsolete == 0 - assert results[36].z_supports_authentication == 1 - assert results[36].z_supports_multiple_accounts == 1 - assert results[36].z_visibility == 0 + assert not results[36].z_obsolete + assert results[36].z_supports_authentication + assert results[36].z_supports_multiple_accounts + assert not results[36].z_visibility assert results[36].z_account_type_description == "Gmail" assert results[36].z_credential_protection_policy is None assert results[36].z_credential_type == "oauth2" From 57092728fc2d32b0769ff173adc54c1d1425eedb Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:42:14 +0200 Subject: [PATCH 44/52] Add macOS Safari plugins --- .../bsd/darwin/macos/helpers/build_records.py | 7 +- .../darwin/macos/safari/safari_downloads.py | 73 ++++++++++++++++++ .../darwin/macos/safari/safari_favicons.py | 6 +- .../safari/safari_per_site_preferences.py | 4 +- .../safari_per_site_zoom_preferences.py | 76 +++++++++++++++++++ .../darwin/macos/safari/safari_preferences.py | 3 +- .../safari/safari_recently_closed_tabs.py | 43 +++++++++++ .../safari_user_notification_permissions.py | 75 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/time_machine.py | 1 + .../bsd/darwin/macos/safari/Downloads.plist | 3 + .../macos/safari/PerSiteZoomPreferences.plist | 3 + .../macos/safari/RecentlyClosedTabs.plist | 3 + .../safari/UserNotificationPermissions.plist | 3 + .../macos/safari/test_safari_downloads.py | 44 +++++++++++ .../test_safari_per_site_zoom_preferences.py | 48 ++++++++++++ .../test_safari_recently_closed_tabs.py | 69 +++++++++++++++++ ...st_safari_user_notification_permissions.py | 49 ++++++++++++ 17 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_user_notification_permissions.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/RecentlyClosedTabs.plist create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/UserNotificationPermissions.plist create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_user_notification_permissions.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 5d4eaca785..7d56fbfc6a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -401,8 +401,13 @@ def build_plist_records( try: if b"$archiver" in fh.peek(64): fh.seek(0) - data = load_plist_data(fh) + try: + data = load_plist_data(fh) + except Exception: + fh.seek(0) + data = plistlib.load(fh) else: + fh.seek(0) data = plistlib.load(fh) yield from emit_dict_records( diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py new file mode 100644 index 0000000000..c9085d35b9 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +SafariDownloadsRecord = TargetRecordDescriptor( + "macos/safari_downloads", + [ + ("string[]", "download_history"), + ("path", "source"), + ], +) + + +class SafariDownloadsPlugin(Plugin): + """macOS Safari property list (plist) plugin. + + This plist file contains a record of downloaded files. + This data is automatically deleted after one day by default. + + References: + - https://medium.com/@cyberengage.org/p13-analyzing-safari-browser-apple-mail-data-and-recents-database-artifacts-on-macos-9b58848d70ec + """ + + USER_PATH = ("Library/Safari/Downloads.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No Downloads.plist files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariDownloadsRecord) + def safari_downloads(self) -> Iterator[SafariDownloadsRecord]: + """Return macOS Safari downloads. + + Yields SafariDownloadsRecords with the following fields: + + .. code-block:: text + + download_history (string[]): Download history. + source (path): Path to the Downloads.plist file. + """ + for file in self.files: + plist = plistlib.load(file.open()) + + yield SafariDownloadsRecord( + download_history=plist.get("DownloadHistory"), + source=file, + _target=self.target, + ) + + +# Download history is empty in current test data +# TODO: Get test file with actual data diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py index f2e9cbc269..2bb23c815f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py @@ -14,7 +14,7 @@ from dissect.target import Target PageURLRecord = TargetRecordDescriptor( - "macos/safari/favicon/page_url", + "macos/safari_favicons/page_url", [ ("string", "table"), ("string", "url"), @@ -24,7 +24,7 @@ ) IconInfoRecord = TargetRecordDescriptor( - "macos/safari/favicon/icon_info", + "macos/safari_favicons/icon_info", [ ("string", "table"), ("string", "uuid"), @@ -38,7 +38,7 @@ ) RejectedResourcesRecord = TargetRecordDescriptor( - "macos/safari/favicon/rejected_resources", + "macos/safari_favicons/rejected_resources", [ ("string", "table"), ("string", "page_url"), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py index 1145d2cfe6..3ce6847a07 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_preferences.py @@ -15,7 +15,7 @@ SQLiteSequenceRecord = TargetRecordDescriptor( - "macos/per_site_preferences/sqlite_sequence", + "macos/safari_per_site_preferences/sqlite_sequence", [ ("string", "table"), ("string", "name"), @@ -25,7 +25,7 @@ ) PreferencesValuesRecord = TargetRecordDescriptor( - "macos/safari/per_site_preferences/preferences_values", + "macos/safari_per_site_preferences/preferences_values", [ ("string", "table"), ("varint", "id"), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py new file mode 100644 index 0000000000..4e21a2b067 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +SafariPerSiteZoomPreferencesRecord = TargetRecordDescriptor( + "macos/safari_per_site_zoom_preferences", + [ + ("string", "map_of_hostnames_to_zoom_preferences"), + ("string", "map_of_ck_record_names_to_ck_records"), + ("varint", "zoom_preference_version"), + ("path", "source"), + ], +) + + +class SafariPerSiteZoomPreferencesPlugin(Plugin): + """macOS Safari per site zoom preferences (plist) plugin. + + References: + - https://www.magnetforensics.com/blog/macos-safari-preferences-and-privacy/ + """ + + USER_PATH = ("Library/Safari/PerSiteZoomPreferences.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No PerSiteZoomPreferences.plist files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariPerSiteZoomPreferencesRecord) + def safari_per_site_zoom_preferences(self) -> Iterator[SafariPerSiteZoomPreferencesRecord]: + """Return macOS Safari per site zoom preferences. + + Yields SafariPerSiteZoomPreferencesRecords with the following fields: + + .. code-block:: text + + map_of_hostnames_to_zoom_preferences (string): map of hostnames to zoom preferences. + map_of_ck_record_names_to_ck_records (string): map of ck record names to ck records. + zoom_preference_version (varint): Zoom preference version. + source (path): Path to the PerSiteZoomPreferences.plist file. + """ + for file in self.files: + plist = plistlib.load(file.open()) + + yield SafariPerSiteZoomPreferencesRecord( + map_of_hostnames_to_zoom_preferences=plist.get("MapOfHostnamesToZoomPreferences"), + map_of_ck_record_names_to_ck_records=plist.get("MapOfCKRecordNamesToCKRecords"), + zoom_preference_version=plist.get("ZoomPreferenceVersion"), + source=file, + _target=self.target, + ) + + +# MapOfHostnamesToZoomPreferences and MapOfCKRecordNamesToCKRecords fields are empty in current test data +# TODO: Get test file with actual data diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py index 366a72fd16..06a450d660 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py @@ -23,7 +23,7 @@ class SafariPreferencesPlugin(Plugin): - """macOS Safari favicons SQLite database plugin. + """macOS Safari property list (plist) plugin. Parses Safari's configuration settings and a list of recent searches performed by the user. @@ -67,5 +67,6 @@ def safari_preferences(self) -> Iterator[SafariPreferencesRecord]: _target=self.target, ) + # I was only able to find a IIO_LaunchInfo field in the plist file on a fresh Tahoe system. # TODO: Check if more fields show up in the plist file depending on user activity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py new file mode 100644 index 0000000000..2b1c2a3338 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + + +class SafariRecentlyClosedTabsPlugin(Plugin): + """macOS Safari recently closed tabs (plist) plugin. + + References: + - https://medium.com/@cyberengage.org/p13-analyzing-safari-browser-apple-mail-data-and-recents-database-artifacts-on-macos-9b58848d70ec + """ + + USER_PATH = ("Library/Safari/RecentlyClosedTabs.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No RecentlyClosedTabs.plist found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=DynamicDescriptor(["string"])) + def safari_recently_closed_tabs(self) -> Iterator[DynamicDescriptor]: + """Return macOS Safari recently closed tabs information.""" + yield from build_plist_records(self, self.files, function_name="macos/safari_recently_closed_tabs") diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_user_notification_permissions.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_user_notification_permissions.py new file mode 100644 index 0000000000..8e4a47ccae --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_user_notification_permissions.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import plistlib +from typing import TYPE_CHECKING + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export +from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target import Target + +SafariUserNotificationPermissionsRecord = TargetRecordDescriptor( + "macos/safari_user_notification_permissions", + [ + ("varint", "permission"), + ("datetime", "date_added"), + ("string", "site"), + ("path", "source"), + ], +) + + +class SafariUserNotificationPermissionsPlugin(Plugin): + """macOS Safari user notification permissions property list (plist) plugin. + + References: + - https://www.magnetforensics.com/blog/macos-safari-preferences-and-privacy/ + """ + + USER_PATH = ("Library/Safari/UserNotificationPermissions.plist",) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No UserNotificationPermissions.plist files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.USER_PATH): + files.add(path) + return files + + @export(record=SafariUserNotificationPermissionsRecord) + def safari_user_notification_permissions(self) -> Iterator[SafariUserNotificationPermissionsRecord]: + """Return macOS Safari user notification permissions. + + Yields SafariUserNotificationPermissionsRecords with the following fields: + + .. code-block:: text + + permission (varint): The notification permission value for the site. + date_added (datetime): The timestamp when the permission entry was created. + site (string): The website URL associated with the permission. + source (path): Path to the UserNotificationPermissions.plist file. + """ + for file in self.files: + plist = plistlib.load(file.open()) + # The top-level keys in the plist represent website URLs (e.g., https://www.macworld.com) + # for which Safari has stored notification permission entries. + for key in plist: + data = plist.get(key) + yield SafariUserNotificationPermissionsRecord( + permission=data["Permission"], + date_added=data["Date Added"], + site=key, + source=file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py index 26008bc29e..cb40c3e9e3 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -56,5 +56,6 @@ def time_machine(self) -> Iterator[TimeMachineRecord]: _target=self.target, ) + # I was only able to find a preferences_version field in the plist file on a fresh Tahoe system. # TODO: Check if more fields show up in the plist file depending on user activity. diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist new file mode 100644 index 0000000000..059002889f --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db3f32f1a36c987dbb6ce0ac06b0333f9659733724dadf403fa891e180183bde +size 65 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist new file mode 100644 index 0000000000..c641279e6f --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1eba7652f7bdfa9467fff90f6a700dd8d7f258a9cad6d3823ca6f208f31ff7e1 +size 148 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/RecentlyClosedTabs.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/RecentlyClosedTabs.plist new file mode 100644 index 0000000000..d65b7709a5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/RecentlyClosedTabs.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1506d8796099896966ad75e0bf14f661dc5fce1b0a579fbaf37541229a603d77 +size 177131 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/UserNotificationPermissions.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/UserNotificationPermissions.plist new file mode 100644 index 0000000000..f81cb0e6b5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/UserNotificationPermissions.plist @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bd8ba26d027dbf69f1fe1b0fe624fca18b8218fc43f310d74b5f750c89a3f2e +size 115 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py new file mode 100644 index 0000000000..14c1191538 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_downloads import SafariDownloadsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "Downloads.plist", + ], +) +def test_safari_downloads(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") + fs_unix.map_file(f"Users/user/Library/Safari/{test_file}", data_file) + + target_unix.add_plugin(SafariDownloadsPlugin) + + results = list(target_unix.safari_downloads()) + + assert len(results) == 1 + + assert results[0].download_history == [] + assert results[0].source == "/Users/user/Library/Safari/Downloads.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py new file mode 100644 index 0000000000..3e50cb0844 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_per_site_zoom_preferences import ( + SafariPerSiteZoomPreferencesPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "PerSiteZoomPreferences.plist", + ], +) +def test_safari_per_site_zoom_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") + fs_unix.map_file(f"Users/user/Library/Safari/{test_file}", data_file) + + target_unix.add_plugin(SafariPerSiteZoomPreferencesPlugin) + + results = list(target_unix.safari_per_site_zoom_preferences()) + + assert len(results) == 1 + + assert results[0].map_of_hostnames_to_zoom_preferences == "{}" + assert results[0].map_of_ck_record_names_to_ck_records == "{}" + assert results[0].zoom_preference_version == 1 + assert results[0].source == "/Users/user/Library/Safari/PerSiteZoomPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py new file mode 100644 index 0000000000..82ba26a569 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_recently_closed_tabs import ( + SafariRecentlyClosedTabsPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "RecentlyClosedTabs.plist", + ], +) +def test_safari_recently_closed_tabs(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") + fs_unix.map_file(f"Users/user/Library/Safari/{test_file}", data_file) + + target_unix.add_plugin(SafariRecentlyClosedTabsPlugin) + + results = list(target_unix.safari_recently_closed_tabs()) + + assert len(results) == 53 + + assert results[1].PersistentStateType == 0 + assert results[1].plist_path == "ClosedTabOrWindowPersistentStates[0]" + assert results[1].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" + + assert not results[-1].IsDisposable + assert results[-1].AncestorTabUUIDsKey == [] + assert results[-1].TabGroupTypeForTabKey + assert results[-1].TabGroupForTab == "705CFB6B-1741-456B-8E58-2B213727759E" + assert results[-1].DateClosed == "2026-05-11 08:34:41.497901" + assert results[-1].ProfileUUID == "DefaultProfile" + assert results[-1].SafeToLoad + assert results[-1].TabIndex == 0 + assert results[-1].WindowUUID == "78C7D8F7-2956-4135-93F7-58A57AB07031" + assert results[-1].LastVisitTime == "2026-05-07 12:16:08.853022" + assert results[-1].TabUUID == "E8E9CB9D-F8DD-4AD9-8728-0691560A3DD0" + assert ( + results[-1].TabURL + == "https://apple.stackexchange.com/questions/475455/what-happened-to-the-periodic-scripts-on-macos-sequoia" + ) + assert results[-1].TabStateVersion == 1 + assert results[-1].TabTitle == "What happened to the periodic scripts on macOS Sequoia? - Ask Different" + assert results[-1].ProcessIdentifier == 626 + assert not results[-1].IsMuted + assert results[-1].plist_path == "ClosedTabOrWindowPersistentStates[11]/PersistentState" + assert results[-1].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_user_notification_permissions.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_user_notification_permissions.py new file mode 100644 index 0000000000..34aa1033df --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_user_notification_permissions.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_user_notification_permissions import ( + SafariUserNotificationPermissionsPlugin, +) +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "UserNotificationPermissions.plist", + ], +) +def test_safari_user_notification_permissions(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + user = UnixUserRecord( + name="user", + uid=501, + gid=20, + home="/Users/user", + shell="/bin/zsh", + ) + target_unix.users = lambda: [ + user, + ] + + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") + fs_unix.map_file(f"/Users/user/Library/Safari/{test_file}", data_file) + + target_unix.add_plugin(SafariUserNotificationPermissionsPlugin) + + results = list(target_unix.safari_user_notification_permissions()) + + assert len(results) == 1 + + assert results[0].permission == 0 + assert results[0].date_added == datetime(2026, 5, 4, 13, 18, 13, 813088, tzinfo=timezone.utc) + assert results[0].site == "https://www.macworld.com" + assert results[0].source == "/Users/user/Library/Safari/UserNotificationPermissions.plist" From 8eda47749c631192b08bf65ffc3a12c32b350218 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:54:48 +0200 Subject: [PATCH 45/52] Replace DynamicDescriptor for TargetRecordDescriptors in safari_recently_closed_tabs --- .../bsd/darwin/macos/helpers/build_records.py | 39 ++++ .../safari/safari_recently_closed_tabs.py | 185 +++++++++++++++++- .../test_safari_recently_closed_tabs.py | 64 ++++-- 3 files changed, 262 insertions(+), 26 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index 7d56fbfc6a..c0c0ccb8fb 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -690,6 +690,8 @@ def emit_dict_records( scalar values are retained as attributes dictionary elements are treated as child nodes and processed recursively. + Supports decoding of binary plist values and NSKeyedArchiver blobs. + Collapses specified paths into attribute values instead of recursing. Generates records for nodes containing attribute data, using either dynamic @@ -739,6 +741,43 @@ def emit_dict_records( if cleaned_list or not contains_dict: attributes[k] = cleaned_list + + elif isinstance(v, (bytes, bytearray)) and v.startswith(b"bplist00"): + if is_nskeyedarchive_blob(v): + try: + archiver = NSKeyedArchiver(BytesIO(v)) + decoded_value = archiver.top.get("root") + attributes[k] = decoded_value + except Exception: + plugin.target.log.exception( + "Failed to decode %s value for key %s", + child_path, + k, + ) + else: + try: + plist_data = load_plist_data(v) if b"$archiver" in v[:128] else plistlib.loads(v) + + if isinstance(plist_data, dict): + yield from emit_dict_records( + plugin, + plist_data, + source, + record_descriptors=record_descriptors, + field_mappings=field_mappings, + convert_timestamps=convert_timestamps, + ) + else: + # If binary plist is an attribute and not a dict + attributes[k] = plist_data + + except Exception: + plugin.target.log.exception( + "Failed to parse plist in %s (plist_path=%s, key=%s)", + source, + child_path, + k, + ) else: attributes[k] = v diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py index 2b1c2a3338..2a6dcb8ba4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import DynamicDescriptor +from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records @@ -13,6 +13,115 @@ from dissect.target import Target +SafariRecentlyClosedTabsRecord = TargetRecordDescriptor( + "macos/safari_recently_closed_tabs", + [ + ("boolean", "is_disposable"), + ("string[]", "ancestor_tab_uuids_key"), + ("boolean", "tab_group_type_for_tab_key"), + ("string", "tab_group_for_tab"), + ("datetime", "date_closed"), + ("string", "profile_uuid"), + ("boolean", "safe_to_load"), + ("varint", "tab_index"), + ("string", "window_uuid"), + ("datetime", "last_visit_time"), + ("string", "tab_uuid"), + ("string", "tab_url"), + ("varint", "tab_state_version"), + ("string", "tab_title"), + ("boolean", "is_muted"), + ("varint", "process_identifier"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +WindowRecord = TargetRecordDescriptor( + "macos/safari_recently_closed_tabs/window", + [ + ("varint", "selected_tab_index"), + ("varint", "window_unified_sidebar_mode"), + ("boolean", "tab_bar_hidden"), + ("datetime", "date_closed"), + ("boolean", "favorites_bar_hidden"), + ("boolean", "is_popup_window"), + ("string", "profile_uuid"), + ("string", "window_restoration_archive_data"), + ("boolean", "is_private_window"), + ("boolean", "miniaturized"), + ("boolean", "prefers_reading_list_sidebar_visible"), + ("varint", "selected_pinned_tab_index"), + ("string[]", "unnamed_tab_group_uuids"), + ("string", "window_content_rect"), + ("string", "window_state_version"), + ("string", "window_uuid"), + ("string", "active_tab_group_uuid"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +ClosedPersistentStatesVersionRecord = TargetRecordDescriptor( + "macos/safari_recently_closed_tabs/closed_persistent_states_version", + [ + ("string", "closed_tab_or_window_persistent_states_version"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +PersistentStateTypeRecord = TargetRecordDescriptor( + "macos/safari_recently_closed_tabs/persistent_state_type", + [ + ("varint", "persistent_state_type"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +SafariRecentlyClosedTabsRecords = ( + SafariRecentlyClosedTabsRecord, + WindowRecord, + ClosedPersistentStatesVersionRecord, + PersistentStateTypeRecord, +) + +FIELD_MAPPINGS = { + "IsDisposable": "is_disposable", + "AncestorTabUUIDsKey": "ancestor_tab_uuids_key", + "TabGroupTypeForTabKey": "tab_group_type_for_tab_key", + "TabGroupForTab": "tab_group_for_tab", + "DateClosed": "date_closed", + "ProfileUUID": "profile_uuid", + "SafeToLoad": "safe_to_load", + "TabIndex": "tab_index", + "WindowUUID": "window_uuid", + "LastVisitTime": "last_visit_time", + "TabUUID": "tab_uuid", + "TabURL": "tab_url", + "TabStateVersion": "tab_state_version", + "TabTitle": "tab_title", + "IsMuted": "is_muted", + "ProcessIdentifier": "process_identifier", + "SelectedTabIndex": "selected_tab_index", + "WindowUnifiedSidebarMode": "window_unified_sidebar_mode", + "TabBarHidden": "tab_bar_hidden", + "FavoritesBarHidden": "favorites_bar_hidden", + "IsPopupWindow": "is_popup_window", + "WindowRestorationArchiveData": "window_restoration_archive_data", + "IsPrivateWindow": "is_private_window", + "Miniaturized": "miniaturized", + "PrefersReadingListSidebarVisible": "prefers_reading_list_sidebar_visible", + "SelectedPinnedTabIndex": "selected_pinned_tab_index", + "UnnamedTabGroupUUIDs": "unnamed_tab_group_uuids", + "WindowContentRect": "window_content_rect", + "WindowStateVersion": "window_state_version", + "activeTabGroupUUID": "active_tab_group_uuid", + "PersistentStateType": "persistent_state_type", + "ClosedTabOrWindowPersistentStatesVersion": "closed_tab_or_window_persistent_states_version", +} + class SafariRecentlyClosedTabsPlugin(Plugin): """macOS Safari recently closed tabs (plist) plugin. @@ -29,7 +138,7 @@ def __init__(self, target: Target): def check_compatible(self) -> None: if not (self.files): - raise UnsupportedPluginError("No RecentlyClosedTabs.plist found") + raise UnsupportedPluginError("No RecentlyClosedTabs.plist files found") def _find_files(self) -> set: files = set() @@ -37,7 +146,71 @@ def _find_files(self) -> set: files.add(path) return files - @export(record=DynamicDescriptor(["string"])) - def safari_recently_closed_tabs(self) -> Iterator[DynamicDescriptor]: - """Return macOS Safari recently closed tabs information.""" - yield from build_plist_records(self, self.files, function_name="macos/safari_recently_closed_tabs") + @export(record=SafariRecentlyClosedTabsRecords) + def safari_recently_closed_tabs(self) -> Iterator[SafariRecentlyClosedTabsRecords]: + """Return macOS Safari recently closed tabs information. + + Yields the following record types extracted from the + RecentlyClosedTabs.plist files: + + .. code-block:: text + + SafariRecentlyClosedTabsRecord: + is_disposable (boolean): Indicates whether the tab entry is disposable. + ancestor_tab_uuids_key (string[]): List of ancestor tab UUIDs. + tab_group_type_for_tab_key (boolean): Indicates if the tab belongs to a tab group type. + tab_group_for_tab (string): UUID of the associated tab group. + date_closed (datetime): Timestamp when the tab was closed. + profile_uuid (string): Profile identifier. + safe_to_load (boolean): Indicates if the tab is safe to restore. + tab_index (varint): Index of the tab within the window. + window_uuid (string): UUID of the parent window. + last_visit_time (datetime): Timestamp of the last visit to the tab. + tab_uuid (string): Unique identifier for the tab. + tab_url (string): URL of the tab. + tab_state_version (varint): Version of the tab state. + tab_title (string): Title of the tab. + is_muted (boolean): Indicates whether the tab was muted. + process_identifier (varint): Process identifier associated with the tab. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the RecentlyClosedTabs.plist file. + + WindowRecord: + selected_tab_index (varint): Index of the selected tab in the window. + window_unified_sidebar_mode (varint): Sidebar mode. + tab_bar_hidden (boolean): Indicates if the tab bar is hidden. + date_closed (datetime): Timestamp when the window was closed. + favorites_bar_hidden (boolean): Indicates if the favorites bar is hidden. + is_popup_window (boolean): Indicates if the window is a popup. + profile_uuid (string): Profile identifier. + window_restoration_archive_data (string): Window restoration archive data. + is_private_window (boolean): Indicates if the window was private. + miniaturized (boolean): Indicates if the window was minimized. + prefers_reading_list_sidebar_visible (boolean): Reading list sidebar visibility. + selected_pinned_tab_index (varint): Index of the selected pinned tab. + unnamed_tab_group_uuids (string[]): List of unnamed tab group UUIDs. + window_content_rect (string): Window geometry (position and size). + window_state_version (string): Version of the window state data. + window_uuid (string): Unique identifier for the window. + active_tab_group_uuid (string): UUID of the active tab group. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the RecentlyClosedTabs.plist file. + + ClosedPersistentStatesVersionRecord: + closed_tab_or_window_persistent_states_version (string): Version of the persistent states. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the RecentlyClosedTabs.plist file. + + PersistentStateTypeRecord: + persistent_state_type (varint): Type of the persistent state. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the RecentlyClosedTabs.plist file. + + """ + yield from build_plist_records( + self, + self.files, + SafariRecentlyClosedTabsRecords, + field_mappings=FIELD_MAPPINGS, + function_name="macos/safari_recently_closed_tabs", + ) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py index 82ba26a569..9c212c8bd9 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING import pytest @@ -42,28 +43,51 @@ def test_safari_recently_closed_tabs(test_file: str, target_unix: Target, fs_uni assert len(results) == 53 - assert results[1].PersistentStateType == 0 + assert results[0].closed_tab_or_window_persistent_states_version == "1" + assert results[0].plist_path is None + assert results[0].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" + + assert results[1].persistent_state_type == 0 assert results[1].plist_path == "ClosedTabOrWindowPersistentStates[0]" assert results[1].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" - assert not results[-1].IsDisposable - assert results[-1].AncestorTabUUIDsKey == [] - assert results[-1].TabGroupTypeForTabKey - assert results[-1].TabGroupForTab == "705CFB6B-1741-456B-8E58-2B213727759E" - assert results[-1].DateClosed == "2026-05-11 08:34:41.497901" - assert results[-1].ProfileUUID == "DefaultProfile" - assert results[-1].SafeToLoad - assert results[-1].TabIndex == 0 - assert results[-1].WindowUUID == "78C7D8F7-2956-4135-93F7-58A57AB07031" - assert results[-1].LastVisitTime == "2026-05-07 12:16:08.853022" - assert results[-1].TabUUID == "E8E9CB9D-F8DD-4AD9-8728-0691560A3DD0" + assert not results[2].is_disposable + assert results[2].ancestor_tab_uuids_key == [] + assert results[2].tab_group_type_for_tab_key + assert results[2].tab_group_for_tab == "E3361DD6-9BEB-4FA9-A76E-ACBEFDE36107" + assert results[2].date_closed == datetime(2026, 5, 4, 12, 15, 59, 771103, tzinfo=timezone.utc) + assert results[2].profile_uuid == "DefaultProfile" + assert results[2].safe_to_load + assert results[2].tab_index == 5 + assert results[2].window_uuid == "36576C8E-921A-4E3B-999F-1E1FD0278E5E" + assert results[2].last_visit_time == datetime(2026, 5, 4, 12, 15, 36, 419671, tzinfo=timezone.utc) + assert results[2].tab_uuid == "147E0803-BAE8-429D-A032-A50DE3D559E2" assert ( - results[-1].TabURL - == "https://apple.stackexchange.com/questions/475455/what-happened-to-the-periodic-scripts-on-macos-sequoia" + results[2].tab_url == "https://stackoverflow.com/questions/39872512/what-command-makes-a-new-file-in-terminal" ) - assert results[-1].TabStateVersion == 1 - assert results[-1].TabTitle == "What happened to the periodic scripts on macOS Sequoia? - Ask Different" - assert results[-1].ProcessIdentifier == 626 - assert not results[-1].IsMuted - assert results[-1].plist_path == "ClosedTabOrWindowPersistentStates[11]/PersistentState" - assert results[-1].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" + assert results[2].tab_state_version == 1 + assert results[2].tab_title == "What command makes a new file in terminal? - Stack Overflow" + assert not results[2].is_muted + assert results[2].process_identifier is None + assert results[2].plist_path == "ClosedTabOrWindowPersistentStates[0]/PersistentState" + assert results[2].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" + + assert results[4].selected_tab_index == 0 + assert results[4].window_unified_sidebar_mode == 0 + assert not results[4].tab_bar_hidden + assert results[4].date_closed == datetime(2026, 5, 4, 14, 11, 10, 954509, tzinfo=timezone.utc) + assert results[4].favorites_bar_hidden + assert not results[4].is_popup_window + assert results[4].profile_uuid == "DefaultProfile" + assert results[4].window_restoration_archive_data == "" + assert not results[4].is_private_window + assert not results[4].miniaturized + assert not results[4].prefers_reading_list_sidebar_visible + assert results[4].selected_pinned_tab_index == 9223372036854775807 + assert results[4].unnamed_tab_group_uuids == [] + assert results[4].window_content_rect == "{{87, 79}, {1264, 791}}" + assert results[4].window_state_version == "2.0" + assert results[4].window_uuid == "C177698D-59FA-4A93-BF08-1EB399F01B85" + assert results[4].active_tab_group_uuid == "8B0B9E28-53F6-4C22-A0E7-4EC2DB16ECF4" + assert results[4].plist_path == "ClosedTabOrWindowPersistentStates[1]/PersistentState" + assert results[4].source == "/Users/user/Library/Safari/RecentlyClosedTabs.plist" From 057814b72d52a3337077b407900af925496d8c15 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:24:58 +0200 Subject: [PATCH 46/52] Improve prefix_row docstring --- .../plugins/os/unix/bsd/darwin/macos/helpers/build_records.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index c0c0ccb8fb..c24bfb26e6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -217,6 +217,8 @@ def build_sqlite_records( def prefix_row(row_dict: dict, table: str) -> dict: """Prefix all keys in a row dictionary with the table name. + This prevents duplicate field names when joining multiple tables. + Args: row_dict (dict): Row data as key-value pairs. table (str): Table name to prepend to each key. From 4c99f9bcb6f071f3f57179182b71113960d19b40 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:54:51 +0200 Subject: [PATCH 47/52] Move the periodic TODO comment to periodic.py --- dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py | 4 ++++ tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py index 72bc1b1e5c..52a46c7880 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py @@ -137,6 +137,10 @@ ], ) +# Part of the official FreeBSD periodic.conf documentation. +# Used by scripts in /etc/periodic/security. +# I was not able to find this folder nor these fields on a macOS Ventura system however. +# TODO: Look into this and remove this record descriptor if not applicable to macOS. PeriodicConfSecurityRecord = TargetRecordDescriptor( "macos/periodic_conf/security", [ diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py index dfdf702758..287e5d7162 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py @@ -134,5 +134,3 @@ def test_periodic_conf( assert results[3].monthly_status_security_output is None assert results[3].monthly_local == "/etc/monthly.local" assert results[3].source == "/etc/defaults/periodic.conf" - - # TODO: Add test for PeriodicConfSecurityRecord From 4603625113287a785d64b3d4ec5b3bde0f6bfe2c Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:32:27 +0200 Subject: [PATCH 48/52] Improved some plugins with better test data --- .../darwin/macos/safari/safari_downloads.py | 64 ++++++++--- .../safari_per_site_zoom_preferences.py | 49 ++++++--- .../darwin/macos/safari/safari_preferences.py | 72 ------------- .../os/unix/bsd/darwin/macos/time_machine.py | 100 ++++++++++++++++-- .../darwin/macos/com.apple.TimeMachine.plist | 4 +- .../bsd/darwin/macos/safari/Downloads.plist | 4 +- .../macos/safari/PerSiteZoomPreferences.plist | 4 +- .../macos/safari/com.apple.Safari.plist | 3 - .../macos/safari/test_safari_downloads.py | 38 ++++++- .../test_safari_per_site_zoom_preferences.py | 13 ++- .../macos/safari/test_safari_preferences.py | 44 -------- .../bsd/darwin/macos/test_time_machine.py | 30 +++++- 12 files changed, 257 insertions(+), 168 deletions(-) delete mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py delete mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist delete mode 100644 tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py index c9085d35b9..46aa230ec5 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py @@ -13,10 +13,21 @@ from dissect.target import Target -SafariDownloadsRecord = TargetRecordDescriptor( +SafariDownloadRecord = TargetRecordDescriptor( "macos/safari_downloads", [ - ("string[]", "download_history"), + ("varint", "download_entry_progress_total_to_load"), + ("varint", "download_entry_progress_bytes_so_far"), + ("string", "download_entry_path"), + ("datetime", "download_entry_date_added"), + ("boolean", "download_entry_remove_when_done"), + ("boolean", "download_entry_should_use_request_url_as_origin_url_if_necessary"), + ("string", "download_entry_profile_uuid_string"), + ("datetime", "download_entry_date_finished"), + ("string", "download_entry_url"), + ("string", "download_entry_sandbox_identifier"), + ("bytes", "download_entry_bookmark_blob"), + ("string", "download_entry_identifier"), ("path", "source"), ], ) @@ -48,26 +59,47 @@ def _find_files(self) -> set: files.add(path) return files - @export(record=SafariDownloadsRecord) - def safari_downloads(self) -> Iterator[SafariDownloadsRecord]: + @export(record=SafariDownloadRecord) + def safari_downloads(self) -> Iterator[SafariDownloadRecord]: """Return macOS Safari downloads. - Yields SafariDownloadsRecords with the following fields: + Yields SafariDownloadRecords for each download with the following fields: .. code-block:: text - download_history (string[]): Download history. + download_entry_progress_total_to_load (varint): Total bytes size to download. + download_entry_progress_bytes_so_far (varint): Amount of bytes downloaded so far. + download_entry_path (string): Local file path of the download. + download_entry_date_added (datetime): Timestamp when the download was added. + download_entry_remove_when_done (boolean): Whether the download is removed after completion. + download_entry_should_use_request_url_as_origin_url_if_necessary (boolean): + Whether the request URL should be used as the origin URL if needed. + download_entry_profile_uuid_string (string): Profile UUID associated with the download. + download_entry_date_finished (datetime): Timestamp when the download completed. + download_entry_url (string): Source URL of the download. + download_entry_sandbox_identifier (string): Sandbox identifier for the download. + download_entry_bookmark_blob (bytes): Bookmark data blob for the downloaded file. + download_entry_identifier (string): Unique identifier of the download entry. source (path): Path to the Downloads.plist file. """ for file in self.files: plist = plistlib.load(file.open()) - - yield SafariDownloadsRecord( - download_history=plist.get("DownloadHistory"), - source=file, - _target=self.target, - ) - - -# Download history is empty in current test data -# TODO: Get test file with actual data + for download in plist.get("DownloadHistory"): + yield SafariDownloadRecord( + download_entry_progress_total_to_load=download.get("DownloadEntryProgressTotalToLoad"), + download_entry_progress_bytes_so_far=download.get("DownloadEntryProgressBytesSoFar"), + download_entry_path=download.get("DownloadEntryPath"), + download_entry_date_added=download.get("DownloadEntryDateAddedKey"), + download_entry_remove_when_done=download.get("DownloadEntryRemoveWhenDoneKey"), + download_entry_should_use_request_url_as_origin_url_if_necessary=download.get( + "DownloadEntryShouldUseRequestURLAsOriginURLIfNecessaryKey" + ), + download_entry_profile_uuid_string=download.get("DownloadEntryProfileUUIDStringKey"), + download_entry_date_finished=download.get("DownloadEntryDateFinishedKey"), + download_entry_url=download.get("DownloadEntryURL"), + download_entry_sandbox_identifier=download.get("DownloadEntrySandboxIdentifier"), + download_entry_bookmark_blob=download.get("DownloadEntryBookmarkBlob"), + download_entry_identifier=download.get("DownloadEntryIdentifier"), + source=file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py index 4e21a2b067..9ef688004c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py @@ -16,13 +16,22 @@ SafariPerSiteZoomPreferencesRecord = TargetRecordDescriptor( "macos/safari_per_site_zoom_preferences", [ - ("string", "map_of_hostnames_to_zoom_preferences"), ("string", "map_of_ck_record_names_to_ck_records"), ("varint", "zoom_preference_version"), ("path", "source"), ], ) +HostnameToZoomPreferencesMapRecord = TargetRecordDescriptor( + "macos/safari_per_site_zoom_preferences/hostnames_to_zoom_preferences_map", + [ + ("string", "site"), + ("varint", "page_zoom_factor"), + ("varint", "text_zoom_factor"), + ("path", "source"), + ], +) + class SafariPerSiteZoomPreferencesPlugin(Plugin): """macOS Safari per site zoom preferences (plist) plugin. @@ -47,30 +56,46 @@ def _find_files(self) -> set: files.add(path) return files - @export(record=SafariPerSiteZoomPreferencesRecord) - def safari_per_site_zoom_preferences(self) -> Iterator[SafariPerSiteZoomPreferencesRecord]: + @export(record=(SafariPerSiteZoomPreferencesRecord, HostnameToZoomPreferencesMapRecord)) + def safari_per_site_zoom_preferences(self) -> Iterator[(SafariPerSiteZoomPreferencesRecord, HostnameToZoomPreferencesMapRecord)]: """Return macOS Safari per site zoom preferences. - Yields SafariPerSiteZoomPreferencesRecords with the following fields: + Yields the following record types extracted from the + PerSiteZoomPreferences.plist files: .. code-block:: text - map_of_hostnames_to_zoom_preferences (string): map of hostnames to zoom preferences. - map_of_ck_record_names_to_ck_records (string): map of ck record names to ck records. - zoom_preference_version (varint): Zoom preference version. - source (path): Path to the PerSiteZoomPreferences.plist file. + SafariPerSiteZoomPreferencesRecord: + map_of_ck_record_names_to_ck_records (string): map of ck record names to ck records. + zoom_preference_version (varint): Zoom preference version. + source (path): Path to the PerSiteZoomPreferences.plist file. + + HostnameToZoomPreferencesMapRecord: + site (string): Site the preference applies to. + page_zoom_factor (varint): Page zoom factor. + text_zoom_factor (varint): Text zoom factor. + source (path): Path to the PerSiteZoomPreferences.plist file. """ for file in self.files: plist = plistlib.load(file.open()) yield SafariPerSiteZoomPreferencesRecord( - map_of_hostnames_to_zoom_preferences=plist.get("MapOfHostnamesToZoomPreferences"), map_of_ck_record_names_to_ck_records=plist.get("MapOfCKRecordNamesToCKRecords"), zoom_preference_version=plist.get("ZoomPreferenceVersion"), source=file, _target=self.target, ) - -# MapOfHostnamesToZoomPreferences and MapOfCKRecordNamesToCKRecords fields are empty in current test data -# TODO: Get test file with actual data + map_of_hostnames_to_zoom_preferences = plist.get("MapOfHostnamesToZoomPreferences") + for site, preferences in map_of_hostnames_to_zoom_preferences.items(): + yield HostnameToZoomPreferencesMapRecord( + site=site, + page_zoom_factor=preferences.get("PageZoomFactor"), + text_zoom_factor=preferences.get("TextZoomFactor"), + source=file, + _target=self.target, + ) + +# MapOfCKRecordNamesToCKRecords fields are empty in current test data +# TODO: Look into MapOfCKRecordNamesToCKRecords field and whether +# it should form a separate record like MapOfHostnamesToZoomPreferences diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py deleted file mode 100644 index 06a450d660..0000000000 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_preferences.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -import plistlib -from typing import TYPE_CHECKING - -from dissect.target.exceptions import UnsupportedPluginError -from dissect.target.helpers.record import TargetRecordDescriptor -from dissect.target.plugin import Plugin, export -from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_paths import _build_userdirs - -if TYPE_CHECKING: - from collections.abc import Iterator - - from dissect.target import Target - -SafariPreferencesRecord = TargetRecordDescriptor( - "macos/safari_preferences", - [ - ("varint", "iio_launch_info"), - ("path", "source"), - ], -) - - -class SafariPreferencesPlugin(Plugin): - """macOS Safari property list (plist) plugin. - - Parses Safari's configuration settings and a list of recent searches performed by the user. - - References: - - https://medium.com/@cyberengage.org/p13-analyzing-safari-browser-apple-mail-data-and-recents-database-artifacts-on-macos-9b58848d70ec - """ - - USER_PATH = ("Library/Preferences/com.apple.Safari.plist",) - - def __init__(self, target: Target): - super().__init__(target) - self.files = self._find_files() - - def check_compatible(self) -> None: - if not (self.files): - raise UnsupportedPluginError("No com.apple.Safari.plist files found") - - def _find_files(self) -> set: - files = set() - for _, path in _build_userdirs(self, self.USER_PATH): - files.add(path) - return files - - @export(record=SafariPreferencesRecord) - def safari_preferences(self) -> Iterator[SafariPreferencesRecord]: - """Return macOS Safari preferences. - - Yields SafariPreferencesRecords with the following fields: - - .. code-block:: text - - iio_launch_info (varint): Image I/O launch info. - source (path): Path to the com.apple.Safari.plist file. - """ - for file in self.files: - plist = plistlib.load(file.open()) - - yield SafariPreferencesRecord( - iio_launch_info=plist.get("IIO_LaunchInfo"), - source=file, - _target=self.target, - ) - - -# I was only able to find a IIO_LaunchInfo field in the plist file on a fresh Tahoe system. -# TODO: Check if more fields show up in the plist file depending on user activity. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py index cb40c3e9e3..611bcc7c4b 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -15,7 +15,36 @@ TimeMachineRecord = TargetRecordDescriptor( "macos/time_machine", [ + ("string", "last_destination_id"), + ("varint", "auto_backup_interval"), + ("string[]", "host_uuids"), + ("boolean", "requires_ac_power"), + ("datetime", "suspend_helper_activity_timestamp"), + ("bytes", "backup_alias"), ("varint", "preferences_version"), + ("varint", "auto_backup"), + ("datetime", "last_activity_backup"), + ("path", "source"), + ], +) + +DestinationsRecord = TargetRecordDescriptor( + "macos/time_machine", + [ + ("string[]", "destination_uuids"), + ("string", "last_known_volume_name"), + ("varint", "result"), + ("string", "filesystem_type_name"), + ("string", "last_known_encryption_state"), + ("datetime", "stable_local_snapshot_date"), + ("varint", "inheritance_decision"), + ("string", "destination_id"), + ("varint", "bytes_used"), + ("varint", "destination_version"), + ("varint", "health_check_decision"), + ("varint", "smb_conversion_state"), + ("datetime[]", "attempt_dates"), + ("varint", "bytes_available"), ("path", "source"), ], ) @@ -24,7 +53,7 @@ class TimeMachinePlugin(Plugin): """macOS Time Machine plugin. - Parses Time Machine preferences. Time Machine is macOS's backup system. + Parses Time Machine, macOS's backup system, preferences. """ PATH = "/Library/Preferences/com.apple.TimeMachine.plist" @@ -37,25 +66,78 @@ def check_compatible(self) -> None: if not self.file: raise UnsupportedPluginError("No com.apple.TimeMachine.plist file found") - @export(record=TimeMachineRecord) - def time_machine(self) -> Iterator[TimeMachineRecord]: + @export(record=(TimeMachineRecord, DestinationsRecord)) + def time_machine(self) -> Iterator[(TimeMachineRecord, DestinationsRecord)]: """Return macOS Time Machine preferences. - Yields TimeMachineRecord with the following fields: + Yields the following record types extracted from the + com.apple.TimeMachine.plist file: .. code-block:: text - preferences_version (varint): Version of the Time Machine preferences. - source (path): Path to the com.apple.TimeMachine.plist file. + TimeMachineRecord: + last_destination_id (string): Identifier of the last used backup destination. + auto_backup_interval (varint): Interval between automatic backups. + host_uuids (string[]): List of host UUIDs associated with the backup configuration. + requires_ac_power (boolean): Indicates whether backups require AC power. + suspend_helper_activity_timestamp (datetime): Timestamp when helper activity was suspended. + backup_alias (bytes): Binary backup alias. + preferences_version (varint): Version of the Time Machine preferences. + auto_backup (varint): Flag indicating whether automatic backups are enabled. + last_activity_backup (datetime): Timestamp of the last backup activity. + source (path): Path to the com.apple.TimeMachine.plist file. + + DestinationsRecord: + destination_uuids (string[]): List of destination UUIDs. + last_known_volume_name (string): Name of the backup volume. + result (varint): Result code of the last backup operation. + filesystem_type_name (string): Filesystem type of the destination. + last_known_encryption_state (string): Encryption state of the backup destination. + stable_local_snapshot_date (datetime): Timestamp of the last stable local snapshot. + inheritance_decision (varint): Indicates inheritance decision state. + destination_id (string): Identifier for the destination. + bytes_used (varint): Amount of bytes used on the destination. + destination_version (varint): Destination version. + health_check_decision (varint): Health check decision. + smb_conversion_state (varint): SMB conversion state indicator. + attempt_dates (datetime[]): List of backup attempt timestamps. + bytes_available (varint): Available bytes on the destination. + source (path): Path to the com.apple.TimeMachine.plist file. """ plist = plistlib.load(self.file.open()) yield TimeMachineRecord( + last_destination_id=plist.get("PreferencesLastDestinationIDVersion"), + auto_backup_interval=plist.get("AutoBackupInterval"), + host_uuids=plist.get("HostUUIDs"), + requires_ac_power=plist.get("RequiresACPower"), + suspend_helper_activity_timestamp=plist.get("SuspendHelperActivityTimeStamp"), + backup_alias=plist.get("BackupAlias"), preferences_version=plist.get("PreferencesVersion"), + auto_backup=plist.get("AutoBackup"), + last_activity_backup=plist.get("LastBackupActivity"), source=self.file, _target=self.target, ) - -# I was only able to find a preferences_version field in the plist file on a fresh Tahoe system. -# TODO: Check if more fields show up in the plist file depending on user activity. + destinations = plist.get("Destinations") + if destinations: + for destination in destinations: + yield DestinationsRecord( + destination_uuids=destination.get("DestinationUUIDs"), + last_known_volume_name=destination.get("LastKnownVolumeName"), + result=destination.get("RESULT"), + filesystem_type_name=destination.get("FilesystemTypeName"), + last_known_encryption_state=destination.get("LastKnownEncryptionState"), + stable_local_snapshot_date=destination.get("StableLocalSnapshotDate"), + inheritance_decision=destination.get("InheritanceDecision"), + destination_id=destination.get("DestinationID"), + bytes_used=destination.get("BytesUsed"), + destination_version=destination.get("DestinationVersion"), + health_check_decision=destination.get("HealthCheckDecision"), + smb_conversion_state=destination.get("SMBConversionState"), + attempt_dates=destination.get("AttemptDates"), + bytes_available=destination.get("BytesAvailable"), + source=self.file, + _target=self.target, + ) diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist index 19419bb7a6..11d09bb889 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/com.apple.TimeMachine.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1efbcae1efa883b268a7af209f1353e03292b67f3b119969125a8ee90c0b13e0 -size 69 +oid sha256:248719a7a2e1c4b265818714c6f7a9e4dc5330854be8da60faf8879fa0594973 +size 1198 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist index 059002889f..8d92362e48 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/Downloads.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db3f32f1a36c987dbb6ce0ac06b0333f9659733724dadf403fa891e180183bde -size 65 +oid sha256:f8e4510dca9e037c9aedfbc03c38751ed5ae1873327fe50c938823102074714e +size 2739 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist index c641279e6f..91b2483fa2 100644 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/PerSiteZoomPreferences.plist @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eba7652f7bdfa9467fff90f6a700dd8d7f258a9cad6d3823ca6f208f31ff7e1 -size 148 +oid sha256:bedad05becab5403cffe152e05f878c899b2e1dbb9bea88a9919fda6b221b7b2 +size 249 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist deleted file mode 100644 index fff22d7c58..0000000000 --- a/tests/_data/plugins/os/unix/bsd/darwin/macos/safari/com.apple.Safari.plist +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d2ec8b8f2a4c2c7973ca0a0c3e5eb3fc11dc76661e9347a21291b44cd4d2581 -size 70 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py index 14c1191538..ba2ad4b50a 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING import pytest @@ -38,7 +39,40 @@ def test_safari_downloads(test_file: str, target_unix: Target, fs_unix: VirtualF results = list(target_unix.safari_downloads()) - assert len(results) == 1 + assert len(results) == 2 - assert results[0].download_history == [] + assert results[0].download_entry_progress_total_to_load == 370221 + assert results[0].download_entry_progress_bytes_so_far == 370221 + assert results[0].download_entry_path == "/Users/user/Downloads/another_apple.jpg" + assert results[0].download_entry_date_added == datetime(2026, 6, 11, 7, 47, 5, 68034, tzinfo=timezone.utc) + assert not results[0].download_entry_remove_when_done + assert not results[0].download_entry_should_use_request_url_as_origin_url_if_necessary + assert results[0].download_entry_profile_uuid_string == "DefaultProfile" + assert results[0].download_entry_date_finished == datetime(2026, 6, 11, 7, 47, 15, 349867, tzinfo=timezone.utc) + assert results[0].download_entry_url == ( + "https://images.unsplash.com/photo-1568702846914-96b305d2aaeb" + "?ixlib=rb-4.1.0&q=85&fm=jpg&crop=entropy&cs=srgb" + "&dl=an_vision-gDPaDDy6_WE-unsplash.jpg" + ) + assert results[0].download_entry_sandbox_identifier == "1109426B-5B25-49B3-9FE7-C27B77F36576" + assert results[0].download_entry_bookmark_blob is not None + assert isinstance(results[0].download_entry_bookmark_blob, (bytes, bytearray)) + assert results[0].download_entry_identifier == "AD6033B8-86AA-4CAC-9398-70C180C0FA30" assert results[0].source == "/Users/user/Library/Safari/Downloads.plist" + + assert results[1].download_entry_progress_total_to_load == 1173811 + assert results[1].download_entry_progress_bytes_so_far == 1173811 + assert results[1].download_entry_path == "/Users/user/Documents/apple.jpg" + assert results[1].download_entry_date_added == datetime(2026, 6, 11, 7, 46, 42, 238320, tzinfo=timezone.utc) + assert not results[1].download_entry_remove_when_done + assert not results[1].download_entry_should_use_request_url_as_origin_url_if_necessary + assert results[1].download_entry_profile_uuid_string == "DefaultProfile" + assert results[1].download_entry_date_finished == datetime(2026, 6, 11, 7, 46, 50, 126514, tzinfo=timezone.utc) + assert results[1].download_entry_url == ( + "https://images.unsplash.com/photo-1630563451961-ac2ff27616ab?ixlib=rb-4.1.0&q=85&fm=jpg&crop=entropy&cs=srgb&dl=tobi-zLCR7RsxYGs-unsplash.jpg" + ) + assert results[1].download_entry_sandbox_identifier == "95FB1F71-C489-4A07-AFB9-509F4B2BC105" + assert results[1].download_entry_bookmark_blob is not None + assert isinstance(results[1].download_entry_bookmark_blob, (bytes, bytearray)) + assert results[1].download_entry_identifier == "BD390ED6-0E4C-4B25-ACB1-E50F9E2B2347" + assert results[1].source == "/Users/user/Library/Safari/Downloads.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py index 3e50cb0844..56545cfa22 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py @@ -40,9 +40,18 @@ def test_safari_per_site_zoom_preferences(test_file: str, target_unix: Target, f results = list(target_unix.safari_per_site_zoom_preferences()) - assert len(results) == 1 + assert len(results) == 3 - assert results[0].map_of_hostnames_to_zoom_preferences == "{}" assert results[0].map_of_ck_record_names_to_ck_records == "{}" assert results[0].zoom_preference_version == 1 assert results[0].source == "/Users/user/Library/Safari/PerSiteZoomPreferences.plist" + + assert results[1].site == "google.com" + assert results[1].page_zoom_factor == 0 + assert results[1].text_zoom_factor == 1 + assert results[1].source == "/Users/user/Library/Safari/PerSiteZoomPreferences.plist" + + assert results[2].site == "apple.com" + assert results[2].page_zoom_factor == 1 + assert results[2].text_zoom_factor == 1 + assert results[2].source == "/Users/user/Library/Safari/PerSiteZoomPreferences.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py deleted file mode 100644 index c7516f8b20..0000000000 --- a/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_preferences.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest - -from dissect.target.helpers.record import UnixUserRecord -from dissect.target.plugins.os.unix.bsd.darwin.macos.safari.safari_preferences import SafariPreferencesPlugin -from tests._utils import absolute_path - -if TYPE_CHECKING: - from dissect.target.filesystem import VirtualFilesystem - from dissect.target.target import Target - - -@pytest.mark.parametrize( - "test_file", - [ - "com.apple.Safari.plist", - ], -) -def test_safari_preferences(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: - user = UnixUserRecord( - name="user", - uid=501, - gid=20, - home="/Users/user", - shell="/bin/zsh", - ) - target_unix.users = lambda: [ - user, - ] - - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/safari/{test_file}") - fs_unix.map_file(f"Users/user/Library/Preferences/{test_file}", data_file) - - target_unix.add_plugin(SafariPreferencesPlugin) - - results = list(target_unix.safari_preferences()) - - assert len(results) == 1 - - assert results[0].iio_launch_info == -2 - assert results[0].source == "/Users/user/Library/Preferences/com.apple.Safari.plist" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py index 6ccb8700e0..ba3d6ff551 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime, timezone from typing import TYPE_CHECKING import pytest @@ -25,7 +26,32 @@ def test_aiport_preferences(test_file: str, target_unix: Target, fs_unix: Virtua target_unix.add_plugin(TimeMachinePlugin) results = list(target_unix.time_machine()) - assert len(results) == 1 - + assert len(results) == 2 + + assert results[0].last_destination_id is None + assert results[0].auto_backup_interval == 3600 + assert results[0].host_uuids == ["543ECB5B-2A5B-5E41-8FE8-D1A0E9FF5F7E"] + assert not results[0].requires_ac_power + assert results[0].suspend_helper_activity_timestamp == datetime(1995, 6, 11, 7, 40, 38, tzinfo=timezone.utc) + assert results[0].backup_alias is not None + assert isinstance(results[0].backup_alias, (bytes, bytearray)) assert results[0].preferences_version == 6 + assert results[0].auto_backup == 1 + assert results[0].last_activity_backup == datetime(2026, 5, 11, 13, 7, 9, tzinfo=timezone.utc) assert results[0].source == "/Library/Preferences/com.apple.TimeMachine.plist" + + assert results[1].destination_uuids == ["6041C936-15F2-436F-9320-0BA84E6F27BC"] + assert results[1].last_known_volume_name == "Backups of Testbook1\u2019s MacBook Pro" + assert results[1].result == 0 + assert results[1].filesystem_type_name == "apfs" + assert results[1].last_known_encryption_state == "Encrypted" + assert results[1].stable_local_snapshot_date == datetime(2026, 6, 11, 7, 41, 52, tzinfo=timezone.utc) + assert results[1].inheritance_decision == 0 + assert results[1].destination_id == "D660D3DA-6567-49A9-A133-02E95682CFFE" + assert results[1].bytes_used == 385024 + assert results[1].destination_version == 23 + assert results[1].health_check_decision == 0 + assert results[1].smb_conversion_state == 0 + assert results[1].attempt_dates == [datetime(2026, 6, 11, 7, 41, 45, 410654, tzinfo=timezone.utc)] + assert results[1].bytes_available == 924195943880 + assert results[1].source == "/Library/Preferences/com.apple.TimeMachine.plist" From f059838c92715bc7808475dee28a3c9f83a5502b Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:39:16 +0200 Subject: [PATCH 49/52] Add traceability to plist records yielded from SQLite rows --- .../os/unix/bsd/darwin/macos/call_history.py | 2 ++ .../darwin/macos/duet_activity_scheduler.py | 2 ++ .../bsd/darwin/macos/duet_interaction_c.py | 2 ++ .../unix/bsd/darwin/macos/duet_knowledge_c.py | 2 ++ .../bsd/darwin/macos/helpers/build_records.py | 22 +++++++++++++++++++ .../plugins/os/unix/bsd/darwin/macos/notes.py | 2 ++ .../safari_per_site_zoom_preferences.py | 5 ++++- .../bsd/darwin/macos/text_replacements.py | 2 ++ .../os/unix/bsd/darwin/macos/time_machine.py | 2 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 2 ++ .../bsd/darwin/macos/test_call_history.py | 3 ++- .../macos/test_duet_activity_scheduler.py | 3 ++- .../darwin/macos/test_duet_interaction_c.py | 3 ++- .../bsd/darwin/macos/test_duet_knowledge_c.py | 16 ++------------ .../os/unix/bsd/darwin/macos/test_notes.py | 3 ++- .../darwin/macos/test_text_replacements.py | 3 ++- .../bsd/darwin/macos/test_user_accounts.py | 3 ++- 17 files changed, 55 insertions(+), 22 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py index b471d4f629..c49e21ed41 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py @@ -65,6 +65,7 @@ ("varint", "ns_store_model_version_hashes_version"), ("string", "ns_store_model_version_identifiers"), ("string", "ns_store_type"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -203,6 +204,7 @@ def call_history( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the CallHistory.storedata database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index c7c091d7ae..4fb0de4f9c 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -58,6 +58,7 @@ ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), ("varint", "ns_store_model_version_hashes_version"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -184,6 +185,7 @@ def duet_activity_scheduler( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the DuetActivitySchedulerClassC.db file. ActivityRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py index 34334a28ed..0fb67873ea 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -45,6 +45,7 @@ ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), ("varint", "ns_store_model_version_hashes_version"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -202,6 +203,7 @@ def duet_interaction_c( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the interactionC.db file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index 3cc29f0590..3e4f41647f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -152,6 +152,7 @@ ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), ("varint", "ns_store_model_version_hashes_version"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -442,6 +443,7 @@ def duet_knowledge_c( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the knowledgeC.db database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index c24bfb26e6..eb2d149f36 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -108,10 +108,21 @@ def build_sqlite_records( ) if isinstance(plist_data, dict): + pk = table.primary_key + if pk: + # Add the source row to the plist_path + # format -> "/=" + pk_value = row_dict[pk] + path = "/".join([table.name, f"{pk}={pk_value}"]) + else: + # Add the source row to the plist_path + # format -> "
" + path = table.name yield from emit_dict_records( plugin, plist_data, file, + path=path, record_descriptors=record_descriptors, field_mappings=field_mappings, convert_timestamps=convert_timestamps, @@ -273,6 +284,17 @@ def handle_iterate_join( if parent_dict.get(ij["key1"]) == child_dict.get(ij["key2"]): child_dict.pop(ij["key2"], None) + # Remove fields from child_dict that will be yielded as separate plists + for k, v in child_dict.items(): + if isinstance(v, (bytes, bytearray)) and v.startswith(b"bplist00") and not is_nskeyedarchive_blob(v): + try: + plist_data = load_plist_data(v) if b"$archiver" in v[:128] else plistlib.loads(v) + + if isinstance(plist_data, dict): + child_dict.pop(k) + except Exception: + child_dict.pop(k) + downstream_iterate_rows = defaultdict(list) for j in joins_by_table1[current_join["table2"]]: downstream_ignore = ignore_joins_map[(j["table1"], j["table2"])] diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py index d9066799df..e3037e14e0 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -111,6 +111,7 @@ ("varint", "ns_store_model_version_hashes_version"), ("string", "ns_store_model_version_identifiers"), ("string", "ns_store_type"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -434,6 +435,7 @@ def notes( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py index 9ef688004c..0561b0a6af 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py @@ -57,7 +57,9 @@ def _find_files(self) -> set: return files @export(record=(SafariPerSiteZoomPreferencesRecord, HostnameToZoomPreferencesMapRecord)) - def safari_per_site_zoom_preferences(self) -> Iterator[(SafariPerSiteZoomPreferencesRecord, HostnameToZoomPreferencesMapRecord)]: + def safari_per_site_zoom_preferences( + self, + ) -> Iterator[(SafariPerSiteZoomPreferencesRecord, HostnameToZoomPreferencesMapRecord)]: """Return macOS Safari per site zoom preferences. Yields the following record types extracted from the @@ -96,6 +98,7 @@ def safari_per_site_zoom_preferences(self) -> Iterator[(SafariPerSiteZoomPrefere _target=self.target, ) + # MapOfCKRecordNamesToCKRecords fields are empty in current test data # TODO: Look into MapOfCKRecordNamesToCKRecords field and whether # it should form a separate record like MapOfHostnamesToZoomPreferences diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index ed5b3e4730..ed269548d7 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -78,6 +78,7 @@ ("string", "ns_store_model_version_checksum_key"), ("varint", "ns_persistence_framework_version"), ("varint", "ns_store_model_version_hashes_version"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -230,6 +231,7 @@ def text_replacements( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the TextReplacements.db file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py index 611bcc7c4b..b1ef3624ac 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -29,7 +29,7 @@ ) DestinationsRecord = TargetRecordDescriptor( - "macos/time_machine", + "macos/time_machine/destination", [ ("string[]", "destination_uuids"), ("string", "last_known_volume_name"), diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 88402b99b6..753249a674 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -165,6 +165,7 @@ ("varint", "ns_store_model_version_hashes_version"), ("string", "ns_store_model_version_identifiers"), ("string", "ns_store_type"), + ("string", "plist_path"), ("path", "source"), ], ) @@ -419,6 +420,7 @@ def user_accounts( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. + plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the Accounts*.sqlite database file. NSStoreModelVersionHashesRecord: diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py index 6362b4aebb..c384aa73e2 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py @@ -73,9 +73,10 @@ def test_call_history(test_files: list[str], target_unix: Target, fs_unix: Virtu assert results[5].ns_store_model_version_hashes_version == 3 assert results[5].ns_store_model_version_identifiers == "['43']" assert results[5].ns_store_type == "SQLite" + assert results[5].plist_path == "Z_METADATA/Z_VERSION=1" assert results[5].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" - assert results[6].plist_path == "NSStoreModelVersionHashes" + assert results[6].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[6].call_db_properties is not None assert isinstance(results[6].call_db_properties, (bytes, bytearray)) diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index 650ea885c3..72cb3fb5d5 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -58,6 +58,7 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[51].ns_store_model_version_checksum_key == "rXdwmenydb+cl65S3tSy9rIL6lkwSXqL7UvaJVK21Lc=" assert results[51].ns_persistence_framework_version == 1526 assert results[51].ns_store_model_version_hashes_version == 3 + assert results[51].plist_path == "Z_METADATA/Z_VERSION=1" assert results[51].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" assert results[52].activity is not None @@ -66,7 +67,7 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert isinstance(results[52].group_binary, (bytes, bytearray)) assert results[52].trigger is not None assert isinstance(results[52].trigger, (bytes, bytearray)) - assert results[52].plist_path == "NSStoreModelVersionHashes" + assert results[52].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[52].source == "/var/db/DuetActivityScheduler/DuetActivitySchedulerClassC.db" assert results[53].table == "Z_METADATA" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py index 4338332552..e5a3585ca7 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py @@ -68,6 +68,7 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[8].ns_store_model_version_checksum_key == "yBhxwKvskbIdxbJOzzLgxhbLYTjrWz9otOnAd9BgKA0=" assert results[8].ns_persistence_framework_version == 1526 assert results[8].ns_store_model_version_hashes_version == 3 + assert results[8].plist_path == "Z_METADATA/Z_VERSION=1" assert results[8].source == "/var/db/CoreDuet/People/interactionC.db" assert ( @@ -94,7 +95,7 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ results[9].version_hash == b"\x94\x07\xac\x82%\x9f22\x9c\x162\xe9\xc5\xdb7\xb9\x1e\xf8\x8c(\x8e\xb1\xd7JT\xfd*\xe0\xad\x7f5\x01" ) - assert results[9].plist_path == "NSStoreModelVersionHashes" + assert results[9].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[9].source == "/var/db/CoreDuet/People/interactionC.db" assert results[10].table == "Z_METADATA" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py index 00f0d6c9b9..be350948b4 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py @@ -139,19 +139,7 @@ def test_duet_knowledge_c( assert results[875].ns_store_model_version_checksum_key == "1nRfv9qJjn86Sz4iC1FpuA6z3NAw1YNPMMABL4ISiEA=" assert results[875].ns_persistence_framework_version == 1526 assert results[875].ns_store_model_version_hashes_version == 3 - assert results[875].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" - - assert results[875].ns_persistence_maximum_framework_version == 1526 - assert results[875].ns_store_model_version_identifiers == ["34"] - assert results[875].ns_store_type == "SQLite" - assert results[875].ns_auto_vacuum_level == 2 - assert ( - results[875].ns_store_model_version_hashes_digest - == "3qw03JLBtryClpLQRtPhA43k8KZaw9qHvu+RVzuPBYSEm8++KjboFwDAjFByeXrvryJrBSPo/o4LL6G9pmuo8Q==" - ) - assert results[875].ns_store_model_version_checksum_key == "1nRfv9qJjn86Sz4iC1FpuA6z3NAw1YNPMMABL4ISiEA=" - assert results[875].ns_persistence_framework_version == 1526 - assert results[875].ns_store_model_version_hashes_version == 3 + assert results[875].plist_path == "Z_METADATA/Z_VERSION=1" assert results[875].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" assert ( @@ -218,7 +206,7 @@ def test_duet_knowledge_c( results[876].sync_peer == b"\xb4q\xdc\xac\x9f\x14\x0c\xc6X\x00\x18i\xce\x80\x11\xfc\x8b\x1c\x85\x95\xf44\x17\x8a\xe1MC\xc3\x05\x809l" ) - assert results[876].plist_path == "NSStoreModelVersionHashes" + assert results[876].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[876].source == "/Users/user/Library/Application Support/Knowledge/knowledgeC.db" assert results[877].table == "Z_METADATA" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py index 74aa866664..03b60d685b 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py @@ -115,9 +115,9 @@ def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFiles assert results[29].ns_store_model_version_hashes_version == 3 assert results[29].ns_store_model_version_identifiers == "['']" assert results[29].ns_store_type == "SQLite" + assert results[29].plist_path == "Z_METADATA/Z_VERSION=1" assert results[29].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" - assert results[30].plist_path == "NSStoreModelVersionHashes" assert results[30].account_hash is not None assert isinstance(results[30].account_hash, (bytes, bytearray)) assert results[30].attachment_hash is not None @@ -162,6 +162,7 @@ def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFiles assert isinstance(results[30].update_folder_action, (bytes, bytearray)) assert results[30].update_note_action is not None assert isinstance(results[30].update_note_action, (bytes, bytearray)) + assert results[30].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[30].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" assert results[31].table == "Z_METADATA" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py index 9f9b9b5e76..8a7088bb28 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -89,6 +89,7 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: "nb/qJ+hB9auf83oGYKFndhE+Etk/7JNbNosAbVi5Zu0biqTNK/UfC4ofTqRhou6nHBT1ci00ct9E+U9vnbhfPw==" ) assert results[4].ns_store_model_version_checksum_key == ("c4ljuOCxf+SvLXrpw0Xcnwe7kkFsMpkUuv43N2r16m4=") + assert results[4].plist_path == "Z_METADATA/Z_VERSION=1" assert results[4].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" assert results[5].tr_cloud_kit_sync_state == ( @@ -97,7 +98,7 @@ def test_text_replacements(test_files: list[str], target_unix: Target, fs_unix: assert results[5].text_replacement_entry == ( b"#C\t\xd2l\xa7\xeaK##S\xae\xb7>\x82\xb5\xb5\xc8\xafB\xf4\t\xc4]\xa2)\xb0}+\xd9\xab\x03" ) - assert results[5].plist_path == "NSStoreModelVersionHashes" + assert results[5].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[5].source == "/Users/user/Library/KeyboardServices/TextReplacements.db" assert results[6].table == "Z_METADATA" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py index e3373ddd0d..6bd382ee6e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -137,6 +137,7 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert results[286].ns_store_model_version_hashes_version == 3 assert results[286].ns_store_model_version_identifiers == "['30']" assert results[286].ns_store_type == "SQLite" + assert results[286].plist_path == "Z_METADATA/Z_VERSION=1" assert results[286].source == "/Users/user/Library/Accounts/Accounts4.sqlite" assert results[287].access_options_key is not None @@ -153,7 +154,7 @@ def test_user_accounts(test_files: list[str], target_unix: Target, fs_unix: Virt assert isinstance(results[287].credential_item, (bytes, bytearray)) assert results[287].dataclass is not None assert isinstance(results[287].dataclass, (bytes, bytearray)) - assert results[287].plist_path == "NSStoreModelVersionHashes" + assert results[287].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" assert results[287].source == "/Users/user/Library/Accounts/Accounts4.sqlite" assert results[288].table == "Z_METADATA" From b033eb11c29d7c2628edd9e95768c36151fba849 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:59:26 +0200 Subject: [PATCH 50/52] Add value mapping --- .../os/unix/bsd/darwin/macos/call_history.py | 3 +- .../darwin/macos/duet_activity_scheduler.py | 3 +- .../bsd/darwin/macos/duet_interaction_c.py | 3 +- .../unix/bsd/darwin/macos/duet_knowledge_c.py | 3 +- .../bsd/darwin/macos/helpers/build_records.py | 108 +++++++++++++++++- .../os/unix/bsd/darwin/macos/login_items.py | 44 ++++--- .../os/unix/bsd/darwin/macos/logs/asl.py | 29 +++-- .../plugins/os/unix/bsd/darwin/macos/notes.py | 3 +- .../plugins/os/unix/bsd/darwin/macos/tcc.py | 50 ++++---- .../bsd/darwin/macos/text_replacements.py | 3 +- .../os/unix/bsd/darwin/macos/user_accounts.py | 3 +- .../os/unix/bsd/darwin/macos/logs/test_asl.py | 6 +- .../unix/bsd/darwin/macos/test_login_items.py | 4 +- .../os/unix/bsd/darwin/macos/test_tcc.py | 4 +- 14 files changed, 195 insertions(+), 71 deletions(-) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py index c49e21ed41..dc9243064d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py @@ -204,7 +204,8 @@ def call_history( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the CallHistory.storedata database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index 4fb0de4f9c..b54c5ee3b4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -185,7 +185,8 @@ def duet_activity_scheduler( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the DuetActivitySchedulerClassC.db file. ActivityRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py index 0fb67873ea..2d087a1812 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -203,7 +203,8 @@ def duet_interaction_c( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the interactionC.db file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py index 3e4f41647f..b714687ee4 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -443,7 +443,8 @@ def duet_knowledge_c( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the knowledgeC.db database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index eb2d149f36..a7f1d1e142 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -31,6 +31,7 @@ def build_sqlite_records( record_descriptors: tuple | None = None, joins: tuple = (), field_mappings: dict | None = None, + value_mappings: dict | None = None, convert_timestamps: dict | None = None, ) -> Iterator[TargetRecordDescriptor]: """Extract and normalize records from SQLite databases. @@ -62,6 +63,7 @@ def build_sqlite_records( nested = Attach matching table2 rows as a nested dict in table1 records. ignore = Used to omit duplicate or redundant fields during joins. field_mappings (dict | None): Optional field name mappings. + value_mappings (dict| None): Optional value mappings. convert_timestamps (dict | None): Optional timestamp conversion rules. Yields: @@ -125,6 +127,7 @@ def build_sqlite_records( path=path, record_descriptors=record_descriptors, field_mappings=field_mappings, + value_mappings=value_mappings, convert_timestamps=convert_timestamps, ) row_dict.pop(key) @@ -148,7 +151,13 @@ def build_sqlite_records( if row_dict.get(j2["key2"]) is None: row_dict["table"] = table.name yield build_record( - plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + plugin, + row_dict, + file, + record_descriptors, + field_mappings, + value_mappings, + convert_timestamps, ) break else: @@ -167,6 +176,7 @@ def build_sqlite_records( file, record_descriptors, field_mappings, + value_mappings, convert_timestamps, ) break @@ -213,13 +223,25 @@ def build_sqlite_records( else: # Yield row as record without iterate joins yield build_record( - plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + plugin, + row_dict, + file, + record_descriptors, + field_mappings, + value_mappings, + convert_timestamps, ) # Yield row as record without joins else: row_dict["table"] = table.name yield build_record( - plugin, row_dict, file, record_descriptors, field_mappings, convert_timestamps + plugin, + row_dict, + file, + record_descriptors, + field_mappings, + value_mappings, + convert_timestamps, ) except Exception: plugin.target.log.exception("Failed to process SQLite file: %s", file) @@ -390,6 +412,7 @@ def build_plist_records( record_descriptors: tuple | None = None, collapse_paths: set[tuple[str, bool]] | None = None, field_mappings: dict | None = None, + value_mappings: dict | None = None, convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: @@ -408,6 +431,7 @@ def build_plist_records( record_descriptors (tuple | None): Optional descriptors for record construction. collapse_paths (set[tuple[str, bool]] | None): Plist paths to collapse during recursive traversal. field_mappings (dict | None): Optional field name mappings. + value_mappings (dict| None): Optional value mappings. convert_timestamps (dict | None): Optional timestamp conversion rules. function_name (str | None): Optional name used for dynamic record creation. @@ -441,6 +465,7 @@ def build_plist_records( record_descriptors=record_descriptors, collapse_paths=collapse_paths, field_mappings=field_mappings, + value_mappings=value_mappings, convert_timestamps=convert_timestamps, function_name=function_name, ) @@ -560,13 +585,14 @@ def build_record( source: Path, record_descriptors: tuple, field_mappings: dict | None = None, + value_mappings: dict | None = None, convert_timestamps: dict | None = None, ) -> Record: """Construct a record from a dictionary using a matching record descriptor. Applies provided field mappings, selects an appropriate descriptor based on - the fields in the dictionary, filters unsupported fields, and performs provided - timestamp conversions before instantiating the record. + the fields in the dictionary, filters unsupported fields, performs provided + timestamp and value conversions before instantiating the record. If no matching descriptor is found, logs an error and returns None. @@ -576,6 +602,7 @@ def build_record( source (Path): Source path associated with the record. record_descriptors (tuple): Available record descriptors. field_mappings (dict | None): Optional field name mappings. + value_mappings (dict| None): Optional value mappings. convert_timestamps (dict | None): Optional timestamp conversion rules. Returns: @@ -610,11 +637,74 @@ def build_record( key = format_key(k) if convert_timestamps and key in convert_timestamps: v = convert_timestamp(v, convert_timestamps[key]) + + if value_mappings and key in value_mappings: + v = convert_value(plugin, key, v, value_mappings, source) + record_values[key] = v return desc(**record_values) +def convert_value( + plugin: Plugin, + key: str, + value: Any, + value_mappings: dict, + source: str, +) -> Any: + """Convert a value using configured value mappings. + + Applies direct value mappings where available, and decodes bitmask-style + combined values for integer inputs. If no mapping is found, the original + value is returned and a warning is logged. + + Args: + plugin (Plugin): Plugin instance providing logging and target access. + key (str): Field name associated with the value. + value (Any): Input value to convert. + value_mappings (dict): Mapping of field names to value maps. + source (Path): Source path associated with the record. + + Returns: + Any: Converted value if mapping is found, otherwise the original value. + """ + if not value_mappings or key not in value_mappings: + return value + + value_mapping = value_mappings[key] + + # Try direct mapping + new_value = value_mapping.get(value) + if new_value is not None: + return new_value + + # Try bitmask decoding + if isinstance(value, int) and is_bitmask_mapping(value_mapping): + if value == 0: + return None + + matched = [mapped_value for raw_value, mapped_value in value_mapping.items() if value & raw_value] + + if matched: + return matched + + # No mapping found → log + fallback + plugin.target.log.warning( + "No value mapping defined for field '%s' with value '%s' in %s", + key, + value, + source, + ) + + return value + + +def is_bitmask_mapping(mapping: dict) -> bool: + """Check if all non-zero keys are powers of two (bitmask flags).""" + return all(isinstance(k, int) and (k & (k - 1) == 0) for k in mapping if k != 0) + + def convert_timestamp(value: Any, mode: str) -> datetime | Any: """Convert a value to a datetime based on the specified timestamp format. @@ -703,6 +793,7 @@ def emit_dict_records( record_descriptors: tuple | None = None, collapse_paths: set[tuple[str, bool]] | None = None, field_mappings: dict | None = None, + value_mappings: dict | None = None, convert_timestamps: dict | None = None, function_name: str | None = None, ) -> Iterator[Record]: @@ -731,6 +822,7 @@ def emit_dict_records( record_descriptors (tuple | None): Optional descriptors for record construction. collapse_paths (set[tuple[str, bool]] | None): Paths to collapse instead of recurse. field_mappings (dict | None): Optional field name mappings. + value_mappings (dict| None): Optional value mappings. convert_timestamps (dict | None): Optional timestamp conversion rules. function_name (str | None): Optional name for dynamic record creation. @@ -789,6 +881,7 @@ def emit_dict_records( source, record_descriptors=record_descriptors, field_mappings=field_mappings, + value_mappings=value_mappings, convert_timestamps=convert_timestamps, ) else: @@ -819,7 +912,9 @@ def emit_dict_records( if record_descriptors is None: yield dynamic_build_record(plugin, function_name, record_data, source) else: - yield build_record(plugin, record_data, source, record_descriptors, field_mappings, convert_timestamps) + yield build_record( + plugin, record_data, source, record_descriptors, field_mappings, value_mappings, convert_timestamps + ) for k, child in child_dicts.items(): child_path = f"{path}/{k}" if path else k @@ -832,6 +927,7 @@ def emit_dict_records( record_descriptors=record_descriptors, collapse_paths=collapse_paths, field_mappings=field_mappings, + value_mappings=value_mappings, convert_timestamps=convert_timestamps, function_name=function_name, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py index 475e9539ac..448ac87260 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py @@ -22,7 +22,7 @@ ("string", "container"), ("string", "designated_requirement"), ("string", "developer_name"), - ("varint", "login_item_disposition"), + ("string", "login_item_disposition"), ("datetime", "executable_modification_date"), ("path", "executable_path"), ("varint", "flags"), @@ -34,7 +34,7 @@ ("string", "program_arguments"), ("string", "sha256"), ("string", "team_identifier"), - ("varint", "login_item_type"), + ("string", "login_item_type"), ("string", "url"), ("string", "uuid"), ("string[]", "items"), @@ -75,6 +75,27 @@ "serviceManagementLoginItemsMigrated": "service_management_login_items_migrated", } +VALUE_MAPPINGS = { + "login_item_disposition": { + 1: "Enabled", + 2: "Allowed", + 4: "Hidden", + 8: "Notified", + }, + "login_item_type": { + 1: "user item", + 2: "app", + 4: "login item", + 8: "agent", + 16: "daemon", + 32: "developer", + 64: "spotlight", + 2048: "quicklook", + 65536: "legacy", + 524288: "curated", + }, +} + CONVERT_TIMESTAMPS = { "modification_date": "2001", } @@ -134,11 +155,7 @@ def login_items(self) -> Iterator[LoginItemsRecord]: container (string): Containing app or bundle. designated_requirement (string): Code signing designated requirement. developer_name (string): Developer name. - login_item_disposition (varint): Numeric value describing state: - 1 = Enabled. - 2 = Allowed. - 4 = Hidden. - 8 = Notified. + login_item_disposition (string): Numeric value describing state. executable_modification_date (datetime): Last modification time of the executable. executable_path (path): Path to the executable. flags (varint): Additional flags associated with the item. @@ -150,17 +167,7 @@ def login_items(self) -> Iterator[LoginItemsRecord]: program_arguments (string): Program arguments for execution. sha256 (string): SHA256 hash of the executable. team_identifier (string): Apple developer team identifier. - login_item_type (varint): Numeric value describing the item type: - 1 = user item. - 2 = app. - 4 = login item. - 8 = agent. - 16 = daemon. - 32 = developer. - 64 = spotlight. - 2048 = quicklook. - 65536 = legacy. - 524288 = curated. + login_item_type (string): Numeric value describing the item type. url (string): URL associated with the item. uuid (string): Universally unique identifier. items (string[]): List of items. @@ -180,5 +187,6 @@ def login_items(self) -> Iterator[LoginItemsRecord]: self.login_items_files, LoginItemsRecords, field_mappings=FIELD_MAPPINGS, + value_mappings=VALUE_MAPPINGS, convert_timestamps=CONVERT_TIMESTAMPS, ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py index 1facc6c48f..2a2387f5a3 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py @@ -57,7 +57,7 @@ "macos/logs/asl", [ ("datetime", "ts"), - ("varint", "priority_level"), + ("string", "priority_level"), ("varint", "pid"), ("string", "asl_host"), ("string", "sender"), @@ -187,6 +187,18 @@ def _parse_asl_file(data: bytes) -> Iterator[dict[str, Any]]: pos += rec_len + 2 +PRIORITY_LEVEL_MAP = { + 0: "Emergency", + 1: "Alert", + 2: "Critical", + 3: "Error", + 4: "Warning", + 5: "Notice", + 6: "Informational", + 7: "Debug", +} + + class ASLPlugin(Plugin): """Plugin to parse macOS Apple System Log (ASL) databases. @@ -232,15 +244,7 @@ def asl(self) -> Iterator[ASLRecord]: .. code-block:: text ts (datetime): Timestamp (UTC). - priority_level (varint): ASL priority level: - 0 = Emergency. - 1 = Alert. - 2 = Critical. - 3 = Error. - 4 = Warning. - 5 = Notice. - 6 = Informational. - 7 = Debug. + priority_level (string): ASL priority level. pid (varint): Process ID. asl_host (string): Hostname as stored in the ASL record. sender (string): Sender process name. @@ -258,9 +262,12 @@ def asl(self) -> Iterator[ASLRecord]: continue for rec in records: + level = rec["level"] + priority_level = PRIORITY_LEVEL_MAP.get(level, level) + yield ASLRecord( ts=rec["ts"], - priority_level=rec["level"], + priority_level=priority_level, pid=rec["pid"], asl_host=rec["host"], sender=rec["sender"], diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py index e3037e14e0..6b4769809a 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -435,7 +435,8 @@ def notes( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the database file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py index f07db84cad..f3234f2d9d 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py @@ -20,8 +20,8 @@ ("string", "service"), ("string", "client"), ("varint", "client_type"), - ("varint", "auth_value"), - ("varint", "auth_reason"), + ("string", "auth_value"), + ("string", "auth_reason"), ("varint", "auth_version"), ("bytes", "csreq"), ("string", "policy_id"), @@ -53,6 +53,29 @@ KeyValueRecord, ) +VALUE_MAPPINGS = { + "auth_value": { + 0: "Denied", + 1: "Unknown", + 2: "Allowed", + 3: "Limited", + }, + "auth_reason": { + 1: "Error", + 2: "User Consent", + 3: "User Set", + 4: "System Set", + 5: "Service Policy", + 6: "MDM Policy", + 7: "Override Policy", + 8: "Missing usage string", + 9: "Prompt Timeout", + 10: "Preflight Unknown", + 11: "Entitled", + 12: "App Type Policy.", + }, +} + class TCCPlugin(Plugin): """macOS transparency, consent, control (tcc) framework plugin. @@ -97,25 +120,8 @@ def tcc( service (string): What service access is being restricted to. client (string): Bundle Identifier or absolute path to the program that wants to use the service. client_type (varint): Whether client is a Bundle Identifier(0) or an absolute path(1) - auth_value (varint): Authorization decision: - 0 = denied. - 1 = unknown. - 2 = allowed - 3 = limited. - Observed auth_value = 5 in macOS Tahoe, but unsure about it's meaning. - auth_reason (varint): A code indicating how this auth_value was set: - 1 = Error. - 2 = User Consent. - 3 = User Set. - 4 = System Set. - 5 = Service Policy. - 6 = MDM Policy. - 7 = Override Policy. - 8 = Missing usage string. - 9 = Prompt Timeout. - 10 = Preflight Unknown. - 11 = Entitled. - 12 = App Type Policy. + auth_value (string): Authorization value. + auth_reason (string): Indicates how this auth_value was set. auth_version (varint): Always 1 as of macOS Tahoe. csreq (bytes): Binary code signing requirement blob that the client must satisfy in order for access to be granted. @@ -141,6 +147,6 @@ def tcc( value (string): Value associated with the key. source (path): Path to the TCC.db database file. """ - yield from build_sqlite_records(self, self.files, TCCRecords) + yield from build_sqlite_records(self, self.files, TCCRecords, value_mappings=VALUE_MAPPINGS) # TODO: Add policies, active_policy, access_overrides, expired tables diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py index ed269548d7..1b4e7e0794 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -231,7 +231,8 @@ def text_replacements( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the TextReplacements.db file. NSStoreModelVersionHashesRecord: diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index 753249a674..a30777c9f0 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -420,7 +420,8 @@ def user_accounts( ns_store_model_version_checksum_key (string): Model version checksum key. ns_persistence_framework_version (varint): Persistence framework version. ns_store_model_version_hashes_version (varint): Version of the ns store version hashes. - plist_path (string): Path pointing to the location of the entry within the plist structure. + plist_path (string): Path pointing to the Z_METADATA table and Z_VERSION value of the + Z_PLIST row that this record was extracted from. source (path): Path to the Accounts*.sqlite database file. NSStoreModelVersionHashesRecord: diff --git a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py index 577cf02149..075e702764 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py @@ -31,7 +31,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert len(results) == 11 assert results[0].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[0].priority_level == 5 + assert results[0].priority_level == "Notice" assert results[0].pid == 121 assert results[0].asl_host == "localhost" assert results[0].sender == "syslogd" @@ -44,7 +44,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert results[0].source == "/var/log/asl/2026.05.06.G80.asl" assert results[1].ts == datetime(2026, 5, 6, 7, 45, 10, tzinfo=tz) - assert results[1].priority_level == 5 + assert results[1].priority_level == "Notice" assert results[1].pid == 121 assert results[1].asl_host == "localhost" assert results[1].sender == "syslogd" @@ -57,7 +57,7 @@ def test_system_log(test_file: str, target_unix: Target, fs_unix: VirtualFilesys assert results[1].source == "/var/log/asl/2026.05.06.G80.asl" assert results[-1].ts == datetime(2026, 5, 6, 11, 50, 20, tzinfo=tz) - assert results[-1].priority_level == 5 + assert results[-1].priority_level == "Notice" assert results[-1].pid == 122 assert results[-1].asl_host == "localhost" assert results[-1].sender == "syslogd" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py index 49a270357d..6c947dfaa6 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py @@ -59,7 +59,7 @@ def test_login_items( "/* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = UBF8T346G9" # noqa E501 ) assert results[0].developer_name is None - assert results[0].login_item_disposition == 3 + assert results[0].login_item_disposition == "['Enabled', 'Allowed']" assert results[0].executable_modification_date == datetime(1970, 1, 1, 0, 0, tzinfo=tz) assert results[0].executable_path is None assert results[0].flags == 0 @@ -72,7 +72,7 @@ def test_login_items( assert results[0].program_arguments is None assert results[0].sha256 is None assert results[0].team_identifier == "UBF8T346G9" - assert results[0].login_item_type == 2 + assert results[0].login_item_type == "app" assert results[0].url == "file:///Applications/Visual%20Studio%20Code.app/" assert results[0].uuid == "6f541698-5211-4cf2-95b7-e97534baee39" assert results[0].items == [] diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py index 70ee71c709..7d046045fb 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -70,8 +70,8 @@ def test_tcc( == "/System/Library/PrivateFrameworks/VoiceShortcuts.framework/Versions/A/Support/siriactionsd" ) assert results[1].client_type == 1 - assert results[1].auth_value == 0 - assert results[1].auth_reason == 5 + assert results[1].auth_value == "Denied" + assert results[1].auth_reason == "Service Policy" assert results[1].auth_version == 1 assert ( results[1].csreq From 9ab673d8c51ae062a8dbbdbea46b9779d092f00b Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:21:10 +0200 Subject: [PATCH 51/52] Also parse NoteStore.sqlite in notes.py --- .../darwin/macos/duet_activity_scheduler.py | 18 +- .../bsd/darwin/macos/helpers/build_records.py | 101 +++++--- .../plugins/os/unix/bsd/darwin/macos/notes.py | 233 +++++++++++++++++- .../os/unix/bsd/darwin/macos/user_accounts.py | 4 +- .../bsd/darwin/macos/notes/NoteStore.sqlite | 3 + .../darwin/macos/notes/NoteStore.sqlite-wal | 3 + .../macos/test_duet_activity_scheduler.py | 4 +- .../os/unix/bsd/darwin/macos/test_notes.py | 125 +++++++++- 8 files changed, 430 insertions(+), 61 deletions(-) create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite-wal diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py index b54c5ee3b4..a8a69ae0d6 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -63,11 +63,11 @@ ], ) -ActivityRecord = TargetRecordDescriptor( - "macos/duet_activity_scheduler/activity", +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/ns_store_model_version_hashes", [ ("bytes", "activity"), - ("bytes", "group_binary"), + ("bytes", "group_hash"), ("bytes", "trigger"), ("string", "plist_path"), ("path", "source"), @@ -89,7 +89,7 @@ ZGroupRecord, ZMetadataRecord, ZPlistRecord, - ActivityRecord, + NSStoreModelVersionHashesRecord, ZModelCacheRecord, ) @@ -113,7 +113,7 @@ "NSPersistenceFrameworkVersion": "ns_persistence_framework_version", "NSStoreModelVersionHashesVersion": "ns_store_model_version_hashes_version", "Activity": "activity", - "Group": "group_binary", + "Group": "group_hash", "Trigger": "trigger", } @@ -189,10 +189,10 @@ def duet_activity_scheduler( Z_PLIST row that this record was extracted from. source (path): Path to the DuetActivitySchedulerClassC.db file. - ActivityRecord: - activity (bytes): Binary identifier of the activity. - group_binary (bytes): Binary identifier referencing a group. - trigger (bytes): Binary identifier referencing a trigger. + NSStoreModelVersionHashesRecord: + activity (bytes): Hash for ZACTIVITY entity. + group_hash (bytes): Hash for ZGROUP entity. + trigger (bytes): Hash for ZTRIGGER entity. plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the DuetActivitySchedulerClassC.db file. diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py index a7f1d1e142..6443ba994f 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -12,6 +12,7 @@ from dissect.database.sqlite3 import SQLite3 from dissect.util.plist import NSDictionary, NSKeyedArchiver +from dissect.target.helpers.fsutil import open_decompress from dissect.target.helpers.record import TargetRecordDescriptor if TYPE_CHECKING: @@ -37,7 +38,7 @@ def build_sqlite_records( """Extract and normalize records from SQLite databases. Iterates over provided SQLite files, reads all tables and rows, and converts - them into dictionaries. Supports decoding of binary plist values and + them into dictionaries. Supports decompression. Supports decoding of binary plist values and NSKeyedArchiver blobs. The optional joins arg can be used to combine and filter rows across related tables. @@ -88,6 +89,19 @@ def build_sqlite_records( row_dict = {k: v for k, v in row} # noqa C416 for key, value in list(row_dict.items()): + # Try decompressing binary fields + if isinstance(value, (bytes, bytearray)) and len(value) > 4: + try: + bio = BytesIO(value) + decompressed = open_decompress(fileobj=bio).read() + + if decompressed != value: + row_dict[key] = decompressed + value = decompressed + + except Exception: + pass + # Decode binary plist values (including NSKeyedArchiver blobs) if isinstance(value, (bytes, bytearray)) and value.startswith(b"bplist00"): if is_nskeyedarchive_blob(value): @@ -566,7 +580,11 @@ def select_descriptor( if best_match_count == 0: return None - missing_fields = {key: type_name for key, type_name in formatted_rdict.items() if key not in selected_record.fields} + missing_fields = { + key: type_name + for key, type_name in formatted_rdict.items() + if key not in selected_record.fields and type_name != "NoneType" + } if missing_fields: formatted = ", ".join(f"{k} ({v})" for k, v in sorted(missing_fields.items())) @@ -805,7 +823,7 @@ def emit_dict_records( scalar values are retained as attributes dictionary elements are treated as child nodes and processed recursively. - Supports decoding of binary plist values and NSKeyedArchiver blobs. + Supports decompression. Supports decoding of binary plist values and NSKeyedArchiver blobs. Collapses specified paths into attribute values instead of recursing. @@ -858,43 +876,56 @@ def emit_dict_records( if cleaned_list or not contains_dict: attributes[k] = cleaned_list - elif isinstance(v, (bytes, bytearray)) and v.startswith(b"bplist00"): - if is_nskeyedarchive_blob(v): + elif isinstance(v, (bytes, bytearray)): + if len(v) > 4: try: - archiver = NSKeyedArchiver(BytesIO(v)) - decoded_value = archiver.top.get("root") - attributes[k] = decoded_value + bio = BytesIO(v) + decompressed = open_decompress(fileobj=bio).read() + + if decompressed != v: + v = decompressed + except Exception: - plugin.target.log.exception( - "Failed to decode %s value for key %s", - child_path, - k, - ) - else: - try: - plist_data = load_plist_data(v) if b"$archiver" in v[:128] else plistlib.loads(v) + pass + if isinstance(v, (bytes, bytearray)) and v.startswith(b"bplist00"): + if is_nskeyedarchive_blob(v): + try: + archiver = NSKeyedArchiver(BytesIO(v)) + decoded_value = archiver.top.get("root") + attributes[k] = decoded_value + except Exception: + plugin.target.log.exception( + "Failed to decode %s value for key %s", + child_path, + k, + ) + else: + try: + plist_data = load_plist_data(v) if b"$archiver" in v[:128] else plistlib.loads(v) - if isinstance(plist_data, dict): - yield from emit_dict_records( - plugin, - plist_data, + if isinstance(plist_data, dict): + yield from emit_dict_records( + plugin, + plist_data, + source, + record_descriptors=record_descriptors, + field_mappings=field_mappings, + value_mappings=value_mappings, + convert_timestamps=convert_timestamps, + ) + else: + # If binary plist is an attribute and not a dict + attributes[k] = plist_data + + except Exception: + plugin.target.log.exception( + "Failed to parse plist in %s (plist_path=%s, key=%s)", source, - record_descriptors=record_descriptors, - field_mappings=field_mappings, - value_mappings=value_mappings, - convert_timestamps=convert_timestamps, + child_path, + k, ) - else: - # If binary plist is an attribute and not a dict - attributes[k] = plist_data - - except Exception: - plugin.target.log.exception( - "Failed to parse plist in %s (plist_path=%s, key=%s)", - source, - child_path, - k, - ) + else: + attributes[k] = v else: attributes[k] = v diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py index 6b4769809a..83ac7b4f83 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -15,6 +15,9 @@ from dissect.target import Target +# TODO: Look into changing the implementation to yield 1 record per existing +# or deleted note instead of yielding records for every row in the databases. + ZAccountRecord = TargetRecordDescriptor( "macos/notes/z_account", [ @@ -146,6 +149,36 @@ ], ) +iCloudNSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/notes/icloud_ns_store_model_version_hashes", + [ + ("bytes", "ic_account"), + ("bytes", "ic_account_data"), + ("bytes", "ic_asset_signature"), + ("bytes", "ic_attachment"), + ("bytes", "ic_attachment_location"), + ("bytes", "ic_attachment_preview_image"), + ("bytes", "ic_cloud_state"), + ("bytes", "ic_cloud_syncing_object"), + ("bytes", "ic_device_migration_state"), + ("bytes", "ic_folder"), + ("bytes", "ic_hashtag"), + ("bytes", "ic_inline_attachment"), + ("bytes", "ic_invitation"), + ("bytes", "ic_legacy_tombstone"), + ("bytes", "ic_location"), + ("bytes", "ic_media"), + ("bytes", "ic_note"), + ("bytes", "ic_note_container"), + ("bytes", "ic_note_data"), + ("bytes", "ic_note_participant"), + ("bytes", "ic_search_index_state"), + ("bytes", "ic_server_change_token"), + ("string", "plist_path"), + ("path", "source"), + ], +) + # Contains additional Z_CONTENT field which is a binary blob. This field been removed # from the record descriptor. The field's presence will still be mentioned in a warning. ZModelCacheRecord = TargetRecordDescriptor( @@ -205,6 +238,72 @@ ], ) +ZICCloudStateRecord = TargetRecordDescriptor( + "macos/notes/z_ic_cloud_state", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_current_local_version"), + ("boolean", "z_in_cloud"), + ("varint", "z_latest_version_synced_to_cloud"), + ("string", "z_cloud_syncing_object"), + ("string", "z3_cloud_syncing_object"), + ("datetime", "z_local_version_date"), + ("path", "source"), + ], +) + +# ZICCLOUDSYNCINGOBJECT table contains 200+ more columns, most of which are None in the majority of rows. +# Reduced record descriptor to core fields, other fields will be included in a warning. +ZICCloudSyncingRecord = TargetRecordDescriptor( + "macos/notes/z_ic_cloud_syncing_object", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_cloud_state"), + ("varint", "z_crypto_iteration_count"), + ("boolean", "z_is_password_protected"), + ("boolean", "z_is_share_dirty"), + ("boolean", "marked_for_deletion"), + ("varint", "minimum_supported_notes_version"), + ("boolean", "z_needs_initial_fetch_from_cloud"), + ("string", "z_identifier"), + ("path", "source"), + ], +) + +ZICNoteDataRecord = TargetRecordDescriptor( + "macos/notes/z_ic_note_data", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_note"), + ("string", "z_crypto_initialization_vector"), + ("string", "z_crypto_tag"), + ("bytes", "z_data"), + ("path", "source"), + ], +) + +ZICSearchIndexStateRecord = TargetRecordDescriptor( + "macos/notes/z_ic_search_index_state", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("varint", "z_state_value"), + ("string", "z_identifier"), + ("path", "source"), + ], +) + NotesRecords = ( ZAccountRecord, ZFolderRecord, @@ -212,10 +311,15 @@ ZMetadataRecord, ZPlistRecord, NSStoreModelVersionHashesRecord, + iCloudNSStoreModelVersionHashesRecord, ZModelCacheRecord, AChangeRecord, ATransactionRecord, ATransactionStringRecord, + ZICCloudStateRecord, + ZICCloudSyncingRecord, + ZICNoteDataRecord, + ZICSearchIndexStateRecord, ) FIELD_MAPPINGS = { @@ -311,10 +415,52 @@ "TrashFolder": "trash_folder", "UpdateFolderAction": "update_folder_action", "UpdateNoteAction": "update_note_action", + "ICAccount": "ic_account", + "ICAccountData": "ic_account_data", + "ICAssetSignature": "ic_asset_signature", + "ICAttachment": "ic_attachment", + "ICAttachmentLocation": "ic_attachment_location", + "ICAttachmentPreviewImage": "ic_attachment_preview_image", + "ICCloudState": "ic_cloud_state", + "ICCloudSyncingObject": "ic_cloud_syncing_object", + "ICDeviceMigrationState": "ic_device_migration_state", + "ICFolder": "ic_folder", + "ICHashtag": "ic_hashtag", + "ICInlineAttachment": "ic_inline_attachment", + "ICInvitation": "ic_invitation", + "ICLegacyTombstone": "ic_legacy_tombstone", + "ICLocation": "ic_location", + "ICMedia": "ic_media", + "ICNote": "ic_note", + "ICNoteContainer": "ic_note_container", + "ICNoteData": "ic_note_data", + "ICNoteParticipant": "ic_note_participant", + "ICSearchIndexState": "ic_search_index_state", + "ICServerChangeToken": "ic_server_change_token", + "ZCURRENTLOCALVERSION": "z_current_local_version", + "ZINCLOUD": "z_in_cloud", + "ZLATESTVERSIONSYNCEDTOCLOUD": "z_latest_version_synced_to_cloud", + "ZCLOUDSYNCINGOBJECT": "z_cloud_syncing_object", + "Z3_CLOUDSYNCINGOBJECT": "z3_cloud_syncing_object", + "ZLOCALVERSIONDATE": "z_local_version_date", + "ZNOTE": "z_note", + "ZCRYPTOINITIALIZATIONVECTOR": "z_crypto_initialization_vector", + "ZCRYPTOTAG": "z_crypto_tag", + "ZDATA": "z_data", + "ZSTATEVALUE": "z_state_value", + "ZCLOUDSTATE": "z_cloud_state", + "ZCRYPTOITERATIONCOUNT": "z_crypto_iteration_count", + "ZISPASSWORDPROTECTED": "z_is_password_protected", + "ZISSHAREDIRTY": "z_is_share_dirty", + "ZMARKEDFORDELETION": "marked_for_deletion", + "ZMINIMUMSUPPORTEDNOTESVERSION": "minimum_supported_notes_version", + "ZNEEDSINITIALFETCHFROMCLOUD": "z_needs_initial_fetch_from_cloud", + "ZIDENTIFIER": "z_identifier", } CONVERT_TIMESTAMPS = { "z_timestamp": "2001", + "z_local_version_date": "2001", } @@ -326,9 +472,13 @@ class NotesPlugin(Plugin): References: - https://fatbobman.com/en/posts/tables_and_fields_of_coredata/ - https://developer.apple.com/documentation/coredata/nsstoremodelversionidentifierskey + - https://stackoverflow.com/questions/13503243/what-does-dirty-flag-dirty-values-mean """ - USER_PATH = ("Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV*.storedata",) + USER_PATH = ( + "Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV*.storedata", + "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite", + ) def __init__(self, target: Target): super().__init__(target) @@ -351,7 +501,7 @@ def notes( """Return notes information. Yields the following record types extracted from the - NotesV*.storedata databases: + NotesV*.storedata and NoteStore.sqlite databases: .. code-block:: text @@ -465,10 +615,84 @@ def notes( plist_path (string): Path pointing to the location of the entry within the plist structure. source (path): Path to the database file. + iCloudNSStoreModelVersionHashesRecord: + ic_account (bytes): Hash for account. + ic_account_data (bytes): Hash for account data. + ic_asset_signature (bytes): Hash for ZICASSETSIGNATURE table. + ic_attachment (bytes): Hash for attachment. + ic_attachment_location (bytes): Hash for attachment location. + ic_attachment_preview_image (bytes): Hash for attachment preview image. + ic_cloud_state (bytes): Hash for ZICCLOUDSTATE table. + ic_cloud_syncing_object (bytes): Hash for ZICCLOUDSYNCINGOBJECT table. + ic_device_migration_state (bytes): Hash for device migration state. + ic_folder (bytes): Hash for folder. + ic_hashtag (bytes): Hash for hashtag. + ic_inline_attachment (bytes): Hash for inline attachment. + ic_invitation (bytes): Hash for ZICINVITATION table. + ic_legacy_tombstone (bytes): Hash for legacy tombstone data. + ic_location (bytes): Hash for ZICLOCATION table. + ic_media (bytes): Hash for media. + ic_note (bytes): Hash for note. + ic_note_container (bytes): Hash for note container. + ic_note_data (bytes): Hash for ZICNOTEDATA table. + ic_note_participant (bytes): Hash for ZICNOTEPARTICIPANT table. + ic_search_index_state (bytes): Hash for ZICSEARCHINDEXSTATE table. + ic_server_change_token (bytes): Hash for ZICSERVERCHANGETOKEN table. + plist_path (string): Path pointing to the location of the entry within the plist structure. + source (path): Path to the database file. + ZModelCacheRecord (contains Z_CONTENT field with binary data): table (string): Name of the source table (Z_MODELCACHE). source (path): Path to the database file. + ZICCloudStateRecord: + table (string): Name of the source table (ZICCLOUDSTATE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_current_local_version (varint): Current local version of the object. + z_in_cloud (boolean): Indicates whether the object exists in the cloud. + z_latest_version_synced_to_cloud (varint): Latest version synced to the cloud. + z_cloud_syncing_object (string): Reference to the associated ZICCLOUDSYNCINGOBJECT. + z3_cloud_syncing_object (string): Alternate reference to the ZICCLOUDSYNCINGOBJECT. + z_local_version_date (datetime): Timestamp of the local version. + source (path): Path to the database file. + + ZICCloudSyncingRecord: + table (string): Name of the source table (ZICCLOUDSYNCINGOBJECT). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier (determines object type). + z_opt (varint): The version number of the data record. + z_cloud_state (varint): Cloud state of the object. + z_crypto_iteration_count (varint): Cryptographic iteration count. + z_is_password_protected (boolean): Indicates whether the object is password protected. + z_is_share_dirty (boolean): Indicates whether the shared object has unsynced changes. + marked_for_deletion (boolean): Indicates whether the object is marked for deletion. + minimum_supported_notes_version (varint): Minimum supported notes version. + z_needs_initial_fetch_from_cloud (boolean): Indicates whether initial cloud fetch is required. + z_identifier (string): Identifier for the object. + source (path): Path to the database file. + + ZICNoteDataRecord: + table (string): Name of the source table (ZICNOTEDATA). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_note (varint): Reference to the associated note. + z_crypto_initialization_vector (string): Initialization vector used for cryptographic encryption. + z_crypto_tag (string): Cryptography tag. + z_data (bytes): Note data blob, contains note body. + source (path): Path to the database file. + + ZICSearchIndexStateRecord: + table (string): Name of the source table (ZICSEARCHINDEXSTATE). + z_pk (varint): The autoincrement primary key of the table. + z_ent (varint): Entity identifier. + z_opt (varint): The version number of the data record. + z_state_value (varint): State value of the search index. + z_identifier (string): Identifier for the indexed object. + source (path): Path to the database file. + AChangeRecord: table (string): Name of the source table (ACHANGE). z_pk (varint): The autoincrement primary key of the table. @@ -510,4 +734,7 @@ def notes( self, self.files, NotesRecords, field_mappings=FIELD_MAPPINGS, convert_timestamps=CONVERT_TIMESTAMPS ) - # TODO: Add ZNOTE, ZNOTEBODY, ZOFFLINEACTION, ZATTACHMENT, tables + # TODO: Add ZNOTE, ZNOTEBODY, ZOFFLINEACTION, ZATTACHMENT, tables for NotesV*.storedata + + # TODO: Add ZICASSETSIGNATURE, ZICINVITATION, ZICLOCATION, ZICNOTEPARTICIPANT, + # ZICSERVERCHANGETOKEN, tables for NoteStore.sqlite diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py index a30777c9f0..32edef36cc 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -62,7 +62,7 @@ ("string", "z_modification_id"), ("string", "z_owning_bundle_id"), ("string", "z_username"), - ("bytes", "z_dataclass_properties"), + ("string", "z_dataclass_properties"), ("path", "source"), ], ) @@ -347,7 +347,7 @@ def user_accounts( z_modification_id (string): Modification identifier. z_owning_bundle_id (string): Bundle ID of owning service/app. z_username (string): Username for the account. - z_dataclass_properties (bytes): Dataclass properties. + z_dataclass_properties (string): Dataclass properties. source (path): Path to the Accounts*.sqlite database file. ZEnabledDataClassesRecord: diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite new file mode 100644 index 0000000000..f625e7eb4e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2432e9ef782387885e279df9b401af7ac97ce87a6a358c5f95816a33fe57e177 +size 356352 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite-wal b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite-wal new file mode 100644 index 0000000000..446c41fc01 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/notes/NoteStore.sqlite-wal @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eb2b4c133c0db92012d851b025bd524580e51895de9f03efd9721401ce74d73 +size 1668632 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py index 72cb3fb5d5..70bf8f9ee5 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -63,8 +63,8 @@ def test_duet_activity_scheduler(test_files: list[str], target_unix: Target, fs_ assert results[52].activity is not None assert isinstance(results[52].activity, (bytes, bytearray)) - assert results[52].group_binary is not None - assert isinstance(results[52].group_binary, (bytes, bytearray)) + assert results[52].group_hash is not None + assert isinstance(results[52].group_hash, (bytes, bytearray)) assert results[52].trigger is not None assert isinstance(results[52].trigger, (bytes, bytearray)) assert results[52].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py index 03b60d685b..26e495635e 100644 --- a/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py @@ -15,15 +15,25 @@ @pytest.mark.parametrize( - "test_files", + ("names", "paths"), [ - [ - "NotesV7.storedata", - "NotesV7.storedata-wal", - ] + ( + ( + "NotesV7.storedata", + "NotesV7.storedata-wal", + "NoteStore.sqlite", + "NoteStore.sqlite-wal", + ), + ( + "Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata", + "Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata-wal", + "Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite", + "Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite-wal", + ), + ), ], ) -def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFilesystem) -> None: +def test_notes(names: tuple[str, ...], paths: tuple[str, ...], target_unix: Target, fs_unix: VirtualFilesystem) -> None: user = UnixUserRecord( name="user", uid=501, @@ -34,15 +44,16 @@ def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFiles target_unix.users = lambda: [ user, ] - for test_file in test_files: - data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/notes/{test_file}") - fs_unix.map_file(f"Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/{test_file}", data_file) + for name, path in zip(names, paths, strict=True): + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/notes/{name}") + fs_unix.map_file(path, data_file) target_unix.add_plugin(NotesPlugin) results = list(target_unix.notes()) + results.sort(key=lambda r: r.source) - assert len(results) == 42 + assert len(results) == 1026 assert results[0].table == "ZACCOUNT" assert results[0].z_pk == 1 @@ -206,3 +217,97 @@ def test_notes(test_files: list[str], target_unix: Target, fs_unix: VirtualFiles assert results[40].z_opt is None assert results[40].z_name == "com.apple.Notes" assert results[40].source == "/Users/user/Library/Containers/com.apple.Notes/Data/Library/Notes/NotesV7.storedata" + + assert results[42].table == "ZICCLOUDSTATE" + assert results[42].z_pk == 1 + assert results[42].z_ent == 2 + assert results[42].z_opt == 1 + assert results[42].z_current_local_version == 1 + assert not results[42].z_in_cloud + assert results[42].z_latest_version_synced_to_cloud == 0 + assert results[42].z_cloud_syncing_object is None + assert results[42].z3_cloud_syncing_object is None + assert results[42].z_local_version_date is not None + assert results[42].source == "/Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" + + assert results[48].table == "ZICCLOUDSYNCINGOBJECT" + assert results[48].z_pk == 1 + assert results[48].z_ent == 14 + assert results[48].z_opt == 3 + assert results[48].z_cloud_state == 2 + assert results[48].z_crypto_iteration_count == 0 + assert not results[48].z_is_password_protected + assert not results[48].z_is_share_dirty + assert not results[48].marked_for_deletion + assert results[48].minimum_supported_notes_version == 0 + assert not results[48].z_needs_initial_fetch_from_cloud + assert results[48].z_identifier == "LocalAccount" + assert results[48].source == "/Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" + + assert results[54].table == "ZICNOTEDATA" + assert results[54].z_pk == 1 + assert results[54].z_ent == 19 + assert results[54].z_opt == 114 + assert results[54].z_note == 4 + assert results[54].z_crypto_initialization_vector is None + assert results[54].z_crypto_tag is None + assert results[54].z_data is not None + assert isinstance(results[54].z_data, (bytes, bytearray)) + assert b"This is another note" in results[54].z_data + assert results[54].source == "/Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" + + assert results[56].table == "ZICSEARCHINDEXSTATE" + assert results[56].z_pk == 1 + assert results[56].z_ent == 21 + assert results[56].z_opt == 15 + assert results[56].z_state_value == 4 + assert results[56].z_identifier.startswith("x-coredata://") + assert "/ICFolder/" in results[56].z_identifier + assert results[56].source == "/Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" + + assert results[87].ic_account is not None + assert isinstance(results[87].ic_account, (bytes, bytearray)) + assert results[87].ic_account_data is not None + assert isinstance(results[87].ic_account_data, (bytes, bytearray)) + assert results[87].ic_asset_signature is not None + assert isinstance(results[87].ic_asset_signature, (bytes, bytearray)) + assert results[87].ic_attachment is not None + assert isinstance(results[87].ic_attachment, (bytes, bytearray)) + assert results[87].ic_attachment_location is not None + assert isinstance(results[87].ic_attachment_location, (bytes, bytearray)) + assert results[87].ic_attachment_preview_image is not None + assert isinstance(results[87].ic_attachment_preview_image, (bytes, bytearray)) + assert results[87].ic_cloud_state is not None + assert isinstance(results[87].ic_cloud_state, (bytes, bytearray)) + assert results[87].ic_cloud_syncing_object is not None + assert isinstance(results[87].ic_cloud_syncing_object, (bytes, bytearray)) + assert results[87].ic_device_migration_state is not None + assert isinstance(results[87].ic_device_migration_state, (bytes, bytearray)) + assert results[87].ic_folder is not None + assert isinstance(results[87].ic_folder, (bytes, bytearray)) + assert results[87].ic_hashtag is not None + assert isinstance(results[87].ic_hashtag, (bytes, bytearray)) + assert results[87].ic_inline_attachment is not None + assert isinstance(results[87].ic_inline_attachment, (bytes, bytearray)) + assert results[87].ic_invitation is not None + assert isinstance(results[87].ic_invitation, (bytes, bytearray)) + assert results[87].ic_legacy_tombstone is not None + assert isinstance(results[87].ic_legacy_tombstone, (bytes, bytearray)) + assert results[87].ic_location is not None + assert isinstance(results[87].ic_location, (bytes, bytearray)) + assert results[87].ic_media is not None + assert isinstance(results[87].ic_media, (bytes, bytearray)) + assert results[87].ic_note is not None + assert isinstance(results[87].ic_note, (bytes, bytearray)) + assert results[87].ic_note_container is not None + assert isinstance(results[87].ic_note_container, (bytes, bytearray)) + assert results[87].ic_note_data is not None + assert isinstance(results[87].ic_note_data, (bytes, bytearray)) + assert results[87].ic_note_participant is not None + assert isinstance(results[87].ic_note_participant, (bytes, bytearray)) + assert results[87].ic_search_index_state is not None + assert isinstance(results[87].ic_search_index_state, (bytes, bytearray)) + assert results[87].ic_server_change_token is not None + assert isinstance(results[87].ic_server_change_token, (bytes, bytearray)) + assert results[87].plist_path == "Z_METADATA/Z_VERSION=1/NSStoreModelVersionHashes" + assert results[87].source == "/Users/user/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" From a52abc433e6cb8bb18370c7ebf865e9ed405ee41 Mon Sep 17 00:00:00 2001 From: The Eternal Emperor <127401095+EmperorValkorion@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:50:48 +0200 Subject: [PATCH 52/52] Add FSEvents plugin --- .../os/unix/bsd/darwin/macos/fs_events.py | 306 ++++++++++++++++++ .../os/unix/bsd/darwin/macos/login_window.py | 1 - .../os/unix/bsd/darwin/macos/fc0077112a6fa939 | 3 + .../unix/bsd/darwin/macos/test_fs_events.py | 47 +++ 4 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 dissect/target/plugins/os/unix/bsd/darwin/macos/fs_events.py create mode 100644 tests/_data/plugins/os/unix/bsd/darwin/macos/fc0077112a6fa939 create mode 100644 tests/plugins/os/unix/bsd/darwin/macos/test_fs_events.py diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/fs_events.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/fs_events.py new file mode 100644 index 0000000000..c39e4e3cb1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/fs_events.py @@ -0,0 +1,306 @@ +from __future__ import annotations + +import contextlib +import io +import zipfile +from typing import TYPE_CHECKING, BinaryIO + +from dissect.cstruct import cstruct + +from dissect.target.exceptions import UnsupportedPluginError +from dissect.target.helpers.fsutil import open_decompress +from dissect.target.helpers.record import TargetRecordDescriptor +from dissect.target.plugin import Plugin, export + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +cs = cstruct(endian="<") + +cs.load(""" +struct fsevent_v1 { + uint64 event_id; + uint32 flags; +}; + +struct fsevent_v2 { + uint64 event_id; + uint32 flags; + uint64 node_id; +}; + +struct fsevent_v3 { + uint64 event_id; + uint32 flags; + uint64 node_id; + uint32 padding; +}; +""") + +FSEventRecord = TargetRecordDescriptor( + "macos/fsevents/entry", + [ + ("string", "path"), + ("varint", "event_id"), + ("string[]", "event_flags"), + ("varint", "node_id"), + ("path", "source"), + ], +) + +# FSEvents flag definitions +# TODO: Verify that these flag definitions are correct +# And look into missing definitions (eg. 0x00800000) +FSEVENTS_FLAGS = { + 0x00000001: "MustScanSubDirs", + 0x00000002: "UserDropped", + 0x00000004: "KernelDropped", + 0x00000008: "EventIdsWrapped", + 0x00000010: "HistoryDone", + 0x00000020: "RootChanged", + 0x00000040: "Mount", + 0x00000080: "Unmount", + 0x00000100: "ItemCreated", + 0x00000200: "ItemRemoved", + 0x00000400: "InodeMetaMod", + 0x00000800: "ItemRenamed", + 0x00001000: "ItemModified", + 0x00002000: "ItemFinderInfoMod", + 0x00004000: "ItemChangeOwner", + 0x00008000: "ItemXattrMod", + 0x00010000: "ItemIsFile", + 0x00020000: "ItemIsDir", + 0x00040000: "ItemIsSymlink", + 0x00080000: "OwnEvent", + 0x00100000: "ItemIsHardlink", + 0x00200000: "ItemIsLastHardlink", + 0x00400000: "ItemCloned", +} + +DLS1_MAGIC = b"1SLD" +DLS2_MAGIC = b"2SLD" +DLS3_MAGIC = b"3SLD" + + +def _decode_flags(flags: int) -> list[str] | None: + """Decode FSEvents flag bitmask to a list of human-readable descriptions.""" + if flags == 0: + return None + + parts = [] + known_mask = 0 + + for bit, name in FSEVENTS_FLAGS.items(): + if flags & bit: + parts.append(name) + known_mask |= bit + + # detect unknown bits + unknown = flags & ~known_mask + + if unknown: + # If flags contain bits outside the defined mask, add unknown flag + parts.append(f"Unknown(0x{unknown:08x})") + + return parts + + +def _parse_fsevents_page(data: bytes) -> Iterator[dict]: + """Parse a single uncompressed FSEvents page (DLS1 or DLS2 format). + + Yields dicts with path, event_id, flags, node_id. + """ + if len(data) < 12: + return + + magic = data[:4] + if magic == DLS3_MAGIC: + version = 3 + elif magic == DLS2_MAGIC: + version = 2 + elif magic == DLS1_MAGIC: + version = 1 + else: + return + + pos = 12 # skip header (magic + padding) + + while pos < len(data): + # Read null-terminated path + null_idx = data.find(b"\x00", pos) + if null_idx == -1: + break + + try: + path = data[pos:null_idx].decode("utf-8", errors="replace") + except Exception: + break + pos = null_idx + 1 + + if version == 3: + # DLS3: event_id (uint64) + flags (uint32) + node_id (uint64) + padding (uint32) = 24 bytes + if pos + 24 > len(data): + break + rec = cs.fsevent_v3(data[pos : pos + 24]) + event_id = rec.event_id + flags = rec.flags + node_id = rec.node_id + pos += 24 + elif version == 2: + # DLS2: event_id (uint64) + flags (uint32) + node_id (uint64) = 20 bytes + if pos + 20 > len(data): + break + rec = cs.fsevent_v2(data[pos : pos + 20]) + event_id = rec.event_id + flags = rec.flags + node_id = rec.node_id + pos += 20 + else: + # DLS1: event_id (uint64) + flags (uint32) = 12 bytes + if pos + 12 > len(data): + break + + rec = cs.fsevent_v1(data[pos : pos + 12]) + event_id = rec.event_id + flags = rec.flags + node_id = 0 + pos += 12 + + yield { + "path": path, + "event_id": event_id, + "flags": flags, + "node_id": node_id, + } + + +def _parse_fsevents_stream(data: bytes) -> Iterator[dict]: + pos = 0 + length = len(data) + + while pos < length: + if data[pos : pos + 4] not in (DLS1_MAGIC, DLS2_MAGIC, DLS3_MAGIC): + pos += 1 + continue + + start = pos + pos += 12 + + # find next header + next_pos = pos + while next_pos < length: + if data[next_pos : next_pos + 4] in (DLS1_MAGIC, DLS2_MAGIC, DLS3_MAGIC): + break + next_pos += 1 + + page = data[start:next_pos] + + yield from _parse_fsevents_page(page) + + pos = next_pos + + +def _read_fsevents_file(fh: BinaryIO) -> Iterator[dict]: + """Read and decompress an FSEvents file, then parse all records.""" + raw = fh.read() + + # FSEvents files are gzip-compressed + try: + bio = io.BytesIO(raw) + data = open_decompress(fileobj=bio).read() + yield from _parse_fsevents_stream(data) + except Exception: + pass + else: + return + + # Velociraptor may zip-compress collected files + if raw[:2] == b"PK": + try: + zf = zipfile.ZipFile(io.BytesIO(raw)) + for name in zf.namelist(): + inner = zf.read(name) + # Inner file may be gzip-compressed + with contextlib.suppress(Exception): + bio = io.BytesIO(inner) + inner = open_decompress(fileobj=bio).read() + yield from _parse_fsevents_page(inner) + except Exception: + pass + else: + return + + # May be uncompressed (older macOS or partial) + yield from _parse_fsevents_page(raw) + + +class FSEventsPlugin(Plugin): + """Plugin to parse macOS FSEvents (File System Events). + + FFSEvents is a macOS API that allows applications to register for notifications of + changes like file creation, deletion, modification, and renaming to a given directory tree + which helps applications to keep track of file system changes in real-time without continuously + scanning the disk. + + Forensic value: FSEvents capture detailed information about modifications occurring within + the file system such as file creation, deletion, modification, renaming and mounting. offering + a timeline of changes that can help identify patterns or anomalies, and can uncover critical + artifacts that play a key role in cross-referencing and validating other digital evidence during investigations. + + References: + - https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags + - https://hackmd.io/@M4shl3/FSEvents + """ + + PATHS = ( + ".fseventsd/fc*", + "%2Efseventsd/fc*", + "/var/db/fseventsd/fc*", + "System/Volumes/Data/.fseventsd/fc*", + "System/Volumes/Data/%2Efseventsd/fc*", + ) + + def __init__(self, target: Target): + super().__init__(target) + self.files = self._find_files() + + def _find_files(self) -> set: + files = set() + + for pattern in self.PATHS: + for path in self.target.fs.glob(pattern): + files.add(self.target.fs.path(path)) + + return files + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No FSEvents files found") + + @export(record=FSEventRecord) + def fs_events(self) -> Iterator[FSEventRecord]: + """Parse all FSEvents records showing file system activity. + + Yields FSEventRecords with the following fields: + + .. code-block:: text + + path (string): Path to the affected file or directory. + event_id (varint): Identifier for the event. + flags (string[]): Event flag names decoded from bitmask. + node_id (varint): File system node identifier. + source (path): Path to the fs events file. + """ + for file in self.files: + with file.open("rb") as fh: + for rec in _read_fsevents_file(fh): + yield FSEventRecord( + path=rec["path"], + event_id=rec["event_id"], + event_flags=_decode_flags(rec["flags"]), + node_id=rec["node_id"], + source=file, + _target=self.target, + ) diff --git a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py index 149437def2..0d68165fad 100644 --- a/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -77,7 +77,6 @@ class LoginWindowPlugin(Plugin): SYSTEM_LOGIN_WINDOW_PATHS = ( "/Library/Preferences/com.apple.loginwindow.plist", "/var/root/Library/Preferences/com.apple.loginwindow.plist", - "/private/var/root/Library/Preferences/com.apple.loginwindow.plist", ) USER_LOGIN_WINDOW_PATHS = ( diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/fc0077112a6fa939 b/tests/_data/plugins/os/unix/bsd/darwin/macos/fc0077112a6fa939 new file mode 100644 index 0000000000..0f0da0cbc5 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/fc0077112a6fa939 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:043a86dd5ec8514d63ff3b28d3a8215b1a8cefd8ed68b4911502ae6de5734368 +size 44930 diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_fs_events.py b/tests/plugins/os/unix/bsd/darwin/macos/test_fs_events.py new file mode 100644 index 0000000000..211ced967a --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_fs_events.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.fs_events import FSEventsPlugin +from tests._utils import absolute_path + +if TYPE_CHECKING: + from dissect.target.filesystem import VirtualFilesystem + from dissect.target.target import Target + + +@pytest.mark.parametrize( + "test_file", + [ + "fc0077112a6fa939", + ], +) +def test_fs_events(test_file: str, target_unix: Target, fs_unix: VirtualFilesystem) -> None: + data_file = absolute_path(f"_data/plugins/os/unix/bsd/darwin/macos/{test_file}") + fs_unix.map_file(f"System/Volumes/Data/.fseventsd/{test_file}", data_file) + + target_unix.add_plugin(FSEventsPlugin) + + results = list(target_unix.fs_events()) + + assert len(results) == 2730 + + assert results[0].path == "Library/Caches/com.apple.amsengagementd.classicdatavault" + assert results[0].event_id == 18158644613167937534 + assert results[0].event_flags == ["ItemCreated", "Unknown(0x01000000)"] + assert results[0].node_id == 634146 + assert results[0].source == "/System/Volumes/Data/.fseventsd/fc0077112a6fa939" + + assert results[1].path == "Library/Caches/com.apple.amsengagementd.classicdatavault/analytics/jetpackByteCode" + assert results[1].event_id == 18158644613167937776 + assert results[1].event_flags == ["EventIdsWrapped", "HistoryDone", "Unknown(0x00800000)"] + assert results[1].node_id == 1672774 + assert results[1].source == "/System/Volumes/Data/.fseventsd/fc0077112a6fa939" + + assert results[-1].path == "private/var/root/Library/Preferences/com.apple.xpc.activity2.plist" + assert results[-1].event_id == 18158644613167949036 + assert results[-1].event_flags == ["EventIdsWrapped", "Unknown(0x00800000)"] + assert results[-1].node_id == 1663163 + assert results[-1].source == "/System/Volumes/Data/.fseventsd/fc0077112a6fa939"