-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add plugin to parse recentlyused.xbel files from Linux desktops #715
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,141 @@ | ||||||||
from datetime import datetime | ||||||||
from typing import Iterator, Union | ||||||||
|
||||||||
from defusedxml import ElementTree | ||||||||
from flow.record import GroupedRecord | ||||||||
|
||||||||
from dissect.target.exceptions import UnsupportedPluginError | ||||||||
from dissect.target.helpers.record import TargetRecordDescriptor | ||||||||
from dissect.target.plugin import Plugin, export | ||||||||
|
||||||||
RecentlyUsedRecord = TargetRecordDescriptor( | ||||||||
"unix/linux/recentlyused", | ||||||||
[ | ||||||||
("datetime", "ts"), | ||||||||
("string", "user"), | ||||||||
("string", "source"), | ||||||||
("string", "href"), | ||||||||
("datetime", "added"), | ||||||||
("datetime", "modified"), | ||||||||
("datetime", "visited"), | ||||||||
("string", "mimetype"), | ||||||||
("string", "groups"), | ||||||||
("boolean", "private"), | ||||||||
], | ||||||||
) | ||||||||
|
||||||||
RecentlyUsedIconRecord = TargetRecordDescriptor( | ||||||||
"unix/linux/recentlyusedicon", | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
[ | ||||||||
("string", "type"), | ||||||||
("string", "href"), | ||||||||
("string", "name"), | ||||||||
], | ||||||||
) | ||||||||
|
||||||||
RecentlyUsedApplicationRecord = TargetRecordDescriptor( | ||||||||
"unix/linux/recentlyusedapplication", | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
[ | ||||||||
("datetime", "ts"), | ||||||||
("string", "name"), | ||||||||
("string", "exec"), | ||||||||
("varint", "count"), | ||||||||
], | ||||||||
) | ||||||||
ns = { | ||||||||
"bookmark": "http://www.freedesktop.org/standards/desktop-bookmarks", | ||||||||
"mime": "http://www.freedesktop.org/standards/shared-mime-info", | ||||||||
} | ||||||||
|
||||||||
|
||||||||
def parse_ts(ts): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you add typehints? |
||||||||
"""Parse timestamp format from xbel file""" | ||||||||
# datetime.fromisoformat() doesn´t support the trailing Z in python <= 3.10 | ||||||||
return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S.%fZ") | ||||||||
|
||||||||
|
||||||||
def parse_recentlyused_xbel(username, xbel_file): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
could you also add typehints? |
||||||||
with xbel_file.open() as fh: | ||||||||
et = ElementTree.fromstring(fh.read(), forbid_dtd=True) | ||||||||
for b in et.iter("bookmark"): | ||||||||
# The spec says there should always be exactly one. | ||||||||
# Ignore if there are fewer or more. | ||||||||
mimetypes = b.findall("./info/metadata/mime:mime-type", ns) | ||||||||
if mimetypes and len(mimetypes) == 1: | ||||||||
mimetype = mimetypes[0].get("type") | ||||||||
else: | ||||||||
mimetype = None | ||||||||
|
||||||||
# This is just a list of names, GroupedRecords seem overkill | ||||||||
groups = b.findall("./info/metadata/bookmark:groups/bookmark:group", ns) | ||||||||
group_list = ", ".join(group.text for group in groups) | ||||||||
|
||||||||
# There should be at most one "private" tag, but accept multiple | ||||||||
private_entries = b.findall("./info/metadata/bookmark:private", ns) | ||||||||
private = private_entries is not None and len(private_entries) > 0 | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe |
||||||||
|
||||||||
cur = RecentlyUsedRecord( | ||||||||
ts=parse_ts(b.get("visited")), | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the timestamp fields always available? cause if they are |
||||||||
user=username, | ||||||||
source=xbel_file, | ||||||||
href=b.get("href"), | ||||||||
added=parse_ts(b.get("added")), | ||||||||
modified=parse_ts(b.get("modified")), | ||||||||
visited=parse_ts(b.get("visited")), | ||||||||
mimetype=mimetype, | ||||||||
groups=group_list, | ||||||||
private=private, | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The That being said, I don't think |
||||||||
) | ||||||||
yield cur | ||||||||
|
||||||||
# Icon is optional, spec says at most one. | ||||||||
icons = b.findall("./info/metadata/bookmark:icon", ns) | ||||||||
if icons and len(icons) >= 1: | ||||||||
icon = icons[0] | ||||||||
iconrecord = RecentlyUsedIconRecord( | ||||||||
type=icon.get("type"), | ||||||||
href=icon.get("href"), | ||||||||
name=icon.get("name"), | ||||||||
) | ||||||||
yield GroupedRecord("unix/linux/recentlyused/grouped", [cur, iconrecord]) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
# Spec says there should be at least one application | ||||||||
apps = b.findall("./info/metadata/bookmark:applications/bookmark:application", ns) | ||||||||
for app in apps: | ||||||||
apprecord = RecentlyUsedApplicationRecord( | ||||||||
ts=parse_ts(app.get("modified")), | ||||||||
name=app.get("name"), | ||||||||
exec=app.get("exec"), | ||||||||
count=app.get("count"), | ||||||||
) | ||||||||
yield GroupedRecord("unix/linux/recentlyused/grouped", [cur, apprecord]) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
|
||||||||
class RecentlyUsedPlugin(Plugin): | ||||||||
"""Parse recently-used.xbel files on Gnome-based Linux Desktops. | ||||||||
|
||||||||
Based on the spec on https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/ | ||||||||
""" | ||||||||
|
||||||||
FILEPATH = ".local/share/recently-used.xbel" | ||||||||
|
||||||||
def __init__(self, target): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you add typehints? |
||||||||
super().__init__(target) | ||||||||
self.users_files = [] | ||||||||
for user_details in self.target.user_details.all_with_home(): | ||||||||
xbel_file = user_details.home_path.joinpath(self.FILEPATH) | ||||||||
if not xbel_file.exists(): | ||||||||
continue | ||||||||
self.users_files.append((user_details.user, xbel_file)) | ||||||||
|
||||||||
def check_compatible(self) -> None: | ||||||||
if not len(self.users_files): | ||||||||
raise UnsupportedPluginError("No recently-used.xbel files found") | ||||||||
|
||||||||
@export(record=RecentlyUsedRecord) | ||||||||
def recentlyused(self) -> Iterator[Union[RecentlyUsedRecord, GroupedRecord]]: | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
"""Parse recently-used.xbel files on Linux Desktops.""" | ||||||||
|
||||||||
for user, xbel_file in self.users_files: | ||||||||
for record in parse_recentlyused_xbel(user.name, xbel_file): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
yield record |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,22 @@ | ||||||
from flow.record.fieldtypes import datetime as dt | ||||||
|
||||||
from dissect.target.filesystem import VirtualFilesystem | ||||||
from dissect.target.plugins.os.unix.linux.recentlyused import RecentlyUsedPlugin | ||||||
from dissect.target.target import Target | ||||||
from tests._utils import absolute_path | ||||||
|
||||||
|
||||||
def test_recentlyused(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None: | ||||||
data_file = absolute_path("_data/plugins/os/unix/linux/recently-used.xbel") | ||||||
fs_unix.map_file("/home/user/.local/share/recently-used.xbel", data_file) | ||||||
target_unix_users.add_plugin(RecentlyUsedPlugin) | ||||||
|
||||||
results = list(target_unix_users.recentlyused()) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
assert len(results) == 15 | ||||||
assert results[0].user == "user" | ||||||
assert results[0].href == "file:///home/sjaak/.profile" | ||||||
assert results[0].ts == dt("2023-10-18 13:12:41.905277Z") | ||||||
assert results[0].added == dt("2023-10-18 13:12:41.905276Z") | ||||||
assert results[0].modified == dt("2023-10-18 13:14:09.483576Z") | ||||||
assert results[0].visited == dt("2023-10-18 13:12:41.905277Z") | ||||||
assert results[0].mimetype == "text/plain" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.