diff --git a/src/mvt/android/artifacts/dumpsys_battery_daily.py b/src/mvt/android/artifacts/dumpsys_battery_daily.py index c0f2c5e..2ccb6b7 100644 --- a/src/mvt/android/artifacts/dumpsys_battery_daily.py +++ b/src/mvt/android/artifacts/dumpsys_battery_daily.py @@ -18,18 +18,18 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult: action = record.get("action", "update") package_name = record["package_name"] - vers = record["vers"] + vers = record["version_code"] - if vers == "0": + if vers == 0: data = f"Recorded uninstall of package {package_name} (vers 0)" elif action == "downgrade": - prev_vers = record.get("previous_vers", "unknown") + prev_vers = record.get("previous_version_code", "unknown") data = f"Recorded downgrade of package {package_name} from vers {prev_vers} to vers {vers}" else: data = f"Recorded update of package {package_name} with vers {vers}" return { - "timestamp": record["from"], + "timestamp": record["period_start"], "module": self.__class__.__name__, "event": "battery_daily", "data": data, @@ -48,6 +48,7 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): continue def parse(self, output: str) -> None: + self.results = [] daily = None daily_updates: list[dict[str, Any]] = [] records: list[dict[str, Any]] = [] @@ -59,7 +60,10 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): timeframe = line[13:].strip() date_from, date_to = timeframe.strip(":").split(" to ", 1) - daily = {"from": date_from[0:10], "to": date_to[0:10]} + daily = { + "period_start": self._format_daily_timestamp(date_from), + "period_end": self._format_daily_timestamp(date_to), + } continue if not daily: @@ -70,21 +74,30 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): line = line.strip().replace("Update ", "") package_name, vers = line.split(" ", 1) - vers_nr = vers.split("=", 1)[1] + vers_raw = vers.split("=", 1)[1] + try: + version_code: int | str = int(vers_raw) + except ValueError: + version_code = vers_raw already_seen = False for update in daily_updates: - if package_name == update["package_name"] and vers_nr == update["vers"]: + if ( + package_name == update["package_name"] + and version_code == update["version_code"] + ): + update["occurrences"] += 1 already_seen = True break if not already_seen: update_record: dict[str, Any] = { "action": "update", - "from": daily["from"], - "to": daily["to"], + "period_start": daily["period_start"], + "period_end": daily["period_end"], "package_name": package_name, - "vers": vers_nr, + "version_code": version_code, + "occurrences": 1, } daily_updates.append(update_record) @@ -95,26 +108,31 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): self._detect_uninstalls_and_downgrades(records) self.results.extend(records) - def _detect_uninstalls_and_downgrades( - self, records: list[dict[str, Any]] - ) -> None: + @staticmethod + def _format_daily_timestamp(value: str) -> str: + if len(value) >= 19 and value[10] == "-": + return f"{value[:10]} {value[11:].replace('-', ':')}" + return value + + def _detect_uninstalls_and_downgrades(self, records: list[dict[str, Any]]) -> None: package_versions: dict[str, int] = {} for record in sorted( records, key=lambda record: ( - record["from"], - record["to"], + record["period_start"], + record["period_end"], record["package_name"], ), ): package_name = record["package_name"] - vers_nr = record["vers"] + vers_nr = record["version_code"] - if vers_nr == "0": + if vers_nr == 0: + record["action"] = "uninstall" self.alertstore.medium( f"Detected uninstall of package {package_name} (vers 0)", - record["from"], + record["period_start"], record, ) package_versions.pop(package_name, None) @@ -128,11 +146,11 @@ class DumpsysBatteryDailyArtifact(AndroidArtifact): previous_vers = package_versions.get(package_name) if previous_vers is not None and current_vers < previous_vers: record["action"] = "downgrade" - record["previous_vers"] = str(previous_vers) + record["previous_version_code"] = previous_vers self.alertstore.medium( f"Detected downgrade of package {package_name} " f"from vers {previous_vers} to vers {current_vers}", - record["from"], + record["period_start"], record, ) diff --git a/tests/android/test_artifact_dumpsys_battery_daily.py b/tests/android/test_artifact_dumpsys_battery_daily.py index dbb9c36..5f7f997 100644 --- a/tests/android/test_artifact_dumpsys_battery_daily.py +++ b/tests/android/test_artifact_dumpsys_battery_daily.py @@ -57,18 +57,19 @@ class TestDumpsysBatteryDailyArtifact: assert uninstall_alert.message == ( "Detected uninstall of package com.example.removed (vers 0)" ) - assert uninstall_alert.event_time == "2022-08-16" + assert uninstall_alert.event_time == "2022-08-16 15:56:39" assert uninstall_alert.event["package_name"] == "com.example.removed" - assert uninstall_alert.event["vers"] == "0" + assert uninstall_alert.event["version_code"] == 0 + assert uninstall_alert.event["action"] == "uninstall" assert downgrade_alert.level == AlertLevel.MEDIUM assert downgrade_alert.message == ( "Detected downgrade of package com.example.app from vers 10 to vers 9" ) - assert downgrade_alert.event_time == "2022-08-17" + assert downgrade_alert.event_time == "2022-08-17 15:56:39" assert downgrade_alert.event["package_name"] == "com.example.app" assert downgrade_alert.event["action"] == "downgrade" - assert downgrade_alert.event["previous_vers"] == "10" + assert downgrade_alert.event["previous_version_code"] == 10 def test_newest_first_update_is_not_reported_as_downgrade(self): dba = DumpsysBatteryDailyArtifact() @@ -107,7 +108,19 @@ class TestDumpsysBatteryDailyArtifact: assert downgrade_alert.event_time == "2026-01-10" assert downgrade_alert.event["package_name"] == "com.example.app" assert downgrade_alert.event["action"] == "downgrade" - assert downgrade_alert.event["previous_vers"] == "102" + assert downgrade_alert.event["previous_version_code"] == 102 + + def test_duplicate_updates_retain_occurrence_count(self): + dba = DumpsysBatteryDailyArtifact() + dba.parse( + """ Daily from 2026-01-10-01-02-03 to 2026-01-11-04-05-06: + Update com.example.app vers=12 + Update com.example.app vers=12 +""" + ) + + assert dba.results[0]["occurrences"] == 2 + assert dba.results[0]["period_start"] == "2026-01-10 01:02:03" def test_reinstall_after_uninstall_is_not_reported_as_downgrade(self): dba = DumpsysBatteryDailyArtifact()