diff --git a/src/mvt/android/artifacts/settings.py b/src/mvt/android/artifacts/settings.py index 39257c06..8237ee34 100644 --- a/src/mvt/android/artifacts/settings.py +++ b/src/mvt/android/artifacts/settings.py @@ -255,19 +255,24 @@ 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. Going back eight + years always reaches a leap year. """ 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 - 8, -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] diff --git a/tests/android/test_artifact_settings.py b/tests/android/test_artifact_settings.py index 2192c12b..f114cab7 100644 --- a/tests/android/test_artifact_settings.py +++ b/tests/android/test_artifact_settings.py @@ -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,24 @@ 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_dangerous_setting_is_detected_with_the_changing_package(self): settings = parse_bugreport_settings() settings.check_indicators()