mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-22 15:18:26 +02:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3222bcd98f | ||
|
|
b3aa443819 | ||
|
|
aac41ec8c2 | ||
|
|
1e05f438d7 | ||
|
|
10f739d669 | ||
|
|
94e9cd3712 | ||
|
|
300cc26112 | ||
|
|
3c12e1a019 | ||
|
|
3a462e1e2c | ||
|
|
5169a46b59 | ||
|
|
5a129430ae | ||
|
|
190a5f119f | ||
|
|
02c52f43c8 |
No files matched your search
+1
-1
@@ -66,4 +66,4 @@ docker run --rm -it \
|
||||
ghcr.io/mvt-project/mvt:latest-ios lockdown info
|
||||
```
|
||||
|
||||
If you built the image from source, replace `ghcr.io/mvt-project/mvt` with `mvt`.
|
||||
If you built the image from source, replace `ghcr.io/mvt-project/mvt` with `mvt`.
|
||||
@@ -33,7 +33,12 @@ class DumpsysADBArtifact(AndroidArtifact):
|
||||
line = line.removesuffix(b"\r")
|
||||
# Track the level of indentation
|
||||
indent = len(line) - len(line.lstrip())
|
||||
if indent < cur_indent:
|
||||
# A closing brace is dedented from the block it closes, and the
|
||||
# brace handler below pops that block. Popping on the dedent as
|
||||
# well would close two levels for one brace. A blank line has no
|
||||
# indentation to compare: it ends a multiline value (handled
|
||||
# below) and is otherwise skipped.
|
||||
if indent < cur_indent and line.strip() not in (b"", b"}"):
|
||||
# If the current line is less indented than the previous one, back out
|
||||
if len(stack) <= 1:
|
||||
# Dedenting below the outermost level means this is not the
|
||||
@@ -72,6 +77,9 @@ class DumpsysADBArtifact(AndroidArtifact):
|
||||
stack.pop()
|
||||
current_dict = stack[-1]
|
||||
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if key == "}":
|
||||
if len(stack) <= 1:
|
||||
self.log.error(
|
||||
|
||||
@@ -255,19 +255,25 @@ class Settings(AndroidArtifact):
|
||||
dumpsys prints the change history without a year, so it is resolved
|
||||
against the time the section was dumped: the most recent matching date
|
||||
at or before that time.
|
||||
|
||||
The year is parsed together with the value, because strptime() would
|
||||
otherwise parse it in 1900, which has no 29 February. The most recent
|
||||
matching leap day can be eight years earlier when a century year is
|
||||
not a leap year and the current year's leap day is still in the future.
|
||||
"""
|
||||
if section_end is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
partial = datetime.strptime(value, "%m-%d %H:%M:%S.%f")
|
||||
timestamp = partial.replace(year=section_end.year)
|
||||
if timestamp > section_end:
|
||||
timestamp = partial.replace(year=section_end.year - 1)
|
||||
except ValueError:
|
||||
return None
|
||||
for year in range(section_end.year, section_end.year - 9, -1):
|
||||
try:
|
||||
timestamp = datetime.strptime(f"{year}-{value}", "%Y-%m-%d %H:%M:%S.%f")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return convert_datetime_to_iso(timestamp)
|
||||
if timestamp <= section_end:
|
||||
return convert_datetime_to_iso(timestamp)
|
||||
|
||||
return None
|
||||
|
||||
def _parse_history(
|
||||
self, line: str, section_end: Optional[datetime]
|
||||
|
||||
@@ -145,7 +145,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
||||
|
||||
# Add some extra metadata
|
||||
tombstone_dict["timestamp"] = self._parse_timestamp_string(
|
||||
tombstone_pb.timestamp
|
||||
tombstone_pb.timestamp, file_timestamp
|
||||
)
|
||||
tombstone_dict["file_name"] = file_name
|
||||
tombstone_dict["file_timestamp"] = convert_datetime_to_iso(file_timestamp)
|
||||
@@ -288,11 +288,21 @@ class TombstoneCrashArtifact(AndroidArtifact):
|
||||
|
||||
def _load_timestamp_line(self, line: str, tombstone: dict) -> bool:
|
||||
timestamp = line.split(":", 1)[1].strip()
|
||||
tombstone["timestamp"] = self._parse_timestamp_string(timestamp)
|
||||
tombstone["timestamp"] = self._parse_timestamp_string(timestamp, None)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _parse_timestamp_string(timestamp: str) -> str:
|
||||
def _parse_timestamp_string(
|
||||
timestamp: str, fallback_timestamp: Optional[datetime.datetime]
|
||||
) -> str:
|
||||
"""Parse timestamp string, using fallback if timestamp is empty."""
|
||||
# Handle empty or whitespace-only timestamps
|
||||
if not timestamp or not timestamp.strip():
|
||||
if fallback_timestamp:
|
||||
return convert_datetime_to_iso(fallback_timestamp)
|
||||
else:
|
||||
raise ValueError("Empty timestamp with no fallback provided")
|
||||
|
||||
timestamp_parsed = parser.parse(timestamp)
|
||||
return convert_datetime_to_iso(timestamp_parsed)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from mvt.common.module import MVTModule
|
||||
|
||||
from .dumpsys_accessibility import DumpsysAccessibility
|
||||
from .dumpsys_activities import DumpsysActivities
|
||||
from .dumpsys_adb_state import DumpsysADBState
|
||||
from .dumpsys_appops import DumpsysAppops
|
||||
from .dumpsys_battery_daily import DumpsysBatteryDaily
|
||||
from .dumpsys_battery_history import DumpsysBatteryHistory
|
||||
@@ -15,7 +16,6 @@ from .dumpsys_getprop import DumpsysGetProp
|
||||
from .dumpsys_packages import DumpsysPackages
|
||||
from .dumpsys_platform_compat import DumpsysPlatformCompat
|
||||
from .dumpsys_receivers import DumpsysReceivers
|
||||
from .dumpsys_adb_state import DumpsysADBState
|
||||
from .fs_timestamps import BugReportTimestamps
|
||||
from .tombstones import Tombstones
|
||||
from .mounts import Mounts
|
||||
|
||||
@@ -68,6 +68,11 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule):
|
||||
}
|
||||
try:
|
||||
data = self._get_file_content(file_path)
|
||||
if not data:
|
||||
self.log.debug("Skipping empty tombstone file %s (0 bytes)", file_path)
|
||||
source_info["error"] = "empty tombstone"
|
||||
parsed_sources[source] = source_info
|
||||
continue
|
||||
if source == "protobuf":
|
||||
record = self.parse_protobuf_record(
|
||||
file_name, file_timestamp, data
|
||||
|
||||
@@ -209,6 +209,85 @@ class TestDumpsysADBArtifact:
|
||||
assert len(da_adb.results) == 1
|
||||
assert da_adb.results[0]["user_keys"][0]["user"] == user.decode()
|
||||
|
||||
def test_state_without_keys_is_parsed_without_error(self, caplog):
|
||||
# Shape printed by every device with no stored ADB keys (Samsung
|
||||
# SM-A525F, Android 12). The block's closing brace is dedented from
|
||||
# the last entry and must close exactly one level.
|
||||
da_adb = DumpsysADBArtifact()
|
||||
da_adb.parse(
|
||||
b"ADB MANAGER STATE (dumpsys adb):\n"
|
||||
b"{\n"
|
||||
b" debugging_manager={\n"
|
||||
b" connected_to_adb=false\n"
|
||||
b" }\n"
|
||||
b"}\n"
|
||||
b"--------- 0.015s was the duration of dumpsys adb\n"
|
||||
)
|
||||
|
||||
assert len(da_adb.results) == 1
|
||||
assert da_adb.results[0]["connected_to_adb"] is False
|
||||
assert not [r for r in caplog.records if r.levelname == "ERROR"]
|
||||
|
||||
def test_nested_block_as_last_entry_is_parsed_without_error(self, caplog):
|
||||
# Shape printed by Pixel devices with no stored ADB keys.
|
||||
da_adb = DumpsysADBArtifact()
|
||||
da_adb.parse(
|
||||
b"ADB MANAGER STATE (dumpsys adb):\n"
|
||||
b"{\n"
|
||||
b" debugging_manager={\n"
|
||||
b" connected_to_adb=false\n"
|
||||
b" adb_wifi={\n"
|
||||
b" enabled=false\n"
|
||||
b" tls_port=0\n"
|
||||
b" }\n"
|
||||
b" }\n"
|
||||
b"}\n"
|
||||
)
|
||||
|
||||
assert len(da_adb.results) == 1
|
||||
assert da_adb.results[0]["connected_to_adb"] is False
|
||||
assert da_adb.results[0]["adb_wifi"]["tls_port"] == b"0"
|
||||
assert not [r for r in caplog.records if r.levelname == "ERROR"]
|
||||
|
||||
def test_entry_after_nested_block_stays_in_debugging_manager(self):
|
||||
da_adb = DumpsysADBArtifact()
|
||||
da_adb.parse(
|
||||
b"ADB MANAGER STATE (dumpsys adb):\n"
|
||||
b"{\n"
|
||||
b" debugging_manager={\n"
|
||||
b" adb_wifi={\n"
|
||||
b" enabled=false\n"
|
||||
b" }\n"
|
||||
b" connected_to_adb=true\n"
|
||||
b" }\n"
|
||||
b"}\n"
|
||||
)
|
||||
|
||||
assert len(da_adb.results) == 1
|
||||
assert da_adb.results[0]["connected_to_adb"] is True
|
||||
|
||||
def test_blank_line_after_single_line_value_does_not_raise(self):
|
||||
# `system_keys` is the content of /adb_keys and ends with a newline,
|
||||
# so a blank line follows it. It is not a multiline field, so the
|
||||
# blank line is not a multiline terminator either.
|
||||
da_adb = DumpsysADBArtifact()
|
||||
da_adb.parse(
|
||||
b"ADB MANAGER STATE (dumpsys adb):\n"
|
||||
b"{\n"
|
||||
b" debugging_manager={\n"
|
||||
b" connected_to_adb=true\n"
|
||||
b" user_keys=QUJDRA== host@example\n"
|
||||
b"\n"
|
||||
b" system_keys=RUZHSA== system@example\n"
|
||||
b"\n"
|
||||
b" }\n"
|
||||
b"}\n"
|
||||
)
|
||||
|
||||
assert len(da_adb.results) == 1
|
||||
assert da_adb.results[0]["user_keys"][0]["user"] == "host@example"
|
||||
assert da_adb.results[0]["system_keys"] == "RUZHSA== system@example"
|
||||
|
||||
def test_unbalanced_state_is_reported_rather_than_raising(self):
|
||||
da_adb = DumpsysADBArtifact()
|
||||
da_adb.parse(
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||
# https://license.mvt.re/1.1/
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from mvt.android.artifacts.settings import Settings
|
||||
|
||||
from ..utils import get_artifact
|
||||
@@ -21,6 +23,21 @@ def find(settings: Settings, name: str) -> list:
|
||||
return [result for result in settings.results if result["name"] == name]
|
||||
|
||||
|
||||
def resolve_history_time(time: str, section_end: str) -> Optional[str]:
|
||||
settings = Settings()
|
||||
settings.parse(
|
||||
"SECURE SETTINGS (user 0)\n"
|
||||
"_id:240 name:accessibility_enabled pkg:android value:1\n"
|
||||
"\tHistory (accessibility_enabled)\n"
|
||||
f"\t\ttime:{time} mode:update oldValue:0 newValue:1 "
|
||||
"package:com.example.helper\n"
|
||||
"\n"
|
||||
"--------- 0.019s was the duration of dumpsys settings, "
|
||||
f"ending at: {section_end}\n"
|
||||
)
|
||||
return settings.results[0]["history"][0]["timestamp"]
|
||||
|
||||
|
||||
class TestSettingsArtifact:
|
||||
def test_parsing(self):
|
||||
settings = parse_bugreport_settings()
|
||||
@@ -159,6 +176,32 @@ class TestSettingsArtifact:
|
||||
}
|
||||
]
|
||||
|
||||
def test_leap_day_history_keeps_its_timestamp(self):
|
||||
assert (
|
||||
resolve_history_time("02-29 22:41:07.980", "2024-03-05 23:14:28")
|
||||
== "2024-02-29 22:41:07.980000"
|
||||
)
|
||||
|
||||
def test_leap_day_history_resolved_to_the_last_leap_year(self):
|
||||
# The most recent 29 February at or before the dump can be several
|
||||
# years back when the dump was taken in a year without one.
|
||||
assert (
|
||||
resolve_history_time("02-29 22:41:07.980", "2025-03-01 10:00:00")
|
||||
== "2024-02-29 22:41:07.980000"
|
||||
)
|
||||
assert (
|
||||
resolve_history_time("02-29 22:41:07.980", "2024-01-10 10:00:00")
|
||||
== "2020-02-29 22:41:07.980000"
|
||||
)
|
||||
|
||||
def test_leap_day_history_before_a_leap_day_after_a_skipped_century(self):
|
||||
# 2100 is not a leap year, so before 29 February 2104 the most
|
||||
# recent matching date is eight years earlier, in 2096.
|
||||
assert (
|
||||
resolve_history_time("02-29 22:41:07.980", "2104-01-10 10:00:00")
|
||||
== "2096-02-29 22:41:07.980000"
|
||||
)
|
||||
|
||||
def test_dangerous_setting_is_detected_with_the_changing_package(self):
|
||||
settings = parse_bugreport_settings()
|
||||
settings.check_indicators()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Copyright (c) 2021-2023 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 os
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -130,3 +130,83 @@ class TestTombstoneCrashArtifact:
|
||||
|
||||
# Tombstones include an explicit offset, so normalize them to UTC.
|
||||
assert tombstone_result.get("timestamp") == "2023-04-12 10:32:40.518290"
|
||||
|
||||
def test_tombstone_pb_empty_timestamp(self):
|
||||
"""Test parsing a protobuf tombstone with an empty timestamp."""
|
||||
tombstone_artifact = TombstoneCrashArtifact()
|
||||
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_timestamp.pb"
|
||||
file = get_artifact(artifact_path)
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
file_name = os.path.basename(artifact_path)
|
||||
file_timestamp = datetime.datetime(2024, 1, 15, 10, 30, 45, 123456)
|
||||
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
|
||||
|
||||
assert len(tombstone_artifact.results) == 1
|
||||
result = tombstone_artifact.results[0]
|
||||
|
||||
# When tombstone has empty timestamp, should use file modification time
|
||||
assert result.get("timestamp") == "2024-01-15 10:30:45.123456"
|
||||
assert result.get("pid") == 12345
|
||||
assert result.get("uid") == 1000
|
||||
assert result.get("signal_info", {}).get("name") == "SIGSEGV"
|
||||
|
||||
def test_tombstone_pb_empty_timestamp_with_threads(self):
|
||||
"""Test parsing a protobuf tombstone with empty timestamp and thread info."""
|
||||
tombstone_artifact = TombstoneCrashArtifact()
|
||||
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_timestamp_with_threads.pb"
|
||||
file = get_artifact(artifact_path)
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
file_name = os.path.basename(artifact_path)
|
||||
file_timestamp = datetime.datetime(2024, 2, 20, 14, 15, 30, 0)
|
||||
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
|
||||
|
||||
assert len(tombstone_artifact.results) == 1
|
||||
result = tombstone_artifact.results[0]
|
||||
|
||||
# Verify timestamp fallback
|
||||
assert result.get("timestamp") == "2024-02-20 14:15:30.000000"
|
||||
assert result.get("pid") == 9876
|
||||
assert result.get("uid") == 10001
|
||||
assert result.get("signal_info", {}).get("name") == "SIGABRT"
|
||||
assert result.get("process_name") == "ExampleThread"
|
||||
|
||||
def test_tombstone_pb_whitespace_timestamp(self):
|
||||
"""Test parsing a protobuf tombstone with whitespace-only timestamp."""
|
||||
tombstone_artifact = TombstoneCrashArtifact()
|
||||
artifact_path = (
|
||||
"android_data/bugreport_tombstones/tombstone_whitespace_timestamp.pb"
|
||||
)
|
||||
file = get_artifact(artifact_path)
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
file_name = os.path.basename(artifact_path)
|
||||
file_timestamp = datetime.datetime(2024, 3, 10, 8, 0, 0, 0)
|
||||
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
|
||||
|
||||
assert len(tombstone_artifact.results) == 1
|
||||
result = tombstone_artifact.results[0]
|
||||
|
||||
# Verify whitespace timestamp is treated as empty
|
||||
assert result.get("timestamp") == "2024-03-10 08:00:00.000000"
|
||||
assert result.get("pid") == 11111
|
||||
assert result.get("uid") == 2000
|
||||
assert result.get("signal_info", {}).get("name") == "SIGILL"
|
||||
|
||||
def test_tombstone_pb_empty_file(self):
|
||||
"""Test that empty (0 bytes) tombstone files are handled gracefully."""
|
||||
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_file.pb"
|
||||
file = get_artifact(artifact_path)
|
||||
with open(file, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Verify the file is actually empty
|
||||
assert len(data) == 0, "Test file should be empty (0 bytes)"
|
||||
|
||||
# Empty files should be skipped in the module (not parsed)
|
||||
# The actual skipping happens in the Tombstones module's run() method
|
||||
# This test verifies that empty data is detectable
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
5test/build/fingerprint:12/TEST/V1.0:user/release-keystest-rev-001(¹`0º`8èBu:r:system_app:s0J/system/bin/test_process <RSIGSEGV"SEGV_MAPERR
|
||||
+1
@@ -0,0 +1 @@
|
||||
5test/build/fingerprint:13/TEST/V2.0:user/release-keystest-rev-002(”M0•M8‘NBu:r:untrusted_app:s0J-/data/app/com.example.app/lib/arm64/libapp.so xRSIGABRT"SI_TKILL‚•M•M␍ExampleThread
|
||||
@@ -0,0 +1,2 @@
|
||||
5test/build/fingerprint:11/TEST/V3.0:user/release-keystest-rev-003" (çV0èV8ÐBu:r:shell:s0J/system/bin/app_process64 RSIGILL"
|
||||
ILL_ILLOPN
|
||||
Reference in new issue
Block a user