Import from old repository

This commit is contained in:
Stefan
2020-04-06 18:48:34 +02:00
commit 0da6783a45
762 changed files with 103065 additions and 0 deletions
+83
View File
@@ -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)
+257
View File
@@ -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))
+74
View File
@@ -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()
+201
View File
@@ -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))
+196
View File
@@ -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()
+168
View File
@@ -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
+134
View File
@@ -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()
+139
View File
@@ -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)
+171
View File
@@ -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
+267
View File
@@ -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
+192
View File
@@ -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()