Import from old repository
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""Import statements for analysis plugins and common methods."""
|
||||
|
||||
from plaso.analysis import interface
|
||||
from plaso.lib import errors
|
||||
|
||||
# Import statements of analysis plugins.
|
||||
from plaso.analysis import browser_search
|
||||
from plaso.analysis import chrome_extension
|
||||
from plaso.analysis import windows_services
|
||||
|
||||
|
||||
# TODO: move these functions to a manager class. And add a test for this
|
||||
# function.
|
||||
def ListAllPluginNames(show_all=True):
|
||||
"""Return a list of all available plugin names and it's doc string."""
|
||||
results = []
|
||||
for cls_obj in interface.AnalysisPlugin.classes.itervalues():
|
||||
doc_string, _, _ = cls_obj.__doc__.partition('\n')
|
||||
|
||||
obj = cls_obj(None)
|
||||
if not show_all and cls_obj.ENABLE_IN_EXTRACTION:
|
||||
results.append((obj.plugin_name, doc_string, obj.plugin_type))
|
||||
elif show_all:
|
||||
results.append((obj.plugin_name, doc_string, obj.plugin_type))
|
||||
|
||||
return sorted(results)
|
||||
|
||||
|
||||
def LoadPlugins(plugin_names, incoming_queues, options=None):
|
||||
"""Yield analysis plugins for a given list of plugin names.
|
||||
|
||||
Given a list of plugin names this method finds the analysis
|
||||
plugins, initializes them and returns a generator.
|
||||
|
||||
Args:
|
||||
plugin_names: A list of plugin names that should be loaded up. This
|
||||
should be a list of strings.
|
||||
incoming_queues: A list of queues (QueueInterface object) that the plugin
|
||||
uses to read in incoming events to analyse.
|
||||
options: Optional command line arguments (instance of
|
||||
argparse.Namespace). The default is None.
|
||||
|
||||
Yields:
|
||||
Analysis plugin objects (instances of AnalysisPlugin).
|
||||
|
||||
Raises:
|
||||
errors.BadConfigOption: If plugins_names does not contain a list of
|
||||
strings.
|
||||
"""
|
||||
try:
|
||||
plugin_names_lower = [word.lower() for word in plugin_names]
|
||||
except AttributeError:
|
||||
raise errors.BadConfigOption(u'Plugin names should be a list of strings.')
|
||||
|
||||
for plugin_object in interface.AnalysisPlugin.classes.itervalues():
|
||||
plugin_name = plugin_object.NAME.lower()
|
||||
|
||||
if plugin_name in plugin_names_lower:
|
||||
queue_index = plugin_names_lower.index(plugin_name)
|
||||
|
||||
try:
|
||||
incoming_queue = incoming_queues[queue_index]
|
||||
except (TypeError, IndexError):
|
||||
incoming_queue = None
|
||||
|
||||
yield plugin_object(incoming_queue, options)
|
||||
@@ -0,0 +1,257 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""A plugin that extracts browser history from events."""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from plaso import filters
|
||||
from plaso.analysis import interface
|
||||
from plaso.formatters import manager as formatters_manager
|
||||
from plaso.lib import event
|
||||
|
||||
|
||||
# Create a lightweight object that is used to store timeline based information
|
||||
# about each search term.
|
||||
SEARCH_OBJECT = collections.namedtuple(
|
||||
'SEARCH_OBJECT', 'time source engine search_term')
|
||||
|
||||
|
||||
def ScrubLine(line):
|
||||
"""Scrub the line of most obvious HTML codes.
|
||||
|
||||
An attempt at taking a line and swapping all instances
|
||||
of %XX which represent a character in hex with it's
|
||||
unicode character.
|
||||
|
||||
Args:
|
||||
line: The string that we are about to "fix".
|
||||
|
||||
Returns:
|
||||
String that has it's %XX hex codes swapped for text.
|
||||
"""
|
||||
if not line:
|
||||
return ''
|
||||
|
||||
try:
|
||||
return unicode(urllib.unquote(str(line)), 'utf-8')
|
||||
except UnicodeDecodeError:
|
||||
logging.warning(u'Unable to decode line: {0:s}'.format(line))
|
||||
|
||||
return line
|
||||
|
||||
|
||||
class FilterClass(object):
|
||||
"""A class that contains all the parser functions."""
|
||||
|
||||
@classmethod
|
||||
def _GetBetweenQEqualsAndAmbersand(cls, string):
|
||||
"""Return back string that is defined 'q=' and '&'."""
|
||||
if 'q=' not in string:
|
||||
return string
|
||||
_, _, line = string.partition('q=')
|
||||
before_and, _, _ = line.partition('&')
|
||||
if not before_and:
|
||||
return line
|
||||
return before_and.split()[0]
|
||||
|
||||
@classmethod
|
||||
def _SearchAndQInLine(cls, string):
|
||||
"""Return a bool indicating if the words q= and search appear in string."""
|
||||
return 'search' in string and 'q=' in string
|
||||
|
||||
@classmethod
|
||||
def GoogleSearch(cls, url):
|
||||
"""Return back the extracted string."""
|
||||
if not cls._SearchAndQInLine(url):
|
||||
return
|
||||
|
||||
line = cls._GetBetweenQEqualsAndAmbersand(url)
|
||||
if not line:
|
||||
return
|
||||
|
||||
return line.replace('+', ' ')
|
||||
|
||||
@classmethod
|
||||
def YouTube(cls, url):
|
||||
"""Return back the extracted string."""
|
||||
return cls.GenericSearch(url)
|
||||
|
||||
@classmethod
|
||||
def BingSearch(cls, url):
|
||||
"""Return back the extracted string."""
|
||||
return cls.GenericSearch(url)
|
||||
|
||||
@classmethod
|
||||
def GenericSearch(cls, url):
|
||||
"""Return back the extracted string from a generic search engine."""
|
||||
if not cls._SearchAndQInLine(url):
|
||||
return
|
||||
|
||||
return cls._GetBetweenQEqualsAndAmbersand(url).replace('+', ' ')
|
||||
|
||||
@classmethod
|
||||
def Yandex(cls, url):
|
||||
"""Return back the results from Yandex search engine."""
|
||||
if 'text=' not in url:
|
||||
return
|
||||
_, _, line = url.partition('text=')
|
||||
before_and, _, _ = line.partition('&')
|
||||
if not before_and:
|
||||
return
|
||||
yandex_search_url = before_and.split()[0]
|
||||
|
||||
return yandex_search_url.replace('+', ' ')
|
||||
|
||||
@classmethod
|
||||
def DuckDuckGo(cls, url):
|
||||
"""Return back the extracted string."""
|
||||
if not 'q=' in url:
|
||||
return
|
||||
return cls._GetBetweenQEqualsAndAmbersand(url).replace('+', ' ')
|
||||
|
||||
@classmethod
|
||||
def Gmail(cls, url):
|
||||
"""Return back the extracted string."""
|
||||
if 'search/' not in url:
|
||||
return
|
||||
|
||||
_, _, line = url.partition('search/')
|
||||
first, _, _ = line.partition('/')
|
||||
second, _, _ = first.partition('?compose')
|
||||
|
||||
return second.replace('+', ' ')
|
||||
|
||||
|
||||
class AnalyzeBrowserSearchPlugin(interface.AnalysisPlugin):
|
||||
"""Analyze browser search entries from events."""
|
||||
|
||||
NAME = 'browser_search'
|
||||
|
||||
# Indicate that we do not want to run this plugin during regular extraction.
|
||||
ENABLE_IN_EXTRACTION = False
|
||||
|
||||
# Here we define filters and callback methods for all hits on each filter.
|
||||
FILTERS = (
|
||||
(('url iregexp "(www.|encrypted.|/)google." and url contains "search"'),
|
||||
'GoogleSearch'),
|
||||
('url contains "youtube.com"', 'YouTube'),
|
||||
(('source is "WEBHIST" and url contains "bing.com" and url contains '
|
||||
'"search"'), 'BingSearch'),
|
||||
('url contains "mail.google.com"', 'Gmail'),
|
||||
(('source is "WEBHIST" and url contains "yandex.com" and url contains '
|
||||
'"yandsearch"'), 'Yandex'),
|
||||
('url contains "duckduckgo.com"', 'DuckDuckGo')
|
||||
)
|
||||
|
||||
# We need to implement the interface for analysis plugins, but we don't use
|
||||
# command line options here, so disable checking for unused args.
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, incoming_queue, options=None):
|
||||
"""Initializes the browser search analysis plugin.
|
||||
|
||||
Args:
|
||||
incoming_queue: A queue that is used to listen to incoming events.
|
||||
options: Optional command line arguments (instance of
|
||||
argparse.Namespace). The default is None.
|
||||
"""
|
||||
super(AnalyzeBrowserSearchPlugin, self).__init__(incoming_queue)
|
||||
self._filter_dict = {}
|
||||
self._counter = collections.Counter()
|
||||
|
||||
# Store a list of search terms in a timeline format.
|
||||
# The format is key = timestamp, value = (source, engine, search term).
|
||||
self._search_term_timeline = []
|
||||
|
||||
for filter_str, call_back in self.FILTERS:
|
||||
filter_obj = filters.GetFilter(filter_str)
|
||||
call_back_obj = getattr(FilterClass, call_back, None)
|
||||
if filter_obj and call_back_obj:
|
||||
self._filter_dict[filter_obj] = (call_back, call_back_obj)
|
||||
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def CompileReport(self):
|
||||
"""Compiles a report of the analysis.
|
||||
|
||||
Returns:
|
||||
The analysis report (instance of AnalysisReport).
|
||||
"""
|
||||
report = event.AnalysisReport()
|
||||
|
||||
results = {}
|
||||
for key, count in self._counter.iteritems():
|
||||
search_engine, _, search_term = key.partition(':')
|
||||
results.setdefault(search_engine, {})
|
||||
results[search_engine][search_term] = count
|
||||
report.report_dict = results
|
||||
report.report_array = self._search_term_timeline
|
||||
|
||||
lines_of_text = []
|
||||
for search_engine, terms in sorted(results.items()):
|
||||
lines_of_text.append(u' == ENGINE: {0:s} =='.format(search_engine))
|
||||
|
||||
for search_term, count in sorted(
|
||||
terms.iteritems(), key=lambda x: (x[1], x[0]), reverse=True):
|
||||
lines_of_text.append(u'{0:d} {1:s}'.format(count, search_term))
|
||||
|
||||
# An empty string is added to have SetText create an empty line.
|
||||
lines_of_text.append(u'')
|
||||
|
||||
report.SetText(lines_of_text)
|
||||
|
||||
return report
|
||||
|
||||
def ExamineEvent(
|
||||
self, unused_analysis_context, event_object, **unused_kwargs):
|
||||
"""Analyzes an event object.
|
||||
|
||||
Args:
|
||||
analysis_context: An analysis context object
|
||||
(instance of AnalysisContext).
|
||||
event_object: An event object (instance of EventObject).
|
||||
"""
|
||||
# This event requires an URL attribute.
|
||||
url_attribute = getattr(event_object, 'url', None)
|
||||
|
||||
if not url_attribute:
|
||||
return
|
||||
|
||||
# TODO: refactor this the source should be used in formatting only.
|
||||
# Check if we are dealing with a web history event.
|
||||
source, _ = formatters_manager.EventFormatterManager.GetSourceStrings(
|
||||
event_object)
|
||||
|
||||
if source != 'WEBHIST':
|
||||
return
|
||||
|
||||
for filter_obj, call_backs in self._filter_dict.items():
|
||||
call_back_name, call_back_object = call_backs
|
||||
if filter_obj.Match(event_object):
|
||||
returned_line = ScrubLine(call_back_object(url_attribute))
|
||||
if not returned_line:
|
||||
continue
|
||||
self._counter[u'{0:s}:{1:s}'.format(call_back_name, returned_line)] += 1
|
||||
|
||||
# Add the timeline format for each search term.
|
||||
self._search_term_timeline.append(SEARCH_OBJECT(
|
||||
getattr(event_object, 'timestamp', 0),
|
||||
getattr(event_object, 'plugin', getattr(
|
||||
event_object, 'parser', u'N/A')),
|
||||
call_back_name, returned_line))
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""Tests for the browser search analysis plugin."""
|
||||
|
||||
import unittest
|
||||
|
||||
from plaso.analysis import browser_search
|
||||
from plaso.analysis import test_lib
|
||||
# pylint: disable=unused-import
|
||||
from plaso.formatters import chrome as chrome_formatter
|
||||
from plaso.lib import event
|
||||
from plaso.parsers import sqlite
|
||||
from plaso.parsers.sqlite_plugins import chrome
|
||||
|
||||
|
||||
class BrowserSearchAnalysisTest(test_lib.AnalysisPluginTestCase):
|
||||
"""Tests for the browser search analysis plugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Sets up the needed objects used throughout the test."""
|
||||
self._parser = sqlite.SQLiteParser()
|
||||
|
||||
def testAnalyzeFile(self):
|
||||
"""Read a storage file that contains URL data and analyze it."""
|
||||
knowledge_base = self._SetUpKnowledgeBase()
|
||||
|
||||
test_file = self._GetTestFilePath(['History'])
|
||||
event_queue = self._ParseFile(self._parser, test_file, knowledge_base)
|
||||
|
||||
analysis_plugin = browser_search.AnalyzeBrowserSearchPlugin(event_queue)
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
self.assertEquals(len(analysis_reports), 1)
|
||||
|
||||
analysis_report = analysis_reports[0]
|
||||
|
||||
# Due to the behavior of the join one additional empty string at the end
|
||||
# is needed to create the last empty line.
|
||||
expected_text = u'\n'.join([
|
||||
u' == ENGINE: GoogleSearch ==',
|
||||
u'1 really really funny cats',
|
||||
u'1 java plugin',
|
||||
u'1 funnycats.exe',
|
||||
u'1 funny cats',
|
||||
u'',
|
||||
u''])
|
||||
|
||||
self.assertEquals(analysis_report.text, expected_text)
|
||||
self.assertEquals(analysis_report.plugin_name, 'browser_search')
|
||||
|
||||
expected_keys = set([u'GoogleSearch'])
|
||||
self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""A plugin that gather extension ID's from Chrome history browser."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
from plaso.analysis import interface
|
||||
from plaso.lib import event
|
||||
|
||||
|
||||
class AnalyzeChromeExtensionPlugin(interface.AnalysisPlugin):
|
||||
"""Convert Chrome extension ID's into names, requires Internet connection."""
|
||||
|
||||
NAME = 'chrome_extension'
|
||||
|
||||
# Indicate that we can run this plugin during regular extraction.
|
||||
ENABLE_IN_EXTRACTION = True
|
||||
|
||||
_TITLE_RE = re.compile('<title>([^<]+)</title>')
|
||||
_WEB_STORE_URL = u'https://chrome.google.com/webstore/detail/{xid}?hl=en-US'
|
||||
|
||||
# We need to implement the interface for analysis plugins, but we don't use
|
||||
# command line options here, so disable checking for unused args.
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, incoming_queue, options=None):
|
||||
"""Initializes the Chrome extension analysis plugin.
|
||||
|
||||
Args:
|
||||
incoming_queue: A queue that is used to listen to incoming events.
|
||||
options: Optional command line arguments (instance of
|
||||
argparse.Namespace). The default is None.
|
||||
"""
|
||||
super(AnalyzeChromeExtensionPlugin, self).__init__(incoming_queue)
|
||||
|
||||
self._results = {}
|
||||
self.plugin_type = self.TYPE_REPORT
|
||||
|
||||
# TODO: see if these can be moved to arguments passed to ExamineEvent
|
||||
# or some kind of state object.
|
||||
self._sep = None
|
||||
self._user_paths = None
|
||||
|
||||
# Saved list of already looked up extensions.
|
||||
self._extensions = {}
|
||||
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def _GetChromeWebStorePage(self, extension_id):
|
||||
"""Retrieves the page for the extension from the Chrome store website.
|
||||
|
||||
Args:
|
||||
extension_id: string containing the extension identifier.
|
||||
"""
|
||||
web_store_url = self._WEB_STORE_URL.format(xid=extension_id)
|
||||
try:
|
||||
response = urllib2.urlopen(web_store_url)
|
||||
|
||||
except urllib2.HTTPError as exception:
|
||||
logging.warning((
|
||||
u'[{0:s}] unable to retrieve URL: {1:s} with error: {2:s}').format(
|
||||
self.NAME, web_store_url, exception))
|
||||
return
|
||||
|
||||
except urllib2.URLError as exception:
|
||||
logging.warning((
|
||||
u'[{0:s}] invalid URL: {1:s} with error: {2:s}').format(
|
||||
self.NAME, web_store_url, exception))
|
||||
return
|
||||
|
||||
return response
|
||||
|
||||
def _GetTitleFromChromeWebStore(self, extension_id):
|
||||
"""Retrieves the name of the extension from the Chrome store website.
|
||||
|
||||
Args:
|
||||
extension_id: string containing the extension identifier.
|
||||
"""
|
||||
# Check if we have already looked this extension up.
|
||||
if extension_id in self._extensions:
|
||||
return self._extensions.get(extension_id)
|
||||
|
||||
response = self._GetChromeWebStorePage(extension_id)
|
||||
if not response:
|
||||
logging.warning(
|
||||
u'[{0:s}] no data returned for extension identifier: {1:s}'.format(
|
||||
self.NAME, extension_id))
|
||||
return
|
||||
|
||||
first_line = response.readline()
|
||||
match = self._TITLE_RE.search(first_line)
|
||||
if match:
|
||||
title = match.group(1)
|
||||
if title.startswith(u'Chrome Web Store - '):
|
||||
name = title[19:]
|
||||
elif title.endswith(u'- Chrome Web Store'):
|
||||
name = title[:-19]
|
||||
|
||||
self._extensions[extension_id] = name
|
||||
return name
|
||||
|
||||
self._extensions[extension_id] = u'Not Found'
|
||||
|
||||
def CompileReport(self):
|
||||
"""Compiles a report of the analysis.
|
||||
|
||||
Returns:
|
||||
The analysis report (instance of AnalysisReport).
|
||||
"""
|
||||
report = event.AnalysisReport()
|
||||
|
||||
report.report_dict = self._results
|
||||
|
||||
lines_of_text = []
|
||||
for user, extensions in sorted(self._results.iteritems()):
|
||||
lines_of_text.append(u' == USER: {0:s} =='.format(user))
|
||||
for extension, extension_id in sorted(extensions):
|
||||
lines_of_text.append(u' {0:s} [{1:s}]'.format(extension, extension_id))
|
||||
|
||||
# An empty string is added to have SetText create an empty line.
|
||||
lines_of_text.append(u'')
|
||||
|
||||
report.SetText(lines_of_text)
|
||||
|
||||
return report
|
||||
|
||||
def ExamineEvent(self, analysis_context, event_object, **unused_kwargs):
|
||||
"""Analyzes an event object.
|
||||
|
||||
Args:
|
||||
analysis_context: An analysis context object
|
||||
(instance of AnalysisContext).
|
||||
event_object: An event object (instance of EventObject).
|
||||
"""
|
||||
# Only interested in filesystem events.
|
||||
if event_object.data_type != 'fs:stat':
|
||||
return
|
||||
|
||||
filename = getattr(event_object, 'filename', None)
|
||||
if not filename:
|
||||
return
|
||||
|
||||
# Determine if we have a Chrome extension ID.
|
||||
if u'chrome' not in filename.lower():
|
||||
return
|
||||
|
||||
if not self._sep:
|
||||
self._sep = analysis_context.GetPathSegmentSeparator(filename)
|
||||
|
||||
if not self._user_paths:
|
||||
self._user_paths = analysis_context.GetUserPaths(analysis_context.users)
|
||||
|
||||
if u'{0:s}Extensions{0:s}'.format(self._sep) not in filename:
|
||||
return
|
||||
|
||||
# Now we have extension ID's, let's check if we've got the
|
||||
# folder, nothing else.
|
||||
paths = filename.split(self._sep)
|
||||
if paths[-2] != u'Extensions':
|
||||
return
|
||||
|
||||
extension_id = paths[-1]
|
||||
if extension_id == u'Temp':
|
||||
return
|
||||
|
||||
# Get the user and ID.
|
||||
user = analysis_context.GetUsernameFromPath(
|
||||
self._user_paths, filename, self._sep)
|
||||
|
||||
# We still want this information in here, so that we can
|
||||
# manually deduce the username.
|
||||
if not user:
|
||||
if len(filename) > 25:
|
||||
user = u'Not found ({0:s}...)'.format(filename[0:25])
|
||||
else:
|
||||
user = u'Not found ({0:s})'.format(filename)
|
||||
|
||||
extension = self._GetTitleFromChromeWebStore(extension_id)
|
||||
if not extension:
|
||||
extension = extension_id
|
||||
|
||||
self._results.setdefault(user, [])
|
||||
extension_string = extension.decode('utf-8', 'ignore')
|
||||
if (extension_string, extension_id) not in self._results[user]:
|
||||
self._results[user].append((extension_string, extension_id))
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""Tests for the chrome extension analysis plugin."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from plaso.analysis import chrome_extension
|
||||
from plaso.analysis import test_lib
|
||||
from plaso.engine import queue
|
||||
from plaso.engine import single_process
|
||||
from plaso.lib import event
|
||||
|
||||
# We are accessing quite a lot of protected members in this test file.
|
||||
# Suppressing that message test file wide.
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
class AnalyzeChromeExtensionTestPlugin(
|
||||
chrome_extension.AnalyzeChromeExtensionPlugin):
|
||||
"""Chrome extension analysis plugin used for testing."""
|
||||
|
||||
NAME = 'chrome_extension_test'
|
||||
|
||||
_TEST_DATA_PATH = os.path.join(
|
||||
os.getcwd(), u'test_data', u'chrome_extensions')
|
||||
|
||||
def _GetChromeWebStorePage(self, extension_id):
|
||||
"""Retrieves the page for the extension from the Chrome store test data.
|
||||
|
||||
Args:
|
||||
extension_id: string containing the extension identifier.
|
||||
"""
|
||||
chrome_web_store_file = os.path.join(self._TEST_DATA_PATH, extension_id)
|
||||
if not os.path.exists(chrome_web_store_file):
|
||||
return
|
||||
|
||||
return open(chrome_web_store_file, 'rb')
|
||||
|
||||
|
||||
class ChromeExtensionTest(test_lib.AnalysisPluginTestCase):
|
||||
"""Tests for the chrome extension analysis plugin."""
|
||||
|
||||
# Few config options here.
|
||||
MAC_PATHS = [
|
||||
'/Users/dude/Libary/Application Data/Google/Chrome/Default/Extensions',
|
||||
('/Users/dude/Libary/Application Data/Google/Chrome/Default/Extensions/'
|
||||
'apdfllckaahabafndbhieahigkjlhalf'),
|
||||
'/private/var/log/system.log',
|
||||
'/Users/frank/Library/Application Data/Google/Chrome/Default',
|
||||
'/Users/hans/Library/Application Data/Google/Chrome/Default',
|
||||
('/Users/frank/Library/Application Data/Google/Chrome/Default/'
|
||||
'Extensions/pjkljhegncpnkpknbcohdijeoejaedia'),
|
||||
'/Users/frank/Library/Application Data/Google/Chrome/Default/Extensions',]
|
||||
|
||||
WIN_PATHS = [
|
||||
'C:\\Users\\Dude\\SomeFolder\\Chrome\\Default\\Extensions',
|
||||
('C:\\Users\\Dude\\SomeNoneStandardFolder\\Chrome\\Default\\Extensions\\'
|
||||
'hmjkmjkepdijhoojdojkdfohbdgmmhki'),
|
||||
('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\'
|
||||
'blpcfgokakmgnkcojhhkbfbldkacnbeo'),
|
||||
'\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions',
|
||||
('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\'
|
||||
'icppfcnhkcmnfdhfhphakoifcfokfdhg'),
|
||||
'C:\\Windows\\System32',
|
||||
'\\Stuff/with path separator\\Folder']
|
||||
|
||||
MAC_USERS = [
|
||||
{u'name': u'root', u'path': u'/var/root', u'sid': u'0'},
|
||||
{u'name': u'frank', u'path': u'/Users/frank', u'sid': u'4052'},
|
||||
{u'name': u'hans', u'path': u'/Users/hans', u'sid': u'4352'},
|
||||
{u'name': u'dude', u'path': u'/Users/dude', u'sid': u'1123'}]
|
||||
|
||||
WIN_USERS = [
|
||||
{u'name': u'dude', u'path': u'C:\\Users\\dude', u'sid': u'S-1'},
|
||||
{u'name': u'frank', u'path': u'C:\\Users\\frank', u'sid': u'S-2'}]
|
||||
|
||||
def _CreateTestEventObject(self, path):
|
||||
"""Create a test event object with a particular path."""
|
||||
event_object = event.EventObject()
|
||||
event_object.data_type = 'fs:stat'
|
||||
event_object.timestamp = 12345
|
||||
event_object.timestamp_desc = u'Some stuff'
|
||||
event_object.filename = path
|
||||
|
||||
return event_object
|
||||
|
||||
def testMacAnalyzerPlugin(self):
|
||||
"""Test the plugin against mock events."""
|
||||
knowledge_base = self._SetUpKnowledgeBase(knowledge_base_values={
|
||||
'users': self.MAC_USERS})
|
||||
|
||||
event_queue = single_process.SingleProcessQueue()
|
||||
|
||||
# Fill the incoming queue with events.
|
||||
test_queue_producer = queue.ItemQueueProducer(event_queue)
|
||||
test_queue_producer.ProduceItems([
|
||||
self._CreateTestEventObject(path) for path in self.MAC_PATHS])
|
||||
test_queue_producer.SignalEndOfInput()
|
||||
|
||||
# Initialize plugin.
|
||||
analysis_plugin = AnalyzeChromeExtensionTestPlugin(event_queue)
|
||||
|
||||
# Run the analysis plugin.
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
self.assertEquals(len(analysis_reports), 1)
|
||||
|
||||
analysis_report = analysis_reports[0]
|
||||
|
||||
self.assertEquals(analysis_plugin._sep, u'/')
|
||||
|
||||
# Due to the behavior of the join one additional empty string at the end
|
||||
# is needed to create the last empty line.
|
||||
expected_text = u'\n'.join([
|
||||
u' == USER: dude ==',
|
||||
u' Google Drive [apdfllckaahabafndbhieahigkjlhalf]',
|
||||
u'',
|
||||
u' == USER: frank ==',
|
||||
u' Gmail [pjkljhegncpnkpknbcohdijeoejaedia]',
|
||||
u'',
|
||||
u''])
|
||||
|
||||
self.assertEquals(analysis_report.text, expected_text)
|
||||
self.assertEquals(analysis_report.plugin_name, 'chrome_extension_test')
|
||||
|
||||
expected_keys = set([u'frank', u'dude'])
|
||||
self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys)
|
||||
|
||||
def testWinAnalyzePlugin(self):
|
||||
"""Test the plugin against mock events."""
|
||||
knowledge_base = self._SetUpKnowledgeBase(knowledge_base_values={
|
||||
'users': self.WIN_USERS})
|
||||
|
||||
event_queue = single_process.SingleProcessQueue()
|
||||
|
||||
# Fill the incoming queue with events.
|
||||
test_queue_producer = queue.ItemQueueProducer(event_queue)
|
||||
test_queue_producer.ProduceItems([
|
||||
self._CreateTestEventObject(path) for path in self.WIN_PATHS])
|
||||
test_queue_producer.SignalEndOfInput()
|
||||
|
||||
# Initialize plugin.
|
||||
analysis_plugin = AnalyzeChromeExtensionTestPlugin(event_queue)
|
||||
|
||||
# Run the analysis plugin.
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
self.assertEquals(len(analysis_reports), 1)
|
||||
|
||||
analysis_report = analysis_reports[0]
|
||||
|
||||
self.assertEquals(analysis_plugin._sep, u'\\')
|
||||
|
||||
# Due to the behavior of the join one additional empty string at the end
|
||||
# is needed to create the last empty line.
|
||||
expected_text = u'\n'.join([
|
||||
u' == USER: dude ==',
|
||||
u' Google Keep - notes and lists [hmjkmjkepdijhoojdojkdfohbdgmmhki]',
|
||||
u'',
|
||||
u' == USER: frank ==',
|
||||
u' Google Play Music [icppfcnhkcmnfdhfhphakoifcfokfdhg]',
|
||||
u' YouTube [blpcfgokakmgnkcojhhkbfbldkacnbeo]',
|
||||
u'',
|
||||
u''])
|
||||
|
||||
self.assertEquals(analysis_report.text, expected_text)
|
||||
self.assertEquals(analysis_report.plugin_name, 'chrome_extension_test')
|
||||
|
||||
expected_keys = set([u'frank', u'dude'])
|
||||
self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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.
|
||||
"""The analysis context object."""
|
||||
|
||||
|
||||
class AnalysisContext(object):
|
||||
"""Class that implements the analysis context."""
|
||||
|
||||
def __init__(self, analysis_report_queue_producer, knowledge_base):
|
||||
"""Initializes a analysis plugin context object.
|
||||
|
||||
Args:
|
||||
analysis_report_queue_producer: the analysis report queue producer
|
||||
(instance of ItemQueueProducer).
|
||||
knowledge_base: A knowledge base object (instance of KnowledgeBase),
|
||||
which contains information from the source data needed
|
||||
for analysis.
|
||||
"""
|
||||
super(AnalysisContext, self).__init__()
|
||||
self._analysis_report_queue_producer = analysis_report_queue_producer
|
||||
self._knowledge_base = knowledge_base
|
||||
|
||||
self.number_of_produced_analysis_reports = 0
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""The list of users."""
|
||||
return self._knowledge_base.users
|
||||
|
||||
def GetPathSegmentSeparator(self, path):
|
||||
"""Given a path give back the path separator as a best guess.
|
||||
|
||||
Args:
|
||||
path: the path.
|
||||
|
||||
Returns:
|
||||
The path segment separator.
|
||||
"""
|
||||
if path.startswith(u'\\') or path[1:].startswith(u':\\'):
|
||||
return u'\\'
|
||||
|
||||
if path.startswith(u'/'):
|
||||
return u'/'
|
||||
|
||||
if u'/' and u'\\' in path:
|
||||
# Let's count slashes and guess which one is the right one.
|
||||
forward_count = len(path.split(u'/'))
|
||||
backward_count = len(path.split(u'\\'))
|
||||
|
||||
if forward_count > backward_count:
|
||||
return u'/'
|
||||
else:
|
||||
return u'\\'
|
||||
|
||||
# Now we are sure there is only one type of separators yet
|
||||
# the path does not start with one.
|
||||
if u'/' in path:
|
||||
return u'/'
|
||||
else:
|
||||
return u'\\'
|
||||
|
||||
def GetUsernameFromPath(self, user_paths, file_path, path_segment_separator):
|
||||
"""Return a username based on preprocessing and the path.
|
||||
|
||||
During preprocessing the tool will gather file paths to where each user
|
||||
profile is stored, and which user it belongs to. This function takes in
|
||||
a path to a file and compares it to a list of all discovered usernames
|
||||
and paths to their profiles in the system. If it finds that the file path
|
||||
belongs to a user profile it will return the username that the profile
|
||||
belongs to.
|
||||
|
||||
Args:
|
||||
user_paths: A dictionary object containing the paths per username.
|
||||
file_path: The full path to the file being analyzed.
|
||||
path_segment_separator: String containing the path segment separator.
|
||||
|
||||
Returns:
|
||||
If possible the responsible username behind the file. Otherwise None.
|
||||
"""
|
||||
if not user_paths:
|
||||
return
|
||||
|
||||
if path_segment_separator != u'/':
|
||||
use_path = file_path.replace(path_segment_separator, u'/')
|
||||
else:
|
||||
use_path = file_path
|
||||
|
||||
if use_path[1:].startswith(u':/'):
|
||||
use_path = use_path[2:]
|
||||
|
||||
use_path = use_path.lower()
|
||||
|
||||
for user, path in user_paths.iteritems():
|
||||
if use_path.startswith(path):
|
||||
return user
|
||||
|
||||
def GetUserPaths(self, users):
|
||||
"""Retrieves the user paths.
|
||||
|
||||
Args:
|
||||
users: a list of users.
|
||||
|
||||
Returns:
|
||||
A dictionary object containing the paths per username or None if no users.
|
||||
"""
|
||||
if not users:
|
||||
return
|
||||
|
||||
user_paths = {}
|
||||
|
||||
user_separator = None
|
||||
for user in users:
|
||||
name = user.get('name')
|
||||
path = user.get('path')
|
||||
|
||||
if not path or not name:
|
||||
continue
|
||||
|
||||
if not user_separator:
|
||||
user_separator = self.GetPathSegmentSeparator(path)
|
||||
|
||||
if user_separator != u'/':
|
||||
path = path.replace(user_separator, u'/').replace(u'//', u'/')
|
||||
|
||||
if path[1:].startswith(u':/'):
|
||||
path = path[2:]
|
||||
|
||||
name = name.lower()
|
||||
user_paths[name] = path.lower()
|
||||
|
||||
return user_paths
|
||||
|
||||
def ProcessAnalysisReport(self, analysis_report, plugin_name=None):
|
||||
"""Processes an analysis report before it is emitted to the queue.
|
||||
|
||||
Args:
|
||||
analysis_report: the analysis report object (instance of AnalysisReport).
|
||||
plugin_name: Optional name of the plugin. The default is None.
|
||||
"""
|
||||
if not getattr(analysis_report, 'plugin_name', None) and plugin_name:
|
||||
analysis_report.plugin_name = plugin_name
|
||||
|
||||
def ProduceAnalysisReport(self, analysis_report, plugin_name=None):
|
||||
"""Produces an analysis report onto the queue.
|
||||
|
||||
Args:
|
||||
analysis_report: the analysis report object (instance of AnalysisReport).
|
||||
plugin_name: Optional name of the plugin. The default is None.
|
||||
"""
|
||||
self.ProcessAnalysisReport(analysis_report, plugin_name=plugin_name)
|
||||
|
||||
self._analysis_report_queue_producer.ProduceItem(analysis_report)
|
||||
self.number_of_produced_analysis_reports += 1
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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.
|
||||
"""Tests for the analysis context."""
|
||||
|
||||
import unittest
|
||||
|
||||
from plaso.analysis import context
|
||||
from plaso.analysis import test_lib
|
||||
from plaso.engine import queue
|
||||
from plaso.engine import single_process
|
||||
|
||||
|
||||
class AnalysisContextTest(test_lib.AnalysisPluginTestCase):
|
||||
"""Tests for the analysis context."""
|
||||
|
||||
MAC_PATHS = [
|
||||
'/Users/dude/Library/Application Data/Google/Chrome/Default/Extensions',
|
||||
('/Users/dude/Library/Application Data/Google/Chrome/Default/Extensions/'
|
||||
'apdfllckaahabafndbhieahigkjlhalf'),
|
||||
'/private/var/log/system.log',
|
||||
'/Users/frank/Library/Application Data/Google/Chrome/Default',
|
||||
'/Users/hans/Library/Application Data/Google/Chrome/Default',
|
||||
('/Users/frank/Library/Application Data/Google/Chrome/Default/'
|
||||
'Extensions/pjkljhegncpnkpknbcohdijeoejaedia'),
|
||||
'/Users/frank/Library/Application Data/Google/Chrome/Default/Extensions',]
|
||||
|
||||
WIN_PATHS = [
|
||||
'C:\\Users\\Dude\\SomeFolder\\Chrome\\Default\\Extensions',
|
||||
('C:\\Users\\Dude\\SomeNoneStandardFolder\\Chrome\\Default\\Extensions\\'
|
||||
'hmjkmjkepdijhoojdojkdfohbdgmmhki'),
|
||||
('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\'
|
||||
'blpcfgokakmgnkcojhhkbfbldkacnbeo'),
|
||||
'\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions',
|
||||
('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\'
|
||||
'icppfcnhkcmnfdhfhphakoifcfokfdhg'),
|
||||
'C:\\Windows\\System32',
|
||||
'\\Stuff/with path separator\\Folder']
|
||||
|
||||
MAC_USERS = [
|
||||
{u'name': u'root', u'path': u'/var/root', u'sid': u'0'},
|
||||
{u'name': u'frank', u'path': u'/Users/frank', u'sid': u'4052'},
|
||||
{u'name': u'hans', u'path': u'/Users/hans', u'sid': u'4352'},
|
||||
{u'name': u'dude', u'path': u'/Users/dude', u'sid': u'1123'}]
|
||||
|
||||
WIN_USERS = [
|
||||
{u'name': u'dude', u'path': u'C:\\Users\\dude', u'sid': u'S-1'},
|
||||
{u'name': u'frank', u'path': u'C:\\Users\\frank', u'sid': u'S-2'}]
|
||||
|
||||
def setUp(self):
|
||||
"""Sets up the needed objects used throughout the test."""
|
||||
knowledge_base = self._SetUpKnowledgeBase()
|
||||
|
||||
analysis_report_queue = single_process.SingleProcessQueue()
|
||||
analysis_report_queue_producer = queue.ItemQueueProducer(
|
||||
analysis_report_queue)
|
||||
|
||||
self._analysis_context = context.AnalysisContext(
|
||||
analysis_report_queue_producer, knowledge_base)
|
||||
|
||||
def testGetPathSegmentSeparator(self):
|
||||
"""Tests the GetPathSegmentSeparator function."""
|
||||
for path in self.MAC_PATHS:
|
||||
path_segment_separator = self._analysis_context.GetPathSegmentSeparator(
|
||||
path)
|
||||
self.assertEquals(path_segment_separator, u'/')
|
||||
|
||||
for path in self.WIN_PATHS:
|
||||
path_segment_separator = self._analysis_context.GetPathSegmentSeparator(
|
||||
path)
|
||||
self.assertEquals(path_segment_separator, u'\\')
|
||||
|
||||
def testGetUserPaths(self):
|
||||
"""Tests the GetUserPaths function."""
|
||||
user_paths = self._analysis_context.GetUserPaths(self.MAC_USERS)
|
||||
self.assertEquals(
|
||||
set(user_paths.keys()), set([u'frank', u'dude', u'hans', u'root']))
|
||||
self.assertEquals(user_paths[u'frank'], u'/users/frank')
|
||||
self.assertEquals(user_paths[u'dude'], u'/users/dude')
|
||||
self.assertEquals(user_paths[u'hans'], u'/users/hans')
|
||||
self.assertEquals(user_paths[u'root'], u'/var/root')
|
||||
|
||||
user_paths = self._analysis_context.GetUserPaths(self.WIN_USERS)
|
||||
self.assertEquals(set(user_paths.keys()), set([u'frank', u'dude']))
|
||||
self.assertEquals(user_paths[u'frank'], u'/users/frank')
|
||||
self.assertEquals(user_paths[u'dude'], u'/users/dude')
|
||||
|
||||
def testGetUsernameFromPath(self):
|
||||
"""Tests the GetUsernameFromPath function."""
|
||||
user_paths = self._analysis_context.GetUserPaths(self.MAC_USERS)
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.MAC_PATHS[0], u'/')
|
||||
self.assertEquals(username, u'dude')
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.MAC_PATHS[4], u'/')
|
||||
self.assertEquals(username, u'hans')
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.WIN_PATHS[0], u'/')
|
||||
self.assertEquals(username, None)
|
||||
|
||||
user_paths = self._analysis_context.GetUserPaths(self.WIN_USERS)
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.WIN_PATHS[0], u'\\')
|
||||
self.assertEquals(username, u'dude')
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.WIN_PATHS[2], u'\\')
|
||||
self.assertEquals(username, u'frank')
|
||||
|
||||
username = self._analysis_context.GetUsernameFromPath(
|
||||
user_paths, self.MAC_PATHS[2], u'\\')
|
||||
self.assertEquals(username, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2013 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 basic interface for analysis plugins."""
|
||||
|
||||
import abc
|
||||
|
||||
from plaso.engine import queue
|
||||
from plaso.lib import registry
|
||||
from plaso.lib import timelib
|
||||
|
||||
|
||||
class AnalysisPlugin(queue.EventObjectQueueConsumer):
|
||||
"""Analysis plugin gets a copy of each read event for analysis."""
|
||||
|
||||
__metaclass__ = registry.MetaclassRegistry
|
||||
__abstract = True
|
||||
|
||||
# The URLS should contain a list of URLs with additional information about
|
||||
# this analysis plugin.
|
||||
URLS = []
|
||||
|
||||
# The name of the plugin. This is the name that is matched against when
|
||||
# loading plugins, so it is important that this name is short, concise and
|
||||
# explains the nature of the plugin easily. It also needs to be unique.
|
||||
NAME = 'Plugin'
|
||||
|
||||
# A flag indicating whether or not this plugin should be run during extraction
|
||||
# phase or reserved entirely for post processing stage.
|
||||
# Typically this would mean that the plugin is perhaps too computationally
|
||||
# heavy to be run during event extraction and should rather be run during
|
||||
# post-processing.
|
||||
# Since most plugins should perhaps rather be run during post-processing
|
||||
# this is set to False by default and needs to be overwritten if the plugin
|
||||
# should be able to run during the extraction phase.
|
||||
ENABLE_IN_EXTRACTION = False
|
||||
|
||||
# All the possible report types.
|
||||
TYPE_ANOMALY = 1 # Plugin that is inspecting events for anomalies.
|
||||
TYPE_STATISTICS = 2 # Statistical calculations.
|
||||
TYPE_ANNOTATION = 3 # Inspecting events with the primary purpose of
|
||||
# annotating or tagging them.
|
||||
TYPE_REPORT = 4 # Inspecting events to provide a summary information.
|
||||
|
||||
# Optional arguments to be added to the argument parser.
|
||||
# An example would be:
|
||||
# ARGUMENTS = [('--myparameter', {
|
||||
# 'action': 'store',
|
||||
# 'help': 'This is my parameter help',
|
||||
# 'dest': 'myparameter',
|
||||
# 'default': '',
|
||||
# 'type': 'unicode'})]
|
||||
#
|
||||
# Where all arguments into the dict object have a direct translation
|
||||
# into the argparse parser.
|
||||
ARGUMENTS = []
|
||||
|
||||
# We need to implement the interface for analysis plugins, but we don't use
|
||||
# command line options here, so disable checking for unused args.
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, incoming_queue, options=None):
|
||||
"""Initializes an analysis plugin.
|
||||
|
||||
Args:
|
||||
incoming_queue: A queue that is used to listen to incoming events.
|
||||
options: Optional command line arguments (instance of
|
||||
argparse.Namespace). The default is None.
|
||||
"""
|
||||
super(AnalysisPlugin, self).__init__(incoming_queue)
|
||||
self.plugin_type = self.TYPE_REPORT
|
||||
|
||||
# pylint: enable=unused-argument
|
||||
def _ConsumeEventObject(self, event_object, analysis_context=None, **kwargs):
|
||||
"""Consumes an event object callback for ConsumeEventObjects.
|
||||
|
||||
Args:
|
||||
event_object: An event object (instance of EventObject).
|
||||
analysis_context: Optional analysis context object (instance of
|
||||
AnalysisContext). The default is None.
|
||||
"""
|
||||
self.ExamineEvent(analysis_context, event_object, **kwargs)
|
||||
|
||||
@property
|
||||
def plugin_name(self):
|
||||
"""Return the name of the plugin."""
|
||||
return self.NAME
|
||||
|
||||
@abc.abstractmethod
|
||||
def CompileReport(self):
|
||||
"""Compiles a report of the analysis.
|
||||
|
||||
After the plugin has received every copy of an event to
|
||||
analyze this function will be called so that the report
|
||||
can be assembled.
|
||||
|
||||
Returns:
|
||||
The analysis report (instance of AnalysisReport).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def ExamineEvent(self, analysis_context, event_object, **kwargs):
|
||||
"""Analyzes an event object.
|
||||
|
||||
Args:
|
||||
analysis_context: An analysis context object (instance of
|
||||
AnalysisContext).
|
||||
event_object: An event object (instance of EventObject).
|
||||
"""
|
||||
|
||||
def RunPlugin(self, analysis_context):
|
||||
"""For each item in the queue send the read event to analysis.
|
||||
|
||||
Args:
|
||||
analysis_context: An analysis context object (instance of
|
||||
AnalysisContext).
|
||||
"""
|
||||
self.ConsumeEventObjects(analysis_context=analysis_context)
|
||||
|
||||
analysis_report = self.CompileReport()
|
||||
|
||||
if analysis_report:
|
||||
# TODO: move this into the plugins?
|
||||
analysis_report.time_compiled = timelib.Timestamp.GetNow()
|
||||
analysis_context.ProduceAnalysisReport(
|
||||
analysis_report, plugin_name=self.plugin_name)
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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.
|
||||
"""Analysis plugin related functions and classes for testing."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from dfvfs.lib import definitions
|
||||
from dfvfs.path import factory as path_spec_factory
|
||||
from dfvfs.resolver import resolver as path_spec_resolver
|
||||
|
||||
from plaso.analysis import context
|
||||
from plaso.artifacts import knowledge_base
|
||||
from plaso.engine import queue
|
||||
from plaso.engine import single_process
|
||||
from plaso.lib import event
|
||||
from plaso.parsers import context as parsers_context
|
||||
|
||||
|
||||
class TestAnalysisReportQueueConsumer(queue.ItemQueueConsumer):
|
||||
"""Class that implements a test analysis report queue consumer."""
|
||||
|
||||
def __init__(self, queue_object):
|
||||
"""Initializes the queue consumer.
|
||||
|
||||
Args:
|
||||
queue_object: the queue object (instance of Queue).
|
||||
"""
|
||||
super(TestAnalysisReportQueueConsumer, self).__init__(queue_object)
|
||||
self.analysis_reports = []
|
||||
|
||||
def _ConsumeItem(self, analysis_report):
|
||||
"""Consumes an item callback for ConsumeItems.
|
||||
|
||||
Args:
|
||||
analysis_report: the analysis report (instance of AnalysisReport).
|
||||
"""
|
||||
self.analysis_reports.append(analysis_report)
|
||||
|
||||
@property
|
||||
def number_of_analysis_reports(self):
|
||||
"""The number of analysis reports."""
|
||||
return len(self.analysis_reports)
|
||||
|
||||
|
||||
class AnalysisPluginTestCase(unittest.TestCase):
|
||||
"""The unit test case for an analysis plugin."""
|
||||
|
||||
_TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data')
|
||||
|
||||
# Show full diff results, part of TestCase so does not follow our naming
|
||||
# conventions.
|
||||
maxDiff = None
|
||||
|
||||
def _GetAnalysisReportsFromQueue(self, analysis_report_queue_consumer):
|
||||
"""Retrieves the analysis reports from the queue consumer.
|
||||
|
||||
Args:
|
||||
analysis_report_queue_consumer: the analysis report queue consumer
|
||||
object (instance of
|
||||
TestAnalysisReportQueueConsumer).
|
||||
|
||||
Returns:
|
||||
A list of analysis reports (instances of AnalysisReport).
|
||||
"""
|
||||
analysis_report_queue_consumer.ConsumeItems()
|
||||
|
||||
analysis_reports = []
|
||||
for analysis_report in analysis_report_queue_consumer.analysis_reports:
|
||||
self.assertIsInstance(analysis_report, event.AnalysisReport)
|
||||
analysis_reports.append(analysis_report)
|
||||
|
||||
return analysis_reports
|
||||
|
||||
def _GetTestFilePath(self, path_segments):
|
||||
"""Retrieves the path of a test file relative to the test data directory.
|
||||
|
||||
Args:
|
||||
path_segments: the path segments inside the test data directory.
|
||||
|
||||
Returns:
|
||||
A path of the test file.
|
||||
"""
|
||||
# Note that we need to pass the individual path segments to os.path.join
|
||||
# and not a list.
|
||||
return os.path.join(self._TEST_DATA_PATH, *path_segments)
|
||||
|
||||
def _ParseFile(self, parser_object, path, knowledge_base_object):
|
||||
"""Parses a file using the parser object.
|
||||
|
||||
Args:
|
||||
parser_object: the parser object.
|
||||
path: the path of the file to parse.
|
||||
knowledge_base_object: the knowledge base object (instance of
|
||||
KnowledgeBase).
|
||||
|
||||
Returns:
|
||||
An event object queue object (instance of Queue).
|
||||
"""
|
||||
event_queue = single_process.SingleProcessQueue()
|
||||
event_queue_producer = queue.ItemQueueProducer(event_queue)
|
||||
|
||||
parse_error_queue = single_process.SingleProcessQueue()
|
||||
|
||||
parser_context = parsers_context.ParserContext(
|
||||
event_queue_producer, parse_error_queue, knowledge_base_object)
|
||||
path_spec = path_spec_factory.Factory.NewPathSpec(
|
||||
definitions.TYPE_INDICATOR_OS, location=path)
|
||||
file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec)
|
||||
|
||||
parser_object.Parse(parser_context, file_entry)
|
||||
event_queue.SignalEndOfInput()
|
||||
|
||||
return event_queue
|
||||
|
||||
def _RunAnalysisPlugin(self, analysis_plugin, knowledge_base_object):
|
||||
"""Analyzes an event object queue using the plugin object.
|
||||
|
||||
Args:
|
||||
analysis_plugin: the analysis plugin object (instance of AnalysisPlugin).
|
||||
knowledge_base_object: the knowledge base object (instance of
|
||||
KnowledgeBase).
|
||||
|
||||
Returns:
|
||||
An event object queue object (instance of Queue).
|
||||
"""
|
||||
analysis_report_queue = single_process.SingleProcessQueue()
|
||||
analysis_report_queue_consumer = TestAnalysisReportQueueConsumer(
|
||||
analysis_report_queue)
|
||||
analysis_report_queue_producer = queue.ItemQueueProducer(
|
||||
analysis_report_queue)
|
||||
|
||||
analysis_context = context.AnalysisContext(
|
||||
analysis_report_queue_producer, knowledge_base_object)
|
||||
|
||||
analysis_plugin.RunPlugin(analysis_context)
|
||||
analysis_report_queue.SignalEndOfInput()
|
||||
|
||||
return analysis_report_queue_consumer
|
||||
|
||||
def _SetUpKnowledgeBase(self, knowledge_base_values=None):
|
||||
"""Sets up a knowledge base.
|
||||
|
||||
Args:
|
||||
knowledge_base_values: optional dict containing the knowledge base
|
||||
values. The default is None.
|
||||
|
||||
Returns:
|
||||
An knowledge base object (instance of KnowledgeBase).
|
||||
"""
|
||||
knowledge_base_object = knowledge_base.KnowledgeBase()
|
||||
if knowledge_base_values:
|
||||
for identifier, value in knowledge_base_values.iteritems():
|
||||
knowledge_base_object.SetValue(identifier, value)
|
||||
|
||||
return knowledge_base_object
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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.
|
||||
"""A plugin to enable quick triage of Windows Services."""
|
||||
|
||||
from plaso.analysis import interface
|
||||
from plaso.lib import event
|
||||
from plaso.winnt import human_readable_service_enums
|
||||
|
||||
# Moving this import to the bottom due to complaints from certain versions of
|
||||
# linters.
|
||||
import yaml
|
||||
|
||||
|
||||
class WindowsService(yaml.YAMLObject):
|
||||
"""Class to represent a Windows Service."""
|
||||
# This is used for comparison operations and defines attributes that should
|
||||
# not be used during evaluation of whether two services are the same.
|
||||
COMPARE_EXCLUDE = frozenset(['sources'])
|
||||
|
||||
KEY_PATH_SEPARATOR = u'\\'
|
||||
|
||||
# YAML attributes
|
||||
yaml_tag = u'!WindowsService'
|
||||
yaml_loader = yaml.SafeLoader
|
||||
yaml_dumper = yaml.SafeDumper
|
||||
|
||||
|
||||
def __init__(self, name, service_type, image_path, start_type, object_name,
|
||||
source, service_dll=None):
|
||||
"""Initializes a new Windows service object.
|
||||
|
||||
Args:
|
||||
name: The name of the service
|
||||
service_type: The value of the Type value of the service key.
|
||||
image_path: The value of the ImagePath value of the service key.
|
||||
start_type: The value of the Start value of the service key.
|
||||
object_name: The value of the ObjectName value of the service key.
|
||||
source: A tuple of (pathspec, Registry key) describing where the
|
||||
service was found
|
||||
service_dll: Optional string value of the ServiceDll value in the
|
||||
service's Parameters subkey. The default is None.
|
||||
|
||||
Raises:
|
||||
TypeError: If a tuple with two elements is not passed as the 'source'
|
||||
argument.
|
||||
"""
|
||||
self.name = name
|
||||
self.service_type = service_type
|
||||
self.image_path = image_path
|
||||
self.start_type = start_type
|
||||
self.service_dll = service_dll
|
||||
self.object_name = object_name
|
||||
if isinstance(source, tuple):
|
||||
if len(source) != 2:
|
||||
raise TypeError(u'Source arguments must be tuple of length 2.')
|
||||
# A service may be found in multiple Control Sets or Registry hives,
|
||||
# hence the list.
|
||||
self.sources = [source]
|
||||
else:
|
||||
raise TypeError(u'Source argument must be a tuple.')
|
||||
self.anomalies = []
|
||||
|
||||
@classmethod
|
||||
def FromEvent(cls, service_event):
|
||||
"""Creates a Service object from an plaso event.
|
||||
|
||||
Args:
|
||||
service_event: The event object (instance of EventObject) to create a new
|
||||
Service object from.
|
||||
|
||||
"""
|
||||
_, _, name = service_event.keyname.rpartition(
|
||||
WindowsService.KEY_PATH_SEPARATOR)
|
||||
service_type = service_event.regvalue.get('Type')
|
||||
image_path = service_event.regvalue.get('ImagePath')
|
||||
start_type = service_event.regvalue.get('Start')
|
||||
service_dll = service_event.regvalue.get('ServiceDll', u'')
|
||||
object_name = service_event.regvalue.get('ObjectName', u'')
|
||||
if service_event.pathspec:
|
||||
source = (service_event.pathspec.location, service_event.keyname)
|
||||
else:
|
||||
source = (u'Unknown', u'Unknown')
|
||||
return cls(
|
||||
name=name, service_type=service_type, image_path=image_path,
|
||||
start_type=start_type, object_name=object_name,
|
||||
source=source, service_dll=service_dll)
|
||||
|
||||
def HumanReadableType(self):
|
||||
"""Return a human readable string describing the type value."""
|
||||
return human_readable_service_enums.SERVICE_ENUMS['Type'].get(
|
||||
self.service_type, u'{0:d}'.format(self.service_type))
|
||||
|
||||
def HumanReadableStartType(self):
|
||||
"""Return a human readable string describing the start_type value."""
|
||||
return human_readable_service_enums.SERVICE_ENUMS['Start'].get(
|
||||
self.start_type, u'{0:d}'.format(self.start_type))
|
||||
|
||||
def __eq__(self, other_service):
|
||||
"""Custom equality method so that we match near-duplicates.
|
||||
|
||||
Compares two service objects together and evaluates if they are
|
||||
the same or close enough to be considered to represent the same service.
|
||||
|
||||
For two service objects to be considered the same they need to
|
||||
have the the same set of attributes and same values for all their
|
||||
attributes, other than those enumerated as reserved in the
|
||||
COMPARE_EXCLUDE constant.
|
||||
|
||||
Args:
|
||||
other_service: The service (instance of WindowsService) we are testing
|
||||
for equality.
|
||||
|
||||
Returns:
|
||||
A boolean value to indicate whether the services are equal.
|
||||
|
||||
"""
|
||||
if not isinstance(other_service, WindowsService):
|
||||
return False
|
||||
|
||||
attributes = set(self.__dict__.keys())
|
||||
other_attributes = set(self.__dict__.keys())
|
||||
|
||||
if attributes != other_attributes:
|
||||
return False
|
||||
|
||||
# We compare the values for all attributes, other than those specifically
|
||||
# enumerated as not relevant for equality comparisons.
|
||||
for attribute in attributes.difference(self.COMPARE_EXCLUDE):
|
||||
if getattr(self, attribute, None) != getattr(
|
||||
other_service, attribute, None):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class WindowsServiceCollection(object):
|
||||
"""Class to hold and de-duplicate Windows Services."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a collection that holds Windows Service."""
|
||||
self._services = []
|
||||
|
||||
def AddService(self, new_service):
|
||||
"""Add a new service to the list of ones we know about.
|
||||
|
||||
Args:
|
||||
new_service: The service (instance of WindowsService) to add.
|
||||
"""
|
||||
for service in self._services:
|
||||
if new_service == service:
|
||||
# If this service is the same as one we already know about, we
|
||||
# just want to add where it came from.
|
||||
service.sources.append(new_service.sources[0])
|
||||
return
|
||||
# We only add a new object to our list if we don't have
|
||||
# an identical one already.
|
||||
self._services.append(new_service)
|
||||
|
||||
@property
|
||||
def services(self):
|
||||
"""Get the services in this collection."""
|
||||
return self._services
|
||||
|
||||
|
||||
class AnalyzeWindowsServicesPlugin(interface.AnalysisPlugin):
|
||||
"""Provides a single list of for Windows services found in the Registry."""
|
||||
|
||||
NAME = 'windows_services'
|
||||
|
||||
# Indicate that we can run this plugin during regular extraction.
|
||||
ENABLE_IN_EXTRACTION = True
|
||||
|
||||
ARGUMENTS = [
|
||||
('--windows-services-output', {
|
||||
'dest': 'windows-services-output',
|
||||
'type': unicode,
|
||||
'help': 'Specify how the results should be displayed. Options are '
|
||||
'text and yaml.',
|
||||
'action': 'store',
|
||||
'default': u'text',
|
||||
'choices': [u'text', u'yaml']}),]
|
||||
|
||||
def __init__(self, incoming_queue, options=None):
|
||||
"""Initializes the Windows Services plugin
|
||||
|
||||
Args:
|
||||
incoming_queue: A queue to read events from.
|
||||
options: Optional command line arguments (instance of
|
||||
argparse.Namespace). The default is None.
|
||||
"""
|
||||
super(AnalyzeWindowsServicesPlugin, self).__init__(incoming_queue)
|
||||
self._service_collection = WindowsServiceCollection()
|
||||
self.plugin_type = interface.AnalysisPlugin.TYPE_REPORT
|
||||
self._output_mode = getattr(options, 'windows-services-output', u'text')
|
||||
|
||||
def ExamineEvent(self, analysis_context, event_object, **kwargs):
|
||||
"""Analyzes an event_object and creates Windows Services as required.
|
||||
|
||||
At present, this method only handles events extracted from the Registry.
|
||||
|
||||
Args:
|
||||
analysis_context: The context object analysis plugins.
|
||||
event_object: The event object (instance of EventObject) to examine.
|
||||
"""
|
||||
# TODO: Handle event log entries here also (ie, event id 4697).
|
||||
if getattr(event_object, 'data_type', None) != 'windows:registry:service':
|
||||
return
|
||||
else:
|
||||
# Create and store the service.
|
||||
service = WindowsService.FromEvent(event_object)
|
||||
self._service_collection.AddService(service)
|
||||
|
||||
def _FormatServiceText(self, service):
|
||||
"""Produces a human readable multi-line string representing the service.
|
||||
|
||||
Args:
|
||||
service: The service (instance of WindowsService) to format.
|
||||
"""
|
||||
string_segments = [
|
||||
service.name,
|
||||
u'\tImage Path = {0:s}'.format(service.image_path),
|
||||
u'\tService Type = {0:s}'.format(service.HumanReadableType()),
|
||||
u'\tStart Type = {0:s}'.format(service.HumanReadableStartType()),
|
||||
u'\tService Dll = {0:s}'.format(service.service_dll),
|
||||
u'\tObject Name = {0:s}'.format(service.object_name),
|
||||
u'\tSources:']
|
||||
for source in service.sources:
|
||||
string_segments.append(u'\t\t{0:s}:{1:s}'.format(source[0], source[1]))
|
||||
return u'\n'.join(string_segments)
|
||||
|
||||
def CompileReport(self):
|
||||
"""Compiles a report of the analysis.
|
||||
|
||||
Returns:
|
||||
The analysis report (instance of AnalysisReport).
|
||||
"""
|
||||
report = event.AnalysisReport()
|
||||
|
||||
if self._output_mode == 'yaml':
|
||||
lines_of_text = []
|
||||
lines_of_text.append(
|
||||
yaml.safe_dump_all(self._service_collection.services))
|
||||
else:
|
||||
lines_of_text = ['Listing Windows Services']
|
||||
for service in self._service_collection.services:
|
||||
lines_of_text.append(self._FormatServiceText(service))
|
||||
# Separate services with a blank line.
|
||||
lines_of_text.append(u'')
|
||||
|
||||
report.SetText(lines_of_text)
|
||||
|
||||
return report
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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.
|
||||
"""Tests for the windows services analysis plugin."""
|
||||
|
||||
import argparse
|
||||
import unittest
|
||||
|
||||
from dfvfs.path import fake_path_spec
|
||||
|
||||
from plaso.analysis import test_lib
|
||||
from plaso.analysis import windows_services
|
||||
from plaso.engine import queue
|
||||
from plaso.engine import single_process
|
||||
from plaso.events import windows_events
|
||||
from plaso.parsers import winreg
|
||||
|
||||
|
||||
class WindowsServicesTest(test_lib.AnalysisPluginTestCase):
|
||||
"""Tests for the Windows Services analysis plugin."""
|
||||
|
||||
SERVICE_EVENTS = [
|
||||
{u'path': u'\\ControlSet001\\services\\TestbDriver',
|
||||
u'text_dict': {u'ImagePath': u'C:\\Dell\\testdriver.sys', u'Type': 2,
|
||||
u'Start': 2, u'ObjectName': u''},
|
||||
u'timestamp': 1346145829002031},
|
||||
# This is almost the same, but different timestamp and source, so that
|
||||
# we can test the service de-duplication.
|
||||
{u'path': u'\\ControlSet003\\services\\TestbDriver',
|
||||
u'text_dict': {u'ImagePath': u'C:\\Dell\\testdriver.sys', u'Type': 2,
|
||||
u'Start': 2, u'ObjectName': u''},
|
||||
u'timestamp': 1346145839002031},
|
||||
]
|
||||
|
||||
def _CreateAnalysisPlugin(self, input_queue, output_mode):
|
||||
"""Create an analysis plugin to test with.
|
||||
|
||||
Args:
|
||||
input_queue: A queue the plugin will read events from.
|
||||
output_mode: The output format the plugin will use.
|
||||
Valid options are 'text' and 'yaml'.
|
||||
|
||||
Returns:
|
||||
An instance of AnalyzeWindowsServicesPlugin.
|
||||
"""
|
||||
argument_parser = argparse.ArgumentParser()
|
||||
plugin_args = windows_services.AnalyzeWindowsServicesPlugin.ARGUMENTS
|
||||
for parameter, config in plugin_args:
|
||||
argument_parser.add_argument(parameter, **config)
|
||||
arguments = ['--windows-services-output', output_mode]
|
||||
options = argument_parser.parse_args(arguments)
|
||||
analysis_plugin = windows_services.AnalyzeWindowsServicesPlugin(
|
||||
input_queue, options)
|
||||
return analysis_plugin
|
||||
|
||||
|
||||
def _CreateTestEventObject(self, service_event):
|
||||
"""Create a test event object with a particular path.
|
||||
|
||||
Args:
|
||||
service_event: A hash containing attributes of an event to add to the
|
||||
queue.
|
||||
|
||||
Returns:
|
||||
An EventObject representing the service to be created.
|
||||
"""
|
||||
test_pathspec = fake_path_spec.FakePathSpec(
|
||||
location=u'C:\\WINDOWS\\system32\\SYSTEM')
|
||||
event_object = windows_events.WindowsRegistryServiceEvent(
|
||||
service_event[u'timestamp'], service_event[u'path'],
|
||||
service_event[u'text_dict'])
|
||||
event_object.pathspec = test_pathspec
|
||||
return event_object
|
||||
|
||||
def testSyntheticKeysText(self):
|
||||
"""Test the plugin against mock events."""
|
||||
event_queue = single_process.SingleProcessQueue()
|
||||
|
||||
# Fill the incoming queue with events.
|
||||
test_queue_producer = queue.ItemQueueProducer(event_queue)
|
||||
events = [self._CreateTestEventObject(service_event)
|
||||
for service_event
|
||||
in self.SERVICE_EVENTS]
|
||||
test_queue_producer.ProduceItems(events)
|
||||
test_queue_producer.SignalEndOfInput()
|
||||
|
||||
# Initialize plugin.
|
||||
analysis_plugin = self._CreateAnalysisPlugin(event_queue, u'text')
|
||||
|
||||
# Run the analysis plugin.
|
||||
knowledge_base = self._SetUpKnowledgeBase()
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
self.assertEquals(len(analysis_reports), 1)
|
||||
|
||||
analysis_report = analysis_reports[0]
|
||||
|
||||
expected_text = (
|
||||
u'Listing Windows Services\n'
|
||||
u'TestbDriver\n'
|
||||
u'\tImage Path = C:\\Dell\\testdriver.sys\n'
|
||||
u'\tService Type = File System Driver (0x2)\n'
|
||||
u'\tStart Type = Auto Start (2)\n'
|
||||
u'\tService Dll = \n'
|
||||
u'\tObject Name = \n'
|
||||
u'\tSources:\n'
|
||||
u'\t\tC:\\WINDOWS\\system32\\SYSTEM:'
|
||||
u'\\ControlSet001\\services\\TestbDriver\n'
|
||||
u'\t\tC:\\WINDOWS\\system32\\SYSTEM:'
|
||||
u'\\ControlSet003\\services\\TestbDriver\n\n')
|
||||
|
||||
self.assertEquals(expected_text, analysis_report.text)
|
||||
self.assertEquals(analysis_report.plugin_name, 'windows_services')
|
||||
|
||||
def testRealEvents(self):
|
||||
"""Test the plugin with text output against real events from the parser."""
|
||||
parser = winreg.WinRegistryParser()
|
||||
# We could remove the non-Services plugins, but testing shows that the
|
||||
# performance gain is negligible.
|
||||
|
||||
knowledge_base = self._SetUpKnowledgeBase()
|
||||
test_path = self._GetTestFilePath(['SYSTEM'])
|
||||
event_queue = self._ParseFile(parser, test_path, knowledge_base)
|
||||
|
||||
# Run the analysis plugin.
|
||||
analysis_plugin = self._CreateAnalysisPlugin(event_queue, u'text')
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
report = analysis_reports[0]
|
||||
text = report.text
|
||||
|
||||
# We'll check that a few strings are in the report, like they're supposed
|
||||
# to be, rather than checking for the exact content of the string,
|
||||
# as that's dependent on the full path to the test files.
|
||||
test_strings = [u'1394ohci', u'WwanSvc', u'Sources:', u'ControlSet001',
|
||||
u'ControlSet002']
|
||||
for string in test_strings:
|
||||
self.assertTrue(string in text)
|
||||
|
||||
def testRealEventsYAML(self):
|
||||
"""Test the plugin with YAML output against real events from the parser."""
|
||||
parser = winreg.WinRegistryParser()
|
||||
# We could remove the non-Services plugins, but testing shows that the
|
||||
# performance gain is negligible.
|
||||
|
||||
knowledge_base = self._SetUpKnowledgeBase()
|
||||
test_path = self._GetTestFilePath(['SYSTEM'])
|
||||
event_queue = self._ParseFile(parser, test_path, knowledge_base)
|
||||
|
||||
# Run the analysis plugin.
|
||||
analysis_plugin = self._CreateAnalysisPlugin(event_queue, 'yaml')
|
||||
analysis_report_queue_consumer = self._RunAnalysisPlugin(
|
||||
analysis_plugin, knowledge_base)
|
||||
analysis_reports = self._GetAnalysisReportsFromQueue(
|
||||
analysis_report_queue_consumer)
|
||||
|
||||
report = analysis_reports[0]
|
||||
text = report.text
|
||||
|
||||
# We'll check that a few strings are in the report, like they're supposed
|
||||
# to be, rather than checking for the exact content of the string,
|
||||
# as that's dependent on the full path to the test files.
|
||||
test_strings = [windows_services.WindowsService.yaml_tag, u'1394ohci',
|
||||
u'WwanSvc', u'ControlSet001', u'ControlSet002']
|
||||
|
||||
for string in test_strings:
|
||||
self.assertTrue(string in text, u'{0:s} not found in report text'.format(
|
||||
string))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user