Files
mvt/tests/test_cmd_check_sysdiagnose.py
T
Donncha Ó Cearbhaill 2eb40b85cf Add a SysdiagnoseInfo module to check-sysdiagnose (#917)
* Add a SysdiagnoseInfo module to check-sysdiagnose

check-sysdiagnose had no module of its own: it prepared the archive for
plugin modules and refused to run without one. SysdiagnoseInfo is the
first built-in module. It writes sysdiagnose_info.json with details
about the device and the archive: product type and model, iOS version
and build, serial number, IMEI, MEID and UDID from remotectl_dumpstate.txt
and the mobile activation request, the Apple account name and email from
the App Store daemon database, and the archive's original file name and
creation time from sysdiagnose.log. The build is checked against the
known iOS versions the way BackupInfo does.

The App Store database is copied out of the archive together with its
-wal and -shm sidecars before it is opened, so rows still in the
write-ahead log are read.

With a built-in module the command's list is never empty, so the "no
custom modules" error and its test go. The module joins
IOS_CHECK_IOCS_MODULES like every other module that writes a results
file.

* Note that newer sysdiagnoses lack the App Store daemon database

* Keep refusing check-sysdiagnose runs without a custom module

* Warn instead of refusing when no forensic sysdiagnose module is loaded
2026-09-05 23:45:14 +02:00

121 lines
3.9 KiB
Python

import io
import tarfile
from datetime import timedelta
from pathlib import Path
from mvt.ios.cmd_check_sysdiagnose import CmdIOSCheckSysdiagnose
from mvt.ios.modules.sysdiagnose import SysdiagnoseExtraction
class SysdiagnoseTestModule(SysdiagnoseExtraction):
supported_commands = (("ios", "check-sysdiagnose"),)
def run(self):
file_path = self._get_files_by_pattern("*/artifact.txt")[0]
self.results = [
{
"content": self._get_file_content(file_path).decode("utf-8"),
"timezone_offset": self._extract_timezone().utcoffset(None).seconds,
}
]
def check_indicators(self):
pass
def serialize(self, result):
return None
def _create_sysdiagnose_folder(tmp_path):
folder = tmp_path / "sysdiagnose"
folder.mkdir()
(folder / "artifact.txt").write_text("artifact", encoding="utf-8")
(folder / "sysdiagnose.log").write_text(
"sysdiagnose_2024.01.02_03-04-05+0200.tar.gz", encoding="utf-8"
)
(folder / "report.ips").write_text('{"bug_type": 210}\nbody', encoding="utf-8")
return folder
def _create_sysdiagnose_archive(tmp_path, folder):
archive_path = tmp_path / "sysdiagnose.tar.gz"
with tarfile.open(archive_path, "w:gz") as archive:
for path in folder.iterdir():
archive.add(path, arcname=f"sysdiagnose/{path.name}")
return archive_path
def _test_module(command):
(module,) = [m for m in command.executed if isinstance(m, SysdiagnoseTestModule)]
return module
def _run_command(path):
command = CmdIOSCheckSysdiagnose(
target_path=str(path), custom_modules=[SysdiagnoseTestModule]
)
command.run()
return command
def test_check_sysdiagnose_from_folder(tmp_path):
command = _run_command(_create_sysdiagnose_folder(tmp_path))
assert _test_module(command).results == [
{"content": "artifact", "timezone_offset": timedelta(hours=2).seconds}
]
assert _test_module(command).ips_files == [
{"file_path": str(tmp_path / "sysdiagnose" / "report.ips"), "bug_type": 210}
]
def test_check_sysdiagnose_from_archive_closes_archive(tmp_path):
folder = _create_sysdiagnose_folder(tmp_path)
command = _run_command(_create_sysdiagnose_archive(tmp_path, folder))
assert _test_module(command).results == [
{"content": "artifact", "timezone_offset": timedelta(hours=2).seconds}
]
assert _test_module(command).ips_files == [
{
"file_path": str(Path(command.extracted_sysdiagnose_path) / "report.ips"),
"bug_type": 210,
}
]
assert command.sysdiagnose_archive is None
def test_archive_is_extracted_once_and_unsafe_members_are_skipped(tmp_path):
archive_path = tmp_path / "sysdiagnose.tar.gz"
escaped_path = tmp_path / "escaped.txt"
content = b"test content"
member = tarfile.TarInfo("sysdiagnose/artifact.txt")
member.size = len(content)
with tarfile.open(archive_path, "w:gz") as archive:
archive.addfile(member, io.BytesIO(content))
escaped = tarfile.TarInfo(f"sysdiagnose/../../{escaped_path.name}")
escaped.size = len(content)
archive.addfile(escaped, io.BytesIO(content))
link = tarfile.TarInfo("sysdiagnose/link")
link.type = tarfile.SYMTYPE
link.linkname = "/etc/hostname"
archive.addfile(link)
command = CmdIOSCheckSysdiagnose(target_path=str(archive_path))
try:
command.init()
extracted_path = Path(command.extracted_sysdiagnose_path)
assert (extracted_path / "artifact.txt").read_bytes() == content
assert not escaped_path.exists()
assert not (extracted_path / "link").exists()
module = SysdiagnoseExtraction()
command.module_init(module)
assert module.tar is None
assert module.parent_path == str(extracted_path.parent)
finally:
command.finish()
assert not extracted_path.exists()