280 lines
9.2 KiB
Python
280 lines
9.2 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 SQLite parser."""
|
|
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
|
|
import sqlite3
|
|
|
|
from plaso.lib import errors
|
|
from plaso.parsers import interface
|
|
from plaso.parsers import manager
|
|
from plaso.parsers import plugins
|
|
|
|
|
|
class SQLiteCache(plugins.BasePluginCache):
|
|
"""A cache storing query results for SQLite plugins."""
|
|
|
|
def CacheQueryResults(
|
|
self, sql_results, attribute_name, key_name, values):
|
|
"""Build a dict object based on a SQL command.
|
|
|
|
This function will take a SQL command, execute it and for
|
|
each resulting row it will store a key in a dictionary.
|
|
|
|
An example:
|
|
sql_results = A SQL result object after executing the
|
|
SQL command: 'SELECT foo, bla, bar FROM my_table'
|
|
attribute_name = 'all_the_things'
|
|
key_name = 'foo'
|
|
values = ['bla', 'bar']
|
|
|
|
Results from running this against the database:
|
|
'first', 'stuff', 'things'
|
|
'second', 'another stuff', 'another thing'
|
|
|
|
This will result in a dict object being created in the
|
|
cache, called 'all_the_things' and it will contain the following value:
|
|
|
|
all_the_things = {
|
|
'first': ['stuff', 'things'],
|
|
'second': ['another_stuff', 'another_thing']}
|
|
|
|
Args:
|
|
sql_results: The SQL result object (sqlite.Cursor) after executing
|
|
a SQL command on the database.
|
|
attribute_name: The attribute name in the cache to store
|
|
results to. This will be the name of the
|
|
dict attribute.
|
|
key_name: The name of the result field that should be used
|
|
as a key in the resulting dict that is created.
|
|
values: A list of result fields that are stored as values
|
|
to the dict. If this list has only one value in it
|
|
the value will be stored directly, otherwise the value
|
|
will be a list containing the extracted results based
|
|
on the names provided in this list.
|
|
"""
|
|
setattr(self, attribute_name, {})
|
|
attribute = getattr(self, attribute_name)
|
|
|
|
row = sql_results.fetchone()
|
|
while row:
|
|
if len(values) == 1:
|
|
attribute[row[key_name]] = row[values[0]]
|
|
else:
|
|
attribute[row[key_name]] = []
|
|
for value in values:
|
|
attribute[row[key_name]].append(row[value])
|
|
|
|
row = sql_results.fetchone()
|
|
|
|
|
|
class SQLiteDatabase(object):
|
|
"""A simple wrapper for opening up a SQLite database."""
|
|
|
|
# Magic value for a SQLite database.
|
|
MAGIC = 'SQLite format 3'
|
|
|
|
_READ_BUFFER_SIZE = 65536
|
|
|
|
def __init__(self, file_entry):
|
|
"""Initializes the database object.
|
|
|
|
Args:
|
|
file_entry: the file entry object.
|
|
"""
|
|
self._cursor = None
|
|
self._database = None
|
|
self._file_entry = file_entry
|
|
self._open = False
|
|
self._tables = []
|
|
self._temp_file_name = ''
|
|
|
|
def __exit__(self, unused_type, unused_value, unused_traceback):
|
|
"""Make usable with "with" statement."""
|
|
self.Close()
|
|
|
|
def __enter__(self):
|
|
"""Make usable with "with" statement."""
|
|
return self
|
|
|
|
@property
|
|
def cursor(self):
|
|
"""Returns a cursor object from the database."""
|
|
if not self._open:
|
|
self.Open()
|
|
|
|
return self._database.cursor()
|
|
|
|
@property
|
|
def tables(self):
|
|
"""Returns a list of all the tables in the database."""
|
|
if not self._open:
|
|
self.Open()
|
|
|
|
return self._tables
|
|
|
|
def Close(self):
|
|
"""Close the database connection and clean up the temporary file."""
|
|
if not self._open:
|
|
return
|
|
|
|
self._database.close()
|
|
|
|
try:
|
|
os.remove(self._temp_file_name)
|
|
except (OSError, IOError) as exception:
|
|
logging.warning((
|
|
u'Unable to remove temporary copy: {0:s} of SQLite database: {1:s} '
|
|
u'with error: {2:s}').format(
|
|
self._temp_file_name, self._file_entry.name, exception))
|
|
|
|
self._tables = []
|
|
self._database = None
|
|
self._temp_file_name = ''
|
|
self._open = False
|
|
|
|
def Open(self):
|
|
"""Opens up a database connection and build a list of table names."""
|
|
file_object = self._file_entry.GetFileObject()
|
|
|
|
# TODO: Remove this when the classifier gets implemented
|
|
# and used. As of now, there is no check made against the file
|
|
# to verify it's signature, thus all files are sent here, meaning
|
|
# that this method assumes everything is a SQLite file and starts
|
|
# copying the content of the file into memory, which is not good
|
|
# for very large files.
|
|
file_object.seek(0, os.SEEK_SET)
|
|
|
|
data = file_object.read(len(self.MAGIC))
|
|
|
|
if data != self.MAGIC:
|
|
file_object.close()
|
|
raise IOError(
|
|
u'File {0:s} not a SQLite database. (invalid signature)'.format(
|
|
self._file_entry.name))
|
|
|
|
# TODO: Current design copies the entire file into a buffer
|
|
# that is parsed by each SQLite parser. This is not very efficient,
|
|
# especially when many SQLite parsers are ran against a relatively
|
|
# large SQLite database. This temporary file that is created should
|
|
# be usable by all SQLite parsers so the file should only be read
|
|
# once in memory and then deleted when all SQLite parsers have completed.
|
|
|
|
# TODO: Change this into a proper implementation using APSW
|
|
# and virtual filesystems when that will be available.
|
|
# Info: http://apidoc.apsw.googlecode.com/hg/vfs.html#vfs and
|
|
# http://apidoc.apsw.googlecode.com/hg/example.html#example-vfs
|
|
# Until then, just copy the file into a tempfile and parse it.
|
|
|
|
# Note that data is filled here with the file header data and
|
|
# that with will explicitly close the temporary files and thus
|
|
# making sure it is available for sqlite3.connect().
|
|
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
|
self._temp_file_name = temp_file.name
|
|
while data:
|
|
temp_file.write(data)
|
|
data = file_object.read(self._READ_BUFFER_SIZE)
|
|
|
|
self._database = sqlite3.connect(self._temp_file_name)
|
|
try:
|
|
self._database.row_factory = sqlite3.Row
|
|
self._cursor = self._database.cursor()
|
|
except sqlite3.DatabaseError as exception:
|
|
logging.debug(
|
|
u'Unable to parse SQLite database: {0:s} with error: {1:s}'.format(
|
|
self._file_entry.name, exception))
|
|
raise
|
|
|
|
# Verify the table by reading in all table names and compare it to
|
|
# the list of required tables.
|
|
try:
|
|
sql_results = self._cursor.execute(
|
|
'SELECT name FROM sqlite_master WHERE type="table"')
|
|
except sqlite3.DatabaseError as exception:
|
|
logging.debug(
|
|
u'Unable to parse SQLite database: {0:s} with error: {1:s}'.format(
|
|
self._file_entry.name, exception))
|
|
raise
|
|
|
|
self._tables = []
|
|
for row in sql_results:
|
|
self._tables.append(row[0])
|
|
|
|
self._open = True
|
|
|
|
|
|
class SQLiteParser(interface.BasePluginsParser):
|
|
"""A SQLite parser for Plaso."""
|
|
|
|
# Name of the parser, which enables all plugins by default.
|
|
NAME = 'sqlite'
|
|
DESCRIPTION = u'Parser for SQLite database files.'
|
|
|
|
_plugin_classes = {}
|
|
|
|
def __init__(self):
|
|
"""Initializes a parser object."""
|
|
super(SQLiteParser, self).__init__()
|
|
self._local_zone = False
|
|
self._plugins = SQLiteParser.GetPluginObjects()
|
|
self.db = None
|
|
|
|
def Parse(self, parser_context, file_entry, parser_chain=None):
|
|
"""Parses an SQLite database.
|
|
|
|
Args:
|
|
parser_context: A parser context object (instance of ParserContext).
|
|
file_entry: A file entry object (instance of dfvfs.FileEntry).
|
|
parser_chain: Optional string containing the parsing chain up to this
|
|
point. The default is None.
|
|
|
|
Returns:
|
|
A event object generator (EventObjects) extracted from the database.
|
|
"""
|
|
with SQLiteDatabase(file_entry) as database:
|
|
try:
|
|
database.Open()
|
|
except IOError as exception:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to open database with error: {0:s}'.format(
|
|
repr(exception)))
|
|
except sqlite3.DatabaseError as exception:
|
|
raise errors.UnableToParseFile(
|
|
u'Unable to parse SQLite database with error: {0:s}.'.format(
|
|
repr(exception)))
|
|
|
|
parser_chain = self._BuildParserChain(parser_chain)
|
|
# Create a cache in which the resulting tables are cached.
|
|
cache = SQLiteCache()
|
|
for plugin_object in self._plugins:
|
|
try:
|
|
plugin_object.Process(
|
|
parser_context, file_entry=file_entry, parser_chain=parser_chain,
|
|
cache=cache, database=database)
|
|
|
|
except errors.WrongPlugin:
|
|
logging.debug(
|
|
u'Plugin: {0:s} cannot parse database: {1:s}'.format(
|
|
plugin_object.NAME, file_entry.name))
|
|
|
|
|
|
manager.ParsersManager.RegisterParser(SQLiteParser)
|