diff --git a/plaso/data/formatters/android.yaml b/plaso/data/formatters/android.yaml index bd76492cdd..95d5350889 100644 --- a/plaso/data/formatters/android.yaml +++ b/plaso/data/formatters/android.yaml @@ -130,6 +130,89 @@ short_source: 'Android app usage' source: 'Android SQLite App Usage' --- type: 'conditional' +data_type: 'android:sqlite:downloads' +enumeration_helpers: +- input_attribute: 'status' + output_attribute: 'status_string' + values: + 190: 'STATUS_PENDING' + 192: 'STATUS_RUNNING' + 193: 'STATUS_PAUSED_BY_APP' + 194: 'STATUS_WAITING_TO_RETRY' + 195: 'STATUS_WAITING_FOR_NETWORK' + 196: 'STATUS_QUEUED_FOR_WIFI' + 198: 'STATUS_INSUFFICIENT_SPACE_ERROR' + 199: 'STATUS_DEVICE_NOT_FOUND_ERROR' + 200: 'STATUS_SUCCESS' + 400: 'STATUS_BAD_REQUEST' + 406: 'STATUS_NOT_ACCEPTABLE' + 411: 'STATUS_LENGTH_REQUIRED' + 412: 'STATUS_PRECONDITION_FAILED' + 488: 'STATUS_FILE_ALREADY_EXISTS_ERROR' + 489: 'STATUS_CANNOT_RESUME' + 490: 'STATUS_CANCELED' + 491: 'STATUS_UNKNOWN_ERROR' + 492: 'STATUS_FILE_ERROR' + 493: 'STATUS_UNHANDLED_REDIRECT' + 494: 'STATUS_UNHANDLED_HTTP_CODE' + 495: 'STATUS_HTTP_DATA_ERROR' + 496: 'STATUS_HTTP_EXCEPTION' + 497: 'STATUS_TOO_MANY_REDIRECTS' + 1000: 'ERROR_UNKNOWN' + 1001: 'ERROR_FILE_ERROR' + 1002: 'ERROR_UNHANDLED_HTTP_CODE' + 1004: 'ERROR_HTTP_DATA_ERROR' + 1005: 'ERROR_TOO_MANY_REDIRECTS' + 1007: 'ERROR_DEVICE_NOT_FOUND' + 1008: 'ERROR_CANNOT_RESUME' + 1009: 'ERROR_FILE_ALREADY_EXISTS' + 1010: 'ERROR_BLOCKED' + 1: 'PAUSED_WAITING_TO_RETRY' + 2: 'PAUSED_WAITING_FOR_NETWORK' + 3: 'PAUSED_QUEUED_FOR_WIFI' + 4: 'PAUSED_UNKNOWN' +- input_attribute: 'destination' + output_attribute: 'destination_string' + values: + 0: 'DESTINATION_EXTERNAL' + 1: 'DESTINATION_CACHE_PARTITION' + 2: 'DESTINATION_CACHE_PARTITION_PURGEABLE' + 3: 'DESTINATION_CACHE_PARTITION_NOROAMING' + 4: 'DESTINATION_FILE_URI' + 5: 'DESTINATION_SYSTEMCACHE_PARTITION' + 6: 'DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD' +- input_attribute: 'ui_visibility' + output_attribute: 'ui_visibility_string' + values: + 0: 'VISIBILITY_VISIBLE' + 1: 'VISIBILITY_VISIBLE_NOTIFY_COMPLETED' + 2: 'VISIBILITY_HIDDEN' + 3: 'VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION' +message: +- 'ID: {id}' +- 'URI: {uri}' +- 'MIME Type: {mimetype}' +- 'Total Bytes: {total_bytes}' +- 'Current Bytes: {current_bytes}' +- 'Download Status: {status_string}' +- 'Saved to: {saved_to}' +- 'Is Deleted: {deleted}' +- 'Notification Package: {notification_package}' +- 'Title: {title}' +- 'Media Provider URI: {media_provider_uri}' +- 'Error Msg: {error_msg}' +- 'Is Visible in Downloads UI: {is_visible_in_downloads_ui}' +- 'Destination Type: {destination_string}' +- 'UI Visibility: {ui_visibility_string}' +- 'ETag: {e_tag}' +- 'Description: {description}' +short_message: +- 'URI: {uri}' +- 'Download Status: {status_string}' +short_source: 'Android Native Downloads' +source: 'Android native downloads (downloads.db)' +--- +type: 'conditional' data_type: 'android:tango:contact' message: - '{first_name}' diff --git a/plaso/data/timeliner.yaml b/plaso/data/timeliner.yaml index 1681f33d74..bc87a5f75a 100644 --- a/plaso/data/timeliner.yaml +++ b/plaso/data/timeliner.yaml @@ -54,6 +54,12 @@ attribute_mappings: description: 'Start Time' place_holder_event: false --- +data_type: 'android:sqlite:downloads' +attribute_mappings: +- name: 'lastmod' + description: 'Last Modified Time' +place_holder_event: true +--- data_type: 'android:tango:conversation' place_holder_event: true --- diff --git a/plaso/parsers/sqlite_plugins/__init__.py b/plaso/parsers/sqlite_plugins/__init__.py index dfa7c0a680..6ea85a10aa 100644 --- a/plaso/parsers/sqlite_plugins/__init__.py +++ b/plaso/parsers/sqlite_plugins/__init__.py @@ -4,6 +4,7 @@ from plaso.parsers.sqlite_plugins import android_app_usage from plaso.parsers.sqlite_plugins import android_calls from plaso.parsers.sqlite_plugins import android_hangouts +from plaso.parsers.sqlite_plugins import android_native_downloads from plaso.parsers.sqlite_plugins import android_sms from plaso.parsers.sqlite_plugins import android_tango from plaso.parsers.sqlite_plugins import android_turbo diff --git a/plaso/parsers/sqlite_plugins/android_native_downloads.py b/plaso/parsers/sqlite_plugins/android_native_downloads.py new file mode 100644 index 0000000000..8104f89d3f --- /dev/null +++ b/plaso/parsers/sqlite_plugins/android_native_downloads.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +"""SQLite parser plugin for Android Native Downloads database files.""" + +from dfdatetime import java_time as dfdatetime_java_time +from plaso.containers import events +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class AndroidNativeDownloadsEventData(events.EventData): + """Android Native Downloads (DownloadProvider) event data. + + Also see : + STATUS_* and DESTINATION_* constants: + https://android.googlesource.com/platform/frameworks/base/ + +/refs/heads/master/core/java/android/provider/Downloads.java + ERROR_*, PAUSED_*, and VISIBILITY_* constants: + https://android.googlesource.com/platform/frameworks/base/ + +/refs/heads/main/core/java/android/app/DownloadManager.java + Basis for what columns to extract: + https://forensafe.com/blogs/Android_Downloads.html + + Attributes: + lastmod (dfdatetime.DateTimeValues): Last modified time of downloaded file. + id (int): An identifier for a particular download, unique across the system. + uri (str): Downloaded URI. + mimetype (str): Internet Media Type of the downloaded file. + total_bytes (int): Total size of the download in bytes. + current_bytes (int): Number of bytes download so far. + status (int): Holds one of the STATUS_* constants. + If an error occurred, this holds the HTTP Status Error Code (RFC 2616), + otherwise it holds one of the ERROR_* constants. + If the download is paused, this holds one of the PAUSED_* constants. + saved_to (str): Path to the downloaded file on disk. + deleted (bool): Set to true if this download is deleted. + Also Removed from the database when MediaProvider database + deletes the metadata associated with this downloaded file. + notification_package (str): Package name associated with notification + of a running download. + title (str): Title of the download. + media_provider_uri (str): The URI to corresponding entry in MediaProvider + for this downloaded entry. If an entry is deleted from downloaded list, + it is also deleted from MediaProvider DB. + error_msg (str): The column with errorMsg for a failed downloaded. + Used only for debugging purposes. + is_visible_in_downloads_ui (int) : Whether or not this download should + be displayed in the system's Downloads UI. Defaults to true. + destination (int): Contains the flag that controls download destination. + See the DESTINATION_* constants for a list of legal values. + ui_visibility (int): Contains the flags that control if the download is + displayed by the UI. + See the VISIBILITY_* constants for a list of legal values. + e_tag (str): ETag of this file. + description (str): The client-supplied description of this download. + This will be displayed in system notifications. + Defaults to empty string. + """ + + DATA_TYPE = 'android:sqlite:downloads' + + def __init__(self): + """Initializes event data.""" + super(AndroidNativeDownloadsEventData, self).__init__(data_type=self.DATA_TYPE) + self.lastmod = None + self.id = None + self.uri = None + self.mimetype = None + self.total_bytes = None + self.current_bytes = None + self.status = None + self.saved_to = None + self.deleted = None + self.notification_package = None + self.title = None + self.media_provider_uri = None + self.error_msg = None + self.is_visible_in_downloads_ui = None + self.destination = None + self.ui_visibility = None + self.e_tag = None + self.description = None + + +class AndroidNativeDownloadsPlugin(interface.SQLitePlugin): + """SQLite parser plugin for Android native downloads database file. + + The Android native downloads database file is typically stored in: + com.android.providers.downloads/databases/downloads.db + """ + + NAME = 'android_native_downloads' + DATA_FORMAT = 'Android native downloads SQLite database (downloads.db) file' + + REQUIRED_STRUCTURE = { + 'downloads': frozenset( + ['_id', 'uri', '_data', 'mimetype', 'destination', + 'visibility', 'status', 'lastmod', 'notificationpackage', + 'total_bytes', 'current_bytes', 'etag', 'title', 'description', + 'is_visible_in_downloads_ui', 'mediaprovider_uri', 'deleted', + 'errorMsg'] + )} + + QUERIES = [ + ('SELECT _id, uri, _data, mimetype, destination, ' + 'visibility, status, lastmod, notificationpackage, ' + 'total_bytes, current_bytes, etag, title, description, ' + 'is_visible_in_downloads_ui, mediaprovider_uri, deleted, errorMsg ' + 'FROM downloads', + 'ParseDownloadsRow')] + + SCHEMAS = [{ + 'android_metadata': ( + 'CREATE TABLE android_metadata (locale TEXT) '), + 'downloads': ( + 'CREATE TABLE downloads(_id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'uri TEXT, method INTEGER, entity TEXT, no_integrity BOOLEAN, ' + 'hint TEXT, otaupdate BOOLEAN, _data TEXT, mimetype TEXT, ' + 'destination INTEGER, no_system BOOLEAN, visibility INTEGER, ' + 'control INTEGER, status INTEGER, numfailed INTEGER, ' + 'lastmod BIGINT, notificationpackage TEXT, notificationclass TEXT, ' + 'notificationextras TEXT, cookiedata TEXT, useragent TEXT, ' + 'referer TEXT, total_bytes INTEGER, current_bytes INTEGER, ' + 'etag TEXT, uid INTEGER, otheruid INTEGER, title TEXT, ' + 'description TEXT, scanned BOOLEAN, ' + 'is_public_api INTEGER NOT NULL DEFAULT 0, ' + 'allow_roaming INTEGER NOT NULL DEFAULT 0, ' + 'allowed_network_types INTEGER NOT NULL DEFAULT 0, ' + 'is_visible_in_downloads_ui INTEGER NOT NULL DEFAULT 1, ' + 'bypass_recommended_size_limit INTEGER NOT NULL DEFAULT 0, ' + 'mediaprovider_uri TEXT, deleted BOOLEAN NOT NULL DEFAULT 0, ' + 'errorMsg TEXT, allow_metered INTEGER NOT NULL DEFAULT 1, ' + 'allow_write BOOLEAN NOT NULL DEFAULT 0, ' + 'flags INTEGER NOT NULL DEFAULT 0, ' + 'mediastore_uri TEXT DEFAULT NULL)'), + 'request_headers': ( + 'CREATE TABLE request_headers(id INTEGER PRIMARY KEY AUTOINCREMENT,' + 'download_id INTEGER NOT NULL, ' + 'header TEXT NOT NULL,value TEXT NOT NULL)'), + 'sqlite_sequence': ( + 'CREATE TABLE sqlite_sequence(name,seq)')}] + + 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.JavaTime: date and time value or None if not available. + """ + timestamp = self._GetRowValue(query_hash, row, value_name) + if timestamp is None: + return None + + return dfdatetime_java_time.JavaTime(timestamp=timestamp) + + def ParseDownloadsRow(self, parser_mediator, query, row, **unused_kwargs): + """Parses a download 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 = AndroidNativeDownloadsEventData() + event_data.lastmod = self._GetDateTimeRowValue(query_hash, row, 'lastmod') + event_data.id = self._GetRowValue(query_hash, row, '_id') + event_data.uri = self._GetRowValue(query_hash, row, 'uri') + event_data.mimetype = self._GetRowValue(query_hash, row, 'mimetype') + event_data.total_bytes = self._GetRowValue(query_hash, row, 'total_bytes') + event_data.current_bytes = self._GetRowValue( + query_hash, row, 'current_bytes') + event_data.status = self._GetRowValue(query_hash, row, 'status') + event_data.saved_to = self._GetRowValue( + query_hash, row, '_data') + event_data.deleted = self._GetRowValue(query_hash, row, 'deleted') + event_data.notification_package = self._GetRowValue( + query_hash, row, 'notificationpackage') + event_data.title = self._GetRowValue(query_hash, row, 'title') + event_data.error_msg = self._GetRowValue(query_hash, row, 'errorMsg') + event_data.is_visible_in_downloads_ui = self._GetRowValue( + query_hash, row, 'is_visible_in_downloads_ui') + event_data.media_provider_uri = self._GetRowValue( + query_hash, row, 'mediaprovider_uri') + event_data.destination = self._GetRowValue(query_hash, row, 'destination') + event_data.ui_visibility = self._GetRowValue(query_hash, row, 'visibility') + event_data.e_tag = self._GetRowValue(query_hash, row, 'etag') + event_data.description = self._GetRowValue(query_hash, row, 'description') + + parser_mediator.ProduceEventData(event_data) + + +sqlite.SQLiteParser.RegisterPlugin(AndroidNativeDownloadsPlugin) diff --git a/test_data/downloads.db b/test_data/downloads.db new file mode 100644 index 0000000000..2aaf65210a Binary files /dev/null and b/test_data/downloads.db differ diff --git a/tests/parsers/sqlite_plugins/android_native_downloads.py b/tests/parsers/sqlite_plugins/android_native_downloads.py new file mode 100644 index 0000000000..51ffced546 --- /dev/null +++ b/tests/parsers/sqlite_plugins/android_native_downloads.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for the Android SMS plugin.""" + +import unittest + +from plaso.parsers.sqlite_plugins import android_native_downloads + +from tests.parsers.sqlite_plugins import test_lib + +class AndroidNativeDownloadsTest(test_lib.SQLitePluginTestCase): + """Tests for the Android native downloads database plugin.""" + + def testProcess(self): + """Test the Process function on an Android native downloads database (downloads.db) file.""" + plugin = android_native_downloads.AndroidNativeDownloadsPlugin() + storage_writer = self._ParseDatabaseFileWithPlugin(['downloads.db'], plugin) + + # The Native Downloads database file contains 11 events. + number_of_event_data = storage_writer.GetNumberOfAttributeContainers( + 'event_data') + self.assertEqual(number_of_event_data, 11) + + number_of_warnings = storage_writer.GetNumberOfAttributeContainers( + 'extraction_warning') + self.assertEqual(number_of_warnings, 0) + + number_of_warnings = storage_writer.GetNumberOfAttributeContainers( + 'recovery_warning') + self.assertEqual(number_of_warnings, 0) + + expected_event_values = { + 'lastmod': '2022-11-12T15:32:28.279+00:00', + 'id': 46, + 'uri': 'https://cdn.discordapp.com/attachments/622810296226152474/1041012392089370735/IMG_1953.jpg', + 'mimetype': 'image/jpeg', + 'total_bytes': 2149749, + 'current_bytes': 2149749, + 'status': 200, + 'saved_to': '/storage/emulated/0/Download/IMG_1953.jpg', + 'deleted': 0, + 'notification_package': 'com.discord', + 'title': 'IMG_1953.jpg', + 'media_provider_uri': 'content://media/external_primary/images/media/1000000486', + 'error_msg': None, + 'is_visible_in_downloads_ui': 1, + 'destination': 4, + 'ui_visibility': 1, + 'e_tag': '"932f5b7818a3c0c1284cda69b6d7ea30"', + 'description': '', + } + + event_data = storage_writer.GetAttributeContainerByIndex('event_data', 0) + self.CheckEventData(event_data, expected_event_values) + + if __name__ == '__main__': + unittest.main()