diff --git a/scripts/artifacts/gmailEmails.py b/scripts/artifacts/gmailEmails.py index 019ad1a5..2569f364 100644 --- a/scripts/artifacts/gmailEmails.py +++ b/scripts/artifacts/gmailEmails.py @@ -110,6 +110,7 @@ def gmailEmails(files_found, report_folder, seeker, wrap_text): attachname = row[15] attachhash = row[16] attachment = '' + messagehtml = '' to = (message.get('1', '')).get('2', '') if '1' in message and '2' in message['1'] else '' #receiver if isinstance(to, bytes): @@ -257,4 +258,4 @@ def gmailDownloadRequests(files_found, report_folder, seeker, wrap_text): data_list.append((record[0],record[1],record[2],record[3],record[4],record[5],record[6],record[7])) data_headers = (('Timestamp Requested','datetime'),'Account Name','Download Type','Message ID','URL','Target File Path','Target File Size','Priority') - return data_headers, data_list, downloaderDB \ No newline at end of file + return data_headers, data_list, downloaderDB diff --git a/scripts/artifacts/googleDuo.py b/scripts/artifacts/googleDuo.py index 2717ec5f..93fbefa3 100755 --- a/scripts/artifacts/googleDuo.py +++ b/scripts/artifacts/googleDuo.py @@ -16,30 +16,48 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): db = open_sqlite_db_readonly(file_found) cursor = db.cursor() - cursor.execute(''' + cursor.execute("PRAGMA table_info(activity_history);") + ah_cols = {col[1] for col in cursor.fetchall()} + + has_other = 'other_id' in ah_cols + has_call_state = 'call_state' in ah_cols + has_outgoing = 'outgoing' in ah_cols + + local_user_expr = "''" + remote_user_expr = "substr(other_id, 0,instr(other_id, '|'))" if has_other else "''" + call_status_expr = """case call_state + when 0 then 'Left Message' + when 1 then 'Missed Call' + when 2 then 'Answered' + when 4 then '' + end""" if has_call_state else "''" + direction_expr = """case outgoing + when 0 then 'Incoming' + when 1 then 'Outgoing' + end""" if has_outgoing else "''" + join_clause = "left join duo_users on duo_users.user_id = substr(other_id, 0,instr(other_id, '|'))" if has_other else "" + + query = f''' select datetime(timestamp_usec/1000000, 'unixepoch') as 'Timestamp', - substr(self_id, 0,instr(self_id, '|')) as 'Local User', - substr(other_id, 0,instr(other_id, '|')) as 'Remote User', + {local_user_expr} as 'Local User', + {remote_user_expr} as 'Remote User', duo_users.contact_display_name as 'Contact Name', case activity_type when 1 then 'Call' when 2 then 'Note' when 4 then 'Reaction' end as 'Activity Type', - case call_state - when 0 then 'Left Message' - when 1 then 'Missed Call' - when 2 then 'Answered' - when 4 then '' - end as 'Call Status', - case outgoing - when 0 then 'Incoming' - when 1 then 'Outgoing' - end as 'Direction' + {call_status_expr} as 'Call Status', + {direction_expr} as 'Direction' from activity_history - left join duo_users on duo_users.user_id = substr(other_id, 0,instr(other_id, '|')) - ''') + {join_clause} + ''' + try: + cursor.execute(query) + except sqlite3.OperationalError as e: + logfunc(f'Call history query failed in {file_found}: {e}') + continue all_rows = cursor.fetchall() usageentries = len(all_rows) @@ -99,7 +117,19 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): else: logfunc('No Google Duo - Contacts data available') - cursor.execute(''' + cursor.execute("PRAGMA table_info(messages);") + msg_cols = {col[1] for col in cursor.fetchall()} + + has_size = 'content_size_bytes' in msg_cols + has_saved = 'saved_status' in msg_cols + + size_expr = 'content_size_bytes' if has_size else "''" + saved_expr = """case saved_status + when 0 then '' + when 1 then 'Yes' + end""" if has_saved else "''" + + notes_query = f''' select case sent_timestamp_millis when 0 then '' @@ -117,13 +147,15 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): 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' + {size_expr} as 'Content Size', + {saved_expr} as 'File Saved' from messages - ''') + ''' + try: + cursor.execute(notes_query) + except sqlite3.OperationalError as e: + logfunc(f'Google Duo notes query failed in {file_found}: {e}') + continue all_rows = cursor.fetchall() usageentries = len(all_rows) @@ -174,4 +206,4 @@ def get_googleDuo(files_found, report_folder, seeker, wrap_text): "Google Duo", ('*/com.google.android.apps.tachyon/databases/tachyon.db*','*/com.google.android.apps.tachyon/files/media/*.*'), get_googleDuo) -} \ No newline at end of file +} diff --git a/scripts/artifacts/googleMapsGmm.py b/scripts/artifacts/googleMapsGmm.py index 991cb3db..036efcb8 100644 --- a/scripts/artifacts/googleMapsGmm.py +++ b/scripts/artifacts/googleMapsGmm.py @@ -85,16 +85,26 @@ def get_googleMapsGmm(files_found, report_folder, seeker, wrap_text): db = open_sqlite_db_readonly(file_found) file_found_myplaces = file_found cursor = db.cursor() - cursor.execute(''' - select - rowid, - key_string, - round(latitude*.000001,6), - round(longitude*.000001,6), - sync_item, - timestamp - from sync_item - ''') + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sync_item';") + if not cursor.fetchone(): + logfunc(f'sync_item table not found in {file_found_myplaces}, skipping.') + db.close() + continue + try: + cursor.execute(''' + select + rowid, + key_string, + round(latitude*.000001,6), + round(longitude*.000001,6), + sync_item, + timestamp + from sync_item + ''') + except sqlite3.OperationalError as e: + logfunc(f'sync_item table query failed in {file_found_myplaces}: {e}') + db.close() + continue all_rows = cursor.fetchall() for row in all_rows: @@ -150,4 +160,4 @@ def get_googleMapsGmm(files_found, report_folder, seeker, wrap_text): tlactivity = f'Google Maps Label Places' timeline(report_folder, tlactivity, data_list_myplaces, data_headers) else: - logfunc('No Google Maps Label Places data available') \ No newline at end of file + logfunc('No Google Maps Label Places data available') diff --git a/scripts/artifacts/googleMessages.py b/scripts/artifacts/googleMessages.py index 38c9eb04..95c33fc2 100755 --- a/scripts/artifacts/googleMessages.py +++ b/scripts/artifacts/googleMessages.py @@ -20,7 +20,19 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): db = open_sqlite_db_readonly(file_found) cursor = db.cursor() - cursor.execute(''' + cursor.execute("PRAGMA table_info(parts);") + parts_cols = {col[1] for col in cursor.fetchall()} + + has_size = 'file_size_bytes' in parts_cols + has_cache = 'local_cache_path' in parts_cols + size_expr = 'parts.file_size_bytes' if has_size else "''" + cache_expr = 'parts.local_cache_path' if has_cache else ( + 'parts.storage_uri' if 'storage_uri' in parts_cols else ( + 'parts.uri' if 'uri' in parts_cols else "''" + ) + ) + + query = f''' SELECT datetime(parts.timestamp/1000,'unixepoch') AS "Timestamp (UTC)", parts.content_type AS "Message Type", @@ -28,17 +40,24 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): 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 + WHEN {size_expr}=-1 THEN "N/A" + ELSE {size_expr} END AS "Attachment Byte Size", - parts.local_cache_path AS "Attachment Location" + {cache_expr} 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(query) + except sqlite3.OperationalError as e: + logfunc(f'Google Messages query failed in {file_found}: {e}') + db.close() + continue all_rows = cursor.fetchall() usageentries = len(all_rows) @@ -69,4 +88,4 @@ def get_googleMessages(files_found, report_folder, seeker, wrap_text): "Google Messages", ('*/com.google.android.apps.messaging/databases/bugle_db*'), get_googleMessages) -} \ No newline at end of file +} diff --git a/scripts/artifacts/notificationHistory.py b/scripts/artifacts/notificationHistory.py index 05c90e48..88477b3e 100644 --- a/scripts/artifacts/notificationHistory.py +++ b/scripts/artifacts/notificationHistory.py @@ -47,8 +47,8 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): value = setting.attrib.get('value') value = "Enabled" if value == "1" else "Disabled" if value == "0" else "Unknown" data_list.append((value, user)) - else: - pass # setting not available + else: + pass # setting not available if data_list: description = f'Indicates whether "Notification History" feature is enabled.' @@ -65,6 +65,8 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): else: logfunc('No Android Notification History - Status data available') + continue + #parsing notification_policy.xml if file_name.endswith('notification_policy.xml'): data_list = [] @@ -97,9 +99,11 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): tsvname = f'Android Notification History - Snoozed notifications' tsv(report_folder, data_headers, data_list, tsvname) - + else: logfunc('No Android Notification History - Snoozed notifications data available') + + continue else: #iterate through the notification pbs @@ -110,6 +114,7 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): 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}') + continue 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 @@ -179,4 +184,4 @@ def get_notificationHistory(files_found, report_folder, seeker, wrap_text): tlactivity = f'Android Notification History - Notifications' timeline(report_folder, tlactivity, data_pb_list, data_headers) else: - logfunc(f'No Android Notification History - Notifications available') \ No newline at end of file + logfunc(f'No Android Notification History - Notifications available') diff --git a/scripts/artifacts/wellbeing.py b/scripts/artifacts/wellbeing.py index a2cdfc0c..f2757a28 100755 --- a/scripts/artifacts/wellbeing.py +++ b/scripts/artifacts/wellbeing.py @@ -63,34 +63,39 @@ def get_wellbeing(files_found, report_folder, seeker, wrap_text): data_list.append((event_ts, row[2], row[3], file_found)) cursor = db.cursor() - cursor.execute(''' - SELECT - datetime(component_events.timestamp/1000, "UNIXEPOCH") as timestamp, - component_events._id, - components.package_id, - packages.package_name, - components.component_name as website, - CASE - when component_events.type=1 THEN 'ACTIVITY_RESUMED' - when component_events.type=2 THEN 'ACTIVITY_PAUSED' - else component_events.type - END as eventType - FROM component_events - INNER JOIN components ON component_events.component_id=components._id - INNER JOIN packages ON components.package_id=packages._id - ORDER BY timestamp - ''') + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='component_events';") + has_component_events = cursor.fetchone() is not None + if has_component_events: + cursor.execute(''' + SELECT + datetime(component_events.timestamp/1000, "UNIXEPOCH") as timestamp, + component_events._id, + components.package_id, + packages.package_name, + components.component_name as website, + CASE + when component_events.type=1 THEN 'ACTIVITY_RESUMED' + when component_events.type=2 THEN 'ACTIVITY_PAUSED' + else component_events.type + END as eventType + FROM component_events + INNER JOIN components ON component_events.component_id=components._id + INNER JOIN packages ON components.package_id=packages._id + ORDER BY timestamp + ''') - all_rows = cursor.fetchall() - usageentries = len(all_rows) - if usageentries > 0: - for row in all_rows: - event_ts = row[0] - if event_ts is None: - pass - else: - event_ts = convert_utc_human_to_timezone(convert_ts_human_to_utc(event_ts),'UTC') - data_list_url.append((event_ts, row[1], row[2], row[3], row[4], row[5], file_found)) + all_rows = cursor.fetchall() + usageentries = len(all_rows) + if usageentries > 0: + for row in all_rows: + event_ts = row[0] + if event_ts is None: + pass + else: + event_ts = convert_utc_human_to_timezone(convert_ts_human_to_utc(event_ts),'UTC') + data_list_url.append((event_ts, row[1], row[2], row[3], row[4], row[5], file_found)) + else: + logfunc('No component_events table in Digital Wellbeing database; skipping URL events.') db.close() else: @@ -128,4 +133,4 @@ def get_wellbeing(files_found, report_folder, seeker, wrap_text): tlactivity = f'Digital Wellbeing - URL Events' timeline(report_folder, tlactivity, data_list, data_headers) else: - logfunc('No Digital Wellbeing - URL Events data available') \ No newline at end of file + logfunc('No Digital Wellbeing - URL Events data available') diff --git a/scripts/artifacts/wireMessenger.py b/scripts/artifacts/wireMessenger.py index cf052453..f6e88b40 100644 --- a/scripts/artifacts/wireMessenger.py +++ b/scripts/artifacts/wireMessenger.py @@ -236,11 +236,36 @@ def get_wire_messages(files_found, report_folder, seeker, wrap_text): timestamp FROM MsgDeletion; ''') - + deleted_rows = cursor.fetchall() if len(deleted_rows) == 0: - cursor.execute(''' + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Assets2';") + has_assets2 = cursor.fetchone() is not None + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Assets';") + has_assets = cursor.fetchone() is not None + asset_join = "" + asset_name_expr = "''" + cursor.execute("PRAGMA table_info(Messages);") + msg_cols = {c[1] for c in cursor.fetchall()} + has_asset_id = 'asset_id' in msg_cols + if has_asset_id and has_assets2: + cursor.execute("PRAGMA table_info(Assets2);") + cols = {c[1] for c in cursor.fetchall()} + asset_col = "name" if "name" in cols else "_id" + asset_join = "LEFT JOIN Assets2 ON Messages.asset_id = Assets2._id" + asset_name_expr = f"Assets2.{asset_col}" + elif has_asset_id and has_assets: + cursor.execute("PRAGMA table_info(Assets);") + cols = {c[1] for c in cursor.fetchall()} + asset_col = "name" if "name" in cols else "_id" + asset_join = "LEFT JOIN Assets ON Messages.asset_id = Assets._id" + asset_name_expr = f"Assets.{asset_col}" + else: + asset_join = "" + asset_name_expr = "''" + + base_query = f''' Select datetime(Messages.time / 1000, 'unixepoch'), Messages._id AS "Message ID", @@ -251,14 +276,15 @@ def get_wire_messages(files_found, report_folder, seeker, wrap_text): datetime(Likings."timestamp" / 1000, 'unixepoch'), Users1.name As "Liked By", time(Messages.duration / 1000, 'unixepoch'), - Assets2.name + {asset_name_expr} From Messages Left Join Users On Users._id = Messages.user_id Left Join Likings On Messages._id = Likings.message_id Left Join Users Users1 On Likings.user_id = Users1._id - LEFT JOIN Assets2 ON Messages.asset_id = Assets2._id + {asset_join} Order By time; - ''') + ''' + cursor.execute(base_query) #closing read only db and opening write mode #inserting the deleted messages into the Messages table @@ -277,25 +303,51 @@ def get_wire_messages(files_found, report_folder, seeker, wrap_text): SET msg_type = 'Deleted' WHERE msg_type = ''; ''') - cursor.execute(''' + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Assets2';") + has_assets2 = cursor.fetchone() is not None + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Assets';") + has_assets = cursor.fetchone() is not None + asset_join = "" + asset_name_expr = "''" + cursor.execute("PRAGMA table_info(Messages);") + msg_cols = {c[1] for c in cursor.fetchall()} + has_asset_id = 'asset_id' in msg_cols + if has_asset_id and has_assets2: + cursor.execute("PRAGMA table_info(Assets2);") + cols = {c[1] for c in cursor.fetchall()} + asset_col = "name" if "name" in cols else "_id" + asset_join = "LEFT JOIN Assets2 ON Messages.asset_id = Assets2._id" + asset_name_expr = f"Assets2.{asset_col}" + elif has_asset_id and has_assets: + cursor.execute("PRAGMA table_info(Assets);") + cols = {c[1] for c in cursor.fetchall()} + asset_col = "name" if "name" in cols else "_id" + asset_join = "LEFT JOIN Assets ON Messages.asset_id = Assets._id" + asset_name_expr = f"Assets.{asset_col}" + else: + asset_join = "" + asset_name_expr = "''" + + base_query = f''' Select datetime(Messages.time / 1000, 'unixepoch'), Messages._id AS "Message ID", Users.name, Messages.msg_type, - json_extract(Messages.content, '$[0].content')AS "Content", + json_extract(Messages.content, '$[0].content') AS "Content", CASE Likings."action" WHEN 1 THEN "Liked" END, datetime(Likings."timestamp" / 1000, 'unixepoch'), Users1.name As "Liked By", time(Messages.duration / 1000, 'unixepoch'), - Assets2.name + {asset_name_expr} From Messages Left Join Users On Users._id = Messages.user_id Left Join Likings On Messages._id = Likings.message_id Left Join Users Users1 On Likings.user_id = Users1._id - LEFT JOIN Assets2 ON Messages.asset_id = Assets2._id + {asset_join} Order By time; - ''') + ''' + cursor.execute(base_query) all_rows = cursor.fetchall() db.close() @@ -337,4 +389,4 @@ def get_wire_messages(files_found, report_folder, seeker, wrap_text): tsvname = 'Wire User Contacts' tsv(report_folder, data_headers, messages_data, tsvname) else: - logfunc("No message data located!") \ No newline at end of file + logfunc("No message data located!")