Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
898b07a
some working stuff
qmadev Mar 16, 2026
1e8d95c
lint stuff
qmadev Mar 16, 2026
6db60f2
add groups and systemlog
qmadev Mar 22, 2026
7e4c235
Merge branch 'main' into feature/macos-plugins
qmadev Mar 22, 2026
a33ccab
add empty test files
qmadev Mar 22, 2026
bc39d35
Fix macOS parsers and add tests
EmperorValkorion Apr 3, 2026
5a31d6e
Update comments
EmperorValkorion Apr 7, 2026
4742b4a
Remove unnecessary print
EmperorValkorion Apr 7, 2026
4a9acd1
Add macOS .plist parsers
EmperorValkorion Apr 21, 2026
44c2850
Merge remote-tracking branch 'upstream/main' into feature/macos-plugins
EmperorValkorion Apr 21, 2026
57debef
Move parse_timestamp function to helper file and remove DynamicDescri…
EmperorValkorion Apr 22, 2026
f2fc33d
add macOS SQLite parsers
EmperorValkorion Apr 30, 2026
f5ee807
Merge remote-tracking branch 'upstream/main' into feature/macos-plugins
EmperorValkorion Apr 30, 2026
820629c
add macOS persistence parsers
EmperorValkorion May 7, 2026
d1d73c8
add macOS parser for at jobs
EmperorValkorion May 7, 2026
ba73adb
Use exsiting unix cronjobs parser instead
EmperorValkorion May 8, 2026
175eda3
Cleaned up installog and systemlog parsers
EmperorValkorion May 8, 2026
d12c07c
add command field for at jobs
EmperorValkorion May 11, 2026
6ffa21e
Improve log plugins & tests
EmperorValkorion May 13, 2026
2fd1cba
Plugin renames
EmperorValkorion May 13, 2026
17da16e
Add parser for WiFi logs
EmperorValkorion May 13, 2026
285cd0b
Return None instead of skipping lines without timestamp in year_rollo…
EmperorValkorion May 14, 2026
188326d
Merge branch 'yrh-multiline-fix' into feature/macos-plugins
EmperorValkorion May 14, 2026
ca71719
Only iterate over logs once
EmperorValkorion May 14, 2026
4c76edf
Improve build_sqlite_records helper function
EmperorValkorion May 18, 2026
0c09f0b
Changed at jobs test file
EmperorValkorion May 18, 2026
90a832c
add fsck_apfs_log plugin
EmperorValkorion May 18, 2026
2488a8b
parse ASL logs
EmperorValkorion May 26, 2026
da8c071
Process feedback & general improvements
EmperorValkorion May 26, 2026
cace2ae
Update dissect/target/plugins/os/unix/bsd/darwin/macos/airport_prefer…
EmperorValkorion May 26, 2026
220c262
Update airport_preferences.py
EmperorValkorion May 26, 2026
0f2e889
minor improvements
EmperorValkorion May 26, 2026
4a4895c
minor imrpovements
EmperorValkorion May 27, 2026
20e87cc
helper function for finding bundle files
EmperorValkorion May 27, 2026
4d85beb
Improve plugins + tests and add docstrings
EmperorValkorion Jun 4, 2026
3fa433a
Add docstrings for find_bundle_files() and parse_timestamp() helpers
EmperorValkorion Jun 4, 2026
2df67f5
Add docstrings for build_records helpers
EmperorValkorion Jun 4, 2026
ecc58e5
Fix typo in docstring
EmperorValkorion Jun 4, 2026
4930701
Removed unused mocks
EmperorValkorion Jun 5, 2026
f9a0fe2
Add Duet KnowledgeC plugin
EmperorValkorion Jun 5, 2026
0c0890d
Add Duet InteractionC Plugin
EmperorValkorion Jun 8, 2026
aa8757c
Minor fixes & additional comments for build_sqlite_records
EmperorValkorion Jun 8, 2026
736ae05
Merge remote-tracking branch 'upstream/main' into feature/macos-plugins
EmperorValkorion Jun 8, 2026
e607bc6
add notes & call_history plugins
EmperorValkorion Jun 9, 2026
7704d03
Add Safari favicons and per site preferences plugins
EmperorValkorion Jun 9, 2026
de3d9d3
Add safari_preferences plugin
EmperorValkorion Jun 9, 2026
52ff082
Add some more fields to LauncherRecord from the launchd manual
EmperorValkorion Jun 10, 2026
3658cb6
Replace true/false varints with booleans
EmperorValkorion Jun 10, 2026
5709272
Add macOS Safari plugins
EmperorValkorion Jun 10, 2026
8eda477
Replace DynamicDescriptor for TargetRecordDescriptors in safari_recen…
EmperorValkorion Jun 10, 2026
057814b
Improve prefix_row docstring
EmperorValkorion Jun 11, 2026
4c99f9b
Move the periodic TODO comment to periodic.py
EmperorValkorion Jun 11, 2026
4603625
Improved some plugins with better test data
EmperorValkorion Jun 11, 2026
f059838
Add traceability to plist records yielded from SQLite rows
EmperorValkorion Jun 11, 2026
b033eb1
Add value mapping
EmperorValkorion Jun 11, 2026
9ab673d
Also parse NoteStore.sqlite in notes.py
EmperorValkorion Jun 12, 2026
a52abc4
Add FSEvents plugin
EmperorValkorion Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dissect/target/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
4 changes: 2 additions & 2 deletions dissect/target/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import plistlib
from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import TargetRecordDescriptor
from dissect.target.plugin import Plugin, export

if TYPE_CHECKING:
from collections.abc import Iterator

from dissect.target import Target

AirportPreferencesRecord = TargetRecordDescriptor(
"macos/airport_preferences",
[
("varint", "counter"),
("string", "device_uuid"),
("string", "version"),
("string", "preferred_order"),
("path", "source"),
],
)


class AirportPreferencesPlugin(Plugin):
"""macOS AirPort (WiFi) preferences plugin."""

PATH = "/Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist"

def __init__(self, target: Target):
super().__init__(target)
self.file = None
self._resolve_file()

def _resolve_file(self) -> None:
path = self.target.fs.path(self.PATH)
if path.exists():
self.file = path

def check_compatible(self) -> None:
if not self.file:
raise UnsupportedPluginError("No com.apple.airport.preferences.plist file found")

@export(record=AirportPreferencesRecord)
def airport_preferences(self) -> Iterator[AirportPreferencesRecord]:
"""Yield AirPort preference information."""
plist = plistlib.load(self.file.open())

counter = plist.get("Counter")
version = plist.get("Version")
device_uuid = plist.get("DeviceUUID")
preferred_order = plist.get("PreferredOrder")

yield AirportPreferencesRecord(
counter=counter,
device_uuid=device_uuid,
version=version,
preferred_order=preferred_order,
source=self.file,
_target=self.target,
)
Comment thread
EmperorValkorion marked this conversation as resolved.
107 changes: 107 additions & 0 deletions dissect/target/plugins/os/unix/bsd/darwin/macos/authorization_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import TargetRecordDescriptor
from dissect.target.plugin import Plugin, export
from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_sqlite_records

if TYPE_CHECKING:
from collections.abc import Iterator

from dissect.target import Target

AuthorizationRulesRecord = TargetRecordDescriptor(
"macos/authorization_rules",
[
("string[]", "tables"),
("varint", "rules_id"),
("string", "rules_name"),
("varint", "rules_type"),
("varint", "rules_class"),
("string", "rules_group"),
("varint", "rules_kofn"),
("varint", "rules_timeout"),
("varint", "rules_flags"),
("string", "rules_tries"),
("varint", "rules_version"),
("datetime", "rules_created"),
("datetime", "rules_modified"),
("string", "rules_hash"),
("string", "rules_identifier"),
("string", "rules_requirement"),
("string", "rules_comment"),
("string[]", "rules_delegates_map"),
("datetime", "rules_history_timestamp"),
("string", "rules_history_source"),
("varint", "rules_history_operation"),
("varint", "mechanisms_map_m_id"),
("varint", "mechanisms_map_ord"),
("string", "mechanisms_plugin"),
("string", "mechanisms_param"),
("varint", "mechanisms_privileged"),
("path", "source"),
],
)

ConfigTableRecord = TargetRecordDescriptor(
"macos/authorization_rules/config",
[
("string", "table"),
("string", "key"),
("string", "value"),
("path", "source"),
],
)

SQLiteSequenceTableRecord = TargetRecordDescriptor(
"macos/authorization_rules/sqlite_sequence",
[
("string", "table"),
("string", "name"),
("varint", "seq"),
("path", "source"),
],
)

AuthorizationRulesRecords = (
AuthorizationRulesRecord,
ConfigTableRecord,
SQLiteSequenceTableRecord,
)

joins = (
{"table1": "rules", "key1": "name", "table2": "rules_history", "key2": "rule", "join": "iterate"},
{"table1": "rules", "key1": "version", "table2": "rules_history", "key2": "version", "join": "ignore"},
{"table1": "rules", "key1": "id", "table2": "mechanisms_map", "key2": "r_id", "join": "iterate"},
{"table1": "mechanisms_map", "key1": "m_id", "table2": "mechanisms", "key2": "id", "join": "iterate"},
{"table1": "rules", "key1": "id", "table2": "delegates_map", "key2": "r_id", "join": "nested"},
)


class AuthorizationRulesPlugin(Plugin):
"""macOS authorization rules plugin."""

PATH = "/var/db/auth.db"

def __init__(self, target: Target):
super().__init__(target)
self.file = None
self._resolve_file()

def _resolve_file(self) -> None:
path = self.target.fs.path(self.PATH)
if path.exists():
self.file = path

def check_compatible(self) -> None:
if not self.file:
raise UnsupportedPluginError("No auth.db file found")

@export(record=AuthorizationRulesRecords)
def authorization_rules(self) -> Iterator[AuthorizationRulesRecords]:
"""Yield authorization rules information."""
yield from build_sqlite_records(self, (self.file,), AuthorizationRulesRecords, joins)

# Still missing prompts & buttons tables
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import TargetRecordDescriptor
from dissect.target.plugin import Plugin, export
from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records

if TYPE_CHECKING:
from collections.abc import Iterator

from dissect.target.target import Target

re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]")

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"),
],
)

CDHashRecord = TargetRecordDescriptor(
"macos/code_signature_coderesources/cdhash",
[
("string", "cdhash"),
("string", "requirement"),
("string", "plist_path"),
("path", "source"),
],
)


CodeSignatureCodeResourcesRecords = (
OmitRecord,
NestedRecord,
CDHashRecord,
)


class CodeSignatureCodeResourcesPlugin(Plugin):
"""macOS Code signature CodeResources plugin."""

PATHS = (
"/Applications/Utilities/*.app/Contents/_CodeSignature/CodeResources",
Comment thread
EmperorValkorion marked this conversation as resolved.
Outdated
"/System/Library/CoreServices/*.app/Contents/_CodeSignature/CodeResources",
"/System/Library/Extensions/*.kext/Contents/_CodeSignature/CodeResources",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/_CodeSignature/CodeResources",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/_CodeSignature/CodeResources",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources",
"/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/_CodeSignature/CodeResources",
"/System/Library/Filesystems/*/*.kext/Contents/_CodeSignature/CodeResources",
"/System/Library/Filesystems/*/Encodings/*.kext/Contents/_CodeSignature/CodeResource",
"/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/_CodeSignature/CodeResources",
)

def __init__(self, target: Target):
super().__init__(target)
self.files = set()
self._find_files()

def _find_files(self) -> None:
for pattern in self.PATHS:
for path in self.target.fs.glob(pattern):
self.files.add(path)

def check_compatible(self) -> None:
if not (self.files):
raise UnsupportedPluginError("No code signature coderesources files found")

@export(record=CodeSignatureCodeResourcesRecords)
def code_signature_coderesources(self) -> Iterator[CodeSignatureCodeResourcesRecords]:
"""Yield code signature coderesources information."""
yield from build_plist_records(self, self.files, CodeSignatureCodeResourcesRecords)
55 changes: 55 additions & 0 deletions dissect/target/plugins/os/unix/bsd/darwin/macos/contents_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.record import DynamicDescriptor
from dissect.target.plugin import Plugin, export
from dissect.target.plugins.os.unix.bsd.darwin.macos.helpers.build_records import build_plist_records

if TYPE_CHECKING:
from collections.abc import Iterator

from dissect.target.target import Target

re_illegal_characters = re.compile(r"[\(\): \.\-#\/\>\<]")
Comment thread
EmperorValkorion marked this conversation as resolved.
Outdated


class ContentsInfoPlugin(Plugin):
"""macOS contents info plugin."""

PATHS = (
"/Applications/*/*.app/Contents/Info.plist",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about apps in /Applications/*.app?

Might be nice to add a utility for iterating applications and kexts. It could yield paths for every .app and .kext it finds (within known locations), as well as perform some basic nested plugin/bundle checks for each one. That way, you don't end up with an evergrowing list of glob patterns scattered over different plugins. Plugins that act on these packages would then just look into path / "Contents/Info.plist" for every path the helper returns.

I.e. search within certain paths for *.app, .kext, .framework (/Applications, /System/Library/CoreServices, /System//Library/Extensions etc), then for each of those paths, recursively check for Contents/PlugIns and Contents/Resources and additional apps, kexts, frameworks, plugins and bundles in here.

A applications plugin could utilize this information too (as we also have them for Linux and Windows).

Some suggestions:

  • I forget the exact nomenclature, but I believe Apple generally calls these "bundles", so you could make a bundles.py
  • Within there, you could add some exports (or @internal, which are plugin functions that are solely meant to be used by other plugins, not tools like target-query) for some of the individual types of bundles, and something to iterate over them all
  • This plugin and e.g. the code signature one would simple iterate over the results of that plugin

@EmperorValkorion EmperorValkorion May 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have added a find_bundle_files helper function in macos/helpers/build_paths.py, but I'm not quite satisfied with the performance yet.

"/Applications/*/*.app/Contents/Resources/*.help/Contents/Info.plist",
"/System/Library/CoreServices/*.app/Contents/Info.plist",
"/System/Library/Extensions/*.kext/Contents/Info.plist",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Info.plist",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/PlugIns/*.plugin/Contents/Info.plist",
"/System/Library/Extensions/*.kext/Contents/PlugIns/*.kext/Contents/Resources/*.bundle/Contents/Info.plist",
"/System/Library/Extensions/*.kext/Contents/Resources/*.bundle/Contents/Info.plist",
"/System/Library/Extensions/*.kext/PlugIns/*.kext/Info.plist",
"/System/Library/Filesystems/*/*.kext/Contents/Info.plist",
"/System/Library/Filesystems/*/Encodings/*.kext/Contents/Info.plist",
"/System/Library/Frameworks/*.framework/Versions/A/Resources/Info.plist",
"/System/Library/PrivateFrameworks/*.framework/Versions/A/Resources/*.kext/Contents/Info.plist",
)

def __init__(self, target: Target):
super().__init__(target)
self.files = set()
self._find_files()

def _find_files(self) -> None:
for pattern in self.PATHS:
for path in self.target.fs.glob(pattern):
self.files.add(path)

def check_compatible(self) -> None:
if not (self.files):
raise UnsupportedPluginError("No contents info files found")

@export(record=DynamicDescriptor(["string"]))
def contents_info(self) -> Iterator[DynamicDescriptor]:
"""Yield contents info information."""
yield from build_plist_records(self, self.files, function_name="macos/contents_info")
Loading