Skip to content

Commit

Permalink
Add parsing of Firefox extensions to firefox plugin (#689)
Browse files Browse the repository at this point in the history
  • Loading branch information
M1ra1B0T authored Apr 26, 2024
1 parent 34c4aeb commit b1bcb0d
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 2 deletions.
72 changes: 72 additions & 0 deletions dissect/target/plugins/apps/browser/firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dissect.target.plugins.apps.browser.browser import (
GENERIC_COOKIE_FIELDS,
GENERIC_DOWNLOAD_RECORD_FIELDS,
GENERIC_EXTENSION_RECORD_FIELDS,
GENERIC_HISTORY_RECORD_FIELDS,
GENERIC_PASSWORD_RECORD_FIELDS,
BrowserPlugin,
Expand All @@ -43,6 +44,7 @@
except ImportError:
HAS_CRYPTO = False

FIREFOX_EXTENSION_RECORD_FIELDS = [("uri", "source_uri"), ("string[]", "optional_permissions")]

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -76,6 +78,10 @@ class FirefoxPlugin(BrowserPlugin):
"browser/firefox/download", GENERIC_DOWNLOAD_RECORD_FIELDS
)

BrowserExtensionRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"browser/firefox/extension", GENERIC_EXTENSION_RECORD_FIELDS + FIREFOX_EXTENSION_RECORD_FIELDS
)

BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"browser/firefox/password", GENERIC_PASSWORD_RECORD_FIELDS
)
Expand Down Expand Up @@ -305,6 +311,72 @@ def downloads(self) -> Iterator[BrowserDownloadRecord]:
except SQLError as e:
self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)

@export(record=BrowserExtensionRecord)
def extensions(self) -> Iterator[BrowserExtensionRecord]:
"""Return browser extension records for Firefox.
Yields BrowserExtensionRecord with the following fields::
ts_install (datetime): Extension install timestamp.
ts_update (datetime): Extension update timestamp.
browser (string): The browser from which the records are generated.
id (string): Extension unique identifier.
name (string): Name of the extension.
short_name (string): Short name of the extension.
default_title (string): Default title of the extension.
description (string): Description of the extension.
version (string): Version of the extension.
ext_path (path): Relative path of the extension.
from_webstore (boolean): Extension from webstore.
permissions (string[]): Permissions of the extension.
manifest (varint): Version of the extensions' manifest.
optional_permissions (string[]): Optional permissions of the extension.
source_uri (path): Source path from which the extension was downloaded.
source (path): The source file of the download record.
"""
for user, _, profile_dir in self._iter_profiles():
extension_file = profile_dir.joinpath("extensions.json")

if not extension_file.exists():
self.target.log.warning(
"No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
)
continue

try:
extensions = json.load(extension_file.open())

for extension in extensions.get("addons", []):
yield self.BrowserExtensionRecord(
ts_install=extension.get("installDate", 0) // 1000,
ts_update=extension.get("updateDate", 0) // 1000,
browser="firefox",
id=extension.get("id"),
name=extension.get("defaultLocale", {}).get("name"),
short_name=None,
default_title=None,
description=extension.get("defaultLocale", {}).get("description"),
version=extension.get("version"),
ext_path=extension.get("path"),
from_webstore=None,
permissions=extension.get("userPermissions", {}).get("permissions"),
manifest_version=extension.get("manifestVersion"),
source_uri=extension.get("sourceURI"),
optional_permissions=extension.get("optionalPermissions", {}).get("permissions"),
source=extension_file,
_target=self.target,
_user=user.user,
)

except FileNotFoundError:
self.target.log.info(
"No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
)
except json.JSONDecodeError:
self.target.log.warning(
"extensions.json file in directory %s is malformed, consider inspecting the file manually",
profile_dir,
)

@export(record=BrowserPasswordRecord)
def passwords(self) -> Iterator[BrowserPasswordRecord]:
"""Return Firefox browser password records.
Expand Down
3 changes: 3 additions & 0 deletions tests/_data/plugins/apps/browser/firefox/extensions.json
Git LFS file not shown
34 changes: 32 additions & 2 deletions tests/plugins/apps/browser/test_firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
)
from tests._utils import absolute_path

# NOTE: Missing extensions tests for Firefox.


@pytest.fixture
def target_firefox_win(target_win_users: Target, fs_win: VirtualFilesystem) -> Iterator[Target]:
Expand Down Expand Up @@ -109,6 +107,38 @@ def test_firefox_cookies(target_platform: Target, request: pytest.FixtureRequest
]


@pytest.mark.parametrize(
"target_platform",
["target_firefox_win", "target_firefox_unix"],
)
def test_firefox_extensions(target_platform: Target, request: pytest.FixtureRequest) -> None:
target_platform = request.getfixturevalue(target_platform)

records = list(target_platform.firefox.extensions())

assert set(["firefox"]) == set(record.browser for record in records)
assert len(records) == 2
assert records[0].id == "[email protected]"
assert records[0].ts_install == dt("2024-04-23 07:07:21+00:00")
assert records[0].ts_update == dt("2024-04-23 07:07:21+00:00")
assert (
records[0].ext_path == "C:\\Users\\Win11\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles"
"\\9nxit8q0.default-release\\extensions\\[email protected]"
)
assert records[0].permissions == [
"alarms",
"dns",
"menus",
"privacy",
"storage",
"tabs",
"unlimitedStorage",
"webNavigation",
"webRequest",
"webRequestBlocking",
]


@pytest.mark.parametrize(
"target_platform",
["target_firefox_win", "target_firefox_unix"],
Expand Down

0 comments on commit b1bcb0d

Please sign in to comment.