269 lines
9.3 KiB
Python
269 lines
9.3 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright 2012 The Plaso Project Authors.
|
|
# Please see the AUTHORS file for details on individual authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""This file contains a parser for the Google Drive snaphots.
|
|
|
|
The Google Drive snapshots are stored in SQLite database files named
|
|
snapshot.db.
|
|
"""
|
|
|
|
from plaso.events import time_events
|
|
from plaso.lib import eventdata
|
|
from plaso.parsers import sqlite
|
|
from plaso.parsers.sqlite_plugins import interface
|
|
|
|
|
|
__author__ = 'David Nides (david.nides@gmail.com)'
|
|
|
|
|
|
class GoogleDriveSnapshotCloudEntryEvent(time_events.PosixTimeEvent):
|
|
"""Convenience class for a Google Drive snapshot cloud entry."""
|
|
|
|
DATA_TYPE = 'gdrive:snapshot:cloud_entry'
|
|
|
|
# TODO: this could be moved to the formatter.
|
|
# The following definition for values can be found on Patrick Olson's blog:
|
|
# http://www.sysforensics.org/2012/05/google-drive-forensics-notes.html
|
|
_DOC_TYPES = {
|
|
0: u'FOLDER',
|
|
1: u'FILE',
|
|
2: u'PRESENTATION',
|
|
3: u'UNKNOWN',
|
|
4: u'SPREADSHEET',
|
|
5: u'DRAWING',
|
|
6: u'DOCUMENT',
|
|
7: u'TABLE',
|
|
}
|
|
|
|
def __init__(self, posix_time, usage, url, path, size, doc_type, shared):
|
|
"""Initializes the event.
|
|
|
|
Args:
|
|
posix_time: The POSIX time value.
|
|
usage: The description of the usage of the time value.
|
|
url: The URL of the file as in the cloud.
|
|
path: The path of the file.
|
|
size: The size of the file.
|
|
doc_type: Integer value containing the document type.
|
|
shared: A string indicating whether or not this is a shared document.
|
|
"""
|
|
super(GoogleDriveSnapshotCloudEntryEvent, self).__init__(
|
|
posix_time, usage)
|
|
|
|
self.url = url
|
|
self.path = path
|
|
self.size = size
|
|
self.document_type = self._DOC_TYPES.get(doc_type, u'UNKNOWN')
|
|
self.shared = shared
|
|
|
|
|
|
class GoogleDriveSnapshotLocalEntryEvent(time_events.PosixTimeEvent):
|
|
"""Convenience class for a Google Drive snapshot local entry event."""
|
|
|
|
DATA_TYPE = 'gdrive:snapshot:local_entry'
|
|
|
|
def __init__(self, posix_time, local_path, size):
|
|
"""Initializes the event object.
|
|
|
|
Args:
|
|
posix_time: The POSIX time value.
|
|
local_path: The local path of the file.
|
|
size: The size of the file.
|
|
"""
|
|
super(GoogleDriveSnapshotLocalEntryEvent, self).__init__(
|
|
posix_time, eventdata.EventTimestamp.MODIFICATION_TIME)
|
|
|
|
self.path = local_path
|
|
self.size = size
|
|
|
|
|
|
class GoogleDrivePlugin(interface.SQLitePlugin):
|
|
"""SQLite plugin for Google Drive snapshot.db files."""
|
|
|
|
NAME = 'google_drive'
|
|
DESCRIPTION = u'Parser for Google Drive SQLite database files.'
|
|
|
|
# Define the needed queries.
|
|
QUERIES = [
|
|
((u'SELECT e.resource_id, e.filename, e.modified, e.created, e.size, '
|
|
u'e.doc_type, e.shared, e.checksum, e.url, r.parent_resource_id FROM '
|
|
u'cloud_entry AS e, cloud_relations AS r WHERE r.child_resource_id = '
|
|
u'e.resource_id AND e.modified IS NOT NULL;'), 'ParseCloudEntryRow'),
|
|
((u'SELECT inode_number, filename, modified, checksum, size FROM '
|
|
u'local_entry WHERE modified IS NOT NULL;'), 'ParseLocalEntryRow')]
|
|
|
|
# The required tables.
|
|
REQUIRED_TABLES = frozenset([
|
|
'cloud_entry', 'cloud_relations', 'local_entry', 'local_relations',
|
|
'mapping', 'overlay_status'])
|
|
|
|
# Queries used to build cache.
|
|
LOCAL_PATH_CACHE_QUERY = (
|
|
u'SELECT r.child_inode_number, r.parent_inode_number, e.filename FROM '
|
|
u'local_relations AS r, local_entry AS e WHERE r.child_inode_number = '
|
|
u'e.inode_number')
|
|
CLOUD_PATH_CACHE_QUERY = (
|
|
u'SELECT e.filename, e.resource_id, r.parent_resource_id AS parent '
|
|
u'FROM cloud_entry AS e, cloud_relations AS r WHERE e.doc_type = 0 '
|
|
u'AND e.resource_id = r.child_resource_id')
|
|
|
|
def GetLocalPath(self, inode, cache, database):
|
|
"""Return local path for a given inode.
|
|
|
|
Args:
|
|
inode: The inode number for the file.
|
|
cache: A cache object (instance of SQLiteCache).
|
|
database: A database object (instance of SQLiteDatabase).
|
|
|
|
Returns:
|
|
A full path, including the filename of the given inode value.
|
|
"""
|
|
local_path = cache.GetResults('local_path')
|
|
if not local_path:
|
|
cursor = database.cursor
|
|
results = cursor.execute(self.LOCAL_PATH_CACHE_QUERY)
|
|
cache.CacheQueryResults(
|
|
results, 'local_path', 'child_inode_number',
|
|
('parent_inode_number', 'filename'))
|
|
local_path = cache.GetResults('local_path')
|
|
|
|
parent, path = local_path.get(inode, [None, None])
|
|
|
|
# TODO: Read the local_sync_root from the sync_config.db and use that
|
|
# for a root value.
|
|
root_value = u'%local_sync_root%/'
|
|
|
|
if not path:
|
|
return root_value
|
|
|
|
paths = []
|
|
while path:
|
|
paths.append(path)
|
|
parent, path = local_path.get(parent, [None, None])
|
|
|
|
if not paths:
|
|
return root_value
|
|
|
|
# Paths are built top level to root so we need to reverse the list to
|
|
# represent them in the traditional order.
|
|
paths.reverse()
|
|
return root_value + u'/'.join(paths)
|
|
|
|
def GetCloudPath(self, resource_id, cache, database):
|
|
"""Return cloud path given a resource id.
|
|
|
|
Args:
|
|
resource_id: The resource_id for the file.
|
|
cache: The local cache object.
|
|
database: A database object (instance of SQLiteDatabase).
|
|
|
|
Returns:
|
|
A full path to the resource value.
|
|
"""
|
|
cloud_path = cache.GetResults('cloud_path')
|
|
if not cloud_path:
|
|
cursor = database.cursor
|
|
results = cursor.execute(self.CLOUD_PATH_CACHE_QUERY)
|
|
cache.CacheQueryResults(
|
|
results, 'cloud_path', 'resource_id', ('filename', 'parent'))
|
|
cloud_path = cache.GetResults('cloud_path')
|
|
|
|
if resource_id == u'folder:root':
|
|
return u'/'
|
|
|
|
paths = []
|
|
parent_path, parent_id = cloud_path.get(resource_id, [u'', u''])
|
|
while parent_path:
|
|
if parent_path == u'folder:root':
|
|
break
|
|
paths.append(parent_path)
|
|
parent_path, parent_id = cloud_path.get(parent_id, [u'', u''])
|
|
|
|
if not paths:
|
|
return u'/'
|
|
|
|
# Paths are built top level to root so we need to reverse the list to
|
|
# represent them in the traditional order.
|
|
paths.reverse()
|
|
return u'/{0:s}/'.format(u'/'.join(paths))
|
|
|
|
def ParseCloudEntryRow(
|
|
self, parser_context, row, file_entry=None, parser_chain=None, query=None,
|
|
cache=None, database=None, **unused_kwargs):
|
|
"""Parses a cloud entry row.
|
|
|
|
Args:
|
|
parser_context: A parser context object (instance of ParserContext).
|
|
row: The row resulting from the query.
|
|
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
|
|
The default is None.
|
|
parser_chain: Optional string containing the parsing chain up to this
|
|
point. The default is None.
|
|
query: Optional query string. The default is None.
|
|
cache: The local cache object.
|
|
database: The database object.
|
|
"""
|
|
cloud_path = self.GetCloudPath(row['parent_resource_id'], cache, database)
|
|
cloud_filename = u'{0:s}{1:s}'.format(cloud_path, row['filename'])
|
|
|
|
if row['shared']:
|
|
shared = 'Shared'
|
|
else:
|
|
shared = 'Private'
|
|
|
|
event_object = GoogleDriveSnapshotCloudEntryEvent(
|
|
row['modified'], eventdata.EventTimestamp.MODIFICATION_TIME,
|
|
row['url'], cloud_filename, row['size'], row['doc_type'], shared)
|
|
parser_context.ProduceEvent(
|
|
event_object, query=query, parser_chain=parser_chain,
|
|
file_entry=file_entry)
|
|
|
|
if row['created']:
|
|
event_object = GoogleDriveSnapshotCloudEntryEvent(
|
|
row['created'], eventdata.EventTimestamp.CREATION_TIME,
|
|
row['url'], cloud_filename, row['size'], row['doc_type'], shared)
|
|
parser_context.ProduceEvent(
|
|
event_object, query=query, parser_chain=parser_chain,
|
|
file_entry=file_entry)
|
|
|
|
def ParseLocalEntryRow(
|
|
self, parser_context, row, file_entry=None, parser_chain=None, query=None,
|
|
cache=None, database=None, **unused_kwargs):
|
|
"""Parses a local entry row.
|
|
|
|
Args:
|
|
parser_context: A parser context object (instance of ParserContext).
|
|
row: The row resulting from the query.
|
|
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
|
|
The default is None.
|
|
parser_chain: Optional string containing the parsing chain up to this
|
|
point. The default is None.
|
|
query: Optional query string. The default is None.
|
|
cache: The local cache object (instance of SQLiteCache).
|
|
database: A database object (instance of SQLiteDatabase).
|
|
"""
|
|
local_path = self.GetLocalPath(row['inode_number'], cache, database)
|
|
|
|
event_object = GoogleDriveSnapshotLocalEntryEvent(
|
|
row['modified'], local_path, row['size'])
|
|
parser_context.ProduceEvent(
|
|
event_object, query=query, parser_chain=parser_chain,
|
|
file_entry=file_entry)
|
|
|
|
|
|
sqlite.SQLiteParser.RegisterPlugin(GoogleDrivePlugin)
|