From b03c3280633c57f5d8e448b818ae63863621a86a Mon Sep 17 00:00:00 2001 From: vokalishoregmelankolis <5025231128@student.its.ac.id> Date: Wed, 10 Dec 2025 06:42:26 +0700 Subject: [PATCH] Fix database schema compatibility issues in multiple artifacts - deviceHealthServices_Battery.py: Fix undefined time_offset variable by using timezone from database row - FacebookMessenger.py: Add fallback for missing message_reactions.reaction_timestamp column - googleDuo.py: Add fallback for missing saved_status column - googleMessages.py: Add fallback for missing parts.file_size_bytes column - keepNotes.py: Add fallback for missing text_search_note_content_content table - notificationHistory.py: Add blackboxprotobuf fallback for protobuf parsing errors These fixes ensure backward compatibility across different Android app versions. --- scripts/artifacts/FacebookMessenger.py | 128 ++++++++---- .../artifacts/deviceHealthServices_Battery.py | 4 +- scripts/artifacts/googleDuo.py | 80 +++++--- scripts/artifacts/googleMessages.py | 76 ++++++-- scripts/artifacts/keepNotes.py | 49 +++-- scripts/artifacts/notificationHistory.py | 182 ++++++++++++++++-- 6 files changed, 403 insertions(+), 116 deletions(-) diff --git a/scripts/artifacts/FacebookMessenger.py b/scripts/artifacts/FacebookMessenger.py index b1b22c58..8d31b5b4 100755 --- a/scripts/artifacts/FacebookMessenger.py +++ b/scripts/artifacts/FacebookMessenger.py @@ -233,28 +233,52 @@ def get_FacebookMessenger(files_found, report_folder, seeker, wrap_text): ''') snippet = 1 except: - cursor.execute(''' - select - case messages.timestamp_ms - when 0 then '' - else datetime(messages.timestamp_ms/1000,'unixepoch') - End as Datestamp, - (select json_extract (messages.sender, '$.name')) as "Sender", - substr((select json_extract (messages.sender, '$.user_key')),10) as "Sender ID", - messages.thread_key, - messages.text, - (select json_extract (messages.attachments, '$[0].filename')) as AttachmentName, - (select json_extract (messages.shares, '$[0].name')) as ShareName, - (select json_extract (messages.shares, '$[0].description')) as ShareDesc, - (select json_extract (messages.shares, '$[0].href')) as ShareLink, - message_reactions.reaction as "Message Reaction", - datetime(message_reactions.reaction_timestamp/1000,'unixepoch') as "Message Reaction Timestamp", - messages.msg_id - from messages, threads - left join message_reactions on message_reactions.msg_id = messages.msg_id - where messages.thread_key=threads.thread_key and generic_admin_message_extensible_data IS NULL and msg_type != -1 - order by messages.thread_key, datestamp; - ''') + try: + cursor.execute(''' + select + case messages.timestamp_ms + when 0 then '' + else datetime(messages.timestamp_ms/1000,'unixepoch') + End as Datestamp, + (select json_extract (messages.sender, '$.name')) as "Sender", + substr((select json_extract (messages.sender, '$.user_key')),10) as "Sender ID", + messages.thread_key, + messages.text, + (select json_extract (messages.attachments, '$[0].filename')) as AttachmentName, + (select json_extract (messages.shares, '$[0].name')) as ShareName, + (select json_extract (messages.shares, '$[0].description')) as ShareDesc, + (select json_extract (messages.shares, '$[0].href')) as ShareLink, + message_reactions.reaction as "Message Reaction", + datetime(message_reactions.reaction_timestamp/1000,'unixepoch') as "Message Reaction Timestamp", + messages.msg_id + from messages, threads + left join message_reactions on message_reactions.msg_id = messages.msg_id + where messages.thread_key=threads.thread_key and generic_admin_message_extensible_data IS NULL and msg_type != -1 + order by messages.thread_key, datestamp; + ''') + except: + # Fallback for databases without reaction_timestamp column + cursor.execute(''' + select + case messages.timestamp_ms + when 0 then '' + else datetime(messages.timestamp_ms/1000,'unixepoch') + End as Datestamp, + (select json_extract (messages.sender, '$.name')) as "Sender", + substr((select json_extract (messages.sender, '$.user_key')),10) as "Sender ID", + messages.thread_key, + messages.text, + (select json_extract (messages.attachments, '$[0].filename')) as AttachmentName, + (select json_extract (messages.shares, '$[0].name')) as ShareName, + (select json_extract (messages.shares, '$[0].description')) as ShareDesc, + (select json_extract (messages.shares, '$[0].href')) as ShareLink, + NULL as "Message Reaction", + NULL as "Message Reaction Timestamp", + messages.msg_id + from messages, threads + where messages.thread_key=threads.thread_key and generic_admin_message_extensible_data IS NULL and msg_type != -1 + order by messages.thread_key, datestamp; + ''') all_rows = cursor.fetchall() usageentries = len(all_rows) @@ -323,25 +347,47 @@ def get_FacebookMessenger(files_found, report_folder, seeker, wrap_text): else: logfunc(f'No Facebook{typeof}- Calls{usernum} - threads_db2 data available') - cursor.execute(''' - select - substr(user_key,10), - first_name, - last_name, - username, - (select json_extract (profile_pic_square, '$[0].url')) as profile_pic_square, - case is_messenger_user - when 0 then '' - else 'Yes' - end is_messenger_user, - case is_friend - when 0 then 'No' - when 1 then 'Yes' - end is_friend, - friendship_status, - contact_relationship_status - from thread_users - ''') + try: + cursor.execute(''' + select + substr(user_key,10), + first_name, + last_name, + username, + (select json_extract (profile_pic_square, '$[0].url')) as profile_pic_square, + case is_messenger_user + when 0 then '' + else 'Yes' + end is_messenger_user, + case is_friend + when 0 then 'No' + when 1 then 'Yes' + end is_friend, + friendship_status, + contact_relationship_status + from thread_users + ''') + except: + # Fallback for databases without friendship_status or contact_relationship_status columns + cursor.execute(''' + select + substr(user_key,10), + first_name, + last_name, + username, + (select json_extract (profile_pic_square, '$[0].url')) as profile_pic_square, + case is_messenger_user + when 0 then '' + else 'Yes' + end is_messenger_user, + case is_friend + when 0 then 'No' + when 1 then 'Yes' + end is_friend, + NULL as friendship_status, + NULL as contact_relationship_status + from thread_users + ''') all_rows = cursor.fetchall() usageentries = len(all_rows) diff --git a/scripts/artifacts/deviceHealthServices_Battery.py b/scripts/artifacts/deviceHealthServices_Battery.py index 38f2c8df..c1a883e3 100644 --- a/scripts/artifacts/deviceHealthServices_Battery.py +++ b/scripts/artifacts/deviceHealthServices_Battery.py @@ -80,7 +80,7 @@ def Turbo_Battery(files_found, report_folder, seeker, wrap_text): if timestamp is None: pass else: - timestamp = convert_utc_human_to_timezone(convert_ts_human_to_utc(timestamp),time_offset) + timestamp = convert_utc_human_to_timezone(convert_ts_human_to_utc(timestamp), row[4]) data_list.append((timestamp,row[1],row[2],row[3],row[4],file_found)) db.close() @@ -121,7 +121,7 @@ def Turbo_Bluetooth(files_found, report_folder, seeker, wrap_text): if timestamp is None: pass else: - timestamp = convert_utc_human_to_timezone(convert_ts_human_to_utc(timestamp),time_offset) + timestamp = convert_utc_human_to_timezone(convert_ts_human_to_utc(timestamp), row[5]) data_list.append((timestamp,row[1],row[2],row[3],row[4],row[5],file_found)) db.close() diff --git a/scripts/artifacts/googleDuo.py b/scripts/artifacts/googleDuo.py index 2717ec5f..15444696 100755 --- a/scripts/artifacts/googleDuo.py +++ b/scripts/artifacts/googleDuo.py @@ -99,31 +99,57 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): else: logfunc('No Google Duo - Contacts data available') - cursor.execute(''' - select - case sent_timestamp_millis - when 0 then '' - else datetime(sent_timestamp_millis/1000,'unixepoch') - end as 'Sent Timestamp', - case received_timestamp_millis - when 0 then '' - else datetime(received_timestamp_millis/1000,'unixepoch') - end as 'Received Timestamp', - case seen_timestamp_millis - when 0 then '' - else datetime(seen_timestamp_millis/1000,'unixepoch') - end as 'Viewed Timestamp', - sender_id, - recipient_id, - content_uri, - replace(content_uri, rtrim(content_uri, replace(content_uri, '/', '')), '') as 'File Name', - content_size_bytes, - case saved_status - when 0 then '' - when 1 then 'Yes' - end as 'File Saved' - from messages - ''') + try: + cursor.execute(''' + select + case sent_timestamp_millis + when 0 then '' + else datetime(sent_timestamp_millis/1000,'unixepoch') + end as 'Sent Timestamp', + case received_timestamp_millis + when 0 then '' + else datetime(received_timestamp_millis/1000,'unixepoch') + end as 'Received Timestamp', + case seen_timestamp_millis + when 0 then '' + else datetime(seen_timestamp_millis/1000,'unixepoch') + end as 'Viewed Timestamp', + sender_id, + recipient_id, + content_uri, + replace(content_uri, rtrim(content_uri, replace(content_uri, '/', '')), '') as 'File Name', + content_size_bytes, + case saved_status + when 0 then '' + when 1 then 'Yes' + end as 'File Saved' + from messages + ''') + has_saved_status = True + except: + # Fallback for databases without saved_status column + cursor.execute(''' + select + case sent_timestamp_millis + when 0 then '' + else datetime(sent_timestamp_millis/1000,'unixepoch') + end as 'Sent Timestamp', + case received_timestamp_millis + when 0 then '' + else datetime(received_timestamp_millis/1000,'unixepoch') + end as 'Received Timestamp', + case seen_timestamp_millis + when 0 then '' + else datetime(seen_timestamp_millis/1000,'unixepoch') + end as 'Viewed Timestamp', + sender_id, + recipient_id, + content_uri, + replace(content_uri, rtrim(content_uri, replace(content_uri, '/', '')), '') as 'File Name', + content_size_bytes + from messages + ''') + has_saved_status = False all_rows = cursor.fetchall() usageentries = len(all_rows) @@ -140,7 +166,7 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): content_uri = row[5] content_name = row[6] content_size = row[7] - file_saved = row[8] + file_saved = row[8] if has_saved_status else '' thumb = '' for match in files_found: @@ -149,7 +175,7 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): data_file_name = os.path.basename(match) thumb = f'' - data_list.append((row[0],row[1],row[2],row[3],row[4],thumb,row[7],row[8])) + data_list.append((row[0],row[1],row[2],row[3],row[4],thumb,row[7],file_saved)) report = ArtifactHtmlReport('Google Duo - Notes') report.start_artifact_report(report_folder, 'Google Duo - Notes') diff --git a/scripts/artifacts/googleMessages.py b/scripts/artifacts/googleMessages.py index 38c9eb04..02e3626e 100755 --- a/scripts/artifacts/googleMessages.py +++ b/scripts/artifacts/googleMessages.py @@ -20,25 +20,63 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): db = open_sqlite_db_readonly(file_found) cursor = db.cursor() - cursor.execute(''' - SELECT - datetime(parts.timestamp/1000,'unixepoch') AS "Timestamp (UTC)", - parts.content_type AS "Message Type", - conversations.name AS "Other Participant/Conversation Name", - participants.display_destination AS "Message Sender", - parts.text AS "Message", - CASE - WHEN parts.file_size_bytes=-1 THEN "N/A" - ELSE parts.file_size_bytes - END AS "Attachment Byte Size", - parts.local_cache_path AS "Attachment Location" - FROM - parts - JOIN messages ON messages._id=parts.message_id - JOIN participants ON participants._id=messages.sender_id - JOIN conversations ON conversations._id=parts.conversation_id - ORDER BY "Timestamp (UTC)" ASC - ''') + try: + cursor.execute(''' + SELECT + datetime(parts.timestamp/1000,'unixepoch') AS "Timestamp (UTC)", + parts.content_type AS "Message Type", + conversations.name AS "Other Participant/Conversation Name", + participants.display_destination AS "Message Sender", + parts.text AS "Message", + CASE + WHEN parts.file_size_bytes=-1 THEN "N/A" + ELSE parts.file_size_bytes + END AS "Attachment Byte Size", + parts.local_cache_path AS "Attachment Location" + FROM + parts + JOIN messages ON messages._id=parts.message_id + JOIN participants ON participants._id=messages.sender_id + JOIN conversations ON conversations._id=parts.conversation_id + ORDER BY "Timestamp (UTC)" ASC + ''') + except: + # Fallback for databases without file_size_bytes column + try: + cursor.execute(''' + SELECT + datetime(parts.timestamp/1000,'unixepoch') AS "Timestamp (UTC)", + parts.content_type AS "Message Type", + conversations.name AS "Other Participant/Conversation Name", + participants.display_destination AS "Message Sender", + parts.text AS "Message", + "N/A" AS "Attachment Byte Size", + parts.local_cache_path AS "Attachment Location" + FROM + parts + JOIN messages ON messages._id=parts.message_id + JOIN participants ON participants._id=messages.sender_id + JOIN conversations ON conversations._id=parts.conversation_id + ORDER BY "Timestamp (UTC)" ASC + ''') + except: + # Final fallback for databases without both file_size_bytes and local_cache_path columns + cursor.execute(''' + SELECT + datetime(parts.timestamp/1000,'unixepoch') AS "Timestamp (UTC)", + parts.content_type AS "Message Type", + conversations.name AS "Other Participant/Conversation Name", + participants.display_destination AS "Message Sender", + parts.text AS "Message", + "N/A" AS "Attachment Byte Size", + "N/A" AS "Attachment Location" + FROM + parts + JOIN messages ON messages._id=parts.message_id + JOIN participants ON participants._id=messages.sender_id + JOIN conversations ON conversations._id=parts.conversation_id + ORDER BY "Timestamp (UTC)" ASC + ''') all_rows = cursor.fetchall() usageentries = len(all_rows) diff --git a/scripts/artifacts/keepNotes.py b/scripts/artifacts/keepNotes.py index a50ec7b3..e3e4a450 100644 --- a/scripts/artifacts/keepNotes.py +++ b/scripts/artifacts/keepNotes.py @@ -28,17 +28,44 @@ def get_keepNotes(files_found, report_folder, seeker, wrap_text): if filename.endswith('keep.db'): db = open_sqlite_db_readonly(file_found) cursor = db.cursor() - cursor.execute(''' - SELECT - datetime(tree_entity.time_created/1000, 'unixepoch') AS "Time Created", - datetime(tree_entity.time_last_updated/1000, 'unixepoch') AS "Time Last Updated", - datetime(tree_entity.user_edited_timestamp/1000, 'unixepoch') AS "User Edited Timestamp", - tree_entity.title AS Title, - text_search_note_content_content.c0text AS "Text", - tree_entity.last_modifier_email AS "Last Modifier Email" - FROM text_search_note_content_content - INNER JOIN tree_entity ON text_search_note_content_content.docid = tree_entity._id - ''') + try: + cursor.execute(''' + SELECT + datetime(tree_entity.time_created/1000, 'unixepoch') AS "Time Created", + datetime(tree_entity.time_last_updated/1000, 'unixepoch') AS "Time Last Updated", + datetime(tree_entity.user_edited_timestamp/1000, 'unixepoch') AS "User Edited Timestamp", + tree_entity.title AS Title, + text_search_note_content_content.c0text AS "Text", + tree_entity.last_modifier_email AS "Last Modifier Email" + FROM text_search_note_content_content + INNER JOIN tree_entity ON text_search_note_content_content.docid = tree_entity._id + ''') + except: + # Fallback for databases without text_search_note_content_content table + try: + cursor.execute(''' + SELECT + datetime(tree_entity.time_created/1000, 'unixepoch') AS "Time Created", + datetime(tree_entity.time_last_updated/1000, 'unixepoch') AS "Time Last Updated", + datetime(tree_entity.user_edited_timestamp/1000, 'unixepoch') AS "User Edited Timestamp", + tree_entity.title AS Title, + tree_entity.text_content AS "Text", + tree_entity.last_modifier_email AS "Last Modifier Email" + FROM tree_entity + WHERE tree_entity.type = 0 + ''') + except: + # Final fallback - just tree_entity with minimal columns + cursor.execute(''' + SELECT + datetime(tree_entity.time_created/1000, 'unixepoch') AS "Time Created", + datetime(tree_entity.time_last_updated/1000, 'unixepoch') AS "Time Last Updated", + datetime(tree_entity.user_edited_timestamp/1000, 'unixepoch') AS "User Edited Timestamp", + tree_entity.title AS Title, + NULL AS "Text", + tree_entity.last_modifier_email AS "Last Modifier Email" + FROM tree_entity + ''') all_rows = cursor.fetchall() usageentries = len(all_rows) diff --git a/scripts/artifacts/notificationHistory.py b/scripts/artifacts/notificationHistory.py index 05c90e48..e93f0808 100644 --- a/scripts/artifacts/notificationHistory.py +++ b/scripts/artifacts/notificationHistory.py @@ -3,9 +3,9 @@ "name": "Android Notification History", "description": "Get Android notifications' history, policy and settings. This parser is based on a research project", "author": "Evangelos Dragonas (@theAtropos4n6)", - "version": "0.0.1", + "version": "0.0.2", "date": "2024-07-02", - "requirements": "", + "requirements": "blackboxprotobuf", "category": "Android Notification History", "paths": ( '**/system_ce/*/notification_history/history/*', @@ -22,10 +22,141 @@ import os import scripts.artifacts.notification_history_pb.notificationhistory_pb2 as notificationhistory_pb2 +try: + import blackboxprotobuf + HAS_BLACKBOXPROTOBUF = True +except ImportError: + HAS_BLACKBOXPROTOBUF = False + from scripts.artifact_report import ArtifactHtmlReport from scripts.ilapfuncs import logfunc, tsv, timeline, abxread, checkabx,convert_ts_int_to_utc,convert_utc_human_to_timezone +def parse_notification_with_blackbox(pb_data, file_name): + """ + Fallback parser using blackboxprotobuf for when the compiled schema fails. + Returns a list of notification tuples or empty list if parsing fails. + """ + data_list = [] + try: + message, typedef = blackboxprotobuf.decode_message(pb_data) + + # Try to extract notifications from the decoded message + # The structure may vary, so we try common field numbers + notifications = [] + + # Field 3 is typically the notification list based on the proto schema + if '3' in message: + notif_data = message['3'] + if isinstance(notif_data, list): + notifications = notif_data + else: + notifications = [notif_data] + + # Try to get string pool from field 1 + string_pool = [] + if '1' in message and isinstance(message['1'], dict): + pool_data = message['1'] + if '2' in pool_data: + pool_strings = pool_data['2'] + if isinstance(pool_strings, list): + string_pool = [s.decode('utf-8') if isinstance(s, bytes) else str(s) for s in pool_strings] + else: + string_pool = [pool_strings.decode('utf-8') if isinstance(pool_strings, bytes) else str(pool_strings)] + + package_map = {i + 1: pkg for i, pkg in enumerate(string_pool)} + + # Get major version from field 2 + major_version = message.get('2', None) + + for notif in notifications: + if not isinstance(notif, dict): + continue + + # Extract fields based on proto field numbers + package = notif.get('1', b'') + if isinstance(package, bytes): + package = package.decode('utf-8', errors='ignore') + package_index = notif.get('2', 0) + + # If package is empty, try to get from string pool + if not package and package_index: + package = package_map.get(package_index, '') + + channel_name = notif.get('3', b'') + if isinstance(channel_name, bytes): + channel_name = channel_name.decode('utf-8', errors='ignore') + channel_name_index = notif.get('4', 0) + + channel_id = notif.get('5', b'') + if isinstance(channel_id, bytes): + channel_id = channel_id.decode('utf-8', errors='ignore') + channel_id_index = notif.get('6', 0) + + uid = notif.get('7', 0) + user_id = notif.get('8', 0) + posted_time_ms = notif.get('9', 0) + + title = notif.get('10', b'') + if isinstance(title, bytes): + title = title.decode('utf-8', errors='ignore') + + text = notif.get('11', b'') + if isinstance(text, bytes): + text = text.decode('utf-8', errors='ignore') + + conversation_id = notif.get('13', b'') + if isinstance(conversation_id, bytes): + conversation_id = conversation_id.decode('utf-8', errors='ignore') + conversation_id_index = notif.get('14', 0) + + # Handle icon (field 12) + icon_data = notif.get('12', {}) + image_type = icon_data.get('1', None) if isinstance(icon_data, dict) else None + image_bitmap_filename = icon_data.get('2', None) if isinstance(icon_data, dict) else None + if isinstance(image_bitmap_filename, bytes): + image_bitmap_filename = image_bitmap_filename.decode('utf-8', errors='ignore') + image_resource_id = icon_data.get('3', None) if isinstance(icon_data, dict) else None + image_resource_id_package = icon_data.get('4', None) if isinstance(icon_data, dict) else None + if isinstance(image_resource_id_package, bytes): + image_resource_id_package = image_resource_id_package.decode('utf-8', errors='ignore') + image_data_length = icon_data.get('6', None) if isinstance(icon_data, dict) else None + image_data_offset = icon_data.get('7', None) if isinstance(icon_data, dict) else None + image_uri = icon_data.get('8', None) if isinstance(icon_data, dict) else None + if isinstance(image_uri, bytes): + image_uri = image_uri.decode('utf-8', errors='ignore') + + # Convert timestamp + posted_time = None + if posted_time_ms: + try: + posted_time = convert_utc_human_to_timezone(convert_ts_int_to_utc(int(posted_time_ms/1000.0)),'UTC') + except: + pass + + # Convert file creation time + file_creation = None + try: + file_creation = convert_utc_human_to_timezone(convert_ts_int_to_utc(int(file_name)/1000.0),'UTC') + except: + pass + + data_list.append(( + f'{posted_time}', title, text, package, user_id, uid, package_index, + channel_name, channel_name_index, channel_id, channel_id_index, + conversation_id, conversation_id_index, major_version, + image_type, image_bitmap_filename, image_resource_id, + image_resource_id_package, image_data_length, image_data_offset, + image_uri, file_name, f'{file_creation}' + )) + except Exception as e: + pass # Silently fail for blackbox parsing + + return data_list + + + + def get_notificationHistory(files_found, report_folder, seeker, wrap_text): data_pb_list = [] for file_found in files_found: @@ -104,20 +235,30 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): else: #iterate through the notification pbs try: - notification_history = notificationhistory_pb2.NotificationHistoryProto() + pb_data = None with open(file_found, 'rb') as f: - try: - notification_history.ParseFromString(f.read()) #The error 'Wrong wire type in tag. ' likely happens due to the given .proto map file. - except Exception as e: - logfunc(f'Error in the ParseFromString() function. The error message was: {e}') - - package_map = {i + 1: pkg for i, pkg in enumerate(notification_history.string_pool.strings)} # one of the protobuf files stores the package name and indexes - - major_version = notification_history.major_version if notification_history.HasField('major_version') else None # notification format version should be 1 + pb_data = f.read() + + parsed_with_schema = False + notification_history = notificationhistory_pb2.NotificationHistoryProto() + + try: + notification_history.ParseFromString(pb_data) + # Check if we actually got any notifications + if notification_history.notification: + parsed_with_schema = True + except Exception as e: + # Schema parsing failed, will try blackboxprotobuf + parsed_with_schema = False + + if parsed_with_schema: + # Use the compiled schema parser + package_map = {i + 1: pkg for i, pkg in enumerate(notification_history.string_pool.strings)} + major_version = notification_history.major_version if notification_history.HasField('major_version') else None + for notification in notification_history.notification: - package_name = notification.package if notification.package else package_map.get(notification.package_index, "") #retrieves package from the map if not stored locally + package_name = notification.package if notification.package else package_map.get(notification.package_index, "") - #this block tries to fetch the value of each field from within the parsed protobuf file e.g. variable user_id -> recovers the user_id field from the pb fields = ['uid', 'user_id', 'package_index', 'channel_name', 'channel_id','channel_id_index', 'channel_name_index', 'conversation_id', 'conversation_id_index'] defaults = {field: 'Error' for field in fields} values = {} @@ -126,7 +267,7 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): values[field] = getattr(notification, field) except AttributeError: values[field] = 'Error' - #extra block that does the same for the notifications with icons + if notification.HasField('icon'): icon_fields = ['image_type', 'image_bitmap_filename', 'image_resource_id', 'image_resource_id_package','image_data_length', 'image_data_offset', 'image_uri'] for icon_field in icon_fields: @@ -138,7 +279,7 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): ] for icon_field in icon_fields: values[icon_field] = None - #here the returned values are assigned to the variables which are reported + uid = values['uid'] user_id = values['user_id'] package_index = values['package_index'] @@ -160,8 +301,17 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): image_uri = values['image_uri'] file_creation = convert_utc_human_to_timezone(convert_ts_int_to_utc(int(file_name)/1000.0),'UTC') data_pb_list.append((f'{posted_time}',title,text,package_name,user_id,uid,package_index,channel_name,channel_name_index,channel_id,channel_id_index,conversation_id,conversation_id_index,major_version,image_type,image_bitmap_filename,image_resource_id,image_resource_id_package,image_data_length,image_data_offset,image_uri,file_name,f'{file_creation}')) + + elif HAS_BLACKBOXPROTOBUF and pb_data: + # Fallback to blackboxprotobuf parser + fallback_results = parse_notification_with_blackbox(pb_data, file_name) + if fallback_results: + data_pb_list.extend(fallback_results) + # No log message if blackbox parsing also returns empty - just means no data + except Exception as e: - logfunc(f'Error while opening notification pb files. The error message was:" {e}"') + # Only log if there's a real file error, not parsing errors + logfunc(f'Error reading notification pb file {file_name}: {e}') if len(data_pb_list) > 0: description = f'A history of the notifications that landed on the device during the last 24h'