Merge pull request #954 from SomeoneUnlicensed/fix/windows-tests

Make the test suite pass on Windows
This commit is contained in:
besendorf authored and GitHub committed 2026-09-28 16:01:08 +02:00
commit 22d97f0a88
9 files changed
+82 -24

No files matched your search

+6 -2
View File
@@ -82,7 +82,9 @@ class Filesystem(IOSExtraction):
try:
dir_path = os.path.join(root, dir_name)
result = {
"path": os.path.relpath(dir_path, self.target_path),
"path": os.path.relpath(dir_path, self.target_path).replace(
os.sep, "/"
),
"modified": convert_unix_to_iso(os.stat(dir_path).st_mtime),
}
except Exception:
@@ -94,7 +96,9 @@ class Filesystem(IOSExtraction):
try:
file_path = os.path.join(root, file_name)
result = {
"path": os.path.relpath(file_path, self.target_path),
"path": os.path.relpath(file_path, self.target_path).replace(
os.sep, "/"
),
"modified": convert_unix_to_iso(os.stat(file_path).st_mtime),
}
except Exception:
+2 -1
View File
@@ -1,3 +1,4 @@
import shlex
from types import SimpleNamespace
import click
@@ -110,7 +111,7 @@ def test_load_command_option_supports_folders_and_repeated_paths(tmp_path):
def test_loaded_command_participates_in_shell_completion(tmp_path):
command_path = _write_command(tmp_path / "hello.py", "hello")
group = _make_group()
words = f"group --load-command {command_path} he"
words = f"group --load-command {shlex.quote(str(command_path))} he"
result = CliRunner().invoke(
group,
+12 -8
View File
@@ -1,4 +1,5 @@
import json
import re
from types import SimpleNamespace
import pytest
@@ -54,20 +55,23 @@ def _run(command, arguments):
return CliRunner().invoke(command, arguments, env={"COLUMNS": "200"})
def _table_rows(output):
"""Return the content of the table rows, without the header and the box."""
def _table_lines(output):
"""Return the cells of each line of the table, header first."""
return [
[cell.strip() for cell in line.strip().strip("│").split("│")]
[cell.strip() for cell in re.split("[│┃]", line.strip().strip("│┃"))]
for line in output.splitlines()
if "│" in line
if "│" in line or "┃" in line
]
def _table_rows(output):
"""Return the content of the table rows, without the header and the box."""
return _table_lines(output)[1:]
def _table_header(output):
for line in output.splitlines():
if "┃" in line:
return [cell.strip() for cell in line.strip().strip("┃").split("┃")]
return []
lines = _table_lines(output)
return lines[0] if lines else []
def _install(monkeypatch, distributions, entry_points):
+23 -11
View File
@@ -7,6 +7,7 @@ import logging
import threading
from pathlib import Path
import pytest
from Crypto.Cipher import AES
from mvt.ios.decrypt import DecryptBackup, MVTEncryptedBackup
@@ -81,7 +82,10 @@ def test_extract_file_by_id_copies_unencrypted_files(mocker, tmp_path):
assert output_path.read_bytes() == b"plain content"
def test_process_backup_rejects_unsafe_file_ids_and_destinations(mocker, tmp_path):
@pytest.mark.parametrize("with_symlink", [False, True], ids=["file-id", "symlink"])
def test_process_backup_rejects_unsafe_file_ids_and_destinations(
mocker, tmp_path, with_symlink
):
backup_path = tmp_path / "backup"
destination = tmp_path / "destination"
outside = tmp_path / "outside"
@@ -92,20 +96,27 @@ def test_process_backup_rejects_unsafe_file_ids_and_destinations(mocker, tmp_pat
safe_file_id = "ef" + "3" * 38
unsafe_file_id = "../../outside-file"
symlink_file_id = "ab" + "4" * 38
for file_id in (safe_file_id, symlink_file_id):
file_ids = [safe_file_id]
if with_symlink:
file_ids.append(symlink_file_id)
for file_id in file_ids:
source_path = backup_path / file_id[:2] / file_id
source_path.parent.mkdir(parents=True, exist_ok=True)
source_path.write_bytes(b"encrypted")
(destination / "ab").symlink_to(outside, target_is_directory=True)
if with_symlink:
try:
(destination / "ab").symlink_to(outside, target_is_directory=True)
except OSError:
pytest.skip("creating symbolic links is not permitted on this system")
cursor = mocker.MagicMock()
cursor.__iter__.return_value = iter(
[
(safe_file_id, "Domain", "safe", b"plist"),
(unsafe_file_id, "Domain", "unsafe", b"plist"),
(symlink_file_id, "Domain", "symlink", b"plist"),
]
)
records = [
(safe_file_id, "Domain", "safe", b"plist"),
(unsafe_file_id, "Domain", "unsafe", b"plist"),
]
if with_symlink:
records.append((symlink_file_id, "Domain", "symlink", b"plist"))
cursor.__iter__.return_value = iter(records)
cursor_context = mocker.MagicMock()
cursor_context.__enter__.return_value = cursor
@@ -124,7 +135,8 @@ def test_process_backup_rejects_unsafe_file_ids_and_destinations(mocker, tmp_pat
decryptor.process_backup()
assert (destination / safe_file_id[:2] / safe_file_id).read_bytes() == b"decrypted"
assert not (outside / symlink_file_id).exists()
if with_symlink:
assert not (outside / symlink_file_id).exists()
backup.extract_file_by_id.assert_called_once()
assert backup.extract_file_by_id.call_args.kwargs["file_id"] == safe_file_id
+5
View File
@@ -9,6 +9,8 @@ import plistlib
import shutil
import sqlite3
import pytest
from mvt.ios.modules.base import IOSExtraction
from mvt.ios.modules.fs.analytics import Analytics
@@ -45,6 +47,9 @@ def test_open_sqlite_reads_wal_without_modifying_evidence(tmp_path):
assert not os.path.exists(str(evidence_path) + "-shm")
@pytest.mark.skipif(
shutil.which("sqlite3") is None, reason="the recovery needs the sqlite3 binary"
)
def test_recovery_preserves_source_database(tmp_path):
database_path = tmp_path / "source.db"
conn = sqlite3.connect(database_path)
+30
View File
@@ -3,15 +3,45 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
import logging
import ntpath
from types import SimpleNamespace
from mvt.common.indicators import Indicators
from mvt.common.module import run_module
from mvt.ios.modules.fs import filesystem
from mvt.ios.modules.fs.filesystem import Filesystem
from ..utils import get_ios_backup_folder
class TestFilesystem:
def test_windows_paths_are_normalized_for_indicators(
self, monkeypatch, indicators_factory
):
m = Filesystem(target_path=r"C:\dump")
m.indicators = indicators_factory(
file_paths=["matched/directory"], processes=["matched"]
)
monkeypatch.setattr(
filesystem,
"os",
SimpleNamespace(
sep="\\",
path=ntpath,
walk=lambda _: [(r"C:\dump\matched", ["directory"], ["file.txt"])],
stat=lambda _: SimpleNamespace(st_mtime=0),
),
)
m.run()
m.check_indicators()
assert {result["path"] for result in m.results} == {
"matched/directory",
"matched/file.txt",
}
assert len(m.alertstore.alerts) == 3
def test_filesystem(self):
m = Filesystem(target_path=get_ios_backup_folder())
run_module(m)
+1 -1
View File
@@ -85,7 +85,7 @@ class TestCheckAndroidqfCommand:
def test_acquisition_context_falls_back_to_public_key_file(self, tmp_path):
data_path = tmp_path / "androidqf"
data_path.mkdir()
(data_path / "adb_host_key.pub").write_text("QUJDRA== acquisition@host\n")
(data_path / "adb_host_key.pub").write_bytes(b"QUJDRA== acquisition@host\n")
command = CmdAndroidCheckAndroidQF(target_path=str(data_path))
command.init()
+1 -1
View File
@@ -116,7 +116,7 @@ def test_archive_is_extracted_once_and_unsafe_members_are_skipped(tmp_path):
module = SysdiagnoseExtraction()
command.module_init(module)
assert module.tar is None
assert module.parent_path == str(extracted_path.parent)
assert Path(module.parent_path) == extracted_path.parent
finally:
command.finish()
+2
View File
@@ -43,6 +43,7 @@ class TestCompletionCommand:
def test_completion_install_updates_bashrc_once(self, tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
runner = CliRunner()
result = runner.invoke(mvt_cli, ["completion", "bash", "--install"])
@@ -67,6 +68,7 @@ class TestCompletionCommand:
self, tmp_path, monkeypatch
):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
runner = CliRunner()
result = runner.invoke(mvt_cli, ["completion", "fish", "--install"])