From cf6fb1f56608e0297f72094dc3c8b4cf79cee623 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Dec 2024 10:18:32 +0700 Subject: [PATCH] ios_tiktok_contacts --- plaso/data/formatters/ios.yaml | 11 ++ plaso/data/timeliner.yaml | 6 + plaso/parsers/sqlite_plugins/__init__.py | 1 + .../sqlite_plugins/ios_tiktok_contacts.py | 103 ++++++++++++++++++ test_data/AwemeIM.db | Bin 0 -> 32768 bytes .../sqlite_plugins/ios_tiktok_contacts.py | 47 ++++++++ 6 files changed, 168 insertions(+) create mode 100644 plaso/parsers/sqlite_plugins/ios_tiktok_contacts.py create mode 100644 test_data/AwemeIM.db create mode 100644 tests/parsers/sqlite_plugins/ios_tiktok_contacts.py diff --git a/plaso/data/formatters/ios.yaml b/plaso/data/formatters/ios.yaml index bd3c3b005c..4f246702f3 100644 --- a/plaso/data/formatters/ios.yaml +++ b/plaso/data/formatters/ios.yaml @@ -187,6 +187,17 @@ short_source: 'LOG' source: 'iOS sysdiag log' --- type: 'conditional' +data_type: 'ios:tiktok:contact' +message: +- 'Nickname: {nickname}' +- 'URL: {url}' +short_message: +- 'Nickname: {nickname}' +- 'URL: {url}' +short_source: 'SQLITE' +source: 'iOS TikTok contact database' +--- +type: 'conditional' data_type: 'ios:twitter:contact' enumeration_helpers: - input_attribute: 'following' diff --git a/plaso/data/timeliner.yaml b/plaso/data/timeliner.yaml index 1681f33d74..b4a1adaee5 100644 --- a/plaso/data/timeliner.yaml +++ b/plaso/data/timeliner.yaml @@ -565,6 +565,12 @@ attribute_mappings: description: 'Content Modification Time' place_holder_event: true --- +data_type: 'ios:tiktok:contact' +attribute_mappings: +- name: 'chat_timestamp' + description: 'Latest Chat Time' +place_holder_event: true +--- data_type: 'ios:twitter:contact' attribute_mappings: - name: 'creation_time' diff --git a/plaso/parsers/sqlite_plugins/__init__.py b/plaso/parsers/sqlite_plugins/__init__.py index dfa7c0a680..f72e746d71 100644 --- a/plaso/parsers/sqlite_plugins/__init__.py +++ b/plaso/parsers/sqlite_plugins/__init__.py @@ -26,6 +26,7 @@ from plaso.parsers.sqlite_plugins import ios_netusage from plaso.parsers.sqlite_plugins import ios_powerlog from plaso.parsers.sqlite_plugins import ios_screentime +from plaso.parsers.sqlite_plugins import ios_tiktok_contacts from plaso.parsers.sqlite_plugins import ios_twitter from plaso.parsers.sqlite_plugins import kodi from plaso.parsers.sqlite_plugins import ls_quarantine diff --git a/plaso/parsers/sqlite_plugins/ios_tiktok_contacts.py b/plaso/parsers/sqlite_plugins/ios_tiktok_contacts.py new file mode 100644 index 0000000000..01ce7c5932 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/ios_tiktok_contacts.py @@ -0,0 +1,103 @@ +# -*- coding:utf-8 -*- +"""SQLite parser plugin for TikTok contacts database on iOS.""" + +from dfdatetime import posix_time as dfdatetime_posix_time + +from plaso.containers import events +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class IOSTikTokContactsEventData(events.EventData): + """iOS TikTok contacts event data. + + Attributes: + chat_timestamp (dfdatetime.DateTimeValues): latest chat timestamp. + nickname (str): nickname of the contact. + url (str): url of the contact. + """ + + DATA_TYPE = 'ios:tiktok:contact' + + def __init__(self): + """Initializes event data.""" + super(IOSTikTokContactsEventData, self).__init__(data_type=self.DATA_TYPE) + self.chat_timestamp = None + self.nickname = None + self.url = None + + +class IOSTikTokContactsPlugin(interface.SQLitePlugin): + """SQLite parser plugin for TikTok contacts database on iOS. + + The TikTok contacts are stored in a SQLite database file named AwemeIM.db. + """ + + NAME = 'ios_tiktok_contacts' + DATA_FORMAT = 'iOS TikTok contacts SQLite database file AwemeIM.db' + + REQUIRED_STRUCTURE = { + 'AwemeContactsV5': frozenset([ + 'latestChatTimestamp', 'nickname', 'url1'])} + + QUERIES = [(( + 'SELECT latestChatTimestamp, nickname, url1 FROM AwemeContactsV5'), + 'ParseContactRow')] + + SCHEMAS = { + 'AwemeContactsV5': ( + 'CREATE TABLE AwemeContactsV5 (uid TEXT PRIMARY KEY, ' + 'accountType INTEGER, alias TEXT, aliasPinYin TEXT, ' + 'commerceUserLevel BLOB, contactName TEXT, contactNamePinYin TEXT, ' + 'customID TEXT, customVerifyInfo TEXT, enterpriseVerifyInfo TEXT, ' + 'followStatus INTEGER, followerCount BLOB, followingCount BLOB, ' + 'hasSetWelcomeMessage INTEGER, latestChatTimestamp INTEGER, ' + 'mentionAccessModel BLOB, nickname TEXT, nicknamePinYin TEXT, ' + 'recType INTEGER, secUserID TEXT, shortID TEXT, signature TEXT, ' + 'updatedTriggeredByContactModule INTEGER, url1 TEXT, url2 TEXT, ' + 'url3 TEXT, verificationType INTEGER)')} + + REQUIRE_SCHEMA_MATCH = False + + def _GetDateTimeRowValue(self, query_hash, row, value_name): + """Retrieves a date and time value from the row. + + Args: + query_hash (int): hash of the query, that uniquely identifies the query + that produced the row. + row (sqlite3.Row): row. + value_name (str): name of the value. + + Returns: + dfdatetime.CocoaTime: date and time value or None if not available. + """ + timestamp = self._GetRowValue(query_hash, row, value_name) + if timestamp is None: + return None + + # Convert the floating point value to an integer. + timestamp = int(timestamp) + return dfdatetime_posix_time.PosixTime(timestamp=timestamp) + + # pylint: disable=unused-argument + def ParseContactRow(self, parser_mediator, query, row, **unused_kwargs): + """Parses a contact row. + + Args: + parser_mediator (ParserMediator): mediates interactions between parsers + and other components, such as storage and dfVFS. + query (str): query that created the row. + row (sqlite3.Row): row. + """ + query_hash = hash(query) + + event_data = IOSTikTokContactsEventData() + event_data.chat_timestamp = self._GetDateTimeRowValue( + query_hash, row, 'latestChatTimestamp') + event_data.nickname = self._GetRowValue(query_hash, row, 'nickname') + event_data.url = self._GetRowValue(query_hash, row, 'url1') + + parser_mediator.ProduceEventData(event_data) + + +sqlite.SQLiteParser.RegisterPlugin(IOSTikTokContactsPlugin) diff --git a/test_data/AwemeIM.db b/test_data/AwemeIM.db new file mode 100644 index 0000000000000000000000000000000000000000..1e7e4919f75357fa23ac24c2755b78ab61b44b15 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/tests/parsers/sqlite_plugins/ios_tiktok_contacts.py b/tests/parsers/sqlite_plugins/ios_tiktok_contacts.py new file mode 100644 index 0000000000..464863a8f6 --- /dev/null +++ b/tests/parsers/sqlite_plugins/ios_tiktok_contacts.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for TikTok on iOS SQLite database plugin.""" + +import unittest + +from plaso.parsers.sqlite_plugins import ios_tiktok_contacts + +from tests.parsers.sqlite_plugins import test_lib + + +class IOSTikTokContactsTest(test_lib.SQLitePluginTestCase): + """Tests for TikTok on iOS SQLite database plugin.""" + + def testProcess(self): + """Test the Process function.""" + plugin = ios_tiktok_contacts.IOSTikTokContactsPlugin() + storage_writer = self._ParseDatabaseFileWithPlugin( + ['AwemeIM.db'], plugin) + + number_of_event_data = storage_writer.GetNumberOfAttributeContainers( + 'event_data') + self.assertGreater(number_of_event_data, 0) + + number_of_warnings = storage_writer.GetNumberOfAttributeContainers( + 'extraction_warning') + self.assertEqual(number_of_warnings, 0) + + number_of_recovery_warnings = ( + storage_writer.GetNumberOfAttributeContainers('recovery_warning')) + self.assertEqual(number_of_recovery_warnings, 0) + + # Test a TikTok contact database entry. + expected_event_values = { + 'chat_timestamp': '2021-12-03T00:00:00+00:00', + 'data_type': 'ios:tiktok:contact', + 'nickname': 'sample_user', + 'url': 'https://www.tiktok.com/@sample_user' + } + + event_data = storage_writer.GetAttributeContainerByIndex( + 'event_data', 0) + self.CheckEventData(event_data, expected_event_values) + + +if __name__ == '__main__': + unittest.main()