Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SQLite parser for Android Native Downloads (downloads.db) file #4929

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
83 changes: 83 additions & 0 deletions plaso/data/formatters/android.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand Down
6 changes: 6 additions & 0 deletions plaso/data/timeliner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
1 change: 1 addition & 0 deletions plaso/parsers/sqlite_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
199 changes: 199 additions & 0 deletions plaso/parsers/sqlite_plugins/android_native_downloads.py
Original file line number Diff line number Diff line change
@@ -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)
Binary file added test_data/downloads.db
Binary file not shown.
57 changes: 57 additions & 0 deletions tests/parsers/sqlite_plugins/android_native_downloads.py
Original file line number Diff line number Diff line change
@@ -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()
Loading