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 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9d5435a
Add Android Native Downloads Sqlite Plugin
ChristopherGammaWau Dec 1, 2024
c82dc1e
Add Android Native Downloads Unit Test
ChristopherGammaWau Dec 1, 2024
c9c86b9
Add Artifact: downloads.db
ChristopherGammaWau Dec 1, 2024
a0ae191
Modify __init__.py for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
cc09a76
Modify timeliner.yaml for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
ad2e8bc
Modify android.yaml for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
ca65e82
Remove unused import android_native_downloads.py.
ChristopherGammaWau Dec 1, 2024
5b89ea4
Add Android Native Downloads Sqlite Plugin
ChristopherGammaWau Dec 1, 2024
5d9ae71
Add Android Native Downloads Unit Test
ChristopherGammaWau Dec 1, 2024
70b34af
Add Artifact: downloads.db
ChristopherGammaWau Dec 1, 2024
20fe568
Modify __init__.py for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
bcc0802
Modify timeliner.yaml for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
20075d5
Modify android.yaml for android downloads.db plugin
ChristopherGammaWau Dec 1, 2024
66a5d6c
Remove unused import android_native_downloads.py.
ChristopherGammaWau Dec 1, 2024
7306f28
Fix pylint
barpeot Dec 3, 2024
0ddfd00
Revert super() command changes
barpeot Dec 3, 2024
a285a95
Fixed parser query typo
barpeot Dec 3, 2024
cb5c1ea
Update lint error in tests
ChristopherGammaWau Mar 2, 2025
c199258
Merge branch 'main' of https://github.com/ChristopherGammaWau/Android…
ChristopherGammaWau Mar 2, 2025
d42f74b
Revert "Update lint error in tests"
ChristopherGammaWau Mar 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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