Files
mvt/tests/android/test_browser_history.py
T

224 lines
7.6 KiB
Python

# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2026 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import json
import sqlite3
import zipfile
from pathlib import Path
import pytest
from mvt.android.modules.androidqf.browser_history import (
BrowserHistory as AndroidQFBrowserHistory,
)
from mvt.android.modules.fs.browser_history import BrowserHistory as FSBrowserHistory
from mvt.android.cmd_check_fs import CmdAndroidCheckFS
CHROME_TIME = 13_348_540_800_000_000
URL = "https://example.org/path"
def create_history_database(path: Path, *, url: str = URL) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(path) as connection:
connection.executescript(
"""
CREATE TABLE urls (
id INTEGER PRIMARY KEY,
url TEXT,
title TEXT,
visit_count INTEGER,
typed_count INTEGER
);
CREATE TABLE visits (
id INTEGER PRIMARY KEY,
url INTEGER,
visit_time INTEGER,
from_visit INTEGER,
transition INTEGER
);
"""
)
connection.execute("INSERT INTO urls VALUES (1, ?, 'Example', 1, 0)", (url,))
connection.execute(
"INSERT INTO visits VALUES (7, 1, ?, 0, 805306368)",
(CHROME_TIME,),
)
def manifest(database_paths: list[tuple[str, str]]) -> dict:
return {
"schema_version": 1,
"status": "collected",
"databases": [
{
"browser": "Chrome",
"package": package,
"profile": "Default",
"device_path": f"/data/data/{package}/app_chrome/Default/History",
"archive_path": archive_path,
"sidecars": [],
}
for package, archive_path in database_paths
],
}
@pytest.mark.parametrize("use_zip", [False, True])
def test_androidqf_browser_history_directory_and_zip(tmp_path, use_zip):
source_database = tmp_path / "source" / "History"
create_history_database(source_database)
archive_path = "browser_history/com.android.chrome/Default/History"
manifest_data = manifest([("com.android.chrome", archive_path)])
module = AndroidQFBrowserHistory()
if use_zip:
acquisition_path = tmp_path / "acquisition.zip"
with zipfile.ZipFile(acquisition_path, "w") as archive:
archive.write(source_database, archive_path)
archive.writestr("browser_history/manifest.json", json.dumps(manifest_data))
with zipfile.ZipFile(acquisition_path) as archive:
module.from_zip(archive, archive.namelist())
module.run()
else:
acquisition = tmp_path / "acquisition"
database_path = acquisition / archive_path
database_path.parent.mkdir(parents=True)
database_path.write_bytes(source_database.read_bytes())
manifest_path = acquisition / "browser_history" / "manifest.json"
manifest_path.write_text(json.dumps(manifest_data))
files = [
path.relative_to(tmp_path).as_posix()
for path in acquisition.rglob("*")
if path.is_file()
]
module.from_dir(str(tmp_path), files)
module.run()
assert len(module.results) == 1
assert module.results[0]["url"] == URL
assert module.results[0]["browser"] == "Chrome"
assert module.results[0]["source_path"].endswith("/History")
module.collect_url_results()
module.to_timeline()
assert module.url_results[0]["url"] == URL
assert module.timeline[0]["event"] == "browser_history"
def test_androidqf_browser_history_isolates_malformed_database(tmp_path, caplog):
acquisition = tmp_path / "acquisition"
good_archive_path = "browser_history/com.android.chrome/Default/History"
bad_archive_path = "browser_history/com.brave.browser/Default/History"
create_history_database(acquisition / good_archive_path)
bad_path = acquisition / bad_archive_path
bad_path.parent.mkdir(parents=True)
bad_path.write_bytes(b"not sqlite")
manifest_path = acquisition / "browser_history" / "manifest.json"
manifest_path.write_text(
json.dumps(
manifest(
[
("com.brave.browser", bad_archive_path),
("com.android.chrome", good_archive_path),
]
)
)
)
module = AndroidQFBrowserHistory()
files = [
path.relative_to(tmp_path).as_posix()
for path in acquisition.rglob("*")
if path.is_file()
]
module.from_dir(str(tmp_path), files)
module.run()
assert [result["url"] for result in module.results] == [URL]
assert "Unable to parse browser history database" in caplog.text
def test_androidqf_browser_history_rejects_unsafe_manifest_path(tmp_path, caplog):
acquisition = tmp_path / "acquisition"
manifest_path = acquisition / "browser_history" / "manifest.json"
manifest_path.parent.mkdir(parents=True)
manifest_path.write_text(
json.dumps(manifest([("com.android.chrome", "browser_history/../secret")]))
)
module = AndroidQFBrowserHistory()
module.from_dir(str(tmp_path), [manifest_path.relative_to(tmp_path).as_posix()])
module.run()
assert module.results == []
assert "unsafe browser history archive path" in caplog.text
def test_filesystem_browser_history_reads_wal_only_visit(tmp_path):
database_path = tmp_path / "History"
connection = sqlite3.connect(database_path)
connection.execute("PRAGMA journal_mode=WAL")
connection.executescript(
"""
CREATE TABLE urls (
id INTEGER PRIMARY KEY,
url TEXT,
title TEXT,
visit_count INTEGER,
typed_count INTEGER
);
CREATE TABLE visits (
id INTEGER PRIMARY KEY,
url INTEGER,
visit_time INTEGER,
from_visit INTEGER,
transition INTEGER
);
"""
)
connection.commit()
connection.execute("PRAGMA wal_checkpoint(TRUNCATE)")
connection.execute("INSERT INTO urls VALUES (1, ?, 'WAL', 1, 0)", (URL,))
connection.execute(
"INSERT INTO visits VALUES (8, 1, ?, 0, 805306368)", (CHROME_TIME,)
)
connection.commit()
try:
assert Path(f"{database_path}-wal").stat().st_size > 0
module = FSBrowserHistory(target_path=str(database_path))
module.run()
finally:
connection.close()
assert [result["url"] for result in module.results] == [URL]
assert module.results[0]["browser"] == "Chromium"
def test_android_check_fs_finds_all_supported_browser_paths(tmp_path):
for index, relative_path in enumerate(
(
"data/data/com.android.chrome/app_chrome/Default/History",
"data/data/com.brave.browser/app_chrome/Default/History",
"data/data/com.microsoft.emmx/app_chrome/Default/History",
"data/data/com.sec.android.app.sbrowser/app_sbrowser/Default/History",
)
):
create_history_database(
tmp_path / relative_path, url=f"https://example.org/{index}"
)
command = CmdAndroidCheckFS(target_path=str(tmp_path), module_name="BrowserHistory")
command.run()
assert len(command.executed) == 1
assert {result["package"] for result in command.executed[0].results} == {
"com.android.chrome",
"com.brave.browser",
"com.microsoft.emmx",
"com.sec.android.app.sbrowser",
}
assert len(command.url_results) == 4
assert len(command.timeline) == 4