plaso-rubanetra/plaso/parsers/sqlite.py
2020-04-06 18:48:34 +02:00

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)