diff --git a/dissect/target/container.py b/dissect/target/container.py index 6931b2d5b9..d896ef90d1 100644 --- a/dissect/target/container.py +++ b/dissect/target/container.py @@ -258,3 +258,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/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/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..d7ae42ae4b --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/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/at_jobs.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py new file mode 100644 index 0000000000..259bb457a2 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/at_jobs.py @@ -0,0 +1,126 @@ +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 + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +AtJobsRecord = TargetRecordDescriptor( + "macos/at_jobs", + [ + ("string", "queue"), + ("varint", "seq"), + ("datetime", "execution_time"), + ("string", "command"), + ("path", "source"), + ], +) + + +class AtJobsPlugin(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 = 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) -> 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 + + @export(record=AtJobsRecord) + def at_jobs(self) -> Iterator[AtJobsRecord]: + """Return macOS `at` job records. + + Yields AtJobsRecord with the following fields: + + .. 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 + + Where: + Q = queue identifier + S = sequence number (hexadecimal) + T = execution time (hexadecimal, in minutes) + + Lines following the environment setup (typically after 'export OLDPWD') are treated + as the command content. + """ + for file in self.at_jobs_files: + name = file.name + + if name in (".SEQ", ".lockfile"): + continue + + if len(name) < 6: + continue + + queue = name[0] + seq = int(name[1:6], 16) + time_hex = name[6:] + + execution_time = None + try: + minutes = int(time_hex, 16) + execution_time = minutes * 60 + except ValueError: + pass + + command_line = False + command = "" + with 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, + command=command, + source=file, + ) 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..db0185875e --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.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 + +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"), + ("bytes", "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"}, +) + +CONVERT_TIMESTAMPS = { + "rules_created": "2001", + "rules_modified": "2001", +} + + +class AuthorizationRulesPlugin(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 = 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 auth.db file found") + + @export(record=AuthorizationRulesRecords) + def authorization_rules(self) -> Iterator[AuthorizationRulesRecords]: + """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/call_history.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py new file mode 100644 index 0000000000..dc9243064d --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/call_history.py @@ -0,0 +1,225 @@ +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"), + ("string", "plist_path"), + ("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. + 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: + 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/code_signature_coderesources.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py new file mode 100644 index 0000000000..ad8f630165 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/code_signature_coderesources.py @@ -0,0 +1,160 @@ +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 find_bundle_files +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.target import Target + +OmitRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/omit", + [ + ("boolean", "omit"), + ("varint", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NestedRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/nested", + [ + ("boolean", "nested"), + ("varint", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +OptionalRecord = TargetRecordDescriptor( + "macos/code_signature_coderesources/optional", + [ + ("boolean", "optional"), + ("varint", "weight"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +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", + [ + ("string", "cdhash"), + ("string", "requirement"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +CodeSignatureCodeResourcesRecords = ( + OmitRecord, + NestedRecord, + OptionalRecord, + HashRecord, + HashTwoRecord, + CDHashRecord, +) + +FIELD_MAPPINGS = { + "Resources_PROMISE_icns": "resources_promise_icns", +} + + +class CodeSignatureCodeResourcesPlugin(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) + self.files = find_bundle_files(self.target, "/_CodeSignature/CodeResources") + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No code signature coderesources files found") + + @export(record=CodeSignatureCodeResourcesRecords) + def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]: + """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 new file mode 100644 index 0000000000..5832be599a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py @@ -0,0 +1,35 @@ +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 find_bundle_files +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.target import Target + + +class ContentsInfoPlugin(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) + self.files = find_bundle_files(self.target, "Info.plist") + + 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_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 new file mode 100644 index 0000000000..b84b2993d0 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/contents_version.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_paths import find_bundle_files +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.target import Target + +ContentsVersionRecord = TargetRecordDescriptor( + "macos/contents_version", + [ + ("string", "build_alias_of"), + ("varint", "build_version"), + ("string", "cf_bundle_short_version_string"), + ("string", "cf_bundle_version"), + ("string", "project_name"), + ("varint", "source_version"), + ("path", "source"), + ], +) + + +ContentsVersionRecords = (ContentsVersionRecord,) + +FIELD_MAPPINGS = { + "BuildAliasOf": "build_alias_of", + "BuildVersion": "build_version", + "CFBundleShortVersionString": "cf_bundle_short_version_string", + "CFBundleVersion": "cf_bundle_version", + "ProjectName": "project_name", + "SourceVersion": "source_version", +} + + +class ContentsVersionPlugin(Plugin): + """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) + self.files = self.files = find_bundle_files(self.target, "version.plist") + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No contents version.plist files found") + + @export(record=ContentsVersionRecord) + def contents_version(self) -> Iterator[ContentsVersionRecord]: + """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 new file mode 100644 index 0000000000..d3a015047a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/directory_services_local_nodes.py @@ -0,0 +1,129 @@ +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. + + 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 = 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 sqlindex file found") + + @export(record=DirectoryServicesLocalNodesRecord) + def directory_services_local_nodes( + self, + ) -> Iterator[DirectoryServicesLocalNodesRecord]: + """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 = { + "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 ATTRIBUTE_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, + ) + + # 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 new file mode 100644 index 0000000000..a8a69ae0d6 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_activity_scheduler.py @@ -0,0 +1,211 @@ +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"), + ("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"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_activity_scheduler/ns_store_model_version_hashes", + [ + ("bytes", "activity"), + ("bytes", "group_hash"), + ("bytes", "trigger"), + ("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_activity_scheduler/z_model_cache", + [ + ("string", "table"), + ("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", + "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", + "Activity": "activity", + "Group": "group_hash", + "Trigger": "trigger", +} + + +class DuetActivitySchedulerPlugin(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 = 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 DuetActivitySchedulerClassC.db file found") + + @export(record=DuetActivityRecords) + def duet_activity_scheduler( + self, + ) -> Iterator[DuetActivityRecords]: + """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): 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 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): 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. + + 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 ns store version hashes. + 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. + + 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. + + 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,), + DuetActivityRecords, + field_mappings=FIELD_MAPPINGS, + ) + + # TODO: Add ZACTIVITY, Z_1TRIGGERS, ZTRIGGER, + # Z_PRIMARYKEY, Z_METADATA,Z_MODELCACHE tables 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..2d087a1812 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_interaction_c.py @@ -0,0 +1,250 @@ +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"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_interaction_c/ns_store_model_version_hashes", + [ + ("bytes", "attachment_hash"), + ("bytes", "contacts"), + ("bytes", "interactions"), + ("bytes", "keywords_hash"), + ("bytes", "metadata"), + ("bytes", "version_hash"), + ("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_version", + [ + ("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_hash", + "Contacts": "contacts", + "Interactions": "interactions", + "Keywords": "keywords_hash", + "Metadata": "metadata", + "Version": "version_hash", +} + +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): 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 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. + 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: + attachment_hash (bytes): Hash for ZATTACHMENT entity. + contacts (bytes): Hash for ZCONTACTS entity. + interactions (bytes): Hash for ZINTERACTIONS entity. + keywords_hash (bytes): Hash for ZKEYWORDS entity. + metadata (bytes): Hash for ZMETADATA 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. + + 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): Entity identifier. + 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): Entity identifier. + 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 new file mode 100644 index 0000000000..b714687ee4 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/duet_knowledge_c.py @@ -0,0 +1,484 @@ +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"), + ("boolean", "z_is_active"), + ("boolean", "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"), + ("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"), + ("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 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", + [ + ("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_knowledge_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"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/duet_knowledge_c/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. + + Parses information about app and system activities. + + 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/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): 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. + 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): Entity identifier. + z_opt (varint): The version number of the data record. + 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. + 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): 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. + 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 (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. + 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, identifies the activity. + 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): 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. + 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): 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): 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 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 ns store version hashes. + 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: + 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/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/gkopaque.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py new file mode 100644 index 0000000000..50706ff047 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/gkopaque.py @@ -0,0 +1,105 @@ +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"), + ("bytes", "current"), + ("bytes", "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. + + 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 = 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 gkopaque.db file found") + + @export(record=GatekeeperOpaqueConfigurationRecords) + def gkopaque(self) -> Iterator[GatekeeperOpaqueConfigurationRecords]: + """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 + ) + + # 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 new file mode 100644 index 0000000000..950988b1c2 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/global_user_preferences.py @@ -0,0 +1,44 @@ +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.target import Target + + +class GlobalUserPreferencesPlugin(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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No global user preferences files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.PATHS): + files.add(path) + return files + + @export(record=DynamicDescriptor(["string"])) + def global_user_preferences(self) -> Iterator[DynamicDescriptor]: + """Yield global user preference information.""" + 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/groups.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py new file mode 100644 index 0000000000..194bb8bc32 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/groups.py @@ -0,0 +1,83 @@ +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. + + 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 = self._resolve_files() + + def check_compatible(self) -> None: + if not self.group_files: + raise UnsupportedPluginError("No group files found") + + def _resolve_files(self) -> set(): + files = set() + for file in self.target.fs.glob(self.GROUP_PATH_GLOB): + files.add(file) + return files + + @export(record=GroupInfoRecord) + def groups(self) -> Iterator[GroupInfoRecord]: + """Return group information. + + Yields GroupInfoRecords with the following fields: + + .. code-block:: text + + 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=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/__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/build_paths.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py new file mode 100644 index 0000000000..cc19fb4a67 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_paths.py @@ -0,0 +1,118 @@ +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 + from dissect.target.target import Target + + +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 + 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 + + +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: + """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, target.fs.path(base))) + + return results + + +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. + + 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}"): + 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/helpers/build_records.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py new file mode 100644 index 0000000000..6443ba994f --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/build_records.py @@ -0,0 +1,997 @@ +from __future__ import annotations + +import plistlib +import re +import uuid +from collections import defaultdict +from datetime import datetime, timedelta, timezone +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.fsutil import open_decompress +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, + value_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 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. + + 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): 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. + value_mappings (dict| None): Optional value 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) + + 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: + 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()): + # 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): + 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: + try: + plist_data = ( + load_plist_data(value) + if b"$archiver" in value[:128] + else plistlib.loads(value) + ) + + 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, + value_mappings=value_mappings, + convert_timestamps=convert_timestamps, + ) + 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: + 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: + # 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 + yield build_record( + plugin, + row_dict, + file, + record_descriptors, + field_mappings, + value_mappings, + convert_timestamps, + ) + 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, + value_mappings, + convert_timestamps, + ) + break + + elif table.name in joins_by_table1: + tables = set() + 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"])] + + 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 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()) + 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, + convert_timestamps=convert_timestamps, + ) + else: + # Yield row as record without iterate joins + yield build_record( + 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, + value_mappings, + convert_timestamps, + ) + except Exception: + plugin.target.log.exception("Failed to process SQLite file: %s", file) + + +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. + + Returns: + dict: A new dictionary with prefixed keys. + """ + 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_by_table1: defaultdict(list), + 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"])] + + 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) + + # 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"])] + + 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"]) + 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: + """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"]]: + 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: + """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: + 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, + value_mappings: dict | None = None, + 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. + 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. + + Yields: + Record: Normalized records constructed from plist contents. + """ + 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) + 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( + plugin, + data, + file, + record_descriptors=record_descriptors, + collapse_paths=collapse_paths, + field_mappings=field_mappings, + value_mappings=value_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. + + 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 = { + "_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)) + elif isinstance(v, list): + record_fields.append(("string[]", k)) + else: + record_fields.append(("string", k)) + + record_values[k] = v + + record_fields.append(("path", "source")) + + desc = TargetRecordDescriptor(function_name, record_fields) + + return desc(**record_values) + + +def select_descriptor( + record_descriptors: tuple, + rdict: dict, + plugin: Plugin, + 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 + best_match_count = 0 + best_match_length = 0 + + for record in record_descriptors: + record_keys = set(record.fields.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 = { + 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())) + plugin.target.log.warning( + "Source %s contains fields not defined in the selected record descriptor: %s", + source, + formatted, + ) + + return selected_record + + +def build_record( + plugin: Plugin, + rdict: dict, + 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, performs provided + timestamp and value 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. + value_mappings (dict| None): Optional value 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(): + if format_key(key) == src: + rdict[dst] = rdict.pop(key) + + 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, + typed_fields, + ) + 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(): + 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. + + 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": + return datetime(2001, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=float(value)) + + return value + + +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. + + 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) + + 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: + """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 + 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, + *, + 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, + value_mappings: dict | None = None, + 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. + + Supports decompression. 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 + 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. + 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. + + Yields: + Record: Records generated from dictionary contents. + """ + 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 + + 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 + + elif isinstance(v, (bytes, bytearray)): + if len(v) > 4: + try: + bio = BytesIO(v) + decompressed = open_decompress(fileobj=bio).read() + + if decompressed != v: + v = decompressed + + except Exception: + 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, + 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, + child_path, + k, + ) + else: + attributes[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, value_mappings, convert_timestamps + ) + + 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, + value_mappings=value_mappings, + convert_timestamps=convert_timestamps, + 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: + """Load and normalize data from an NSKeyedArchiver-encoded plist.""" + ns = NSKeyedArchiver(fh) + root = ns.get("store") + return normalize_nsobj(root) 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..88f06f5e01 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/helpers/parse_timestamp.py @@ -0,0 +1,31 @@ +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: + """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: + 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 new file mode 100644 index 0000000000..8137fe0253 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/identity_services.py @@ -0,0 +1,86 @@ +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.build_paths 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. + + 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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No ids.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=[ + SqliteDatabasePropertiesRecord, + ] + ) + def identity_services( + self, + ) -> Iterator[ + [ + SqliteDatabasePropertiesRecord, + ] + ]: + """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(): + yield SqliteDatabasePropertiesRecord( + table="_SqliteDatabaseProperties", + key=row.key, + value=row.value, + source=file, + ) + + # 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 new file mode 100644 index 0000000000..07b93c19dc --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/installation_history.py @@ -0,0 +1,70 @@ +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", "ts"), + ("string", "display_name"), + ("string", "display_version"), + ("string", "process_name"), + ("path", "source"), + ], +) + + +class InstallationHistoryPlugin(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 = 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.plist file found") + + @export(record=InstallationHistoryRecord) + def installation_history(self) -> Iterator[InstallationHistoryRecord]: + """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] + + yield InstallationHistoryRecord( + 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 new file mode 100644 index 0000000000..9b173cd9e5 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keyboard_layout.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 + +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_layout"), + ("boolean", "selected_layout"), + ("boolean", "current_layout"), + ("path", "source"), + ], +) + + +class KeyboardLayoutPlugin(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 = 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.HIToolbox.plist file found") + + @export(record=KeyboardLayoutRecord) + def keyboard_layout(self) -> Iterator[KeyboardLayoutRecord]: + """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", []): + yield KeyboardLayoutRecord( + input_source_kind=source.get("InputSourceKind"), + keyboard_layout_name=source.get("KeyboardLayout Name"), + keyboard_layout_id=source.get("KeyboardLayout ID"), + 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..d4819d42b6 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/keychain.py @@ -0,0 +1,253 @@ +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 + +GenpRecord = TargetRecordDescriptor( + "macos/keychain/genp", + [ + ("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", "svce"), + ("bytes", "gena"), + ("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"), + ], +) + + +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"), + ], +) + +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"), + ("varint", "keyclass"), + ("varint", "actual_keyclass"), + ("string", "data"), + ("path", "source"), + ], +) + +KeychainRecords = ( + GenpRecord, + InetRecord, + KeysRecord, + SqliteSequenceRecord, + TVersionRecord, + MetaDataKeysRecord, +) + +FIELD_MAPPINGS = { + "actualKeyclass": "actual_keyclass", + "rowid": "row_id", + "path": "path_binary", + "type": "keychain_type", +} + +CONVERT_TIMESTAMPS = { + "cdat": "2001", + "mdat": "2001", +} + + +class KeychainPlugin(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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No keychain-2.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=KeychainRecords) + def keychain( + self, + ) -> Iterator[KeychainRecords]: + """Return macOS Keychain database entries.""" + yield from build_sqlite_records( + self, self.files, KeychainRecords, field_mappings=FIELD_MAPPINGS, convert_timestamps=CONVERT_TIMESTAMPS + ) + + # 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, + # sharingMirror, sharingOutgoingQueue tables 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..713781a944 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/launchers.py @@ -0,0 +1,498 @@ +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_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +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[]", "path_state"), + ("string[]", "other_job_enabled"), + ("boolean", "low_priority_background_io"), + ("string", "plist_path"), + ("path", "source"), +] + +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"), + ("string", "plist_path"), + ("path", "source"), +] + +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"), +] + +UNNotificationRecord = [ + ("string", "un_notification_icon_default"), + ("string", "un_notification_icon_settings"), + ("string", "plist_path"), + ("path", "source"), +] + +ListenersRecord = [ + ("string[]", "listeners"), + ("string", "plist_path"), + ("path", "source"), +] + +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", + "path_state": "PathState", + "other_job_enabled": "OtherJobEnabled", + "low_priority_background_io": "LowPriorityBackgroundIO", + # 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", + # 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", + LauncherRecord, + ), + TargetRecordDescriptor( + "macos/launch_agents/socket", + SocketRecord, + ), + TargetRecordDescriptor( + "macos/launch_agents/un", + UNRecord, + ), + TargetRecordDescriptor( + "macos/launch_agents/un_notification", + UNNotificationRecord, + ), +) + +LaunchDaemonRecords = ( + TargetRecordDescriptor( + "macos/launch_daemons", + LauncherRecord, + ), + TargetRecordDescriptor( + "macos/launch_daemons/socket", + SocketRecord, + ), + TargetRecordDescriptor( + "macos/launch_daemons/listeners", + ListenersRecord, + ), +) + +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. + + 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", + "/Library/LaunchAgents/*.plist", + ) + + 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) + + 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_agent_files(self) -> set: + launch_agent_files = set() + for pattern in self.SYSTEM_LAUNCH_AGENT_PATHS: + for path in self.target.fs.glob(pattern): + launch_agent_files.add(path) + for _, path in _build_userdirs(self, self.USER_LAUNCH_AGENT_PATHS): + launch_agent_files.add(path) + return launch_agent_files + + 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): + launch_daemon_files.add(path) + for _, path in _build_userdirs(self, self.USER_LAUNCH_DAEMON_PATHS): + launch_daemon_files.add(path) + return launch_daemon_files + + @export(record=LaunchAgentRecords) + def launch_agents(self) -> Iterator[LaunchAgentRecords]: + """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]: + """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 new file mode 100644 index 0000000000..d8ae7c3103 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/locale.py @@ -0,0 +1,96 @@ +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 +from dissect.target.plugin import export +from dissect.target.plugins.os.default.locale import LocalePlugin + +if TYPE_CHECKING: + from dissect.target.target import Target + + +class LocalePlugin(LocalePlugin): + """macOS locale plugin. + + This plugin retrieves locale information from the system. + """ + + 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") + + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + + @export(property=True) + def timezone(self) -> str | None: + """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.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: + result = self.check_timezone(entry) + if result: + return result + + return None + + 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: + """Return a list of installed languages on the system.""" + 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: + """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: + """Return whether location services are active.""" + 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_items.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py new file mode 100644 index 0000000000..448ac87260 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_items.py @@ -0,0 +1,192 @@ +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_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +LoginItemsRecord = TargetRecordDescriptor( + "macos/login_items", + [ + ("string", "associated_bundle_identifiers"), + ("bytes", "bookmark"), + ("string", "bundle_identifier"), + ("string", "container"), + ("string", "designated_requirement"), + ("string", "developer_name"), + ("string", "login_item_disposition"), + ("datetime", "executable_modification_date"), + ("path", "executable_path"), + ("varint", "flags"), + ("varint", "generation"), + ("string", "identifier"), + ("bytes", "lightweight_requirement"), + ("datetime", "modification_date"), + ("string", "name"), + ("string", "program_arguments"), + ("string", "sha256"), + ("string", "team_identifier"), + ("string", "login_item_type"), + ("string", "url"), + ("string", "uuid"), + ("string[]", "items"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +LoginItemsMetadataRecord = TargetRecordDescriptor( + "macos/login_items/metadata", + [ + ("varint", "generation"), + ("varint", "background_app_refresh_load_count"), + ("boolean", "launch_services_items_imported"), + ("boolean", "service_management_login_items_migrated"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +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", +} + +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", +} + + +class LoginItemsPlugin(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",) + + 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 = 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) -> set: + login_items_files = set() + for pattern in self.SYSTEM_LOGIN_ITEMS_PATHS: + for path in self.target.fs.glob(pattern): + login_items_files.add(path) + + for _, path in _build_userdirs(self, self.USER_LOGIN_ITEMS_PATHS): + login_items_files.add(path) + + return login_items_files + + @export(record=LoginItemsRecord) + def login_items(self) -> Iterator[LoginItemsRecord]: + """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 (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. + 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 (string): Numeric value describing the item type. + 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, + value_mappings=VALUE_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 new file mode 100644 index 0000000000..0d68165fad --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/login_window.py @@ -0,0 +1,135 @@ +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_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + 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. + + 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", + "/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 = 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) -> set: + login_window_files = set() + for pattern in self.SYSTEM_LOGIN_WINDOW_PATHS: + for path in self.target.fs.glob(pattern): + login_window_files.add(path) + for _, path in _build_userdirs(self, self.USER_LOGIN_WINDOW_PATHS): + 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. + + 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/__init__.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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..2a2387f5a3 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/asl.py @@ -0,0 +1,278 @@ +from __future__ import annotations + +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; +}; +""") + +cs.load(""" +struct asl_string_header { + uint16 marker; + uint32 length; +}; +""") + + +ASLRecord = TargetRecordDescriptor( + "macos/logs/asl", + [ + ("datetime", "ts"), + ("string", "priority_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: + # 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 = (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": + 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") + + 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]]: + # 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 # 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") + + # Skip invalid or unrealistic record sizes + if rec_len < 120 or rec_len > 65535 or pos + rec_len + 2 > len(data): + pos += 2 + continue + + try: + # Parse record structure using cstruct + rec = cs.asl_record(data[pos + 2 : pos + 2 + rec_len]) + except Exception: + pos += 2 + continue + + # 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, + 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 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): + sender = None + if not _valid_value(facility): + facility = None + 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, + "pid": rec.pid, + "host": host, + "sender": sender, + "facility": facility, + "message": message, + } + + # Move to next record + 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. + + 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", + "var/log/powermanagement/*.asl", + "var/log/DiagnosticMessages/*.asl", + ) + + def __init__(self, target: Target): + super().__init__(target) + self._asl_files = self._find_files() + + def _find_files(self) -> set: + files = set() + + for pattern in self.ASL_PATHS: + for path in self.target.fs.path("/").glob(pattern): + if path.is_file(): + files.add(path) + + return files + + 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 macOS Apple System Log (ASL) messages. + + Yields ASLRecord with the following fields: + + .. code-block:: text + + ts (datetime): Timestamp (UTC). + 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. + 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: + 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: + level = rec["level"] + priority_level = PRIORITY_LEVEL_MAP.get(level, level) + + yield ASLRecord( + ts=rec["ts"], + priority_level=priority_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/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..46d1885e1a --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/fsck_apfs_log.py @@ -0,0 +1,85 @@ +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 + + +FsckAPFSLogRecord = TargetRecordDescriptor( + "macos/fsck_apfs_log", + [ + ("datetime", "ts"), + ("string", "disk_path"), + ("string", "message"), + ("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): + """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" + + 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 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": + 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 new file mode 100644 index 0000000000..2254ce80d1 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/install_log.py @@ -0,0 +1,112 @@ +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.parse_timestamp import parse_timestamp + +if TYPE_CHECKING: + from collections.abc import Iterator + + +InstallLogRecord = TargetRecordDescriptor( + "macos/install_log", + [ + ("datetime", "ts"), + ("string", "host"), + ("string", "component"), + ("string", "message"), + ("path", "source"), + ], +) + +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 InstallLogPlugin(Plugin): + """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" + + 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. + + Yields InstallLogRecord with the following fields: + + .. code-block:: text + + 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 = "" + 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/logs/system_log.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py new file mode 100644 index 0000000000..144aea1d7b --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/logs/system_log.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +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 + +SystemLogRecord = TargetRecordDescriptor( + "macos/system_log", + [ + ("datetime", "ts"), + ("string", "host"), + ("string", "component"), + ("string", "message"), + ("path", "source"), + ], +) + + +class SystemLogPlugin(Plugin): + """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*" + + def __init__(self, target: Target): + super().__init__(target) + 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) -> 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. + + 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) + + lines = [] + + for ts, line in year_rollover_helper(filepath, RE_TS, "%b %d %H:%M:%S", timezone.utc): + 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) + + yield SystemLogRecord( + ts=ts, + host=hostname.strip(), + component=component.strip(), + message=message.strip(), + source=filepath, + _target=self.target, + ) + + lines = [] 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..aefb6137b9 --- /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): + """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 = 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 wifi.log file found") + + @export(record=WifiLogRecord) + def wifi_log(self) -> Iterator[WifiLogRecord]: + """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( + 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=ts, + host=hostname.strip(), + message=message.strip(), + source=self.file, + _target=self.target, + ) + + current_buf = "" 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..83ac7b4f83 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/notes.py @@ -0,0 +1,740 @@ +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 + +# 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", + [ + ("string", "table"), + ("varint", "z_pk"), + ("varint", "z_ent"), + ("varint", "z_opt"), + ("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"), + ("boolean", "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"), + ("boolean", "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"), + ("string", "plist_path"), + ("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"), + ], +) + +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( + "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"), + ], +) + +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, + ZPrimaryKeyRecord, + ZMetadataRecord, + ZPlistRecord, + NSStoreModelVersionHashesRecord, + iCloudNSStoreModelVersionHashesRecord, + ZModelCacheRecord, + AChangeRecord, + ATransactionRecord, + ATransactionStringRecord, + ZICCloudStateRecord, + ZICCloudSyncingRecord, + ZICNoteDataRecord, + ZICSearchIndexStateRecord, +) + +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", + "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", +} + + +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 + - https://stackoverflow.com/questions/13503243/what-does-dirty-flag-dirty-values-mean + """ + + 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) + 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 and NoteStore.sqlite 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 (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 (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. + 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 (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. + 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. + 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: + 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. + + 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. + 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 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/periodic.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py new file mode 100644 index 0000000000..52a46c7880 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/periodic.py @@ -0,0 +1,427 @@ +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 + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + +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"), + ], +) + +# 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", + [ + ("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. + + 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/*", + "/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]: + """Return macOS periodic script paths.""" + for file in self.periodic_scripts_files: + yield PeriodicScriptsRecord( + source=file, + ) + + @export(record=PeriodicConfRecords) + def periodic_conf(self) -> Iterator[PeriodicConfRecords]: + """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()) + 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/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..1cf104c6c5 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_info_strings.py @@ -0,0 +1,102 @@ +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 find_bundle_files +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.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 plugin. + + Parses 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) + self.files = find_bundle_files(self.target, "InfoPlist.strings") + + def check_compatible(self) -> None: + if not self.files: + raise UnsupportedPluginError("No Resources InfoPlist.strings files found") + + @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 new file mode 100644 index 0000000000..1c469fa95e --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/resources_localizable_strings.py @@ -0,0 +1,37 @@ +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 find_bundle_files +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.target import Target + + +class ResourcesLocalizableStringsPlugin(Plugin): + """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) + self.files = find_bundle_files(self.target, "Localizable.strings") + + 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_plist_records(self, self.files, function_name="macos/resources_localizable_strings") 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/safari_downloads.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py new file mode 100644 index 0000000000..46aa230ec5 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_downloads.py @@ -0,0 +1,105 @@ +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 + +SafariDownloadRecord = TargetRecordDescriptor( + "macos/safari_downloads", + [ + ("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"), + ], +) + + +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=SafariDownloadRecord) + def safari_downloads(self) -> Iterator[SafariDownloadRecord]: + """Return macOS Safari downloads. + + Yields SafariDownloadRecords for each download with the following fields: + + .. code-block:: text + + 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()) + 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_favicons.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py new file mode 100644 index 0000000000..2bb23c815f --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_favicons.py @@ -0,0 +1,113 @@ +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_favicons/page_url", + [ + ("string", "table"), + ("string", "url"), + ("string", "uuid"), + ("path", "source"), + ], +) + +IconInfoRecord = TargetRecordDescriptor( + "macos/safari_favicons/icon_info", + [ + ("string", "table"), + ("string", "uuid"), + ("string", "url"), + ("datetime", "timestamp"), + ("varint", "width"), + ("varint", "height"), + ("boolean", "has_generated_representations"), + ("path", "source"), + ], +) + +RejectedResourcesRecord = TargetRecordDescriptor( + "macos/safari_favicons/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 (boolean): Indicates whether the favicon has generated representations. + 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..3ce6847a07 --- /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/safari_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/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..0561b0a6af --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_per_site_zoom_preferences.py @@ -0,0 +1,104 @@ +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_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. + + 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, 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 + PerSiteZoomPreferences.plist files: + + .. code-block:: text + + 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_ck_record_names_to_ck_records=plist.get("MapOfCKRecordNamesToCKRecords"), + zoom_preference_version=plist.get("ZoomPreferenceVersion"), + source=file, + _target=self.target, + ) + + 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_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..2a6dcb8ba4 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/safari/safari_recently_closed_tabs.py @@ -0,0 +1,216 @@ +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_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + 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. + + 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 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=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/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/shadow.py b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py new file mode 100644 index 0000000000..b86836d5c6 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/shadow.py @@ -0,0 +1,90 @@ +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 + +ShadowRecord = TargetRecordDescriptor( + "macos/shadow", + [ + ("string", "name"), + ("string", "hash"), + ("string", "salt"), + ("varint", "iterations"), + ("string", "algorithm"), + ("path", "source"), + ], +) + + +class ShadowPlugin(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 = self._resolve_files() + + def check_compatible(self) -> None: + if not self.user_files: + raise UnsupportedPluginError("No shadow files found") + + def _resolve_files(self) -> set: + user_files = set() + for file in self.target.fs.glob(self.USER_FILE_GLOB): + user_files.add(file) + return user_files + + @export(record=ShadowRecord) + def passwords(self) -> Iterator[ShadowRecord]: + """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()) + 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 ShadowRecord( + name=username, + hash=hash, + salt=salt, + iterations=iterations, + algorithm=key, + source=path, + _target=self.target, + ) 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..c45ba98645 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/software_update_preferences.py @@ -0,0 +1,97 @@ +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. + + 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 = 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.SoftwareUpdate.plist file found") + + @export(record=SoftwareUpdatePreferencesRecord) + def software_update_preferences(self) -> Iterator[SoftwareUpdatePreferencesRecord]: + """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( + 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..a053c089f4 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/system_preferences.py @@ -0,0 +1,39 @@ +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_records import build_plist_records + +if TYPE_CHECKING: + from collections.abc import Iterator + + from dissect.target.target import Target + + +class SystemPreferencesPlugin(Plugin): + """macOS system preferences plugin.""" + + PATHS = ("/Library/Preferences/**/*.plist",) + + 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(path) + return files + + 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_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..f3234f2d9d --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/tcc.py @@ -0,0 +1,152 @@ +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 + +AccessRecord = TargetRecordDescriptor( + "macos/tcc/access", + [ + ("string", "table"), + ("string", "service"), + ("string", "client"), + ("varint", "client_type"), + ("string", "auth_value"), + ("string", "auth_reason"), + ("varint", "auth_version"), + ("bytes", "csreq"), + ("string", "policy_id"), + ("string", "indirect_object_identifier"), + ("string", "indirect_object_identifier_type"), + ("bytes", "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, +) + +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. + + 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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No TCC.db files found") + + 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): + files.add(path) + return files + + @export(record=TCCRecords) + def tcc( + self, + ) -> Iterator[TCCRecords]: + """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 (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. + 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, 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 new file mode 100644 index 0000000000..1b4e7e0794 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/text_replacements.py @@ -0,0 +1,254 @@ +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 + + +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"), + ("datetime", "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"), + ("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"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/text_replacements/ns_store_model_version_hashes", + [ + ("bytes", "tr_cloud_kit_sync_state"), + ("bytes", "text_replacement_entry"), + ("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/text_replacements/z_model_cache", + [ + ("string", "table"), + ("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", + "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", +} + +CONVERT_TIMESTAMPS = { + "z_timestamp": "2001", +} + + +class TextReplacementsPlugin(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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No TextReplacements.db files found") + + def _find_files(self) -> set: + files = set() + for _, path in _build_userdirs(self, self.PATH): + files.add(path) + return files + + @export(record=TextReplacementsRecords) + def text_replacements( + self, + ) -> Iterator[TextReplacementsRecords]: + """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): 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. + 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): 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. + source (path): Path to the TextReplacements.db 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 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 ns store version hashes. + 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: + 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 new file mode 100644 index 0000000000..b1ef3624ac --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/time_machine.py @@ -0,0 +1,143 @@ +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", + [ + ("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/destination", + [ + ("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"), + ], +) + + +class TimeMachinePlugin(Plugin): + """macOS Time Machine plugin. + + Parses Time Machine, macOS's backup system, preferences. + """ + + PATH = "/Library/Preferences/com.apple.TimeMachine.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.TimeMachine.plist file found") + + @export(record=(TimeMachineRecord, DestinationsRecord)) + def time_machine(self) -> Iterator[(TimeMachineRecord, DestinationsRecord)]: + """Return macOS Time Machine preferences. + + Yields the following record types extracted from the + com.apple.TimeMachine.plist file: + + .. code-block:: text + + 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, + ) + + 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/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..32edef36cc --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/darwin/macos/user_accounts.py @@ -0,0 +1,444 @@ +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 + + +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"), + ("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"), + ("datetime", "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"), + ("string", "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"), + ("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"), + ("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"), + ], +) + +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", + [ + ("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"), + ("string", "plist_path"), + ("path", "source"), + ], +) + +NSStoreModelVersionHashesRecord = TargetRecordDescriptor( + "macos/user_accounts/ns_store_model_version_hashes", + [ + ("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"), + ], +) + +# 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", + [ + ("string", "table"), + ("path", "source"), + ], +) + +UserAccountRecords = ( + ZAccessOptionsKeyRecord, + ZOwningAccountTypesRecord, + ZAccountRecord, + ZEnabledDataClassesRecord, + ZAccountPropertyRecord, + ZAccountTypeRecord, + ZSupportedDataClassesRecord, + ZSyncableDataClassesRecord, + 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_hash", + "AccountProperty": "account_property", + "AccountType": "account_type", + "Authorization": "authorization", + "CredentialItem": "credential_item", + "Dataclass": "dataclass", +} + + +class UserAccountsPlugin(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 = self._find_files() + + def check_compatible(self) -> None: + if not (self.files): + raise UnsupportedPluginError("No user account database 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=UserAccountRecords) + def user_accounts( + self, + ) -> 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): 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. + 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): Entity identifier. + z_opt (varint): The version number of the data record. + 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. + 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 (string): 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): 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. + 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): Entity identifier. + z_opt (varint): The version number of the data record. + 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. + 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): 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 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 ns store version hashes. + 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: + 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) + + # TODO: Add Z_2PROVISIONEDDATACLASSES, ZAUTHORIZATION, ZCREDENTIALITEM tables diff --git a/dissect/target/plugins/os/unix/cronjobs.py b/dissect/target/plugins/os/unix/cronjobs.py index 6f946d149b..e28bff860f 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/_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/BackgroundItems-v16.btm b/tests/_data/plugins/os/unix/bsd/darwin/macos/BackgroundItems-v16.btm new file mode 100644 index 0000000000..d40ee54705 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/BackgroundItems-v16.btm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c69561684eb1c6fca809cc223dc21f5a53c5b1d99a6a8e72ebfc3257680cb86e +size 2961 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/a0000901c45260 b/tests/_data/plugins/os/unix/bsd/darwin/macos/a0000901c45260 new file mode 100644 index 0000000000..00a19a35d2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/a0000901c45260 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dca01c23993861cfdaeb3de764125e6e69693e2ac8bee8b928a30680bc5b81b +size 1452 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/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/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/Ethernet b/tests/_data/plugins/os/unix/bsd/darwin/macos/code_signature/Ethernet new file mode 100644 index 0000000000..a7c10a15fe --- /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: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 new file mode 100644 index 0000000000..6e2dc2379d --- /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: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 new file mode 100644 index 0000000000..432cbc476f --- /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:4db0d9039339dcadd2a333f76468110c66b0f70656238b515990e732d2adce6a +size 1021 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/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..11d09bb889 --- /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:248719a7a2e1c4b265818714c6f7a9e4dc5330854be8da60faf8879fa0594973 +size 1198 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..98cec89b3e --- /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: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 new file mode 100644 index 0000000000..b63606cc40 --- /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: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 new file mode 100644 index 0000000000..f450a1d749 --- /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:020880a0444267fb7cccce89f241c46ddc7b4655c2d4009bf42bb8f8fa0b1c39 +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/cronjobs/one b/tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/one new file mode 100644 index 0000000000..10568b6a42 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/cronjobs/two b/tests/_data/plugins/os/unix/bsd/darwin/macos/cronjobs/two new file mode 100644 index 0000000000..10568b6a42 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/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/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/_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/_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/_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/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/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/_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/launchers/com.apple.ecosystemagent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.apple.ecosystemagent.plist new file mode 100644 index 0000000000..cd072ecbb1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/launchers/com.openssh.ssh-agent.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/com.openssh.ssh-agent.plist new file mode 100644 index 0000000000..0ec0d20dc2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/launchers/org.cups.cupsd.plist b/tests/_data/plugins/os/unix/bsd/darwin/macos/launchers/org.cups.cupsd.plist new file mode 100644 index 0000000000..290f696ffb --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/locale/.AppleSetupDone b/tests/_data/plugins/os/unix/bsd/darwin/macos/locale/.AppleSetupDone new file mode 100644 index 0000000000..e69de29bb2 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/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.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/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/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/_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/_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/system.log b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/system.log new file mode 100644 index 0000000000..ea5388deb1 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/logs/system_log/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/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/_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/_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/_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/_data/plugins/os/unix/bsd/darwin/macos/periodic/110.clean-tmps b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/110.clean-tmps new file mode 100644 index 0000000000..1153df6257 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/periodic/199.rotate-fax b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/199.rotate-fax new file mode 100644 index 0000000000..8fa82b514e --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/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/periodic/periodic.conf b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/periodic.conf new file mode 100644 index 0000000000..a79145da40 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/periodic/periodic.conf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee479a7a0dd839c4a08e73d5a6fcffbe750bd47b82cf7c633ed20c5e63b8ed03 +size 4954 diff --git a/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/OSvKernDSPLib.strings b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/OSvKernDSPLib.strings new file mode 100644 index 0000000000..9242e828d2 --- /dev/null +++ b/tests/_data/plugins/os/unix/bsd/darwin/macos/resources_info_strings/OSvKernDSPLib.strings @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0038c966736bfa285115eab47574a334562598110262f5448905acfb971778c8 +size 172 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/_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..8d92362e48 --- /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: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 new file mode 100644 index 0000000000..91b2483fa2 --- /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:bedad05becab5403cffe152e05f878c899b2e1dbb9bea88a9919fda6b221b7b2 +size 249 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/_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/_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/_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/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_asl.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py new file mode 100644 index 0000000000..075e702764 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_asl.py @@ -0,0 +1,70 @@ +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.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 + 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) + + 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].priority_level == "Notice" + 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 == "Notice" + 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 == "Notice" + 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 new file mode 100644 index 0000000000..dae81db10a --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_fsck_apfs_log.py @@ -0,0 +1,45 @@ +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.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) + + 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" 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 new file mode 100644 index 0000000000..e30406e3b3 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_install_log.py @@ -0,0 +1,48 @@ +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.logs.install_log 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) + + target_unix.add_plugin(InstallLogPlugin) + + results = list(target_unix.install_log()) + 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[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" 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..fca33742e8 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_system_log.py @@ -0,0 +1,75 @@ +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: 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}") + 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.reverse() + 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_wifi_log.py b/tests/plugins/os/unix/bsd/darwin/macos/logs/test_wifi_log.py new file mode 100644 index 0000000000..4a205e21d2 --- /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, 776000, tzinfo=tz) + assert results[0].host == "[airport]/114" + assert ( + 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" 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_downloads.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py new file mode 100644 index 0000000000..ba2ad4b50a --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_downloads.py @@ -0,0 +1,78 @@ +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_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) == 2 + + 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_favicons.py b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_favicons.py new file mode 100644 index 0000000000..949b775477 --- /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 + 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" 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..56545cfa22 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_per_site_zoom_preferences.py @@ -0,0 +1,57 @@ +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) == 3 + + 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_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..9c212c8bd9 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/safari/test_safari_recently_closed_tabs.py @@ -0,0 +1,93 @@ +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_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[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[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[2].tab_url == "https://stackoverflow.com/questions/39872512/what-command-makes-a-new-file-in-terminal" + ) + 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" 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" 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..824f8fa03d --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_airport_preferences.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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) + + 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_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 new file mode 100644 index 0000000000..e8f19560f9 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_at_jobs.py @@ -0,0 +1,38 @@ +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.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", + [ + "a0000901c45260", + ], +) +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) + + target_unix.add_plugin(AtJobsPlugin) + + 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" 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..67c04eab79 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_authorization_rules.py @@ -0,0 +1,190 @@ +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.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) + + 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_call_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py new file mode 100644 index 0000000000..c384aa73e2 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_call_history.py @@ -0,0 +1,97 @@ +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].plist_path == "Z_METADATA/Z_VERSION=1" + assert results[5].source == "/Users/user/Library/Application Support/CallHistoryDB/CallHistory.storedata" + + 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)) + 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_code_signature_coderesources.py b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py new file mode 100644 index 0000000000..62360686cf --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_code_signature_coderesources.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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", + "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", + ), + ) + ], +) +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) + + 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 new file mode 100644 index 0000000000..110fca8bac --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_info.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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) + + 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..e6dbba0f04 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_contents_version.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.contents_version import ( + ContentsVersionPlugin, +) +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) + + target_unix.add_plugin(ContentsVersionPlugin) + + results = list(target_unix.contents_version()) + results.sort(key=lambda r: r.source) + + 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[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" 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..5326e66f0c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_directory_services_local_nodes.py @@ -0,0 +1,53 @@ +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.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) + + 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..70bf8f9ee5 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_activity_scheduler.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +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: + 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) + + 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].plist_path == "Z_METADATA/Z_VERSION=1" + 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_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" + 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_duet_interaction_c.py b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py new file mode 100644 index 0000000000..e5a3585ca7 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_interaction_c.py @@ -0,0 +1,107 @@ +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].plist_path == "Z_METADATA/Z_VERSION=1" + assert results[8].source == "/var/db/CoreDuet/People/interactionC.db" + + 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 == "Z_METADATA/Z_VERSION=1/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" 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..be350948b4 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_duet_knowledge_c.py @@ -0,0 +1,229 @@ +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 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 + 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].plist_path == "Z_METADATA/Z_VERSION=1" + 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 == "Z_METADATA/Z_VERSION=1/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" 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" 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..61fe98690d --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_gkopaque.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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) + + target_unix.add_plugin(GatekeeperOpaqueConfigurationPlugin) + + results = list(target_unix.gkopaque()) + + 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[-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 new file mode 100644 index 0000000000..2705f6730c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_global_user_preferences.py @@ -0,0 +1,106 @@ +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.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) + + target_unix.add_plugin(GlobalUserPreferencesPlugin) + + results = list(target_unix.global_user_preferences()) + results.sort(key=lambda r: r.source) + + 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[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[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 new file mode 100644 index 0000000000..9ddadfa42d --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_groups.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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: 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/groups/{test_file}") + fs_unix.map_file(f"/var/db/dslocal/nodes/Default/groups/{test_file}", data_file) + + 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 new file mode 100644 index 0000000000..0de0c04320 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_identity_services.py @@ -0,0 +1,51 @@ +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.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) + + 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_installation_history.py b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py new file mode 100644 index 0000000000..2dad7ca294 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_installation_history.py @@ -0,0 +1,36 @@ +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.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) + + target_unix.add_plugin(InstallationHistoryPlugin) + + 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" 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..fd9380cb16 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keyboard_layout.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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) + + 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 new file mode 100644 index 0000000000..a52edc9074 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_keychain.py @@ -0,0 +1,190 @@ +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.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) + + 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 new file mode 100644 index 0000000000..02c74db059 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_launchers.py @@ -0,0 +1,314 @@ +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.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.ecosystemagent.plist", + "com.openssh.ssh-agent.plist", + ), + ( + "/System/Library/LaunchAgents/com.apple.ecosystemagent.plist", + "/Users/user/Library/LaunchAgents/com.openssh.ssh-agent.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/launchers/{name}") + fs_unix.map_file(path, data_file) + + target_unix.add_plugin(LaunchersPlugin) + results = list(target_unix.launch_agents()) + results.sort(key=lambda r: (r.source, getattr(r, "plist_path", "") or "")) + + assert len(results) == 6 + + 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].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" + + 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].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].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" + + 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( + ("names", "paths"), + [ + ( + ("org.cups.cupsd.plist",), + ("/System/Library/LaunchDaemons/org.cups.cupsd.plist",), + ), + ], +) +def test_launch_daemons( + 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/launchers/{name}") + fs_unix.map_file(path, data_file) + + 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 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].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" + + 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_locale.py b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py new file mode 100644 index 0000000000..eb304dfc54 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_locale.py @@ -0,0 +1,71 @@ +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 LocalePlugin +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", + "com.apple.timezone.auto.plist", + ), + ( + "/Library/Preferences/.GlobalPreferences.plist", + "/private/var/db/.AppleSetupDone", + "/Library/Preferences/com.apple.timezone.auto.plist", + ), + ), + ], +) +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(LocalePlugin) + + 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 + 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_login_items.py b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py new file mode 100644 index 0000000000..6c947dfaa6 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_items.py @@ -0,0 +1,101 @@ +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.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: + 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/{name}") + fs_unix.map_file(path, data_file) + + 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].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 == "['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 + 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 == "app" + 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[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" 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..dbcfbcadfa --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_login_window.py @@ -0,0 +1,79 @@ +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.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", + "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", + ), + ), + ], +) +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) + + target_unix.add_plugin(LoginWindowPlugin) + + results = list(target_unix.login_window()) + results.sort(key=lambda r: r.source) + + 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[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" 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..26e495635e --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_notes.py @@ -0,0 +1,313 @@ +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( + ("names", "paths"), + [ + ( + ( + "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(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/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) == 1026 + + assert results[0].table == "ZACCOUNT" + assert results[0].z_pk == 1 + assert results[0].z_ent == 4 + assert results[0].z_opt == 2 + 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 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 + 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].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].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].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" + 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" + + 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" diff --git a/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py new file mode 100644 index 0000000000..287e5d7162 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_periodic.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.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/periodic/{name}") + fs_unix.map_file(path, data_file) + + 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/periodic/{name}") + fs_unix.map_file(path, data_file) + + 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" 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..71262b533c --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_info_strings.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from dissect.target.plugins.os.unix.bsd.darwin.macos.resources_info_strings import ResourcesInfoStringsPlugin +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"), + [ + ( + ("WiFiAgent.strings", "OSvKernDSPLib.strings"), + ( + "/System/Library/CoreServices/WiFiAgent.app/Contents/Resources/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) + + 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 + + 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_resources_localizable_strings.py b/tests/plugins/os/unix/bsd/darwin/macos/test_resources_localizable_strings.py new file mode 100644 index 0000000000..a31d7f2f95 --- /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_shadow.py b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py new file mode 100644 index 0000000000..b98b5cb759 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_shadow.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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: 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/shadow/{test_file}") + fs_unix.map_file(f"/var/db/dslocal/nodes/Default/users/{test_file}", data_file) + + 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 new file mode 100644 index 0000000000..d1d0552498 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_software_update_preferences.py @@ -0,0 +1,46 @@ +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.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) + + 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..84f613e9a8 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_system_preferences.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +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) + + 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_tcc.py b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py new file mode 100644 index 0000000000..7d046045fb --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_tcc.py @@ -0,0 +1,95 @@ +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.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) + + 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[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 == "Denied" + assert results[1].auth_reason == "Service Policy" + 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" 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..8a7088bb28 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_text_replacements.py @@ -0,0 +1,110 @@ +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.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) + + 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].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 == ( + 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 == "Z_METADATA/Z_VERSION=1/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 new file mode 100644 index 0000000000..ba3d6ff551 --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_time_machine.py @@ -0,0 +1,57 @@ +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.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) + + target_unix.add_plugin(TimeMachinePlugin) + + results = list(target_unix.time_machine()) + 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" 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..6bd382ee6e --- /dev/null +++ b/tests/plugins/os/unix/bsd/darwin/macos/test_user_accounts.py @@ -0,0 +1,166 @@ +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.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) + + 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 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" + 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 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" + 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].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 + 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 == "Z_METADATA/Z_VERSION=1/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" 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"