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
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2012 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This file contains an import statement for each parser."""
from plaso.parsers import asl
from plaso.parsers import android_app_usage
from plaso.parsers import bencode_parser
from plaso.parsers import bsm
from plaso.parsers import chrome_cache
from plaso.parsers import cups_ipp
from plaso.parsers import custom_destinations
from plaso.parsers import esedb
from plaso.parsers import filestat
from plaso.parsers import firefox_cache
from plaso.parsers import hachoir
from plaso.parsers import iis
from plaso.parsers import java_idx
from plaso.parsers import mac_appfirewall
from plaso.parsers import mac_keychain
from plaso.parsers import mac_securityd
from plaso.parsers import mac_wifi
from plaso.parsers import mactime
from plaso.parsers import mcafeeav
from plaso.parsers import msiecf
from plaso.parsers import olecf
from plaso.parsers import opera
from plaso.parsers import oxml
from plaso.parsers import pcap
from plaso.parsers import plist
from plaso.parsers import popcontest
from plaso.parsers import pls_recall
from plaso.parsers import recycler
from plaso.parsers import rubanetra
from plaso.parsers import selinux
from plaso.parsers import skydrivelog
from plaso.parsers import skydrivelogerr
from plaso.parsers import sqlite
from plaso.parsers import symantec
from plaso.parsers import syslog
from plaso.parsers import utmp
from plaso.parsers import utmpx
from plaso.parsers import winevt
from plaso.parsers import winevtx
from plaso.parsers import winfirewall
from plaso.parsers import winjob
from plaso.parsers import winlnk
from plaso.parsers import winprefetch
from plaso.parsers import winreg
from plaso.parsers import xchatlog
from plaso.parsers import xchatscrollback
# Register plugins.
from plaso.parsers import bencode_plugins
from plaso.parsers import esedb_plugins
from plaso.parsers import olecf_plugins
from plaso.parsers import plist_plugins
from plaso.parsers import sqlite_plugins
from plaso.parsers import winreg_plugins
+126
View File
@@ -0,0 +1,126 @@
#!/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.
"""This file contains a parser for the Android usage-history.xml file."""
import os
from xml.etree import ElementTree
from dfvfs.helpers import text_file
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
class AndroidAppUsageEvent(event.EventObject):
"""EventObject for an Android Application Last Resumed event."""
DATA_TYPE = 'android:event:last_resume_time'
def __init__(self, last_resume_time, package, component):
"""Initializes the event object.
Args:
last_resume_time: The Last Resume Time of an Android App with details of
individual components. The timestamp contains the number of
milliseconds since Jan 1, 1970 00:00:00 UTC.
package: The name of the Android App.
component: The individual component of the App.
"""
super(AndroidAppUsageEvent, self).__init__()
self.timestamp = timelib.Timestamp.FromJavaTime(last_resume_time)
self.package = package
self.component = component
self.timestamp_desc = eventdata.EventTimestamp.LAST_RESUME_TIME
class AndroidAppUsageParser(interface.BaseParser):
"""Parses the Android usage-history.xml file."""
NAME = 'android_app_usage'
DESCRIPTION = u'Parser for the Android usage-history.xml file.'
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract the Android usage-history file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
text_file_object = text_file.TextFile(file_object)
# Need to verify the first line to make sure this is a) XML and
# b) the right XML.
first_line = text_file_object.readline(90)
# Note that we must check the data here as a string first, otherwise
# forcing first_line to convert to Unicode can raise a UnicodeDecodeError.
if not first_line.startswith('<?xml'):
raise errors.UnableToParseFile(
u'Not an Android usage history file [not XML]')
# We read in the second line due to the fact that ElementTree
# reads the entire file in memory to parse the XML string and
# we only care about the XML file with the correct root key,
# which denotes a typed_history.xml file.
second_line = text_file_object.readline(50).strip()
if second_line != u'<usage-history>':
raise errors.UnableToParseFile(
u'Not an Android usage history file [wrong XML root key]')
# For ElementTree to work we need to work on a filehandle seeked
# to the beginning.
file_object.seek(0, os.SEEK_SET)
xml = ElementTree.parse(file_object)
root = xml.getroot()
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
for app in root:
for part in app.iter():
if part.tag == 'comp':
package = app.get(u'name', '')
component = part.get(u'name', '')
try:
last_resume_time = int(part.get('lrt', u''), 10)
except ValueError:
continue
event_object = AndroidAppUsageEvent(
last_resume_time, package, component)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
file_object.close()
manager.ParsersManager.RegisterParser(AndroidAppUsageParser)
+85
View File
@@ -0,0 +1,85 @@
#!/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 Android Application Usage history parsers."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import android_app_usage as android_app_usage_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import android_app_usage
from plaso.parsers import test_lib
class AndroidAppUsageParserTest(test_lib.ParserTestCase):
"""Tests for the Android Application Usage History parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = android_app_usage.AndroidAppUsageParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['usage-history.xml'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 28)
event_object = event_objects[22]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-09 19:28:33.047000')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.component,
'com.sec.android.widgetapp.ap.hero.accuweather.menu.MenuAdd')
expected_msg = (
u'Package: '
u'com.sec.android.widgetapp.ap.hero.accuweather '
u'Component: '
u'com.sec.android.widgetapp.ap.hero.accuweather.menu.MenuAdd')
expected_msg_short = (
u'Package: com.sec.android.widgetapp.ap.hero.accuweather '
u'Component: com.sec.and...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[17]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-09-27 19:45:55.675000')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.package, 'com.google.android.gsf.login')
expected_msg = (
u'Package: '
u'com.google.android.gsf.login '
u'Component: '
u'com.google.android.gsf.login.NameActivity')
expected_msg_short = (
u'Package: com.google.android.gsf.login '
u'Component: com.google.android.gsf.login...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+412
View File
@@ -0,0 +1,412 @@
#!/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.
"""The Apple System Log Parser."""
import construct
import logging
import os
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
# TODO: get the real name for the user of the group having the uid or gid.
class AslEvent(event.EventObject):
"""Convenience class for an asl event."""
DATA_TYPE = 'mac:asl:event'
def __init__(
self, timestamp, record_position, message_id,
level, record_header, read_uid, read_gid, computer_name,
sender, facility, message, extra_information):
"""Initializes the event object.
Args:
timestamp: timestamp of the entry.
record_position: position where the record start.
message_id: Identification value for an ASL message.
level: level of criticality.
record_header: header of the entry.
pid: identification number of the process.
uid: identification number of the owner of the process.
gid: identification number of the group of the process.
read_uid: the user ID that can read this file. If -1: all.
read_gid: the group ID that can read this file. If -1: all.
computer_name: name of the host.
sender: the process that insert the event.
facility: the part of the sender that create the event.
message: message of the event.
extra_information: extra fields associated to each entry.
"""
super(AslEvent, self).__init__()
self.pid = record_header.pid
self.user_sid = unicode(record_header.uid)
self.group_id = record_header.gid
self.timestamp = timestamp
self.timestamp_desc = eventdata.EventTimestamp.CREATION_TIME
self.record_position = record_position
self.message_id = message_id
self.level = level
self.read_uid = read_uid
self.read_gid = read_gid
self.computer_name = computer_name
self.sender = sender
self.facility = facility
self.message = message
self.extra_information = extra_information
class AslParser(interface.BaseParser):
"""Parser for ASL log files."""
NAME = 'asl_log'
DESCRIPTION = u'Parser for ASL log files.'
ASL_MAGIC = 'ASL DB\x00\x00\x00\x00\x00\x00'
# If not right assigned, the value is "-1".
ASL_NO_RIGHTS = 'ffffffff'
# Priority level (criticity)
ASL_MESSAGE_PRIORITY = {
0 : 'EMERGENCY',
1 : 'ALERT',
2 : 'CRITICAL',
3 : 'ERROR',
4 : 'WARNING',
5 : 'NOTICE',
6 : 'INFO',
7 : 'DEBUG'}
# ASL File header.
# magic: magic number that identify ASL files.
# version: version of the file.
# offset: first record in the file.
# timestamp: epoch time when the first entry was written.
# last_offset: last record in the file.
ASL_HEADER_STRUCT = construct.Struct(
'asl_header_struct',
construct.String('magic', 12),
construct.UBInt32('version'),
construct.UBInt64('offset'),
construct.UBInt64('timestamp'),
construct.UBInt32('cache_size'),
construct.UBInt64('last_offset'),
construct.Padding(36))
# The record structure is:
# [HEAP][STRUCTURE][4xExtraField][2xExtraField]*[PreviousEntry]
# Record static structure.
# tam_entry: it contains the number of bytes from this file position
# until the end of the record, without counts itself.
# next_offset: next record. If is equal to 0x00, it is the last record.
# asl_message_id: integer that has the numeric identification of the event.
# timestamp: Epoch integer that has the time when the entry was created.
# nanosecond: nanosecond to add to the timestamp.
# level: level of priority.
# pid: process identification that ask to save the record.
# uid: user identification that has lunched the process.
# gid: group identification that has lunched the process.
# read_uid: identification id of a user. Only applied if is not -1 (all FF).
# Only root and this user can read the entry.
# read_gid: the same than read_uid, but for the group.
ASL_RECORD_STRUCT = construct.Struct(
'asl_record_struct',
construct.Padding(2),
construct.UBInt32('tam_entry'),
construct.UBInt64('next_offset'),
construct.UBInt64('asl_message_id'),
construct.UBInt64('timestamp'),
construct.UBInt32('nanosec'),
construct.UBInt16('level'),
construct.UBInt16('flags'),
construct.UBInt32('pid'),
construct.UBInt32('uid'),
construct.UBInt32('gid'),
construct.UBInt32('read_uid'),
construct.UBInt32('read_gid'),
construct.UBInt64('ref_pid'))
ASL_RECORD_STRUCT_SIZE = ASL_RECORD_STRUCT.sizeof()
# 8-byte fields, they can be:
# - String: [Nibble = 1000 (8)][Nibble = Length][7 Bytes = String].
# - Integer: integer that has the byte position in the file that points
# to an ASL_RECORD_DYN_VALUE struct. If the value of the integer
# is equal to 0, it means that it has not data (skip).
# If the field is a String, we use this structure to decode each
# integer byte in the corresponding character (ASCII Char).
ASL_OCTET_STRING = construct.ExprAdapter(
construct.Octet('string'),
encoder=lambda obj, ctx: ord(obj),
decoder=lambda obj, ctx: chr(obj))
# Field string structure. If the first bit is 1, it means that it
# is a String (1000) = 8, then the next nibble has the number of
# characters. The last 7 bytes are the number of bytes.
ASL_STRING = construct.BitStruct(
'string',
construct.Flag('type'),
construct.Bits('filler', 3),
construct.If(
lambda ctx: ctx.type,
construct.Nibble('string_length')),
construct.If(
lambda ctx: ctx.type,
construct.Array(7, ASL_OCTET_STRING)))
# 8-byte pointer to a byte position in the file.
ASL_POINTER = construct.UBInt64('pointer')
# Dynamic data structure pointed by a pointer that contains a String:
# [2 bytes padding][4 bytes lenght of String][String].
ASL_RECORD_DYN_VALUE = construct.Struct(
'asl_record_dyn_value',
construct.Padding(2),
construct.PascalString(
'value',
length_field=construct.UBInt32('length')))
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract entries from an ASL file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
try:
header = self.ASL_HEADER_STRUCT.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
file_object.close()
raise errors.UnableToParseFile(
u'Unable to parse ASL Header with error: {0:s}.'.format(exception))
if header.magic != self.ASL_MAGIC:
file_object.close()
raise errors.UnableToParseFile(u'Not an ASL Header, unable to parse.')
# Get the first and the last entry.
offset = header.offset
old_offset = header.offset
last_offset_header = header.last_offset
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
# If the ASL file has entries.
if offset:
event_object, offset = self.ReadAslEvent(file_object, offset)
while event_object:
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
# TODO: an anomaly object must be emitted once that is implemented.
# Sanity check, the last read element must be the same as
# indicated by the header.
if offset == 0 and old_offset != last_offset_header:
logging.warning(u'Parsing ended before the header ends.')
old_offset = offset
event_object, offset = self.ReadAslEvent(file_object, offset)
file_object.close()
def ReadAslEvent(self, file_object, offset):
"""Returns an AslEvent from a single ASL entry.
Args:
file_object: a file-like object that points to an ASL file.
offset: offset where the static part of the entry starts.
Returns:
An event object constructed from a single ASL record.
"""
# The heap of the entry is saved to try to avoid seek (performance issue).
# It has the real start position of the entry.
dynamic_start = file_object.tell()
dynamic_part = file_object.read(offset - file_object.tell())
if not offset:
return None, None
try:
record_header = self.ASL_RECORD_STRUCT.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
logging.warning(
u'Unable to parse ASL event with error: {0:s}'.format(exception))
return None, None
# Variable tam_fields = is the real length of the dynamic fields.
# We have this: [Record_Struct] + [Dynamic_Fields] + [Pointer_Entry_Before]
# In Record_Struct we have a field called tam_entry, where it has the number
# of bytes until the end of the entry from the position that the field is.
# The tam_entry is between the 2th and the 6th byte in the [Record_Struct].
# tam_entry = ([Record_Struct]-6)+[Dynamic_Fields]+[Pointer_Entry_Before]
# Also, we do not need [Point_Entry_Before] and then we delete the size of
# [Point_Entry_Before] that it is 8 bytes (8):
# tam_entry = ([Record_Struct]-6)+[Dynamic_Fields]+[Pointer_Entry_Before]
# [Dynamic_Fields] = tam_entry - [Record_Struct] + 6 - 8
# [Dynamic_Fields] = tam_entry - [Record_Struct] - 2
tam_fields = record_header.tam_entry - self.ASL_RECORD_STRUCT_SIZE - 2
# Dynamic part of the entry that contains minimal four fields of 8 bytes
# plus 2x[8bytes] fields for each extra ASL_Field.
# The four first fields are always the Host, Sender, Facility and Message.
# After the four first fields, the entry might have extra ASL_Fields.
# For each extra ASL_field, it has a pair of 8-byte fields where the first
# 8 bytes contains the name of the extra ASL_field and the second 8 bytes
# contains the text of the exta field.
# All of this 8-byte field can be saved using one of these three differents
# types:
# - Null value ('0000000000000000'): nothing to do.
# - String: It is string if first bit = 1 or first nibble = 8 (1000).
# Second nibble has the length of string.
# The next 7 bytes have the text characters of the string
# padding the end with null characters: '0x00'.
# Example: [8468 6964 6400 0000]
# [8] String, [4] length, value: [68 69 64 64] = hidd.
# - Pointer: static position in the file to a special struct
# implemented as an ASL_RECORD_DYN_VALUE.
# Example: [0000 0000 0000 0077]
# It points to the file position 0x077 that has a
# ASL_RECORD_DYN_VALUE structure.
values = []
while tam_fields > 0:
try:
raw_field = file_object.read(8)
except (IOError, construct.FieldError) as exception:
logging.warning(
u'Unable to parse ASL event with error: {0:d}'.format(exception))
return None, None
try:
# Try to read as a String.
field = self.ASL_STRING.parse(raw_field)
values.append(''.join(field.string[0:field.string_length]))
# Go to parse the next extra field.
tam_fields -= 8
continue
except ValueError:
pass
# If it is not a string, it must be a pointer.
try:
field = self.ASL_POINTER.parse(raw_field)
except ValueError as exception:
logging.warning(
u'Unable to parse ASL event with error: {0:s}'.format(exception))
return None, None
if field != 0:
# The next IF ELSE is only for performance issues, avoiding seek.
# If the pointer points a lower position than where the actual entry
# starts, it means that it points to a previuos entry.
pos = field - dynamic_start
# Bigger or equal 0 means that the data is in the actual entry.
if pos >= 0:
try:
values.append((self.ASL_RECORD_DYN_VALUE.parse(
dynamic_part[pos:])).value.partition('\x00')[0])
except (IOError, construct.FieldError) as exception:
logging.warning(
u'Unable to parse ASL event with error: {0:s}'.format(
exception))
return None, None
else:
# Only if it is a pointer that points to the
# heap from another entry we use the seek method.
main_position = file_object.tell()
# If the pointer is in a previous entry.
if main_position > field:
file_object.seek(field - main_position, os.SEEK_CUR)
try:
values.append((self.ASL_RECORD_DYN_VALUE.parse_stream(
file_object)).value.partition('\x00')[0])
except (IOError, construct.FieldError):
logging.warning((
u'The pointer at {0:d} (0x{0:x}) points to invalid '
u'information.').format(
main_position - self.ASL_POINTER.sizeof()))
# Come back to the position in the entry.
_ = file_object.read(main_position - file_object.tell())
else:
_ = file_object.read(field - main_position)
values.append((self.ASL_RECORD_DYN_VALUE.parse_stream(
file_object)).value.partition('\x00')[0])
# Come back to the position in the entry.
file_object.seek(main_position - file_object.tell(), os.SEEK_CUR)
# Next extra field: 8 bytes more.
tam_fields -= 8
# Read the last 8 bytes of the record that points to the previous entry.
_ = file_object.read(8)
# Parsed section, we translate the read data to an appropriate format.
microsecond = record_header.nanosec // 1000
timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond(
record_header.timestamp, microsecond)
record_position = offset
message_id = record_header.asl_message_id
level = u'{0} ({1})'.format(
self.ASL_MESSAGE_PRIORITY[record_header.level], record_header.level)
# If the value is -1 (FFFFFFFF), it can be read by everyone.
if record_header.read_uid != int(self.ASL_NO_RIGHTS, 16):
read_uid = record_header.read_uid
else:
read_uid = 'ALL'
if record_header.read_gid != int(self.ASL_NO_RIGHTS, 16):
read_gid = record_header.read_gid
else:
read_gid = 'ALL'
# Parsing the dynamic values (text or pointers to position with text).
# The first four are always the host, sender, facility, and message.
computer_name = values[0]
sender = values[1]
facility = values[2]
message = values[3]
# If the entry has an extra fields, they works as a pairs:
# The first is the name of the field and the second the value.
extra_information = ''
if len(values) > 4:
values = values[4:]
for index in xrange(0, len(values) // 2):
extra_information += (u'[{0}: {1}]'.format(
values[index * 2], values[(index * 2) + 1]))
# Return the event and the offset for the next entry.
return AslEvent(
timestamp, record_position, message_id, level, record_header, read_uid,
read_gid, computer_name, sender, facility, message,
extra_information), record_header.next_offset
manager.ParsersManager.RegisterParser(AslParser)
+96
View File
@@ -0,0 +1,96 @@
#!/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 Apple System Log file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import asl as asl_formatter
from plaso.lib import timelib_test
from plaso.parsers import asl
from plaso.parsers import test_lib
class AslParserTest(test_lib.ParserTestCase):
"""Tests for Apple System Log file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = asl.AslParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['applesystemlog.asl'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 2)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-25 09:45:35.705481')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.record_position, 442)
self.assertEqual(event_object.message_id, 101406)
self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local')
self.assertEqual(event_object.sender, u'locationd')
self.assertEqual(event_object.facility, u'com.apple.locationd')
self.assertEqual(event_object.pid, 69)
self.assertEqual(event_object.user_sid, u'205')
self.assertEqual(event_object.group_id, 205)
self.assertEqual(event_object.read_uid, 205)
self.assertEqual(event_object.read_gid, 'ALL')
self.assertEqual(event_object.level, u'WARNING (4)')
expected_message = (
u'Incorrect NSStringEncoding value 0x8000100 detected. '
u'Assuming NSASCIIStringEncoding. Will stop this compatiblity '
u'mapping behavior in the near future.')
self.assertEqual(event_object.message, expected_message)
expected_extra = (
u'[CFLog Local Time: 2013-11-25 09:45:35.701]'
u'[CFLog Thread: 1007]'
u'[Sender_Mach_UUID: 50E1F76A-60FF-368C-B74E-EB48F6D98C51]')
self.assertEqual(event_object.extra_information, expected_extra)
expected_msg = (
u'MessageID: 101406 '
u'Level: WARNING (4) '
u'User ID: 205 '
u'Group ID: 205 '
u'Read User: 205 '
u'Read Group: ALL '
u'Host: DarkTemplar-2.local '
u'Sender: locationd '
u'Facility: com.apple.locationd '
u'Message: {0:s} {1:s}').format(expected_message, expected_extra)
expected_msg_short = (
u'Sender: locationd '
u'Facility: com.apple.locationd')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
#!/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 the Bencode Parser.
Plaso's engine calls BencodeParser when it encounters bencoded files to be
processed, typically seen for BitTorrent data.
"""
import logging
import re
import os
import bencode
from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager
class BencodeParser(interface.BasePluginsParser):
"""Deserializes bencoded file; produces a dictionary containing bencoded data.
The Plaso engine calls parsers by their Parse() method. This parser's
Parse() has GetTopLevel() which deserializes bencoded files using the
BitTorrent-bencode library and calls plugins (BencodePlugin) registered
through the interface by their Process() to produce event objects.
Plugins are how this parser understands the content inside a bencoded file,
each plugin holds logic specific to a particular bencoded file. See the
bencode_plugins / directory for examples of how bencode plugins are
implemented.
"""
# Regex match for a bencode dictionary followed by a field size.
BENCODE_RE = re.compile('d[0-9]')
NAME = 'bencode'
DESCRIPTION = u'Parser for bencoded files.'
_plugin_classes = {}
def __init__(self):
"""Initializes a parser object."""
super(BencodeParser, self).__init__()
self._plugins = BencodeParser.GetPluginObjects()
def GetTopLevel(self, file_object):
"""Returns deserialized content of a bencoded file as a dictionary object.
Args:
file_object: A file-like object.
Returns:
Dictionary object representing the contents of the bencoded file.
"""
header = file_object.read(2)
file_object.seek(0, os.SEEK_SET)
if not self.BENCODE_RE.match(header):
raise errors.UnableToParseFile(u'Not a valid Bencoded file.')
try:
data_object = bencode.bdecode(file_object.read())
except (IOError, bencode.BTFailure) as exception:
raise errors.UnableToParseFile(
u'Unable to parse invalid Bencoded file with error: {0:s}'.format(
exception))
if not data_object:
raise errors.UnableToParseFile(u'Not a valid Bencoded file.')
return data_object
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Parse and extract values from a bencoded file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
data_object = self.GetTopLevel(file_object)
if not data_object:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse: {1:s}. Skipping.'.format(
self.NAME, file_entry.name))
parser_chain = self._BuildParserChain(parser_chain)
for plugin_object in self._plugins:
try:
plugin_object.Process(
parser_context, data=data_object, file_entry=file_entry,
parser_chain=parser_chain)
except errors.WrongBencodePlugin as exception:
logging.debug(u'[{0:s}] wrong plugin: {1:s}'.format(
self.NAME, exception))
file_object.close()
manager.ParsersManager.RegisterParser(BencodeParser)
+143
View File
@@ -0,0 +1,143 @@
#!/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 Bencode file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import bencode_parser as bencode_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import bencode_parser
from plaso.parsers import test_lib
class BencodeTest(test_lib.ParserTestCase):
"""Tests for Bencode file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = bencode_parser.BencodeParser()
# TODO: Move this to bencode_plugins/tranmission_test.py
def testTransmissionPlugin(self):
"""Read Transmission activity files and make few tests."""
test_file = self._GetTestFilePath(['bencode_transmission'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 3)
event_object = event_objects[0]
destination_expected = u'/Users/brian/Downloads'
self.assertEqual(event_object.destination, destination_expected)
self.assertEqual(event_object.seedtime, 4)
description_expected = eventdata.EventTimestamp.ADDED_TIME
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-08 15:31:20')
self.assertEqual(event_object.timestamp, expected_timestamp)
# Test on second event of first torrent.
event_object = event_objects[1]
self.assertEqual(event_object.destination, destination_expected)
self.assertEqual(event_object.seedtime, 4)
description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-08 18:24:24')
self.assertEqual(event_object.timestamp, expected_timestamp)
def testUTorrentPlugin(self):
"""Parse a uTorrent resume.dat file and make a few tests."""
test_file = self._GetTestFilePath(['bencode_utorrent'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 4)
caption_expected = u'plaso test'
path_expected = u'e:\\torrent\\files\\plaso test'
# First test on when the torrent was added to the client.
event_object = event_objects[3]
self.assertEqual(event_object.caption, caption_expected)
self.assertEqual(event_object.path, path_expected)
self.assertEqual(event_object.seedtime, 511)
description_expected = eventdata.EventTimestamp.ADDED_TIME
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-08-03 14:52:12')
self.assertEqual(event_object.timestamp, expected_timestamp)
# Second test on when the torrent file was completely downloaded.
event_object = event_objects[2]
self.assertEqual(event_object.caption, caption_expected)
self.assertEqual(event_object.path, path_expected)
self.assertEqual(event_object.seedtime, 511)
description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-08-03 18:11:35')
self.assertEqual(event_object.timestamp, expected_timestamp)
# Third test on when the torrent was first modified.
event_object = event_objects[0]
self.assertEqual(event_object.caption, caption_expected)
self.assertEqual(event_object.path, path_expected)
self.assertEqual(event_object.seedtime, 511)
description_expected = eventdata.EventTimestamp.MODIFICATION_TIME
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-08-03 18:11:34')
self.assertEqual(event_object.timestamp, expected_timestamp)
# Fourth test on when the torrent was again modified.
event_object = event_objects[1]
self.assertEqual(event_object.caption, caption_expected)
self.assertEqual(event_object.path, path_expected)
self.assertEqual(event_object.seedtime, 511)
description_expected = eventdata.EventTimestamp.MODIFICATION_TIME
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-08-03 16:27:59')
self.assertEqual(event_object.timestamp, expected_timestamp)
if __name__ == '__main__':
unittest.main()
+20
View File
@@ -0,0 +1,20 @@
#!/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 an import statement for each bencode related plugin."""
from plaso.parsers.bencode_plugins import transmission
from plaso.parsers.bencode_plugins import utorrent
+205
View File
@@ -0,0 +1,205 @@
#!/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.
"""bencode_interface contains basic interface for bencode plugins within Plaso.
Bencoded files are only one example of a type of object that the Plaso tool is
expected to encounter and process. There can be and are many other parsers
which are designed to process specific data types.
BencodePlugin defines the attributes necessary for registration, discovery
and operation of plugins for bencoded files which will be used by
BencodeParser.
"""
import abc
import logging
from plaso.lib import errors
from plaso.parsers import plugins
class BencodePlugin(plugins.BasePlugin):
"""This is an abstract class from which plugins should be based."""
# BENCODE_KEYS is a list of keys required by a plugin.
# This is expected to be overridden by the processing plugin.
# Ex. frozenset(['activity-date', 'done-date'])
BENCODE_KEYS = frozenset(['any'])
# This is expected to be overridden by the processing plugin.
# URLS should contain a list of URLs with additional information about
# this key or value.
# Ex. ['https://wiki.theory.org/BitTorrentSpecification#Bencoding']
URLS = []
NAME = 'bencode'
def _GetKeys(self, data, keys, depth=1):
"""Helper function to return keys nested in a bencode dict.
By default this function will return the values for the named keys requested
by a plugin in match{}. The default setting is to look a single layer down
from the root (same as the check for plugin applicability). This level is
suitable for most cases.
For cases where there is variability in the name at the first level
(e.g. it is the MAC addresses of a device, or a UUID) it is possible to
override the depth limit and use _GetKeys to fetch from a deeper level.
Args:
data: bencode data in dictionary form.
keys: A list of keys that should be returned.
depth: Defines how many levels deep to check for a match.
Returns:
A dictionary with just the keys requested.
"""
keys = set(keys)
match = {}
if depth == 1:
for key in keys:
match[key] = data[key]
else:
for _, parsed_key, parsed_value in self._RecurseKey(
data, depth=depth):
if parsed_key in keys:
match[parsed_key] = parsed_value
if set(match.keys()) == keys:
return match
return match
def _RecurseKey(self, recur_item, root='', depth=15):
"""Flattens nested dictionaries and lists by yielding it's values.
The hierarchy of a bencode file is a series of nested dictionaries and
lists. This is a helper function helps plugins navigate the structure
without having to reimplement their own recursive methods.
This method implements an overridable depth limit to prevent processing
extremely deeply nested dictionaries. If the limit is reached a debug
message is logged indicating which key processing stopped on.
Args:
recur_item: An object to be checked for additional nested items.
root: The pathname of the current working key.
depth: A counter to ensure we stop at the maximum recursion depth.
Yields:
A tuple of the root, key, and value from a bencoded file.
"""
if depth < 1:
logging.debug(u'Recursion limit hit for key: {0:s}'.format(root))
return
if type(recur_item) in (list, tuple):
for recur in recur_item:
for key in self._RecurseKey(recur, root, depth):
yield key
return
if not hasattr(recur_item, 'iteritems'):
return
for key, value in recur_item.iteritems():
yield root, key, value
if isinstance(value, dict):
value = [value]
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
for keyval in self._RecurseKey(
item, root=root + u'/' + key, depth=depth - 1):
yield keyval
@abc.abstractmethod
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, data=None,
match=None, **kwargs):
"""Extracts event object from the values of entries within a bencoded file.
This is the main method that a bencode plugin needs to implement.
The contents of the bencode keys defined in BENCODE_KEYS can be made
available to the plugin as both a matched{'KEY': 'value'} and as the
entire bencoded data dictionary. The plugin should implement logic to parse
the most relevant data set into a useful event for incorporation into the
Plaso timeline.
The attributes for a BencodeEvent should include the following:
root = Root key this event was extracted from.
key = Key the value resided in.
time = Date this artifact was created in microseconds(usec) from epoch.
desc = Short description.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
data: Bencode data in dictionary form. The default is None.
match: Optional dictionary containing only the keys selected in the
BENCODE_KEYS. The default is None.
"""
def Process(
self, parser_context, file_entry=None, parser_chain=None,
data=None, **kwargs):
"""Determine if this is the correct plugin; if so proceed with processing.
Process() checks if the current bencode file being processed is a match for
a plugin by comparing the PATH and KEY requirements defined by a plugin. If
both match processing continues; else raise WrongBencodePlugin.
This function also extracts the required keys as defined in
self.BENCODE_KEYS from the file and stores the result in match[key]
and calls self.GetEntries() which holds the processing logic implemented by
the plugin.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
data: Bencode data in dictionary form. The default is None.
Raises:
WrongBencodePlugin: If this plugin is not able to process the given file.
ValueError: If top level is not set.
"""
if data is None:
raise ValueError(u'Data is not set.')
if not set(data.keys()).issuperset(self.BENCODE_KEYS):
raise errors.WrongBencodePlugin(self.NAME)
# This will raise if unhandled keyword arguments are passed.
super(BencodePlugin, self).Process(parser_context, **kwargs)
logging.debug(u'Bencode Plugin Used: {0:s}'.format(self.NAME))
match = self._GetKeys(data, self.BENCODE_KEYS, 3)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
self.GetEntries(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
data=data, match=match)
+24
View File
@@ -0,0 +1,24 @@
#!/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.
"""Bencode plugin related functions and classes for testing."""
from plaso.parsers import test_lib
class BencodePluginTestCase(test_lib.ParserTestCase):
"""The unit test case for a bencode plugin."""
@@ -0,0 +1,102 @@
#!/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 a bencode plugin for Transmission BitTorrent data."""
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import bencode_parser
from plaso.parsers.bencode_plugins import interface
class TransmissionEvent(time_events.PosixTimeEvent):
"""Convenience class for a Transmission BitTorrent activity event."""
DATA_TYPE = 'p2p:bittorrent:transmission'
def __init__(self, timestamp, timestamp_description, destination, seedtime):
"""Initializes the event.
Args:
timestamp: The POSIX timestamp of the event.
timestamp_desc: A short description of the meaning of the timestamp.
destination: Downloaded file name within .torrent file
seedtime: Number of seconds client seeded torrent
"""
super(TransmissionEvent, self).__init__(timestamp, timestamp_description)
self.destination = destination
self.seedtime = seedtime // 60 # Convert seconds to minutes.
class TransmissionPlugin(interface.BencodePlugin):
"""Parse Transmission BitTorrent activity file for current torrents."""
NAME = 'bencode_transmission'
DESCRIPTION = u'Parser for Transmission bencoded files.'
BENCODE_KEYS = frozenset([
'activity-date', 'done-date', 'added-date', 'destination',
'seeding-time-seconds'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, data=None,
**unused_kwargs):
"""Extract data from Transmission's resume folder files.
This is the main parsing engine for the parser. It determines if
the selected file is the proper file to parse and extracts current
running torrents.
Transmission stores an individual Bencoded file for each active download
in a folder named resume under the user's application data folder.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
data: Optional bencode data in dictionary form. The default is None.
"""
# Place the obtained values into the event.
destination = data.get('destination', None)
seeding_time = data.get('seeding-time-seconds', None)
# Create timeline events based on extracted values.
if data.get('added-date', 0):
event_object = TransmissionEvent(
data.get('added-date'), eventdata.EventTimestamp.ADDED_TIME,
destination, seeding_time)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if data.get('done-date', 0):
event_object = TransmissionEvent(
data.get('done-date'), eventdata.EventTimestamp.FILE_DOWNLOADED,
destination, seeding_time)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if data.get('activity-date', None):
event_object = TransmissionEvent(
data.get('activity-date'), eventdata.EventTimestamp.ACCESS_TIME,
destination, seeding_time)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
bencode_parser.BencodeParser.RegisterPlugin(TransmissionPlugin)
+137
View File
@@ -0,0 +1,137 @@
#!/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 a bencode plugin for uTorrent data."""
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers import bencode_parser
from plaso.parsers.bencode_plugins import interface
class UTorrentEvent(time_events.PosixTimeEvent):
"""Convenience class for a uTorrent active torrents history entries."""
DATA_TYPE = 'p2p:bittorrent:utorrent'
def __init__(
self, timestamp, timestamp_description, path, caption, seedtime):
"""Initialize the event.
Args:
path: Torrent download location
caption: Official name of package
seedtime: Number of seconds client seeded torrent
"""
super(UTorrentEvent, self).__init__(timestamp, timestamp_description)
self.path = path
self.caption = caption
self.seedtime = seedtime // 60 # Convert seconds to minutes.
class UTorrentPlugin(interface.BencodePlugin):
"""Plugin to extract uTorrent active torrent events."""
NAME = 'bencode_utorrent'
DESCRIPTION = u'Parser for uTorrent bencoded files.'
# The following set is used to determine if the bencoded data is appropriate
# for this plugin. If there's a match, the entire bencoded data block is
# returned for analysis.
BENCODE_KEYS = frozenset(['.fileguard'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, data=None,
**unused_kwargs):
"""Extracts uTorrent active torrents.
This is the main parsing engine for the plugin. It determines if
the selected file is the proper file to parse and extracts current
running torrents.
interface.Process() checks for the given BENCODE_KEYS set, ensures
that it matches, and then passes the bencoded data to this function for
parsing. This plugin then parses the entire set of bencoded data to extract
the variable file-name keys to retrieve their values.
uTorrent creates a file, resume.dat, and a backup, resume.dat.old, to
for all active torrents. This is typically stored in the user's
application data folder.
These files, at a minimum, contain a '.fileguard' key and a dictionary
with a key name for a particular download with a '.torrent' file
extension.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
data: Optional bencode data in dictionary form. The default is None.
"""
# Walk through one of the torrent keys to ensure it's from a valid file.
for key, value in data.iteritems():
if not u'.torrent' in key:
continue
caption = value.get('caption')
path = value.get('path')
seedtime = value.get('seedtime')
if not caption or not path or seedtime < 0:
raise errors.WrongBencodePlugin(self.NAME)
for torrent, value in data.iteritems():
if not u'.torrent' in torrent:
continue
path = value.get('path', None)
caption = value.get('caption', None)
seedtime = value.get('seedtime', None)
# Create timeline events based on extracted values.
for event_key, event_value in value.iteritems():
if event_key == 'added_on':
event_object = UTorrentEvent(
event_value, eventdata.EventTimestamp.ADDED_TIME,
path, caption, seedtime)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
elif event_key == 'completed_on':
event_object = UTorrentEvent(
event_value, eventdata.EventTimestamp.FILE_DOWNLOADED,
path, caption, seedtime)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
elif event_key == 'modtimes':
for modtime in event_value:
# Some values are stored as 0, skip those.
if not modtime:
continue
event_object = UTorrentEvent(
modtime, eventdata.EventTimestamp.MODIFICATION_TIME,
path, caption, seedtime)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain,
file_entry=file_entry)
bencode_parser.BencodeParser.RegisterPlugin(UTorrentPlugin)
+1145
View File
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
#!/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 Basic Security Module (BSM) file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import bsm as bsm_formatter
from plaso.lib import timelib_test
from plaso.parsers import bsm
from plaso.parsers import test_lib
class MacOSXBsmParserTest(test_lib.ParserTestCase):
"""Tests for Basic Security Module (BSM) file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = bsm.BsmParser()
def testParse(self):
"""Tests the Parse function on a Mac OS X BSM file."""
knowledge_base_values = {'guessed_os': 'MacOSX'}
test_file = self._GetTestFilePath(['apple.bsm'])
event_queue_consumer = self._ParseFile(
self._parser, test_file, knowledge_base_values=knowledge_base_values)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 54)
event_object = event_objects[0]
self.assertEqual(event_object.data_type, 'mac:bsm:event')
expected_msg = (
u'Type: audit crash recovery (45029) '
u'Return: [BSM_TOKEN_RETURN32: Success (0), System call status: 0] '
u'Information: [BSM_TOKEN_TEXT: launchctl::Audit recovery]. '
u'[BSM_TOKEN_PATH: /var/audit/20131104171720.crash_recovery]')
expected_msg_short = (
u'Type: audit crash recovery (45029) '
u'Return: [BSM_TOKEN_RETURN32: Success (0), ...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-04 18:36:20.000381')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.event_type, u'audit crash recovery (45029)')
expected_extra_tokens = (
u'[BSM_TOKEN_TEXT: launchctl::Audit recovery]. '
u'[BSM_TOKEN_PATH: /var/audit/20131104171720.crash_recovery]')
self.assertEqual(event_object.extra_tokens, expected_extra_tokens)
expected_return_value = (
u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]')
self.assertEqual(event_object.return_value, expected_return_value)
event_object = event_objects[15]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-04 18:36:26.000171')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.event_type, u'user authentication (45023)')
expected_extra_tokens = (
u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(92), egid(92), uid(92), '
u'gid(92), pid(143), session_id(100004), terminal_port(143), '
u'terminal_ip(0.0.0.0)]. '
u'[BSM_TOKEN_TEXT: Verify password for record type Users '
u'\'moxilo\' node \'/Local/Default\']')
self.assertEqual(event_object.extra_tokens, expected_extra_tokens)
expected_return_value = (
u'[BSM_TOKEN_RETURN32: Unknown (255), System call status: 5000]')
self.assertEqual(event_object.return_value, expected_return_value)
event_object = event_objects[31]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-04 18:36:26.000530')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.event_type, u'SecSrvr AuthEngine (45025)')
expected_extra_tokens = (
u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(0), egid(0), uid(0), '
u'gid(0), pid(67), session_id(100004), terminal_port(67), '
u'terminal_ip(0.0.0.0)]. '
u'[BSM_TOKEN_TEXT: system.login.done]. '
u'[BSM_TOKEN_TEXT: system.login.done]')
self.assertEqual(event_object.extra_tokens, expected_extra_tokens)
expected_return_value = (
u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]')
self.assertEqual(event_object.return_value, expected_return_value)
event_object = event_objects[50]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-04 18:37:36.000399')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.event_type, u'session end (44903)')
expected_extra_tokens = (
u'[BSM_TOKEN_ARGUMENT64: sflags(1) is 0x0]. '
u'[BSM_TOKEN_ARGUMENT32: am_success(2) is 0x3000]. '
u'[BSM_TOKEN_ARGUMENT32: am_failure(3) is 0x3000]. '
u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(0), egid(0), uid(0), '
u'gid(0), pid(0), session_id(100015), terminal_port(0), '
u'terminal_ip(0.0.0.0)]')
self.assertEqual(event_object.extra_tokens, expected_extra_tokens)
expected_return_value = (
u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]')
self.assertEqual(event_object.return_value, expected_return_value)
class OpenBsmParserTest(test_lib.ParserTestCase):
"""Tests for Basic Security Module (BSM) file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = bsm.BsmParser()
def testParse(self):
"""Tests the Parse function on a "generic" BSM file."""
knowledge_base_values = {'guessed_os': 'openbsm'}
test_file = self._GetTestFilePath(['openbsm.bsm'])
event_queue_consumer = self._ParseFile(
self._parser, test_file, knowledge_base_values=knowledge_base_values)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 50)
expected_extra_tokens = [
u'[BSM_TOKEN_ARGUMENT32: test_arg32_token(3) is 0xABCDEF00]',
u'[BSM_TOKEN_DATA: Format data: String, Data: SomeData]',
u'[BSM_TOKEN_FILE: test, timestamp: 1970-01-01 20:42:45]',
u'[BSM_TOKEN_ADDR: 192.168.100.15]',
u'[IPv4_Header: 0x400000145478000040010000c0a8649bc0a86e30]',
u'[BSM_TOKEN_IPC: object type 1, object id 305419896]',
u'[BSM_TOKEN_PORT: 20480]',
u'[BSM_TOKEN_OPAQUE: aabbccdd]',
u'[BSM_TOKEN_PATH: /test/this/is/a/test]',
(u'[BSM_TOKEN_PROCESS32: aid(305419896), euid(19088743), '
u'egid(591751049), uid(2557891634), gid(159868227), '
u'pid(321140038), session_id(2542171492), '
u'terminal_port(374945606), terminal_ip(127.0.0.1)]'),
(u'[BSM_TOKEN_PROCESS64: aid(305419896), euid(19088743), '
u'egid(591751049), uid(2557891634), gid(159868227), '
u'pid(321140038), session_id(2542171492), '
u'terminal_port(374945606), terminal_ip(127.0.0.1)]'),
(u'[BSM_TOKEN_RETURN32: Invalid argument (22), '
u'System call status: 305419896]'),
u'[BSM_TOKEN_SEQUENCE: 305419896]',
(u'[BSM_TOKEN_AUT_SOCKINET32_EX: '
u'from 127.0.0.1 port 0 to 127.0.0.1 port 0]'),
(u'[BSM_TOKEN_SUBJECT32: aid(305419896), euid(19088743), '
u'egid(591751049), uid(2557891634), gid(159868227), '
u'pid(321140038), session_id(2542171492), '
u'terminal_port(374945606), terminal_ip(127.0.0.1)]'),
(u'[BSM_TOKEN_SUBJECT32_EX: aid(305419896), euid(19088743), '
u'egid(591751049), uid(2557891634), gid(159868227), '
u'pid(321140038), session_id(2542171492), '
u'terminal_port(374945606), terminal_ip(fe80::1)]'),
u'[BSM_TOKEN_TEXT: This is a test.]',
u'[BSM_TOKEN_ZONENAME: testzone]',
(u'[BSM_TOKEN_RETURN32: Argument list too long (7), '
u'System call status: 4294967295]')]
extra_tokens = []
for event_object_index in range(0, 19):
extra_tokens.append(event_objects[event_object_index].extra_tokens)
self.assertEqual(extra_tokens, expected_extra_tokens)
if __name__ == '__main__':
unittest.main()
+441
View File
@@ -0,0 +1,441 @@
#!/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.
"""Parser for Google Chrome and Chromium Cache files."""
import logging
import os
import construct
from dfvfs.resolver import resolver as path_spec_resolver
from dfvfs.path import factory as path_spec_factory
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers import interface
from plaso.parsers import manager
class CacheAddress(object):
"""Class that contains a cache address."""
FILE_TYPE_SEPARATE = 0
FILE_TYPE_BLOCK_RANKINGS = 1
FILE_TYPE_BLOCK_256 = 2
FILE_TYPE_BLOCK_1024 = 3
FILE_TYPE_BLOCK_4096 = 4
_BLOCK_DATA_FILE_TYPES = [
FILE_TYPE_BLOCK_RANKINGS,
FILE_TYPE_BLOCK_256,
FILE_TYPE_BLOCK_1024,
FILE_TYPE_BLOCK_4096]
_FILE_TYPE_BLOCK_SIZES = [0, 36, 256, 1024, 4096]
def __init__(self, cache_address):
"""Initializes the cache address object.
Args:
cache_address: the cache address value.
"""
super(CacheAddress, self).__init__()
self.block_number = None
self.block_offset = None
self.block_size = None
self.filename = None
self.value = cache_address
if cache_address & 0x80000000:
self.is_initialized = u'True'
else:
self.is_initialized = u'False'
self.file_type = (cache_address & 0x70000000) >> 28
if not cache_address == 0x00000000:
if self.file_type == self.FILE_TYPE_SEPARATE:
file_selector = cache_address & 0x0fffffff
self.filename = u'f_{0:06x}'.format(file_selector)
elif self.file_type in self._BLOCK_DATA_FILE_TYPES:
file_selector = (cache_address & 0x00ff0000) >> 16
self.filename = u'data_{0:d}'.format(file_selector)
file_block_size = self._FILE_TYPE_BLOCK_SIZES[self.file_type]
self.block_number = cache_address & 0x0000ffff
self.block_size = (cache_address & 0x03000000) >> 24
self.block_size *= file_block_size
self.block_offset = 8192 + (self.block_number * file_block_size)
class CacheEntry(object):
"""Class that contains a cache entry."""
def __init__(self):
"""Initializes the cache entry object."""
super(CacheEntry, self).__init__()
self.creation_time = None
self.hash = None
self.key = None
self.next = None
self.rankings_node = None
class IndexFile(object):
"""Class that contains an index file."""
SIGNATURE = 0xc103cac3
_FILE_HEADER = construct.Struct(
'chrome_cache_index_file_header',
construct.ULInt32('signature'),
construct.ULInt16('minor_version'),
construct.ULInt16('major_version'),
construct.ULInt32('number_of_entries'),
construct.ULInt32('stored_data_size'),
construct.ULInt32('last_created_file_number'),
construct.ULInt32('unknown1'),
construct.ULInt32('unknown2'),
construct.ULInt32('table_size'),
construct.ULInt32('unknown3'),
construct.ULInt32('unknown4'),
construct.ULInt64('creation_time'),
construct.Padding(208))
def __init__(self):
"""Initializes the index file object."""
super(IndexFile, self).__init__()
self._file_object = None
self.creation_time = None
self.version = None
self.index_table = []
def _ReadFileHeader(self):
"""Reads the file header.
Raises:
IOError: if the file header cannot be read.
"""
self._file_object.seek(0, os.SEEK_SET)
try:
file_header = self._FILE_HEADER.parse_stream(self._file_object)
except construct.FieldError as exception:
raise IOError(u'Unable to parse file header with error: {0:s}'.format(
exception))
signature = file_header.get('signature')
if signature != self.SIGNATURE:
raise IOError(u'Unsupported index file signature')
self.version = u'{0:d}.{1:d}'.format(
file_header.get('major_version'),
file_header.get('minor_version'))
if self.version not in [u'2.0', u'2.1']:
raise IOError(u'Unsupported index file version: {0:s}'.format(
self.version))
self.creation_time = file_header.get('creation_time')
def _ReadIndexTable(self):
"""Reads the index table."""
cache_address_data = self._file_object.read(4)
while len(cache_address_data) == 4:
value = construct.ULInt32('cache_address').parse(cache_address_data)
if value:
cache_address = CacheAddress(value)
self.index_table.append(cache_address)
cache_address_data = self._file_object.read(4)
def Close(self):
"""Closes the index file."""
if self._file_object:
self._file_object.close()
self._file_object = None
def Open(self, file_object):
"""Opens the index file.
Args:
file_object: the file object.
"""
self._file_object = file_object
self._ReadFileHeader()
# Skip over the LRU data, which is 112 bytes in size.
self._file_object.seek(112, os.SEEK_CUR)
self._ReadIndexTable()
class DataBlockFile(object):
"""Class that contains a data block file."""
SIGNATURE = 0xc104cac3
_FILE_HEADER = construct.Struct(
'chrome_cache_data_file_header',
construct.ULInt32('signature'),
construct.ULInt16('minor_version'),
construct.ULInt16('major_version'),
construct.ULInt16('file_number'),
construct.ULInt16('next_file_number'),
construct.ULInt32('block_size'),
construct.ULInt32('number_of_entries'),
construct.ULInt32('maximum_number_of_entries'),
construct.Array(4, construct.ULInt32('emtpy')),
construct.Array(4, construct.ULInt32('hints')),
construct.ULInt32('updating'),
construct.Array(5, construct.ULInt32('user')))
_CACHE_ENTRY = construct.Struct(
'chrome_cache_entry',
construct.ULInt32('hash'),
construct.ULInt32('next_address'),
construct.ULInt32('rankings_node_address'),
construct.ULInt32('reuse_count'),
construct.ULInt32('refetch_count'),
construct.ULInt32('state'),
construct.ULInt64('creation_time'),
construct.ULInt32('key_size'),
construct.ULInt32('long_key_address'),
construct.Array(4, construct.ULInt32('data_stream_sizes')),
construct.Array(4, construct.ULInt32('data_stream_addresses')),
construct.ULInt32('flags'),
construct.Padding(16),
construct.ULInt32('self_hash'),
construct.Array(160, construct.UBInt8('key')))
def __init__(self):
"""Initializes the data block file object."""
super(DataBlockFile, self).__init__()
self._file_object = None
self.creation_time = None
self.block_size = None
self.number_of_entries = None
self.version = None
def _ReadFileHeader(self):
"""Reads the file header.
Raises:
IOError: if the file header cannot be read.
"""
self._file_object.seek(0, os.SEEK_SET)
try:
file_header = self._FILE_HEADER.parse_stream(self._file_object)
except construct.FieldError as exception:
raise IOError(u'Unable to parse file header with error: {0:s}'.format(
exception))
signature = file_header.get('signature')
if signature != self.SIGNATURE:
raise IOError(u'Unsupported data block file signature')
self.version = u'{0:d}.{1:d}'.format(
file_header.get('major_version'),
file_header.get('minor_version'))
if self.version not in [u'2.0', u'2.1']:
raise IOError(u'Unsupported data block file version: {0:s}'.format(
self.version))
self.block_size = file_header.get('block_size')
self.number_of_entries = file_header.get('number_of_entries')
def ReadCacheEntry(self, block_offset):
"""Reads a cache entry."""
self._file_object.seek(block_offset, os.SEEK_SET)
try:
cache_entry_struct = self._CACHE_ENTRY.parse_stream(self._file_object)
except construct.FieldError as exception:
raise IOError(u'Unable to parse cache entry with error: {0:s}'.format(
exception))
cache_entry = CacheEntry()
cache_entry.hash = cache_entry_struct.get('hash')
cache_entry.next = CacheAddress(cache_entry_struct.get('next_address'))
cache_entry.rankings_node = CacheAddress(cache_entry_struct.get(
'rankings_node_address'))
cache_entry.creation_time = cache_entry_struct.get('creation_time')
byte_array = cache_entry_struct.get('key')
string = u''.join(map(chr, byte_array))
cache_entry.key, _, _ = string.partition(u'\x00')
return cache_entry
def Close(self):
"""Closes the data block file."""
if self._file_object:
self._file_object.close()
self._file_object = None
def Open(self, file_object):
"""Opens the data block file.
Args:
file_object: the file object.
"""
self._file_object = file_object
self._ReadFileHeader()
class ChromeCacheEntryEvent(time_events.WebKitTimeEvent):
"""Class that contains a Chrome Cache event."""
DATA_TYPE = 'chrome:cache:entry'
def __init__(self, cache_entry):
"""Initializes the event object.
Args:
cache_entry: the cache entry (instance of CacheEntry).
"""
super(ChromeCacheEntryEvent, self).__init__(
cache_entry.creation_time, eventdata.EventTimestamp.CREATION_TIME)
self.original_url = cache_entry.key
class ChromeCacheParser(interface.BaseParser):
"""Parses Chrome Cache files."""
NAME = 'chrome_cache'
DESCRIPTION = u'Parser for Chrome Cache files.'
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract event objects from Chrome Cache files.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
index_file = IndexFile()
try:
index_file.Open(file_object)
except IOError as exception:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse index file {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, exception))
# Build a lookup table for the data block files.
file_system = file_entry.GetFileSystem()
path_segments = file_system.SplitPath(file_entry.path_spec.location)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
data_block_files = {}
for cache_address in index_file.index_table:
if cache_address.filename not in data_block_files:
# Remove the previous filename from the path segments list and
# add one of the data block file.
path_segments.pop()
path_segments.append(cache_address.filename)
# We need to pass only used arguments to the path specification
# factory otherwise it will raise.
kwargs = {}
if file_entry.path_spec.parent:
kwargs['parent'] = file_entry.path_spec.parent
kwargs['location'] = file_system.JoinPath(path_segments)
data_block_file_path_spec = path_spec_factory.Factory.NewPathSpec(
file_entry.path_spec.TYPE_INDICATOR, **kwargs)
try:
data_block_file_entry = path_spec_resolver.Resolver.OpenFileEntry(
data_block_file_path_spec)
except RuntimeError as exception:
logging.error((
u'[{0:s}] Unable to open data block file: {1:s} while parsing '
u'{2:s} with error: {3:s}').format(
parser_chain, kwargs['location'],
file_entry.path_spec.comparable, exception))
data_block_file_entry = None
if not data_block_file_entry:
logging.error(u'Missing data block file: {0:s}'.format(
cache_address.filename))
data_block_file = None
else:
data_block_file_object = data_block_file_entry.GetFileObject()
data_block_file = DataBlockFile()
try:
data_block_file.Open(data_block_file_object)
except IOError as exception:
logging.error((
u'Unable to open data block file: {0:s} with error: '
u'{1:s}').format(cache_address.filename, exception))
data_block_file = None
data_block_files[cache_address.filename] = data_block_file
# Parse the cache entries in the data block files.
for cache_address in index_file.index_table:
cache_address_chain_length = 0
while cache_address.value != 0x00000000:
if cache_address_chain_length >= 64:
logging.error(u'Maximum allowed cache address chain length reached.')
break
data_file = data_block_files.get(cache_address.filename, None)
if not data_file:
logging.debug(u'Cache address: 0x{0:08x} missing data file.'.format(
cache_address.value))
break
try:
cache_entry = data_file.ReadCacheEntry(cache_address.block_offset)
except (IOError, UnicodeDecodeError) as exception:
logging.error(
u'Unable to parse cache entry with error: {0:s}'.format(
exception))
break
event_object = ChromeCacheEntryEvent(cache_entry)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
cache_address = cache_entry.next
cache_address_chain_length += 1
for data_block_file in data_block_files.itervalues():
if data_block_file:
data_block_file.Close()
index_file.Close()
manager.ParsersManager.RegisterParser(ChromeCacheParser)
+60
View File
@@ -0,0 +1,60 @@
#!/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 Chrome Cache files parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import chrome_cache as chrome_cache_formatter
from plaso.lib import timelib_test
from plaso.parsers import test_lib
from plaso.parsers import chrome_cache
class ChromeCacheParserTest(test_lib.ParserTestCase):
"""Tests for the Chrome Cache files parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = chrome_cache.ChromeCacheParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['chrome_cache', 'index'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 217)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-04-30 16:44:36.226091')
self.assertEqual(event_object.timestamp, expected_timestamp)
expected_original_url = (
u'https://s.ytimg.com/yts/imgbin/player-common-vfliLfqPT.webp')
self.assertEqual(event_object.original_url, expected_original_url)
expected_string = u'Original URL: {0:s}'.format(expected_original_url)
self._TestGetMessageStrings(event_object, expected_string, expected_string)
if __name__ == '__main__':
unittest.main()
+289
View File
@@ -0,0 +1,289 @@
#!/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 parser context object."""
import os
from dfvfs.lib import definitions as dfvfs_definitions
from plaso.lib import event
from plaso.lib import utils
class ParserContext(object):
"""Class that implements the parser context."""
def __init__(
self, event_queue_producer, parse_error_queue_producer, knowledge_base):
"""Initializes a parser context object.
Args:
event_queue_producer: the event object queue producer (instance of
ItemQueueProducer).
parse_error_queue_producer: the parse error queue producer (instance of
ItemQueueProducer).
knowledge_base: A knowledge base object (instance of KnowledgeBase),
which contains information from the source data needed
for parsing.
"""
super(ParserContext, self).__init__()
self._abort = False
self._event_queue_producer = event_queue_producer
self._filter_object = None
self._knowledge_base = knowledge_base
self._mount_path = None
self._parse_error_queue_producer = parse_error_queue_producer
self._text_prepend = None
self.number_of_events = 0
self.number_of_parse_errors = 0
@property
def abort(self):
"""Read-only value to indicate the parsing should be aborted."""
return self._abort
@property
def codepage(self):
"""The codepage."""
return self._knowledge_base.codepage
@property
def hostname(self):
"""The hostname."""
return self._knowledge_base.hostname
@property
def knowledge_base(self):
"""The knowledge base."""
return self._knowledge_base
@property
def platform(self):
"""The platform."""
return self._knowledge_base.platform
@property
def timezone(self):
"""The timezone object."""
return self._knowledge_base.timezone
@property
def year(self):
"""The year."""
return self._knowledge_base.year
def GetDisplayName(self, file_entry):
"""Retrieves the display name for the file entry.
Args:
file_entry: a file entry object (instance of dfvfs.FileEntry).
Returns:
A string containing the display name.
"""
relative_path = self.GetRelativePath(file_entry)
if not relative_path:
return file_entry.name
return u'{0:s}:{1:s}'.format(
file_entry.path_spec.type_indicator, relative_path)
def GetRelativePath(self, file_entry):
"""Retrieves the relative path of the file entry.
Args:
file_entry: a file entry object (instance of dfvfs.FileEntry).
Returns:
A string containing the relative path or None.
"""
path_spec = getattr(file_entry, 'path_spec', None)
if not path_spec:
return
# TODO: Solve this differently, quite possibly inside dfVFS using mount
# path spec.
file_path = getattr(path_spec, 'location', None)
if path_spec.type_indicator != dfvfs_definitions.TYPE_INDICATOR_OS:
return file_path
# If we are parsing a mount point we don't want to include the full
# path to file's location here, we are only interested in the relative
# path to the mount point.
if self._mount_path:
_, _, file_path = file_path.partition(self._mount_path)
return file_path
def MatchesFilter(self, event_object):
"""Checks if the event object matces the filter.
Args:
event_object: the event object (instance of EventObject).
Returns:
A boolean value indicating if the event object matches the filter.
"""
return self._filter_object and self._filter_object.Matches(event_object)
def ProcessEvent(
self, event_object, parser_chain=None, file_entry=None, query=None):
"""Processes an event before it is emitted to the event queue.
Args:
event_object: the event object (instance of EventObject).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
query: Optional query string. The default is None.
"""
if not getattr(event_object, 'parser', None) and parser_chain:
event_object.parser = parser_chain
# TODO: deprecate text_prepend in favor of an event tag.
if not getattr(event_object, 'text_prepend', None) and self._text_prepend:
event_object.text_prepend = self._text_prepend
display_name = None
if file_entry:
event_object.pathspec = file_entry.path_spec
if not getattr(event_object, 'filename', None):
event_object.filename = self.GetRelativePath(file_entry)
if not display_name:
# TODO: dfVFS refactor: move display name to output since the path
# specification contains the full information.
display_name = self.GetDisplayName(file_entry)
stat_object = file_entry.GetStat()
inode_number = getattr(stat_object, 'ino', None)
if not hasattr(event_object, 'inode') and inode_number:
# TODO: clean up the GetInodeValue function.
event_object.inode = utils.GetInodeValue(inode_number)
if not getattr(event_object, 'display_name', None) and display_name:
event_object.display_name = display_name
if not getattr(event_object, 'hostname', None) and self.hostname:
event_object.hostname = self.hostname
if not getattr(event_object, 'username', None):
user_sid = getattr(event_object, 'user_sid', None)
username = self._knowledge_base.GetUsernameByIdentifier(user_sid)
if username:
event_object.username = username
if not getattr(event_object, 'query', None) and query:
event_object.query = query
def ProduceEvent(
self, event_object, parser_chain=None, file_entry=None, query=None):
"""Produces an event onto the queue.
Args:
event_object: the event object (instance of EventObject).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
query: Optional query string. The default is None.
"""
self.ProcessEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry,
query=query)
if self.MatchesFilter(event_object):
return
self._event_queue_producer.ProduceItem(event_object)
self.number_of_events += 1
def ProduceEvents(
self, event_objects, parser_chain=None, file_entry=None, query=None):
"""Produces events onto the queue.
Args:
event_objects: a list or generator of event objects (instances of
EventObject).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
query: Optional query string. The default is None.
"""
for event_object in event_objects:
self.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry,
query=query)
def ProduceParseError(self, name, description, file_entry=None):
"""Produces a parse error.
Args:
name: The parser or plugin name.
description: The description of the error.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
"""
if self._parse_error_queue_producer:
path_spec = getattr(file_entry, 'path_spec', None)
parse_error = event.ParseError(name, description, path_spec=path_spec)
self._parse_error_queue_producer.ProduceItem(parse_error)
self.number_of_parse_errors += 1
def ResetCounters(self):
"""Resets the counters."""
self.number_of_events = 0
self.number_of_parse_errors = 0
def SetFilterObject(self, filter_object):
"""Sets the filter object.
Args:
filter_object: the filter object (instance of objectfilter.Filter).
"""
self._filter_object = filter_object
def SetMountPath(self, mount_path):
"""Sets the mount path.
Args:
mount_path: string containing the mount path.
"""
# Remove a trailing path separator from the mount path so the relative
# paths will start with a path separator.
if mount_path and mount_path.endswith(os.sep):
mount_path = mount_path[:-1]
self._mount_path = mount_path
def SetTextPrepend(self, text_prepend):
"""Sets the text prepend.
Args:
text_prepend: string that contains the text to prepend to every event.
"""
self._text_prepend = text_prepend
def SignalAbort(self):
"""Signals the parsers to abort."""
self._abort = True
+19
View File
@@ -0,0 +1,19 @@
#!/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 an import statement for each browser cookie plugin."""
from plaso.parsers.cookie_plugins import ganalytics
+221
View File
@@ -0,0 +1,221 @@
#!/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 a plugin for parsing Google Analytics cookies."""
import urllib
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers.cookie_plugins import interface
class GoogleAnalyticsEvent(time_events.PosixTimeEvent):
"""A simple placeholder for a Google Analytics event."""
DATA_TYPE = u'cookie:google:analytics'
def __init__(
self, timestamp, timestamp_desc, url, data_type_append, cookie_name,
**kwargs):
"""Initialize a Google Analytics event.
Args:
timestamp: The timestamp in a POSIX format.
timestamp_desc: A string describing the timestamp.
url: The full URL where the cookie got set.
data_type_append: String to append to the data type.
cookie_name: The name of the cookie.
"""
super(GoogleAnalyticsEvent, self).__init__(
timestamp, timestamp_desc, u'{0:s}:{1:s}'.format(
self.DATA_TYPE, data_type_append))
self.url = url
self.cookie_name = cookie_name
for key, value in kwargs.iteritems():
setattr(self, key, value)
class GoogleAnalyticsUtmzPlugin(interface.CookiePlugin):
"""A browser cookie plugin for Google Analytics cookies."""
NAME = 'google_analytics_utmz'
COOKIE_NAME = u'__utmz'
# Point to few sources for URL information.
URLS = [
(u'http://www.dfinews.com/articles/2012/02/'
u'google-analytics-cookies-and-forensic-implications')]
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None,
cookie_data=None, url=None, **unused_kwargs):
"""Extracts event objects from the cookie.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
cookie_data: The cookie data, as a byte string.
url: The full URL or path where the cookie got set.
"""
# The structure of the field:
# <domain hash>.<last time>.<sessions>.<sources>.<variables>
fields = cookie_data.split('.')
if len(fields) > 5:
variables = u'.'.join(fields[4:])
fields = fields[0:4]
fields.append(variables)
if len(fields) != 5:
raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 5]'.format(
len(fields)))
domain_hash, last, sessions, sources, variables = fields
extra_variables = variables.split(u'|')
kwargs = {}
for variable in extra_variables:
key, _, value = variable.partition(u'=')
try:
value_line = unicode(urllib.unquote(str(value)), 'utf-8')
except UnicodeDecodeError:
value_line = repr(value)
kwargs[key] = value_line
event_object = GoogleAnalyticsEvent(
int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME,
url, 'utmz', self.COOKIE_NAME, domain_hash=domain_hash,
sessions=int(sessions, 10), sources=int(sources, 10),
**kwargs)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
class GoogleAnalyticsUtmaPlugin(interface.CookiePlugin):
"""A browser cookie plugin for Google Analytics cookies."""
NAME = 'google_analytics_utma'
COOKIE_NAME = u'__utma'
# Point to few sources for URL information.
URLS = [
(u'http://www.dfinews.com/articles/2012/02/'
u'google-analytics-cookies-and-forensic-implications')]
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None,
cookie_data=None, url=None, **unused_kwargs):
"""Extracts event objects from the cookie.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
cookie_data: The cookie data, as a byte string.
url: The full URL or path where the cookie got set.
"""
# Values has the structure of:
# <domain hash>.<visitor ID>.<first visit>.<previous>.<last>.<# of
# sessions>
fields = cookie_data.split(u'.')
# Check for a valid record.
if len(fields) != 6:
raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 6]'.format(
len(fields)))
domain_hash, visitor_id, first_visit, previous, last, sessions = fields
# TODO: Double check this time is stored in UTC and not local time.
first_epoch = int(first_visit, 10)
event_object = GoogleAnalyticsEvent(
first_epoch, 'Analytics Creation Time', url, 'utma', self.COOKIE_NAME,
domain_hash=domain_hash, visitor_id=visitor_id,
sessions=int(sessions, 10))
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
event_object = GoogleAnalyticsEvent(
int(previous, 10), 'Analytics Previous Time', url, 'utma',
self.COOKIE_NAME, domain_hash=domain_hash, visitor_id=visitor_id,
sessions=int(sessions, 10))
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
event_object = GoogleAnalyticsEvent(
int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME,
url, 'utma', self.COOKIE_NAME, domain_hash=domain_hash,
visitor_id=visitor_id, sessions=int(sessions, 10))
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
class GoogleAnalyticsUtmbPlugin(interface.CookiePlugin):
"""A browser cookie plugin for Google Analytics cookies."""
NAME = 'google_analytics_utmb'
COOKIE_NAME = u'__utmb'
# Point to few sources for URL information.
URLS = [
(u'http://www.dfinews.com/articles/2012/02/'
u'google-analytics-cookies-and-forensic-implications')]
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None,
cookie_data=None, url=None, **unused_kwargs):
"""Extracts event objects from the cookie.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
cookie_data: The cookie data, as a byte string.
url: The full URL or path where the cookie got set.
"""
# Values has the structure of:
# <domain hash>.<pages viewed>.10.<last time>
fields = cookie_data.split(u'.')
# Check for a valid record.
if len(fields) != 4:
raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 4]'.format(
len(fields)))
domain_hash, pages_viewed, _, last = fields
event_object = GoogleAnalyticsEvent(
int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME,
url, 'utmb', self.COOKIE_NAME, domain_hash=domain_hash,
pages_viewed=int(pages_viewed, 10))
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
@@ -0,0 +1,139 @@
#!/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 Google Analytics cookies."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import ganalytics as ganalytics_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers.cookie_plugins import ganalytics
from plaso.parsers.sqlite_plugins import chrome_cookies
from plaso.parsers.sqlite_plugins import firefox_cookies
from plaso.parsers.sqlite_plugins import test_lib
class GoogleAnalyticsPluginTest(test_lib.SQLitePluginTestCase):
"""Tests for the Google Analytics plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
def _GetAnalyticsCookies(self, event_queue_consumer):
"""Return a list of analytics cookies."""
cookies = []
for event_object in self._GetEventObjectsFromQueue(event_queue_consumer):
if isinstance(event_object, ganalytics.GoogleAnalyticsEvent):
cookies.append(event_object)
return cookies
def testParsingFirefox29CookieDatabase(self):
"""Tests the Process function on a Firefox 29 cookie database file."""
plugin = firefox_cookies.FirefoxCookiePlugin()
test_file = self._GetTestFilePath(['firefox_cookies.sqlite'])
event_queue_consumer = self._ParseDatabaseFileWithPlugin(plugin, test_file)
event_objects = self._GetAnalyticsCookies(event_queue_consumer)
self.assertEquals(len(event_objects), 25)
event_object = event_objects[14]
self.assertEquals(
event_object.utmcct,
u'/frettir/erlent/2013/10/30/maelt_med_kerfisbundnum_hydingum/')
self.assertEquals(
event_object.timestamp, timelib_test.CopyStringToTimestamp(
'2013-10-30 21:56:06'))
self.assertEquals(event_object.url, u'http://ads.aha.is/')
self.assertEquals(event_object.utmcsr, u'mbl.is')
expected_msg = (
u'http://ads.aha.is/ (__utmz) Sessions: 1 Domain Hash: 137167072 '
u'Sources: 1 Last source used to access: mbl.is Ad campaign '
u'information: (referral) Last type of visit: referral Path to '
u'the page of referring link: /frettir/erlent/2013/10/30/'
u'maelt_med_kerfisbundnum_hydingum/')
self._TestGetMessageStrings(
event_object, expected_msg, u'http://ads.aha.is/ (__utmz)')
def testParsingChromeCookieDatabase(self):
"""Test the process function on a Chrome cookie database."""
plugin = chrome_cookies.ChromeCookiePlugin()
test_file = self._GetTestFilePath(['cookies.db'])
event_queue_consumer = self._ParseDatabaseFileWithPlugin(plugin, test_file)
event_objects = self._GetAnalyticsCookies(event_queue_consumer)
# The cookie database contains 560 entries in total. Out of them
# there are 75 events created by the Google Analytics plugin.
self.assertEquals(len(event_objects), 75)
# Check few "random" events to verify.
# Check an UTMZ Google Analytics event.
event_object = event_objects[39]
self.assertEquals(event_object.utmctr, u'enders game')
self.assertEquals(event_object.domain_hash, u'68898382')
self.assertEquals(event_object.sessions, 1)
expected_msg = (
u'http://imdb.com/ (__utmz) Sessions: 1 Domain Hash: 68898382 '
u'Sources: 1 Last source used to access: google Ad campaign '
u'information: (organic) Last type of visit: organic Keywords '
u'used to find site: enders game')
self._TestGetMessageStrings(
event_object, expected_msg, u'http://imdb.com/ (__utmz)')
# Check the UTMA Google Analytics event.
event_object = event_objects[41]
self.assertEquals(event_object.timestamp_desc, u'Analytics Previous Time')
self.assertEquals(event_object.cookie_name, u'__utma')
self.assertEquals(event_object.visitor_id, u'1827102436')
self.assertEquals(event_object.sessions, 2)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-03-22 01:55:29')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'http://assets.tumblr.com/ (__utma) Sessions: 2 Domain Hash: '
u'151488169 Visitor ID: 151488169')
self._TestGetMessageStrings(
event_object, expected_msg, u'http://assets.tumblr.com/ (__utma)')
# Check the UTMB Google Analytics event.
event_object = event_objects[34]
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME)
self.assertEquals(event_object.cookie_name, u'__utmb')
self.assertEquals(event_object.domain_hash, u'154523900')
self.assertEquals(event_object.pages_viewed, 1)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-03-22 01:48:30')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'http://upressonline.com/ (__utmb) Pages Viewed: 1 Domain Hash: '
u'154523900')
self._TestGetMessageStrings(
event_object, expected_msg, u'http://upressonline.com/ (__utmb)')
if __name__ == '__main__':
unittest.main()
+115
View File
@@ -0,0 +1,115 @@
#!/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 an interface for browser cookie plugins."""
import abc
from plaso.lib import errors
from plaso.lib import registry
from plaso.parsers import plugins
# TODO: move this into the parsers and plugins manager.
def GetPlugins():
"""Returns a list of all cookie plugins."""
plugins_list = []
for plugin_cls in CookiePlugin.classes.itervalues():
parent_name = getattr(plugin_cls, 'parent_class_name', 'NOTHERE')
if parent_name != 'cookie':
continue
plugins_list.append(plugin_cls())
return plugins_list
class CookiePlugin(plugins.BasePlugin):
"""A browser cookie plugin for Plaso.
This is a generic cookie parsing interface that can handle parsing
cookies from all browsers.
"""
__metaclass__ = registry.MetaclassRegistry
__abstract = True
NAME = 'cookie'
# The name of the cookie value that this plugin is designed to parse.
# This value is used to evaluate whether the plugin is the correct one
# to parse the browser cookie.
COOKIE_NAME = u''
def __init__(self):
"""Initialize the browser cookie plugin."""
super(CookiePlugin, self).__init__()
self.cookie_data = ''
@abc.abstractmethod
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None,
cookie_data=None, url=None, **kwargs):
"""Extract and return EventObjects from the data structure.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
cookie_data: Optional cookie data, as a byte string.
url: Optional URL or path where the cookie got set.
"""
def Process(
self, parser_context, file_entry=None, parser_chain=None,
cookie_name=None, cookie_data=None, url=None, **kwargs):
"""Determine if this is the right plugin for this cookie.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
cookie_name: The name of the cookie value.
cookie_data: The cookie data, as a byte string.
url: The full URL or path where the cookie got set.
Raises:
errors.WrongPlugin: If the cookie name differs from the one
supplied in COOKIE_NAME.
ValueError: If cookie_name or cookie_data are not set.
"""
if cookie_name is None or cookie_data is None:
raise ValueError(u'Cookie name or data are not set.')
if cookie_name != self.COOKIE_NAME:
raise errors.WrongPlugin(
u'Not the correct cookie plugin for: {0:s} [{1:s}]'.format(
cookie_name, self.NAME))
# This will raise if unhandled keyword arguments are passed.
super(CookiePlugin, self).Process(parser_context, **kwargs)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
self.GetEntries(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
cookie_data=cookie_data, url=url)
+24
View File
@@ -0,0 +1,24 @@
#!/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.
"""Browser cookie plugin related functions and classes for testing."""
from plaso.parsers import test_lib
class CookiePluginTestCase(test_lib.ParserTestCase):
"""The unit test case for a browser cookie plugin."""
+345
View File
@@ -0,0 +1,345 @@
#!/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 CUPS IPP Control Files Parser.
CUPS IPP version 1.0:
* http://tools.ietf.org/html/rfc2565
* http://tools.ietf.org/html/rfc2566
* http://tools.ietf.org/html/rfc2567
* http://tools.ietf.org/html/rfc2568
* http://tools.ietf.org/html/rfc2569
* http://tools.ietf.org/html/rfc2639
CUPS IPP version 1.1:
* http://tools.ietf.org/html/rfc2910
* http://tools.ietf.org/html/rfc2911
* http://tools.ietf.org/html/rfc3196
* http://tools.ietf.org/html/rfc3510
CUPS IPP version 2.0:
* N/A
"""
import construct
import logging
import os
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
# TODO: RFC Pendings types: resolution, dateTime, rangeOfInteger.
# "dateTime" is not used by Mac OS, instead it uses integer types.
# TODO: Only tested against CUPS IPP Mac OS X.
class CupsIppEvent(event.EventObject):
"""Convenience class for an cups ipp event."""
DATA_TYPE = 'cups:ipp:event'
def __init__(
self, timestamp, timestamp_desc, data_dict):
"""Initializes the event object.
Args:
timestamp: Timestamp of the entry.
timestamp_desc: Description of the timestamp.
data_dict: Dictionary with all the pairs coming from IPP file.
user: String with the system user name.
owner: String with the real name of the user.
computer_name: String with the name of the computer.
printer_id: String with the identification name of the print.
uri: String with the URL of the CUPS service.
job_id: String with the identification id of the job.
job_name: String with the job name.
copies: Integer with the number of copies.
application: String with the application that prints the document.
doc_usingtype: String with the type of document.
data_dict: Dictionary with all the parsed data comming from the file.
"""
super(CupsIppEvent, self).__init__()
self.timestamp = timelib.Timestamp.FromPosixTime(timestamp)
self.timestamp_desc = timestamp_desc
# TODO: Find a better solution than to have join for each attribute.
self.user = self._ListToString(data_dict.get('user', None))
self.owner = self._ListToString(data_dict.get('owner', None))
self.computer_name = self._ListToString(data_dict.get(
'computer_name', None))
self.printer_id = self._ListToString(data_dict.get('printer_id', None))
self.uri = self._ListToString(data_dict.get('uri', None))
self.job_id = self._ListToString(data_dict.get('job_id', None))
self.job_name = self._ListToString(data_dict.get('job_name', None))
self.copies = data_dict.get('copies', 0)[0]
self.application = self._ListToString(data_dict.get('application', None))
self.doc_type = self._ListToString(data_dict.get('doc_type', None))
self.data_dict = data_dict
def _ListToString(self, values):
"""Returns a string from a list value using comma as a delimiter.
If any value inside the list contains comma, which is the delimiter,
the entire field is surrounded with double quotes.
Args:
values: A list or tuple containing the values.
Returns:
A string containing all the values joined using comma as a delimiter
or None.
"""
if values is None:
return
if type(values) not in (list, tuple):
return
for index, value in enumerate(values):
if ',' in value:
values[index] = u'"{0:s}"'.format(value)
try:
return u', '.join(values)
except UnicodeDecodeError as exception:
logging.error(
u'Unable to parse log line, with error: {0:s}'.format(exception))
class CupsIppParser(interface.BaseParser):
"""Parser for CUPS IPP files. """
NAME = 'cups_ipp'
DESCRIPTION = u'Parser for CUPS IPP files.'
# INFO:
# For each file, we have only one document with three different timestamps:
# Created, process and finished.
# Format:
# [HEADER: MAGIC + KNOWN_TYPE][GROUP A]...[GROUP Z][GROUP_END: 0x03]
# GROUP: [GROUP ID][PAIR A]...[PAIR Z] where [PAIR: NAME + VALUE]
# GROUP ID: [1byte ID]
# PAIR: [TagID][\x00][Name][Value])
# TagID: 1 byte integer with the type of "Value".
# Name: [Length][Text][\00]
# Name can be empty when the name has more than one value.
# Example: family name "lopez mata" with more than one surname.
# Type_Text + [0x06, family, 0x00] + [0x05, lopez, 0x00] +
# Type_Text + [0x00, 0x00] + [0x04, mata, 0x00]
# Value: can be integer, boolean, or text provided by TagID.
# If boolean, Value: [\x01][0x00(False)] or [\x01(True)]
# If integer, Value: [\x04][Integer]
# If text, Value: [Length text][Text][\00]
# Magic number that identify the CUPS IPP supported version.
IPP_MAJOR_VERSION = 2
IPP_MINOR_VERSION = 0
# Supported Operation ID.
IPP_OP_ID = 5
# CUPS IPP File header.
CUPS_IPP_HEADER = construct.Struct(
'cups_ipp_header_struct',
construct.UBInt8('major_version'),
construct.UBInt8('minor_version'),
construct.UBInt16('operation_id'),
construct.UBInt32('request_id'))
# Group ID that indicates the end of the IPP Control file.
GROUP_END = 3
# Identification Groups.
GROUP_LIST = [1, 2, 4, 5, 6, 7]
# Type ID.
TYPE_GENERAL_INTEGER = 32
TYPE_INTEGER = 33
TYPE_ENUMERATION = 35
TYPE_BOOL = 34
# Type of values that can be extracted.
INTEGER_8 = construct.UBInt8('integer')
INTEGER_32 = construct.UBInt32('integer')
TEXT = construct.PascalString(
'text',
length_field=construct.UBInt8('length'))
BOOLEAN = construct.Struct(
'boolean_value',
construct.Padding(1),
INTEGER_8)
INTEGER = construct.Struct(
'integer_value',
construct.Padding(1),
INTEGER_32)
# Name of the pair.
PAIR_NAME = construct.Struct(
'pair_name',
TEXT,
construct.Padding(1))
# Specific CUPS IPP to generic name.
NAME_PAIR_TRANSLATION = {
'printer-uri': u'uri',
'job-uuid': u'job_id',
'DestinationPrinterID': u'printer_id',
'job-originating-user-name': u'user',
'job-name': u'job_name',
'document-format': u'doc_type',
'job-originating-host-name': u'computer_name',
'com.apple.print.JobInfo.PMApplicationName': u'application',
'com.apple.print.JobInfo.PMJobOwner': u'owner'}
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract a entry from an CUPS IPP file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
parser_chain = self._BuildParserChain(parser_chain)
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
try:
header = self.CUPS_IPP_HEADER.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
file_object.close()
raise errors.UnableToParseFile(
u'Unable to parse CUPS IPP Header with error: {0:s}'.format(
exception))
if (header.major_version != self.IPP_MAJOR_VERSION or
header.minor_version != self.IPP_MINOR_VERSION):
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] Unsupported version number.'.format(self.NAME))
if header.operation_id != self.IPP_OP_ID:
# Warn if the operation ID differs from the standard one. We should be
# able to parse the file nonetheless.
logging.debug(
u'[{0:s}] Unsupported operation identifier in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
# Read the pairs extracting the name and the value.
data_dict = {}
name, value = self.ReadPair(parser_context, file_entry, file_object)
while name or value:
# Translate the known "name" CUPS IPP to a generic name value.
pretty_name = self.NAME_PAIR_TRANSLATION.get(name, name)
data_dict.setdefault(pretty_name, []).append(value)
name, value = self.ReadPair(parser_context, file_entry, file_object)
if u'time-at-creation' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-creation'][0],
eventdata.EventTimestamp.CREATION_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if u'time-at-processing' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-processing'][0],
eventdata.EventTimestamp.START_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if u'time-at-completed' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-completed'][0],
eventdata.EventTimestamp.END_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
file_object.close()
def ReadPair(self, parser_context, file_entry, file_object):
"""Reads an attribute name and value pair from a CUPS IPP event.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
file_object: a file-like object that points to a file.
Returns:
A list of name and value. If name and value cannot be read both are
set to None.
"""
# Pair = Type ID + Name + Value.
try:
# Can be:
# Group ID + IDtag = Group ID (1byte) + Tag ID (1byte) + '0x00'.
# IDtag = Tag ID (1byte) + '0x00'.
type_id = self.INTEGER_8.parse_stream(file_object)
if type_id == self.GROUP_END:
return None, None
elif type_id in self.GROUP_LIST:
# If it is a group ID we must read the next byte that contains
# the first TagID.
type_id = self.INTEGER_8.parse_stream(file_object)
# 0x00 separator character.
_ = self.INTEGER_8.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported identifier in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
# Name = Length name + name + 0x00
try:
name = self.PAIR_NAME.parse_stream(file_object).text
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported name in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
# Value: can be integer, boolean or text select by Type ID.
try:
if type_id in [
self.TYPE_GENERAL_INTEGER, self.TYPE_INTEGER, self.TYPE_ENUMERATION]:
value = self.INTEGER.parse_stream(file_object).integer
elif type_id == self.TYPE_BOOL:
value = bool(self.BOOLEAN.parse_stream(file_object).integer)
else:
value = self.TEXT.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported value in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
return name, value
manager.ParsersManager.RegisterParser(CupsIppParser)
+98
View File
@@ -0,0 +1,98 @@
#!/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.
"""Parser test for Mac Cups IPP Log files."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import cups_ipp as cups_ipp_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import cups_ipp
from plaso.parsers import test_lib
class CupsIppParserTest(test_lib.ParserTestCase):
"""The unit test for Mac Cups IPP parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = cups_ipp.CupsIppParser()
def testParse(self):
"""Tests the Parse function."""
# TODO: only tested against Mac OS X Cups IPP (Version 2.0)
test_file = self._GetTestFilePath(['mac_cups_ipp'])
events = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(events)
self.assertEqual(len(event_objects), 3)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-03 18:07:21')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(
event_object.timestamp_desc,
eventdata.EventTimestamp.CREATION_TIME)
self.assertEqual(event_object.application, u'LibreOffice')
self.assertEqual(event_object.job_name, u'Assignament 1')
self.assertEqual(event_object.computer_name, u'localhost')
self.assertEqual(event_object.copies, 1)
self.assertEqual(event_object.doc_type, u'application/pdf')
expected_job = u'urn:uuid:d51116d9-143c-3863-62aa-6ef0202de49a'
self.assertEqual(event_object.job_id, expected_job)
self.assertEqual(event_object.owner, u'Joaquin Moreno Garijo')
self.assertEqual(event_object.user, u'moxilo')
self.assertEqual(event_object.printer_id, u'RHULBW')
expected_uri = u'ipp://localhost:631/printers/RHULBW'
self.assertEqual(event_object.uri, expected_uri)
expected_msg = (
u'User: moxilo '
u'Owner: Joaquin Moreno Garijo '
u'Job Name: Assignament 1 '
u'Application: LibreOffice '
u'Printer: RHULBW')
expected_msg_short = (
u'Job Name: Assignament 1')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[1]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-03 18:07:21')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(
event_object.timestamp_desc,
eventdata.EventTimestamp.START_TIME)
event_object = event_objects[2]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-03 18:07:32')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(
event_object.timestamp_desc,
eventdata.EventTimestamp.END_TIME)
if __name__ == '__main__':
unittest.main()
+212
View File
@@ -0,0 +1,212 @@
#!/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.
"""Parser for .customDestinations-ms files."""
import logging
import os
import construct
from dfvfs.lib import definitions
from dfvfs.path import factory as path_spec_factory
from dfvfs.resolver import resolver
from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager
from plaso.parsers import winlnk
class CustomDestinationsParser(interface.BaseParser):
"""Parses .customDestinations-ms files."""
NAME = 'custom_destinations'
DESCRIPTION = u'Parser for *.customDestinations-ms files.'
# We cannot use the parser registry here since winlnk could be disabled.
# TODO: see if there is a more elegant solution for this.
_WINLNK_PARSER = winlnk.WinLnkParser()
_LNK_GUID = '\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46'
_FILE_HEADER = construct.Struct(
'file_header',
construct.ULInt32('unknown1'),
construct.ULInt32('unknown2'),
construct.ULInt32('unknown3'),
construct.ULInt32('header_values_type'))
_HEADER_VALUE_TYPE_0 = construct.Struct(
'header_value_type_0',
construct.ULInt32('number_of_characters'),
construct.String('string', lambda ctx: ctx.number_of_characters * 2),
construct.ULInt32('unknown1'))
_HEADER_VALUE_TYPE_1_OR_2 = construct.Struct(
'header_value_type_1_or_2',
construct.ULInt32('unknown1'))
_ENTRY_HEADER = construct.Struct(
'entry_header',
construct.String('guid', 16))
_FILE_FOOTER = construct.Struct(
'file_footer',
construct.ULInt32('signature'))
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from an *.customDestinations-ms file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
self.ParseFileObject(
parser_context, file_object, file_entry=file_entry,
parser_chain=parser_chain)
file_object.close()
def ParseFileObject(
self, parser_context, file_object, file_entry=None, parser_chain=None):
"""Extract data from an *.customDestinations-ms file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_object: A file-like object.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
UnableToParseFile: when the file cannot be parsed.
"""
parser_chain = self._BuildParserChain(parser_chain)
try:
file_header = self._FILE_HEADER.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
raise errors.UnableToParseFile((
u'Unable to parse Custom Destination file header with error: '
u'{0:s}').format(exception))
if file_header.unknown1 != 2:
raise errors.UnableToParseFile((
u'Unsupported Custom Destination file - invalid unknown1: '
u'{0:d}.').format(file_header.unknown1))
if file_header.header_values_type > 2:
raise errors.UnableToParseFile((
u'Unsupported Custom Destination file - invalid header value type: '
u'{0:d}.').format(file_header.header_values_type))
if file_header.header_values_type == 0:
data_structure = self._HEADER_VALUE_TYPE_0
else:
data_structure = self._HEADER_VALUE_TYPE_1_OR_2
try:
_ = data_structure.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
raise errors.UnableToParseFile((
u'Unable to parse Custom Destination file header value with error: '
u'{0:s}').format(exception))
file_size = file_object.get_size()
file_offset = file_object.get_offset()
remaining_file_size = file_size - file_offset
# The Custom Destination file does not have a unique signature in
# the file header that is why we use the first LNK class identifier (GUID)
# as a signature.
first_guid_checked = False
while remaining_file_size > 4:
try:
entry_header = self._ENTRY_HEADER.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
if not first_guid_checked:
raise errors.UnableToParseFile((
u'Unable to parse Custom Destination file entry header with '
u'error: {0:s}').format(exception))
else:
logging.warning((
u'Unable to parse Custom Destination file entry header with '
u'error: {0:s}').format(exception))
break
if entry_header.guid != self._LNK_GUID:
if not first_guid_checked:
raise errors.UnableToParseFile(
u'Unsupported Custom Destination file - invalid entry header.')
else:
logging.warning(
u'Unsupported Custom Destination file - invalid entry header.')
break
first_guid_checked = True
file_offset += 16
remaining_file_size -= 16
path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_DATA_RANGE, range_offset=file_offset,
range_size=remaining_file_size, parent=file_entry.path_spec)
try:
lnk_file_object = resolver.Resolver.OpenFileObject(path_spec)
except RuntimeError as exception:
logging.error((
u'[{0:s}] Unable to open LNK file from {1:s} with error: '
u'{2:s}').format(
parser_chain,
file_entry.path_spec.comparable.replace(u'\n', u';'),
exception))
return
display_name = u'{0:s} # 0x{1:08x}'.format(
parser_context.GetDisplayName(file_entry), file_offset)
self._WINLNK_PARSER.ParseFileObject(
parser_context, lnk_file_object, file_entry=file_entry,
parser_chain=parser_chain, display_name=display_name)
# We cannot trust the file size in the LNK data so we get the last offset
# that was read instead.
lnk_file_size = lnk_file_object.get_offset()
lnk_file_object.close()
file_offset += lnk_file_size
remaining_file_size -= lnk_file_size
file_object.seek(file_offset, os.SEEK_SET)
try:
file_footer = self._FILE_FOOTER.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
logging.warning((
u'Unable to parse Custom Destination file footer with error: '
u'{0:s}').format(exception))
if file_footer.signature != 0xbabffbab:
logging.warning(
u'Unsupported Custom Destination file - invalid footer signature.')
manager.ParsersManager.RegisterParser(CustomDestinationsParser)
+116
View File
@@ -0,0 +1,116 @@
#!/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 .customDestinations-ms file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import winlnk as winlnk_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import test_lib
from plaso.parsers import custom_destinations
class CustomDestinationsParserTest(test_lib.ParserTestCase):
"""Tests for the .customDestinations-ms file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = custom_destinations.CustomDestinationsParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath([
u'5afe4de1b92fc382.customDestinations-ms'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 108)
# A shortcut event object.
# The last accessed timestamp.
event_object = event_objects[105]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2009-07-13 23:55:56.248103')
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME)
self.assertEquals(event_object.timestamp, expected_timestamp)
# The creation timestamp.
event_object = event_objects[106]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2009-07-13 23:55:56.248103')
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME)
self.assertEquals(event_object.timestamp, expected_timestamp)
# The last modification timestamp.
event_object = event_objects[107]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2009-07-14 01:39:11.388000')
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME)
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'[@%systemroot%\\system32\\oobefldr.dll,-1262] '
u'File size: 11776 '
u'File attribute flags: 0x00000020 '
u'Drive type: 3 '
u'Drive serial number: 0x24ba718b '
u'Local path: C:\\Windows\\System32\\GettingStarted.exe '
u'cmd arguments: {DE3895CB-077B-4C38-B6E3-F3DE1E0D84FC} '
u'%systemroot%\\system32\\control.exe /name Microsoft.Display '
u'env location: %SystemRoot%\\system32\\GettingStarted.exe '
u'Icon location: %systemroot%\\system32\\display.dll '
u'Link target: [My Computer, C:\\, Windows, System32, '
u'GettingStarted.exe]')
expected_msg_short = (
u'[@%systemroot%\\system32\\oobefldr.dll,-1262] '
u'C:\\Windows\\System32\\GettingStarte...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
# A shell item event object.
event_object = event_objects[16]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2010-11-10 07:41:04')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'Name: System32 '
u'Long name: System32 '
u'NTFS file reference: 2331-1 '
u'Origin: 5afe4de1b92fc382.customDestinations-ms')
expected_msg_short = (
u'Name: System32 '
u'NTFS file reference: 2331-1 '
u'Origin: 5afe4de1b92fc382.customDes...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+98
View File
@@ -0,0 +1,98 @@
#!/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.
"""Parser for Extensible Storage Engine (ESE) database files (EDB)."""
import logging
import pyesedb
from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager
from plaso.parsers import plugins
if pyesedb.get_version() < '20140301':
raise ImportWarning(u'EseDbParser requires at least pyesedb 20140301.')
class EseDbCache(plugins.BasePluginCache):
"""A cache storing query results for ESEDB plugins."""
class EseDbParser(interface.BasePluginsParser):
"""Parses Extensible Storage Engine (ESE) database files (EDB)."""
NAME = 'esedb'
DESCRIPTION = u'Parser for Extensible Storage Engine (ESE) database files.'
_plugin_classes = {}
def __init__(self):
"""Initializes a parser object."""
super(EseDbParser, self).__init__()
self._plugins = EseDbParser.GetPluginObjects()
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extracts data from an ESE database File.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
UnableToParseFile: when the file cannot be parsed.
"""
file_object = file_entry.GetFileObject()
esedb_file = pyesedb.file()
try:
esedb_file.open_file_object(file_object)
except IOError as exception:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, exception))
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
# Compare the list of available plugins.
cache = EseDbCache()
for plugin_object in self._plugins:
try:
plugin_object.Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
database=esedb_file, cache=cache)
except errors.WrongPlugin:
logging.debug((
u'[{0:s}] plugin: {1:s} cannot parse the ESE database: '
u'{2:s}').format(
self.NAME, plugin_object.NAME, file_entry.name))
# TODO: explicitly clean up cache.
esedb_file.close()
file_object.close()
manager.ParsersManager.RegisterParser(EseDbParser)
+20
View File
@@ -0,0 +1,20 @@
#!/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.
"""This file contains import statements for the ESE database plugins."""
from plaso.parsers.esedb_plugins import msie_webcache
+303
View File
@@ -0,0 +1,303 @@
#!/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.
"""This file contains the interface for ESE database plugins."""
import construct
import logging
import pyesedb
from plaso.lib import errors
from plaso.parsers import plugins
class EseDbPlugin(plugins.BasePlugin):
"""The ESE database plugin interface."""
NAME = 'esedb'
BINARY_DATA_COLUMN_TYPES = frozenset([
pyesedb.column_types.BINARY_DATA,
pyesedb.column_types.LARGE_BINARY_DATA])
FLOATING_POINT_COLUMN_TYPES = frozenset([
pyesedb.column_types.FLOAT_32BIT,
pyesedb.column_types.DOUBLE_64BIT])
INTEGER_COLUMN_TYPES = frozenset([
pyesedb.column_types.CURRENCY,
pyesedb.column_types.DATE_TIME,
pyesedb.column_types.INTEGER_8BIT_UNSIGNED,
pyesedb.column_types.INTEGER_16BIT_SIGNED,
pyesedb.column_types.INTEGER_16BIT_UNSIGNED,
pyesedb.column_types.INTEGER_32BIT_SIGNED,
pyesedb.column_types.INTEGER_32BIT_UNSIGNED,
pyesedb.column_types.INTEGER_64BIT_SIGNED])
STRING_COLUMN_TYPES = frozenset([
pyesedb.column_types.TEXT,
pyesedb.column_types.LARGE_TEXT])
_UINT64_BIG_ENDIAN = construct.UBInt64('value')
_UINT64_LITTLE_ENDIAN = construct.ULInt64('value')
# Dictionary containing a callback method per table name.
# E.g. 'SystemIndex_0A': 'ParseSystemIndex_0A'
REQUIRED_TABLES = {}
OPTIONAL_TABLES = {}
def __init__(self):
"""Initializes the ESE database plugin."""
super(EseDbPlugin, self).__init__()
self._required_tables = frozenset(self.REQUIRED_TABLES.keys())
self._tables = {}
self._tables.update(self.REQUIRED_TABLES)
self._tables.update(self.OPTIONAL_TABLES)
def _ConvertValueBinaryDataToStringAscii(self, value):
"""Converts a binary data value into a string.
Args:
value: The binary data value containing an ASCII string or None.
Returns:
A string or None if value is None.
"""
if value:
return value.decode('ascii')
def _ConvertValueBinaryDataToStringBase16(self, value):
"""Converts a binary data value into a base-16 (hexadecimal) string.
Args:
value: The binary data value or None.
Returns:
A string or None if value is None.
"""
if value:
return value.encode('hex')
def _ConvertValueBinaryDataToUBInt64(self, value):
"""Converts a binary data value into an integer.
Args:
value: The binary data value containing an unsigned 64-bit big-endian
integer.
Returns:
An integer or None if value is None.
"""
if value:
return self._UINT64_BIG_ENDIAN.parse(value)
def _ConvertValueBinaryDataToULInt64(self, value):
"""Converts a binary data value into an integer.
Args:
value: The binary data value containing an unsigned 64-bit little-endian
integer.
Returns:
An integer or None if value is None.
"""
if value:
return self._UINT64_LITTLE_ENDIAN.parse(value)
def _GetRecordValue(self, record, value_entry):
"""Retrieves a specific value from the record.
Args:
record: The ESE record object (instance of pyesedb.record).
value_entry: The value entry.
Returns:
An object containing the value.
"""
column_type = record.get_column_type(value_entry)
value_data_flags = record.get_value_data_flags(value_entry)
if value_data_flags & pyesedb.value_flags.MULTI_VALUE:
# TODO: implement
pass
elif column_type == pyesedb.column_types.NULL:
return
elif column_type == pyesedb.column_types.BOOLEAN:
# TODO: implement
pass
elif column_type in self.INTEGER_COLUMN_TYPES:
return record.get_value_data_as_integer(value_entry)
elif column_type in self.FLOATING_POINT_COLUMN_TYPES:
return record.get_value_data_as_floating_point(value_entry)
elif column_type in self.STRING_COLUMN_TYPES:
return record.get_value_data_as_string(value_entry)
elif column_type == pyesedb.column_types.GUID:
# TODO: implement
pass
return record.get_value_data(value_entry)
def _GetRecordValues(self, table_name, record, value_mappings=None):
"""Retrieves the values from the record.
Args:
table_name: The name of the table.
record: The ESE record object (instance of pyesedb.record).
value_mappings: Optional dict of value mappings, which map the column
name to a callback method. The default is None.
Returns:
An dict containing the values.
"""
record_values = {}
for value_entry in range(0, record.number_of_values):
column_name = record.get_column_name(value_entry)
if column_name in record_values:
logging.warning(
u'[{0:s}] duplicate column: {1:s} in table: {2:s}'.format(
self.NAME, column_name, table_name))
continue
value_callback = None
if value_mappings and column_name in value_mappings:
value_callback_method = value_mappings.get(column_name)
if value_callback_method:
value_callback = getattr(self, value_callback_method, None)
if value_callback is None:
logging.warning((
u'[{0:s}] missing value callback method: {1:s} for column: '
u'{2:s} in table: {3:s}').format(
self.NAME, value_callback_method, column_name, table_name))
value = self._GetRecordValue(record, value_entry)
if value_callback:
value = value_callback(value)
record_values[column_name] = value
return record_values
def _GetTableNames(self, database):
"""Retrieves the table names in a database.
Args:
database: The ESE database object (instance of pyesedb.file).
Returns:
A list of the table names.
"""
table_names = []
for esedb_table in database.tables:
table_names.append(esedb_table.name)
return table_names
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, database=None,
cache=None, **kwargs):
"""Extracts event objects from the database.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
database: Optional ESE database object (instance of pyesedb.file).
The default is None.
cache: Optional cache object (instance of EseDbCache). The default is
None.
Raises:
ValueError: If the database attribute is not valid.
"""
if database is None:
raise ValueError(u'Invalid database.')
for table_name, callback_method in self._tables.iteritems():
if not callback_method:
# Table names without a callback method are allowed to improve
# the detection of a database based on its table names.
continue
callback = getattr(self, callback_method, None)
if callback is None:
logging.warning(
u'[{0:s}] missing callback method: {1:s} for table: {2:s}'.format(
self.NAME, callback_method, table_name))
continue
esedb_table = database.get_table_by_name(table_name)
if not esedb_table:
logging.warning(u'[{0:s}] missing table: {1:s}'.format(
self.NAME, table_name))
continue
# The database is passed in case the database contains table names
# that are assigned dynamically and cannot be defined by
# the table name-callback mechanism.
callback(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
database=database, table=esedb_table, cache=cache, **kwargs)
def Process(
self, parser_context, file_entry=None, parser_chain=None, database=None,
cache=None, **kwargs):
"""Determines if this is the appropriate plugin for the database.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
database: Optional ESE database object (instance of pyesedb.file).
The default is None.
cache: Optional cache object (instance of EseDbCache). The default is
None.
Raises:
errors.WrongPlugin: If the database does not contain all the tables
defined in the required_tables set.
ValueError: If the database attribute is not valid.
"""
if database is None:
raise ValueError(u'Invalid database.')
table_names = frozenset(self._GetTableNames(database))
if self._required_tables.difference(table_names):
raise errors.WrongPlugin(
u'[{0:s}] required tables not found.'.format(self.NAME))
# This will raise if unhandled keyword arguments are passed.
super(EseDbPlugin, self).Process(parser_context, **kwargs)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
self.GetEntries(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
database=database, cache=cache, **kwargs)
@@ -0,0 +1,366 @@
#!/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.
"""Parser for the Microsoft Internet Explorer WebCache ESE database.
The WebCache database (WebCacheV01.dat or WebCacheV24.dat) are used by MSIE
as of version 10.
"""
import logging
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import esedb
from plaso.parsers.esedb_plugins import interface
class MsieWebCacheContainersEventObject(time_events.FiletimeEvent):
"""Convenience class for a MSIE WebCache Containers table event."""
DATA_TYPE = 'msie:webcache:containers'
def __init__(self, timestamp, usage, record_values):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
record_values: A dict object containing the record values.
"""
super(MsieWebCacheContainersEventObject, self).__init__(timestamp, usage)
self.container_identifier = record_values.get('ContainerId', 0)
self.set_identifier = record_values.get('SetId', 0)
self.name = record_values.get('Name', u'')
self.directory = record_values.get('Directory', u'')
class MsieWebCacheContainerEventObject(time_events.FiletimeEvent):
"""Convenience class for a MSIE WebCache Container table event."""
DATA_TYPE = 'msie:webcache:container'
def __init__(self, timestamp, usage, record_values):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
record_values: A dict object containing the record values.
"""
super(MsieWebCacheContainerEventObject, self).__init__(timestamp, usage)
self.entry_identifier = record_values.get(u'EntryId', 0)
self.container_identifier = record_values.get(u'ContainerId', 0)
self.cache_identifier = record_values.get(u'CacheId', 0)
url = record_values.get(u'Url', u'')
# Ignore URL that start with a binary value.
if ord(url[0]) >= 0x20:
self.url = url
self.redirect_url = record_values.get(u'RedirectUrl', u'')
self.access_count = record_values.get(u'AccessCount', 0)
self.sync_count = record_values.get(u'SyncCount', 0)
self.cached_filename = record_values.get('Filename', u'')
self.file_extension = record_values.get(u'FileExtension', u'')
self.cached_file_size = record_values.get(u'FileSize', 0)
# Ignore non-Unicode request headers values.
request_headers = record_values.get(u'RequestHeaders', u'')
if type(request_headers) == unicode and request_headers:
self.request_headers = request_headers
# Ignore non-Unicode response headers values.
response_headers = record_values.get(u'ResponseHeaders', u'')
if type(response_headers) == unicode and response_headers:
self.response_headers = response_headers
class MsieWebCacheLeakFilesEventObject(time_events.FiletimeEvent):
"""Convenience class for a MSIE WebCache LeakFiles table event."""
DATA_TYPE = 'msie:webcache:leak_file'
def __init__(self, timestamp, usage, record_values):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
record_values: A dict object containing the record values.
"""
super(MsieWebCacheLeakFilesEventObject, self).__init__(timestamp, usage)
self.leak_identifier = record_values.get('LeakId', 0)
self.cached_filename = record_values.get('Filename', u'')
class MsieWebCachePartitionsEventObject(time_events.FiletimeEvent):
"""Convenience class for a MSIE WebCache Partitions table event."""
DATA_TYPE = 'msie:webcache:partitions'
def __init__(self, timestamp, usage, record_values):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
record_values: A dict object containing the record values.
"""
super(MsieWebCachePartitionsEventObject, self).__init__(timestamp, usage)
self.partition_identifier = record_values.get('PartitionId', 0)
self.partition_type = record_values.get('PartitionType', 0)
self.directory = record_values.get('Directory', u'')
self.table_identifier = record_values.get('TableId', 0)
class MsieWebCacheEseDbPlugin(interface.EseDbPlugin):
"""Parses a MSIE WebCache ESE database file."""
NAME = 'msie_webcache'
DESCRIPTION = u'Parser for MSIE WebCache ESE database files.'
# TODO: add support for AppCache_#, AppCacheEntry_#, DependencyEntry_#
REQUIRED_TABLES = {
'Containers': 'ParseContainersTable',
'LeakFiles': 'ParseLeakFilesTable',
'Partitions': 'ParsePartitionsTable'}
_CONTAINER_TABLE_VALUE_MAPPINGS = {
'RequestHeaders': '_ConvertValueBinaryDataToStringAscii',
'ResponseHeaders': '_ConvertValueBinaryDataToStringAscii'}
def _ParseContainerTable(
self, parser_context, file_entry=None, parser_chain=None, table=None,
container_name=u'Unknown'):
"""Parses a Container_# table.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point.
table: Optional table object (instance of pyesedb.table).
container_name: Optional string that contains the container name.
The container name indicates the table type.
The default is a string containing 'Unknown'.
"""
if table is None:
logging.warning(u'[{0:s}] invalid Container_# table'.format(self.NAME))
return
for esedb_record in table.records:
# TODO: add support for:
# wpnidm, iecompat, iecompatua, DNTException, DOMStore
if container_name == u'Content':
value_mappings = self._CONTAINER_TABLE_VALUE_MAPPINGS
else:
value_mappings = None
try:
record_values = self._GetRecordValues(
table.name, esedb_record, value_mappings=value_mappings)
except UnicodeDecodeError as exception:
logging.error((
u'[{0:s}] Unable to return record values for {1:s} with error: '
u'{2:s}').format(
parser_chain,
file_entry.path_spec.comparable.replace(u'\n', u';'),
exception))
continue
if (container_name in [
u'Content', u'Cookies', u'History', u'iedownload'] or
container_name.startswith(u'MSHist')):
timestamp = record_values.get(u'SyncTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, u'Synchronization time', record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'CreationTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, eventdata.EventTimestamp.CREATION_TIME, record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'ExpiryTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, eventdata.EventTimestamp.EXPIRATION_TIME,
record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'ModifiedTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, eventdata.EventTimestamp.MODIFICATION_TIME,
record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'AccessedTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, eventdata.EventTimestamp.ACCESS_TIME, record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'PostCheckTime', 0)
if timestamp:
event_object = MsieWebCacheContainerEventObject(
timestamp, u'Post check time', record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def ParseContainersTable(
self, parser_context, file_entry=None, parser_chain=None, database=None,
table=None, **unused_kwargs):
"""Parses the Containers table.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
database: Optional database object (instance of pyesedb.file).
The default is None.
table: Optional table object (instance of pyesedb.table).
The default is None.
"""
if database is None:
logging.warning(u'[{0:s}] invalid database'.format(self.NAME))
return
if table is None:
logging.warning(u'[{0:s}] invalid Containers table'.format(self.NAME))
return
for esedb_record in table.records:
record_values = self._GetRecordValues(table.name, esedb_record)
timestamp = record_values.get(u'LastScavengeTime', 0)
if timestamp:
event_object = MsieWebCacheContainersEventObject(
timestamp, u'Last Scavenge Time', record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
timestamp = record_values.get(u'LastAccessTime', 0)
if timestamp:
event_object = MsieWebCacheContainersEventObject(
timestamp, eventdata.EventTimestamp.ACCESS_TIME, record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
container_identifier = record_values.get(u'ContainerId', None)
container_name = record_values.get(u'Name', None)
if not container_identifier or not container_name:
continue
table_name = u'Container_{0:d}'.format(container_identifier)
esedb_table = database.get_table_by_name(table_name)
if not esedb_table:
logging.warning(
u'[{0:s}] missing table: {1:s}'.format(self.NAME, table_name))
continue
self._ParseContainerTable(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
table=esedb_table, container_name=container_name)
def ParseLeakFilesTable(
self, parser_context, file_entry=None, parser_chain=None, database=None,
table=None, **unused_kwargs):
"""Parses the LeakFiles table.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
database: Optional database object (instance of pyesedb.file).
The default is None.
table: Optional table object (instance of pyesedb.table).
The default is None.
"""
if database is None:
logging.warning(u'[{0:s}] invalid database'.format(self.NAME))
return
if table is None:
logging.warning(u'[{0:s}] invalid LeakFiles table'.format(self.NAME))
return
for esedb_record in table.records:
record_values = self._GetRecordValues(table.name, esedb_record)
timestamp = record_values.get(u'CreationTime', 0)
if timestamp:
event_object = MsieWebCacheLeakFilesEventObject(
timestamp, eventdata.EventTimestamp.CREATION_TIME, record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def ParsePartitionsTable(
self, parser_context, file_entry=None, parser_chain=None, database=None,
table=None, **unused_kwargs):
"""Parses the Partitions table.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
database: Optional database object (instance of pyesedb.file).
The default is None.
table: Optional table object (instance of pyesedb.table).
The default is None.
"""
if database is None:
logging.warning(u'[{0:s}] invalid database'.format(self.NAME))
return
if table is None:
logging.warning(u'[{0:s}] invalid Partitions table'.format(self.NAME))
return
for esedb_record in table.records:
record_values = self._GetRecordValues(table.name, esedb_record)
timestamp = record_values.get(u'LastScavengeTime', 0)
if timestamp:
event_object = MsieWebCachePartitionsEventObject(
timestamp, u'Last Scavenge Time', record_values)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
esedb.EseDbParser.RegisterPlugin(MsieWebCacheEseDbPlugin)
@@ -0,0 +1,71 @@
#!/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 Microsoft Internet Explorer WebCache database."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import msie_webcache as msie_webcache_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers.esedb_plugins import msie_webcache
from plaso.parsers.esedb_plugins import test_lib
class MsieWebCacheEseDbPluginTest(test_lib.EseDbPluginTestCase):
"""Tests for the MSIE WebCache ESE database plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = msie_webcache.MsieWebCacheEseDbPlugin()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['WebCacheV01.dat'])
event_queue_consumer = self._ParseEseDbFileWithPlugin(
test_file, self._plugin)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 1354)
event_object = event_objects[0]
self.assertEquals(event_object.container_identifier, 1)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-05-12 07:30:25.486198')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME)
expected_msg = (
u'Container identifier: 1 '
u'Set identifier: 0 '
u'Name: Content '
u'Directory: C:\\Users\\test\\AppData\\Local\\Microsoft\\Windows\\'
u'INetCache\\IE\\ '
u'Table: Container_1')
expected_msg_short = (
u'Directory: C:\\Users\\test\\AppData\\Local\\Microsoft\\Windows\\'
u'INetCache\\IE\\')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+76
View File
@@ -0,0 +1,76 @@
#!/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.
"""ESEDB plugin related functions and classes for testing."""
import pyesedb
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.engine import single_process
from plaso.parsers import test_lib
class EseDbPluginTestCase(test_lib.ParserTestCase):
"""The unit test case for ESE database based plugins."""
def _OpenEseDbFile(self, path):
"""Opens an ESE database file and returns back a pyesedb.file object.
Args:
path: The path to the ESE database test file.
"""
path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=path)
file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec)
file_object = file_entry.GetFileObject()
esedb_file = pyesedb.file()
esedb_file.open_file_object(file_object)
return esedb_file
def _ParseEseDbFileWithPlugin(
self, path, plugin_object, knowledge_base_values=None):
"""Parses a file as an ESE database file and returns an event generator.
Args:
path: The path to the ESE database test file.
plugin_object: The plugin object that is used to extract an event
generator.
knowledge_base_values: optional dict containing the knowledge base
values. The default is None.
Returns:
An event object queue consumer object (instance of
TestEventObjectQueueConsumer).
"""
event_queue = single_process.SingleProcessQueue()
event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue)
parse_error_queue = single_process.SingleProcessQueue()
parser_context = self._GetParserContext(
event_queue, parse_error_queue,
knowledge_base_values=knowledge_base_values)
esedb_file = self._OpenEseDbFile(path)
plugin_object.Process(parser_context, database=esedb_file)
return event_queue_consumer
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2012 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""File system stat object parser."""
from dfvfs.lib import definitions as dfvfs_definitions
from plaso.events import time_events
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
class FileStatEvent(time_events.TimestampEvent):
"""File system stat event."""
DATA_TYPE = 'fs:stat'
def __init__(self, timestamp, usage, allocated, size, fs_type):
"""Initializes the event.
Args:
timestamp: The timestamp value.
usage: The usage string describing the timestamp.
allocated: Boolean value to indicate the file entry is allocated.
size: The file size in bytes.
fs_type: The filesystem this timestamp is extracted from.
"""
super(FileStatEvent, self).__init__(timestamp, usage)
self.offset = 0
self.size = size
self.allocated = allocated
self.fs_type = fs_type
class FileStatParser(interface.BaseParser):
"""Class that defines a file system stat object parser."""
NAME = 'filestat'
DESCRIPTION = u'Parser for file system stat information.'
_TIME_ATTRIBUTES = frozenset([
'atime', 'bkup_time', 'ctime', 'crtime', 'dtime', 'mtime'])
def _GetFileSystemTypeFromFileEntry(self, file_entry):
"""Return a filesystem type string from a file entry object.
Args:
file_entry: A file entry object (instance of vfs.file_entry.FileEntry).
Returns:
A string indicating the file system type.
"""
file_system = file_entry.GetFileSystem()
type_indicator = file_system.type_indicator
if type_indicator != dfvfs_definitions.TYPE_INDICATOR_TSK:
return type_indicator
# TODO: Implement fs_type in dfVFS and remove this implementation
# once that is in place.
fs_info = file_system.GetFsInfo()
if fs_info.info:
type_string = unicode(fs_info.info.ftype)
if type_string.startswith('TSK_FS_TYPE'):
return type_string[12:]
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extracts event objects from a file system stat entry.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
stat_object = file_entry.GetStat()
if not stat_object:
return
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
file_system_type = self._GetFileSystemTypeFromFileEntry(file_entry)
is_allocated = getattr(stat_object, 'allocated', True)
file_size = getattr(stat_object, 'size', None),
for time_attribute in self._TIME_ATTRIBUTES:
timestamp = getattr(stat_object, time_attribute, None)
if timestamp is None:
continue
nano_time_attribute = u'{0:s}_nano'.format(time_attribute)
nano_time_attribute = getattr(stat_object, nano_time_attribute, None)
timestamp = timelib.Timestamp.FromPosixTime(timestamp)
if nano_time_attribute is not None:
# Note that the _nano values are in intervals of 100th nano seconds.
timestamp += nano_time_attribute / 10
# TODO: this also ignores any timestamp that equals 0.
# Is this the desired behavior?
if not timestamp:
continue
event_object = FileStatEvent(
timestamp, time_attribute, is_allocated, file_size, file_system_type)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(FileStatParser)
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2012 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for filestat parser."""
import unittest
from dfvfs.lib import definitions
from dfvfs.path import factory as path_spec_factory
# pylint: disable=unused-import
from plaso.formatters import filestat as filestat_formatter
from plaso.parsers import filestat
from plaso.parsers import test_lib
class FileStatTest(test_lib.ParserTestCase):
"""Tests for filestat parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = filestat.FileStatParser()
def testTSKFile(self):
"""Read a file within an image file and make few tests."""
test_file = self._GetTestFilePath([u'ímynd.dd'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
tsk_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_TSK, inode=15, location=u'/passwords.txt',
parent=os_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, tsk_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The TSK file entry has 3 event objects.
self.assertEquals(len(event_objects), 3)
def testZipFile(self):
"""Test a ZIP file."""
test_file = self._GetTestFilePath([u'syslog.zip'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
zip_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_ZIP, location=u'/syslog',
parent=os_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, zip_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The ZIP file has 1 event object.
self.assertEquals(len(event_objects), 1)
def testGzipFile(self):
"""Test a GZIP file."""
test_file = self._GetTestFilePath([u'syslog.gz'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
gzip_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, gzip_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The gzip file has 1 event object.
self.assertEquals(len(event_objects), 1)
def testTarFile(self):
"""Test a TAR file."""
test_file = self._GetTestFilePath([u'syslog.tar'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
tar_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_TAR, location=u'/syslog',
parent=os_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, tar_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The tar file has 1 event object.
self.assertEquals(len(event_objects), 1)
def testNestedFile(self):
"""Test a nested file."""
test_file = self._GetTestFilePath([u'syslog.tgz'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
gzip_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec)
tar_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_TAR, location=u'/syslog',
parent=gzip_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, tar_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The tar file has 1 event object.
self.assertEquals(len(event_objects), 1)
test_file = self._GetTestFilePath([u'syslog.tgz'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
gzip_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, gzip_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The gzip file has 1 event object.
self.assertEquals(len(event_objects), 1)
def testNestedTSK(self):
"""Test a nested TSK file."""
test_file = self._GetTestFilePath([u'syslog_image.dd'])
os_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=test_file)
tsk_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_TSK, inode=11, location=u'/logs/hidden.zip',
parent=os_path_spec)
zip_path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_ZIP, location=u'/syslog',
parent=tsk_path_spec)
event_queue_consumer = self._ParseFileByPathSpec(
self._parser, zip_path_spec)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The ZIP file has 1 event objects.
self.assertEquals(len(event_objects), 1)
if __name__ == '__main__':
unittest.main()
+246
View File
@@ -0,0 +1,246 @@
#!/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.
"""Implements a parser for Firefox cache files."""
import collections
import logging
import os
import construct
import pyparsing
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Petter Bjelland (petter.bjelland@gmail.com)'
class FirefoxCacheEvent(time_events.PosixTimeEvent):
"""Convenience class for a Firefox cache record event."""
DATA_TYPE = 'firefox:cache:record'
def __init__(self, metadata, request_method, url, response_code):
super(FirefoxCacheEvent, self).__init__(
metadata.last_fetched, eventdata.EventTimestamp.ADDED_TIME)
self.last_modified = metadata.last_modified
self.major = metadata.major
self.minor = metadata.minor
self.location = metadata.location
self.last_fetched = metadata.last_fetched
self.expire_time = metadata.expire_time
self.fetch_count = metadata.fetch_count
self.request_size = metadata.request_size
self.info_size = metadata.info_size
self.data_size = metadata.data_size
self.request_method = request_method
self.url = url
self.response_code = response_code
class FirefoxCacheParser(interface.BaseParser):
"""Extract cached records from Firefox."""
NAME = 'firefox_cache'
DESCRIPTION = u'Parser for Firefox Cache files.'
# Number of bytes allocated to a cache record metadata.
RECORD_HEADER_SIZE = 36
# Initial size of Firefox >= 4 cache files.
INITIAL_CACHE_FILE_SIZE = 1024 * 1024 * 4
# Smallest possible block size in Firefox cache files.
MIN_BLOCK_SIZE = 256
RECORD_HEADER_STRUCT = construct.Struct(
'record_header',
construct.UBInt16('major'),
construct.UBInt16('minor'),
construct.UBInt32('location'),
construct.UBInt32('fetch_count'),
construct.UBInt32('last_fetched'),
construct.UBInt32('last_modified'),
construct.UBInt32('expire_time'),
construct.UBInt32('data_size'),
construct.UBInt32('request_size'),
construct.UBInt32('info_size'))
ALTERNATIVE_CACHE_NAME = (
pyparsing.Word(pyparsing.hexnums, exact=5) + pyparsing.Word('m', exact=1)
+ pyparsing.Word(pyparsing.nums, exact=2))
FIREFOX_CACHE_CONFIG = collections.namedtuple(
u'firefox_cache_config',
u'block_size first_record_offset')
REQUEST_METHODS = [
u'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
u'TRACE', 'OPTIONS', 'CONNECT', 'PATCH']
def _GetFirefoxConfig(self, file_entry):
"""Determine cache file block size. Raises exception if not found."""
if file_entry.name[0:9] != '_CACHE_00':
try:
# Match alternative filename. Five hex characters + 'm' + two digit
# number, e.g. '01ABCm02'. 'm' is for metadata. Cache files with 'd'
# instead contain data only.
self.ALTERNATIVE_CACHE_NAME.parseString(file_entry.name)
except pyparsing.ParseException:
raise errors.UnableToParseFile(u'Not a Firefox cache file.')
file_object = file_entry.GetFileObject()
# There ought to be a valid record within the first 4MB. We use this
# limit to prevent reading large invalid files.
to_read = min(file_object.get_size(), self.INITIAL_CACHE_FILE_SIZE)
while file_object.get_offset() < to_read:
offset = file_object.get_offset()
try:
# We have not yet determined the block size, so we use the smallest
# possible size.
record = self.__NextRecord(
file_entry.name, file_object, self.MIN_BLOCK_SIZE)
record_size = (
self.RECORD_HEADER_SIZE + record.request_size + record.info_size)
if record_size >= 4096:
# _CACHE_003_
block_size = 4096
elif record_size >= 1024:
# _CACHE_002_
block_size = 1024
else:
# _CACHE_001_
block_size = 256
return self.FIREFOX_CACHE_CONFIG(block_size, offset)
except IOError:
logging.debug(u'[{0:s}] {1:s}:{2:d}: Invalid record.'.format(
self.NAME, file_entry.name, offset))
raise errors.UnableToParseFile(
u'Could not find a valid cache record. '
u'Not a Firefox cache file.')
def __Accept(self, candidate, block_size):
"""Determine whether the candidate is a valid cache record."""
record_size = (
self.RECORD_HEADER_SIZE + candidate.request_size+ candidate.info_size)
return (
candidate.request_size > 0 and candidate.fetch_count > 0 and
candidate.major == 1 and record_size // block_size < 256)
def __NextRecord(self, filename, file_object, block_size):
"""Provide the next cache record."""
offset = file_object.get_offset()
try:
candidate = self.RECORD_HEADER_STRUCT.parse_stream(file_object)
except (IOError, construct.FieldError):
raise IOError(u'Unable to parse stream.')
if not self.__Accept(candidate, block_size):
# Move reader to next candidate block.
file_object.seek(block_size - self.RECORD_HEADER_SIZE, os.SEEK_CUR)
raise IOError(u'Not a valid Firefox cache record.')
# The last byte in a request is null.
url = file_object.read(candidate.request_size)[:-1]
# HTTP response header, even elements are keys, odd elements values.
headers = file_object.read(candidate.info_size)
request_method, _, _ = (
headers.partition('request-method\x00')[2].partition('\x00'))
_, _, response_head = headers.partition('response-head\x00')
response_code, _, _ = response_head.partition('\r\n')
if request_method not in self.REQUEST_METHODS:
safe_headers = headers.decode('ascii', errors='replace')
logging.debug((
u'[{0:s}] {1:s}:{2:d}: Unknown HTTP method \'{3:s}\'. Response '
u'headers: \'{4:s}\'').format(
self.NAME, filename, offset, request_method, safe_headers))
if response_code[0:4] != 'HTTP':
safe_headers = headers.decode('ascii', errors='replace')
logging.debug((
u'[{0:s}] {1:s}:{2:d}: Could not determine HTTP response code. '
u'Response headers: \'{3:s}\'.').format(
self.NAME, filename, offset, safe_headers))
# A request can span multiple blocks, so we use modulo.
_, remainder = divmod(file_object.get_offset() - offset, block_size)
# Move reader to next candidate block. Include the null-byte skipped above.
file_object.seek(block_size - remainder, os.SEEK_CUR)
return FirefoxCacheEvent(candidate, request_method, url, response_code)
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract records from a Firefox cache file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
firefox_config = self._GetFirefoxConfig(file_entry)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
file_object = file_entry.GetFileObject()
file_object.seek(firefox_config.first_record_offset)
while file_object.get_offset() < file_object.get_size():
try:
event_object = self.__NextRecord(
file_entry.name, file_object, firefox_config.block_size)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
except IOError:
logging.debug(u'[{0:s}] {1:s}:{2:d}: Invalid cache record.'.format(
self.NAME, file_entry.name,
file_object.get_offset() - self.MIN_BLOCK_SIZE))
file_object.close()
manager.ParsersManager.RegisterParser(FirefoxCacheParser)
+188
View File
@@ -0,0 +1,188 @@
#!/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 Firefox cache files parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import firefox_cache as firefox_cache_formatter
from plaso.lib import errors
from plaso.lib import timelib_test
from plaso.parsers import firefox_cache
from plaso.parsers import test_lib
__author__ = 'Petter Bjelland (petter.bjelland@gmail.com)'
class FirefoxCacheTest(test_lib.ParserTestCase):
"""A unit test for the FirefoxCacheParser."""
def setUp(self):
self._parser = firefox_cache.FirefoxCacheParser()
def VerifyMajorMinor(self, events):
"""Verify that valid Firefox cahce version is extracted."""
for event_object in events:
self.assertEquals(event_object.major, 1)
self.assertEquals(event_object.minor, 19)
def testParseCache_InvalidFile(self):
"""Verify that parser do not accept small, invalid files."""
test_file = self._GetTestFilePath(['firefox_cache', 'invalid_file'])
with self.assertRaises(errors.UnableToParseFile):
_ = self._ParseFile(self._parser, test_file)
def testParseCache_001(self):
"""Test Firefox 28 cache file _CACHE_001_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox28', '_CACHE_001_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(574, len(event_objects))
self.assertEquals(
event_objects[1].url, 'HTTP:http://start.ubuntu.com/12.04/sprite.png')
self.assertEquals(event_objects[1].timestamp,
timelib_test.CopyStringToTimestamp('2014-04-21 14:13:35'))
self.VerifyMajorMinor(event_objects)
expected_msg = (
u'Fetched 2 time(s) '
u'[HTTP/1.0 200 OK] GET '
u'"HTTP:http://start.ubuntu.com/12.04/sprite.png"')
expected_msg_short = (
u'[HTTP/1.0 200 OK] GET '
u'"HTTP:http://start.ubuntu.com/12.04/sprite.png"')
self._TestGetMessageStrings(
event_objects[1], expected_msg, expected_msg_short)
def testParseCache_002(self):
"""Test Firefox 28 cache file _CACHE_002_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox28', '_CACHE_002_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(58, len(event_objects))
self.assertEquals(
event_objects[2].url,
('HTTP:http://www.google-analytics.com/__utm.gif?utmwv=5.5.0&utms='
'1&utmn=1106893631&utmhn=www.dagbladet.no&utmcs=windows-1252&ut'
'msr=1920x1080&utmvp=1430x669&utmsc=24-bit&utmul=en-us&utmje=0&'
'utmfl=-&utmdt=Dagbladet.no%20-%20forsiden&utmhid=460894302&utm'
'r=-&utmp=%2F&utmht=1398089458997&utmac=UA-3072159-1&utmcc=__ut'
'ma%3D68537988.718312608.1398089459.1398089459.1398089459.1%3B%'
'2B__utmz%3D68537988.1398089459.1.1.utmcsr%3D(direct)%7Cutmccn'
'%3D(direct)%7Cutmcmd%3D(none)%3B&aip=1&utmu=qBQ~'))
self.assertEquals(event_objects[1].timestamp,
timelib_test.CopyStringToTimestamp('2014-04-21 14:10:58'))
self.VerifyMajorMinor(event_objects)
def testParseCache_003(self):
"""Test Firefox 28 cache file _CACHE_003_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox28', '_CACHE_003_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(4, len(event_objects))
self.assertEquals(
event_objects[3].url,
'HTTP:https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js')
self.assertEquals(
event_objects[3].timestamp,
timelib_test.CopyStringToTimestamp('2014-04-21 14:11:07'))
self.VerifyMajorMinor(event_objects)
def testParseAlternativeFilename(self):
"""Test Firefox 28 cache 003 file with alternative filename."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox28', 'E8D65m01'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(4, len(event_objects))
def testParseLegacyCache_001(self):
"""Test Firefox 3 cache file _CACHE_001_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox3', '_CACHE_001_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(25, len(event_objects))
self.assertEquals(event_objects[0].timestamp,
timelib_test.CopyStringToTimestamp('2014-05-02 14:15:03'))
expected_msg = (
u'Fetched 1 time(s) '
u'[HTTP/1.1 200 OK] GET '
u'"HTTP:http://start.mozilla.org/en-US/"')
expected_msg_short = (
u'[HTTP/1.1 200 OK] GET '
u'"HTTP:http://start.mozilla.org/en-US/"')
self._TestGetMessageStrings(
event_objects[0], expected_msg, expected_msg_short)
def testParseLegacyCache_002(self):
"""Test Firefox 3 cache file _CACHE_002_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox3', '_CACHE_002_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(3, len(event_objects))
self.assertEquals(event_objects[1].timestamp,
timelib_test.CopyStringToTimestamp('2014-05-02 14:25:55'))
def testParseLegacyCache_003(self):
"""Test Firefox 3 cache file _CACHE_003_ parsing."""
test_file = self._GetTestFilePath(
['firefox_cache', 'firefox3', '_CACHE_003_'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(2, len(event_objects))
self.assertEquals(event_objects[1].timestamp,
timelib_test.CopyStringToTimestamp('2014-05-02 14:15:07'))
if __name__ == '__main__':
unittest.main()
+172
View File
@@ -0,0 +1,172 @@
#!/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 a parser for extracting metadata."""
# TODO: Add a unit test for this parser.
import datetime
import hachoir_core.config
# This is necessary to do PRIOR to loading up other parts of hachoir
# framework, otherwise console does not work and other "weird" behavior
# is observed.
hachoir_core.config.unicode_stdout = False
hachoir_core.config.quiet = True
import hachoir_core
import hachoir_parser
import hachoir_metadata
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'David Nides (david.nides@gmail.com)'
class HachoirEvent(time_events.TimestampEvent):
"""Process timestamps from Hachoir Events."""
DATA_TYPE = 'metadata:hachoir'
def __init__(self, dt_timestamp, usage, attributes):
"""An EventObject created from a Hachoir entry.
Args:
dt_timestamp: A python datetime.datetime object.
usage: The description of the usage of the time value.
attributes: A dict containing metadata for the event.
"""
timestamp = timelib.Timestamp.FromPythonDatetime(dt_timestamp)
super(HachoirEvent, self).__init__(timestamp, usage, self.DATA_TYPE)
self.metadata = attributes
class HachoirParser(interface.BaseParser):
"""Parse meta data from files."""
NAME = 'hachoir'
DESCRIPTION = u'Parser that wraps Hachoir.'
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from a file using Hachoir.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
try:
fstream = hachoir_core.stream.InputIOStream(file_object, None, tags=[])
except hachoir_core.error.HachoirError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
if not fstream:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, 'Not fstream'))
try:
doc_parser = hachoir_parser.guessParser(fstream)
except hachoir_core.error.HachoirError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
if not doc_parser:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, 'Not parser'))
try:
metadata = hachoir_metadata.extractMetadata(doc_parser)
except (AssertionError, AttributeError) as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
try:
metatext = metadata.exportPlaintext(human=False)
except AttributeError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
if not metatext:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: No metadata'.format(
self.NAME, file_entry.name))
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
attributes = {}
extracted_events = []
for meta in metatext:
if not meta.startswith('-'):
continue
if len(meta) < 3:
continue
key, _, value = meta[2:].partition(': ')
key2, _, value2 = value.partition(': ')
if key2 == 'LastPrinted' and value2 != 'False':
date_object = timelib.StringToDatetime(
value2, timezone=parser_context.timezone)
if isinstance(date_object, datetime.datetime):
extracted_events.append((date_object, key2))
try:
date = metadata.get(key)
if isinstance(date, datetime.datetime):
extracted_events.append((date, key))
except ValueError:
pass
if key in attributes:
if isinstance(attributes.get(key), list):
attributes[key].append(value)
else:
old_value = attributes.get(key)
attributes[key] = [old_value, value]
else:
attributes[key] = value
if not extracted_events:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, 'No events discovered'))
for date, key in extracted_events:
event_object = HachoirEvent(date, key, attributes)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(HachoirParser)
+234
View File
@@ -0,0 +1,234 @@
#!/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.
"""Parser for Windows IIS Log file.
More documentation on fields can be found here:
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/
IIS/676400bc-8969-4aa7-851a-9319490a9bbb.mspx?mfr=true
"""
import logging
import pyparsing
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser
__author__ = 'Ashley Holtz (ashley.a.holtz@gmail.com)'
class IISEventObject(time_events.TimestampEvent):
"""Convenience class to handle the IIS event object."""
DATA_TYPE = 'iis:log:line'
def __init__(self, timestamp, structure):
"""Initializes the IIS event object.
Args:
timestamp: The timestamp time value, epoch.
structure: The structure with any parsed log values to iterate over.
"""
super(IISEventObject, self).__init__(
timestamp, eventdata.EventTimestamp.WRITTEN_TIME)
for key, value in structure.iteritems():
if key in ('time', 'date'):
continue
if value == u'-':
continue
if type(value) is pyparsing.ParseResults:
setattr(self, key, u''.join(value))
else:
try:
save_value = int(value, 10)
except ValueError:
save_value = value
setattr(self, key, save_value)
class WinIISParser(text_parser.PyparsingSingleLineTextParser):
"""Parses a Microsoft IIS log file."""
NAME = 'winiis'
DESCRIPTION = u'Parser for Microsoft IIS log files.'
# Common Fields (6.0: date time s-sitename s-ip cs-method cs-uri-stem
# cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status
# sc-substatus sc-win32-status.
# Common Fields (7.5): date time s-ip cs-method cs-uri-stem cs-uri-query
# s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus
# sc-win32-status time-taken
# Define common structures.
BLANK = pyparsing.Literal(u'-')
WORD = pyparsing.Word(pyparsing.alphanums + u'-') | BLANK
INT = pyparsing.Word(pyparsing.nums, min=1) | BLANK
IP = (
text_parser.PyparsingConstants.IPV4_ADDRESS |
text_parser.PyparsingConstants.IPV6_ADDRESS | BLANK)
PORT = pyparsing.Word(pyparsing.nums, min=1, max=6) | BLANK
URI = pyparsing.Word(pyparsing.alphanums + u'/.?&+;_=()-:,%') | BLANK
# Define how a log line should look like for version 6.0.
LOG_LINE_6_0 = (
text_parser.PyparsingConstants.DATE.setResultsName('date') +
text_parser.PyparsingConstants.TIME.setResultsName('time') +
WORD.setResultsName('s_sitename') + IP.setResultsName('dest_ip') +
WORD.setResultsName('http_method') + URI.setResultsName('cs_uri_stem') +
URI.setResultsName('cs_uri_query') + PORT.setResultsName('dest_port') +
WORD.setResultsName('cs_username') + IP.setResultsName('source_ip') +
URI.setResultsName('user_agent') + INT.setResultsName('sc_status') +
INT.setResultsName('sc_substatus') +
INT.setResultsName('sc_win32_status'))
_LOG_LINE_STRUCTURES = {}
# Common fields. Set results name with underscores, not hyphens because regex
# will not pick them up.
_LOG_LINE_STRUCTURES['date'] = (
text_parser.PyparsingConstants.DATE.setResultsName('date'))
_LOG_LINE_STRUCTURES['time'] = (
text_parser.PyparsingConstants.TIME.setResultsName('time'))
_LOG_LINE_STRUCTURES['s-sitename'] = WORD.setResultsName('s_sitename')
_LOG_LINE_STRUCTURES['s-ip'] = IP.setResultsName('dest_ip')
_LOG_LINE_STRUCTURES['cs-method'] = WORD.setResultsName('http_method')
_LOG_LINE_STRUCTURES['cs-uri-stem'] = URI.setResultsName('requested_uri_stem')
_LOG_LINE_STRUCTURES['cs-uri-query'] = URI.setResultsName('cs_uri_query')
_LOG_LINE_STRUCTURES['s-port'] = PORT.setResultsName('dest_port')
_LOG_LINE_STRUCTURES['cs-username'] = WORD.setResultsName('cs_username')
_LOG_LINE_STRUCTURES['c-ip'] = IP.setResultsName('source_ip')
_LOG_LINE_STRUCTURES['cs(User-Agent)'] = URI.setResultsName('user_agent')
_LOG_LINE_STRUCTURES['sc-status'] = INT.setResultsName('http_status')
_LOG_LINE_STRUCTURES['sc-substatus'] = INT.setResultsName('sc_substatus')
_LOG_LINE_STRUCTURES['sc-win32-status'] = (
INT.setResultsName('sc_win32_status'))
# Less common fields.
_LOG_LINE_STRUCTURES['s-computername'] = URI.setResultsName('s_computername')
_LOG_LINE_STRUCTURES['sc-bytes'] = INT.setResultsName('sent_bytes')
_LOG_LINE_STRUCTURES['cs-bytes'] = INT.setResultsName('received_bytes')
_LOG_LINE_STRUCTURES['time-taken'] = INT.setResultsName('time_taken')
_LOG_LINE_STRUCTURES['cs-version'] = WORD.setResultsName('protocol_version')
_LOG_LINE_STRUCTURES['cs-host'] = WORD.setResultsName('cs_host')
_LOG_LINE_STRUCTURES['cs(Cookie)'] = URI.setResultsName('cs_cookie')
_LOG_LINE_STRUCTURES['cs(Referrer)'] = URI.setResultsName('cs_referrer')
# Define the available log line structures. Default to the IIS v. 6.0
# common format.
LINE_STRUCTURES = [
('comment', text_parser.PyparsingConstants.COMMENT_LINE_HASH),
('logline', LOG_LINE_6_0)]
# Define a signature value for the log file.
SIGNATURE = '#Software: Microsoft Internet Information Services'
def __init__(self):
"""Initializes a parser object."""
super(WinIISParser, self).__init__()
self.version = None
self.software = None
def VerifyStructure(self, unused_parser_context, line):
"""Verify that this file is an IIS log file.
Args:
parser_context: A parser context object (instance of ParserContext).
line: A single line from the text file.
Returns:
True if this is the correct parser, False otherwise.
"""
# TODO: Examine other versions of the file format and if this parser should
# support them. For now just checking if it contains the IIS header.
if self.SIGNATURE in line:
return True
return False
def ParseRecord(self, unused_parser_context, key, structure):
"""Parse each record structure and return an event object if applicable.
Args:
parser_context: A parser context object (instance of ParserContext).
key: An identification string indicating the name of the parsed
structure.
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
if key == 'comment':
self._ParseCommentRecord(structure)
elif key == 'logline':
return self._ParseLogLine(structure)
else:
logging.warning(
u'Unable to parse record, unknown structure: {0:s}'.format(key))
def _ParseCommentRecord(self, structure):
"""Parse a comment and store appropriate attributes."""
comment = structure[1]
if comment.startswith(u'Version'):
_, _, self.version = comment.partition(u':')
elif comment.startswith(u'Software'):
_, _, self.software = comment.partition(u':')
elif comment.startswith(u'Date'):
# TODO: fix this date is not used here.
_, _, unused_date = comment.partition(u':')
# Check if there's a Fields line. If not, LOG_LINE defaults to IIS 6.0
# common format.
elif comment.startswith(u'Fields'):
log_line = pyparsing.Empty()
for member in comment[7:].split():
log_line += self._LOG_LINE_STRUCTURES.get(member, self.URI)
# TODO: self._line_structures is a work-around and this needs
# a structural fix.
self._line_structures[1] = ('logline', log_line)
def _ParseLogLine(self, structure):
"""Parse a single log line and return an EventObject."""
date = structure.get('date', None)
time = structure.get('time', None)
if not (date and time):
logging.warning((
u'Unable to extract timestamp from IIS log line with structure: '
u'{0:s}.').format(structure))
return
year, month, day = date
hour, minute, second = time
timestamp = timelib.Timestamp.FromTimeParts(
year, month, day, hour, minute, second)
if not timestamp:
return
return IISEventObject(timestamp, structure)
manager.ParsersManager.RegisterParser(WinIISParser)
+96
View File
@@ -0,0 +1,96 @@
#!/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 IIS log parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import iis as iis_formatter
from plaso.lib import timelib_test
from plaso.parsers import test_lib
from plaso.parsers import iis
__author__ = 'Ashley Holtz (ashley.a.holtz@gmail.com)'
class WinIISUnitTest(test_lib.ParserTestCase):
"""Tests for the Windows IIS parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = iis.WinIISParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['iis.log'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 11)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-07-30 00:00:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.source_ip, u'10.10.10.100')
self.assertEquals(event_object.dest_ip, u'10.10.10.100')
self.assertEquals(event_object.dest_port, 80)
expected_msg = (
u'GET /some/image/path/something.jpg '
u'[ 10.10.10.100 > 10.10.10.100 : 80 ] '
u'Http Status: 200 '
u'User Agent: Mozilla/4.0+(compatible;+Win32;'
u'+WinHttp.WinHttpRequest.5)')
expected_msg_short = (
u'GET /some/image/path/something.jpg '
u'[ 10.10.10.100 > 10.10.10.100 : 80 ]')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[5]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-07-30 00:00:05')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.http_method, 'GET')
self.assertEquals(event_object.http_status, 200)
self.assertEquals(
event_object.requested_uri_stem, u'/some/image/path/something.jpg')
event_object = event_objects[1]
expected_msg = (
u'GET /some/image/path/something.htm '
u'[ 22.22.22.200 > 10.10.10.100 : 80 ] '
u'Http Status: 404 '
u'User Agent: Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_6_8)'
u'+AppleWebKit/534.57.2+(KHTML,+like+Gecko)+Version/5.1.7'
u'+Safari/534.57.2')
expected_msg_short = (
u'GET /some/image/path/something.htm '
u'[ 22.22.22.200 > 10.10.10.100 : 80 ]')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2012 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This file contains a class to provide a parsing framework to plaso.
This class contains a base framework class for parsing fileobjects, and
also some implementations that extend it to provide a more comprehensive
parser.
"""
import abc
from plaso.parsers import manager
class BaseParser(object):
"""Class that implements the parser object interface."""
NAME = 'base_parser'
DESCRIPTION = u''
def _BuildParserChain(self, parser_chain=None):
"""Return the parser chain with the addition of the current parser.
Args:
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Returns:
The parser chain, with the addition of the current parser.
"""
if not parser_chain:
return self.NAME
return u'/'.join([parser_chain, self.NAME])
@abc.abstractmethod
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Parsers the file entry and extracts event objects.
This is the main function of the class, the one that actually
goes through the log file and parses each line of it to
produce a parsed line and a timestamp.
It also tries to verify the file structure and see if the class is capable
of parsing the file passed to the module. It will do so with series of tests
that should determine if the file is of the correct structure.
If the class is not capable of parsing the file passed to it an exception
should be raised, an exception of the type UnableToParseFile that indicates
the reason why the class does not parse it.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
NotImplementedError when not implemented.
"""
raise NotImplementedError
@classmethod
def SupportsPlugins(cls):
"""Determines if a parser supports plugins.
Returns:
A boolean value indicating whether the parser supports plugins.
"""
return False
class BasePluginsParser(BaseParser):
"""Class that implements the parser with plugins object interface."""
NAME = 'base_plugin_parser'
DESCRIPTION = u''
# Every child class should define its own _plugin_classes dict.
# We don't define it here to make sure the plugins of different
# classes don't end up in the same dict.
# _plugin_classes = {}
_plugin_classes = None
@classmethod
def DeregisterPlugin(cls, plugin_class):
"""Deregisters a plugin class.
The plugin classes are identified based on their lower case name.
Args:
plugin_class: the class object of the plugin.
Raises:
KeyError: if plugin class is not set for the corresponding name.
"""
plugin_name = plugin_class.NAME.lower()
if plugin_name not in cls._plugin_classes:
raise KeyError(
u'Plugin class not set for name: {0:s}.'.format(
plugin_class.NAME))
del cls._plugin_classes[plugin_name]
@classmethod
def GetPluginNames(cls, parser_filter_string=None):
"""Retrieves the plugin names.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Returns:
A list of plugin names.
"""
plugin_names = []
for plugin_name, _ in cls.GetPlugins(
parser_filter_string=parser_filter_string):
plugin_names.append(plugin_name)
return plugin_names
@classmethod
def GetPluginObjects(cls, parser_filter_string=None):
"""Retrieves the plugin objects.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Returns:
A list of plugin objects (instances of BasePlugin).
"""
plugin_objects = []
for _, plugin_class in cls.GetPlugins(
parser_filter_string=parser_filter_string):
plugin_object = plugin_class()
plugin_objects.append(plugin_object)
return plugin_objects
@classmethod
def GetPlugins(cls, parser_filter_string=None):
"""Retrieves the registered plugins.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Yields:
A tuple that contains the uniquely identifying name of the plugin
and the plugin class (subclass of BasePlugin).
"""
if parser_filter_string:
includes, excludes = manager.ParsersManager.GetFilterListsFromString(
parser_filter_string)
else:
includes = None
excludes = None
for plugin_name, plugin_class in cls._plugin_classes.iteritems():
if excludes and plugin_name in excludes:
continue
if includes and plugin_name not in includes:
continue
yield plugin_name, plugin_class
@abc.abstractmethod
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Parsers the file entry and extracts event objects.
This is the main function of the class, the one that actually
goes through the log file and parses each line of it to
produce a parsed line and a timestamp.
It also tries to verify the file structure and see if the class is capable
of parsing the file passed to the module. It will do so with series of tests
that should determine if the file is of the correct structure.
If the class is not capable of parsing the file passed to it an exception
should be raised, an exception of the type UnableToParseFile that indicates
the reason why the class does not parse it.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
NotImplementedError when not implemented.
"""
raise NotImplementedError
@classmethod
def RegisterPlugin(cls, plugin_class):
"""Registers a plugin class.
The plugin classes are identified based on their lower case name.
Args:
plugin_class: the class object of the plugin.
Raises:
KeyError: if plugin class is already set for the corresponding name.
"""
plugin_name = plugin_class.NAME.lower()
if plugin_name in cls._plugin_classes:
raise KeyError((
u'Plugin class already set for name: {0:s}.').format(
plugin_class.NAME))
cls._plugin_classes[plugin_name] = plugin_class
@classmethod
def RegisterPlugins(cls, plugin_classes):
"""Registers plugin classes.
Args:
plugin_classes: a list of class objects of the plugins.
Raises:
KeyError: if plugin class is already set for the corresponding name.
"""
for plugin_class in plugin_classes:
cls.RegisterPlugin(plugin_class)
@classmethod
def SupportsPlugins(cls):
"""Determines if a parser supports plugins.
Returns:
A boolean value indicating whether the parser supports plugins.
"""
return True
+236
View File
@@ -0,0 +1,236 @@
#!/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.
"""Parser for Java Cache IDX files."""
# TODO:
# * 6.02 files did not retain IP addresses. However, the
# deploy_resource_codebase header field may contain the host IP.
# This needs to be researched further, as that field may not always
# be present. 6.02 files will currently return 'Unknown'.
import construct
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
class JavaIDXEvent(time_events.TimestampEvent):
"""Convenience class for a Java IDX cache file download event."""
DATA_TYPE = 'java:download:idx'
def __init__(
self, timestamp, timestamp_description, idx_version, url, ip_address):
"""Initializes the event object.
Args:
timestamp: The timestamp value.
timestamp_description: The description of the usage of the time value.
idx_version: Version of IDX file.
url: URL of the downloaded file.
ip_address: IP address of the host in the URL.
"""
super(JavaIDXEvent, self).__init__(timestamp, timestamp_description)
self.idx_version = idx_version
self.url = url
self.ip_address = ip_address
class JavaIDXParser(interface.BaseParser):
"""Parse Java IDX files for download events.
There are five structures defined. 6.02 files had one generic section
that retained all data. From 6.03, the file went to a multi-section
format where later sections were optional and had variable-lengths.
6.03, 6.04, and 6.05 files all have their main data section (#2)
begin at offset 128. The short structure is because 6.05 files
deviate after the 8th byte. So, grab the first 8 bytes to ensure it's
valid, get the file version, then continue on with the correct
structures.
"""
NAME = 'java_idx'
DESCRIPTION = u'Parser for Java IDX files.'
IDX_SHORT_STRUCT = construct.Struct(
'magic',
construct.UBInt8('busy'),
construct.UBInt8('incomplete'),
construct.UBInt32('idx_version'))
IDX_602_STRUCT = construct.Struct(
'IDX_602_Full',
construct.UBInt16('null_space'),
construct.UBInt8('shortcut'),
construct.UBInt32('content_length'),
construct.UBInt64('last_modified_date'),
construct.UBInt64('expiration_date'),
construct.PascalString(
'version_string', length_field=construct.UBInt16('length')),
construct.PascalString(
'url', length_field=construct.UBInt16('length')),
construct.PascalString(
'namespace', length_field=construct.UBInt16('length')),
construct.UBInt32('FieldCount'))
IDX_605_SECTION_ONE_STRUCT = construct.Struct(
'IDX_605_Section1',
construct.UBInt8('shortcut'),
construct.UBInt32('content_length'),
construct.UBInt64('last_modified_date'),
construct.UBInt64('expiration_date'),
construct.UBInt64('validation_date'),
construct.UBInt8('signed'),
construct.UBInt32('sec2len'),
construct.UBInt32('sec3len'),
construct.UBInt32('sec4len'))
IDX_605_SECTION_TWO_STRUCT = construct.Struct(
'IDX_605_Section2',
construct.PascalString(
'version', length_field=construct.UBInt16('length')),
construct.PascalString(
'url', length_field=construct.UBInt16('length')),
construct.PascalString(
'namespec', length_field=construct.UBInt16('length')),
construct.PascalString(
'ip_address', length_field=construct.UBInt16('length')),
construct.UBInt32('FieldCount'))
# Java uses Pascal-style strings, but with a 2-byte length field.
JAVA_READUTF_STRING = construct.Struct(
'Java.ReadUTF',
construct.PascalString(
'string', length_field=construct.UBInt16('length')))
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from a Java cache IDX file.
This is the main parsing engine for the parser. It determines if
the selected file is a proper IDX file. It then checks the file
version to determine the correct structure to apply to extract
data.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
try:
magic = self.IDX_SHORT_STRUCT.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
raise errors.UnableToParseFile(
u'Unable to parse Java IDX file with error: {0:s}.'.format(exception))
# Fields magic.busy and magic.incomplete are normally 0x00. They
# are set to 0x01 if the file is currently being downloaded. Logic
# checks for > 1 to avoid a race condition and still reject any
# file with other data.
# Field magic.idx_version is the file version, of which only
# certain versions are supported.
if magic.busy > 1 or magic.incomplete > 1:
raise errors.UnableToParseFile(u'Not a valid Java IDX file')
if not magic.idx_version in [602, 603, 604, 605]:
raise errors.UnableToParseFile(u'Not a valid Java IDX file')
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
# Obtain the relevant values from the file. The last modified date
# denotes when the file was last modified on the HOST. For example,
# when the file was uploaded to a web server.
if magic.idx_version == 602:
section_one = self.IDX_602_STRUCT.parse_stream(file_object)
last_modified_date = section_one.last_modified_date
url = section_one.url
ip_address = 'Unknown'
http_header_count = section_one.FieldCount
elif magic.idx_version in [603, 604, 605]:
# IDX 6.03 and 6.04 have two unused bytes before the structure.
if magic.idx_version in [603, 604]:
file_object.read(2)
# IDX 6.03, 6.04, and 6.05 files use the same structures for the
# remaining data.
section_one = self.IDX_605_SECTION_ONE_STRUCT.parse_stream(file_object)
last_modified_date = section_one.last_modified_date
if file_object.get_size() > 128:
file_object.seek(128) # Static offset for section 2.
section_two = self.IDX_605_SECTION_TWO_STRUCT.parse_stream(file_object)
url = section_two.url
ip_address = section_two.ip_address
http_header_count = section_two.FieldCount
else:
url = 'Unknown'
ip_address = 'Unknown'
http_header_count = 0
# File offset is now just prior to HTTP headers. Make sure there
# are headers, and then parse them to retrieve the download date.
download_date = None
for field in range(0, http_header_count):
field = self.JAVA_READUTF_STRING.parse_stream(file_object)
value = self.JAVA_READUTF_STRING.parse_stream(file_object)
if field.string == 'date':
# Time string "should" be in UTC or have an associated time zone
# information in the string itself. If that is not the case then
# there is no reliable method for plaso to determine the proper
# timezone, so the assumption is that it is UTC.
download_date = timelib.Timestamp.FromTimeString(
value.string, gmt_as_timezone=False)
if not url or not ip_address:
raise errors.UnableToParseFile(
u'Unexpected Error: URL or IP address not found in file.')
last_modified_timestamp = timelib.Timestamp.FromJavaTime(
last_modified_date)
# TODO: Move the timestamp description fields into eventdata.
event_object = JavaIDXEvent(
last_modified_timestamp, 'File Hosted Date', magic.idx_version, url,
ip_address)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if section_one:
expiration_date = section_one.get('expiration_date', None)
if expiration_date:
expiration_timestamp = timelib.Timestamp.FromJavaTime(expiration_date)
event_object = JavaIDXEvent(
expiration_timestamp, 'File Expiration Date', magic.idx_version,
url, ip_address)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if download_date:
event_object = JavaIDXEvent(
download_date, eventdata.EventTimestamp.FILE_DOWNLOADED,
magic.idx_version, url, ip_address)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(JavaIDXParser)
+124
View File
@@ -0,0 +1,124 @@
#!/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 Java Cache IDX file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import java_idx as java_idx_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import java_idx
from plaso.parsers import test_lib
class IDXTest(test_lib.ParserTestCase):
"""Tests for Java Cache IDX file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = java_idx.JavaIDXParser()
def testParse602(self):
"""Tests the Parse function on a version 602 IDX file."""
test_file = self._GetTestFilePath(['java_602.idx'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 2)
event_object = event_objects[0]
idx_version_expected = 602
self.assertEqual(event_object.idx_version, idx_version_expected)
ip_address_expected = u'Unknown'
self.assertEqual(event_object.ip_address, ip_address_expected)
url_expected = u'http://www.gxxxxx.com/a/java/xxz.jar'
self.assertEqual(event_object.url, url_expected)
description_expected = u'File Hosted Date'
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2010-05-05 01:34:19.720')
self.assertEqual(
event_object.timestamp, expected_timestamp)
# Parse second event. Same metadata; different timestamp event.
event_object = event_objects[1]
self.assertEqual(event_object.idx_version, idx_version_expected)
self.assertEqual(event_object.ip_address, ip_address_expected)
self.assertEqual(event_object.url, url_expected)
description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2010-05-05 03:52:31')
self.assertEqual(event_object.timestamp, expected_timestamp)
def testParse605(self):
"""Tests the Parse function on a version 605 IDX file."""
test_file = self._GetTestFilePath(['java.idx'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 2)
event_object = event_objects[0]
idx_version_expected = 605
self.assertEqual(event_object.idx_version, idx_version_expected)
ip_address_expected = '10.7.119.10'
self.assertEqual(event_object.ip_address, ip_address_expected)
url_expected = (
u'http://xxxxc146d3.gxhjxxwsf.xx:82/forum/dare.php?'
u'hsh=6&key=b30xxxx1c597xxxx15d593d3f0xxx1ab')
self.assertEqual(event_object.url, url_expected)
description_expected = 'File Hosted Date'
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2001-07-26 05:00:00'
)
self.assertEqual(event_object.timestamp, expected_timestamp)
# Parse second event. Same metadata; different timestamp event.
event_object = event_objects[1]
self.assertEqual(event_object.idx_version, idx_version_expected)
self.assertEqual(event_object.ip_address, ip_address_expected)
self.assertEqual(event_object.url, url_expected)
description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED
self.assertEqual(event_object.timestamp_desc, description_expected)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-01-13 16:22:01'
)
self.assertEqual(event_object.timestamp, expected_timestamp)
if __name__ == '__main__':
unittest.main()
+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.
"""This file contains a appfirewall.log (Mac OS X Firewall) parser."""
import datetime
import logging
import pyparsing
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class MacAppFirewallLogEvent(time_events.TimestampEvent):
"""Convenience class for a Mac Wifi log line event."""
DATA_TYPE = 'mac:asl:appfirewall:line'
def __init__(self, timestamp, structure, process_name, action):
"""Initializes the event object.
Args:
timestamp: The timestamp time value, epoch.
structure: structure with the parse fields.
computer_name: string with the name of the computer.
agent: string with the agent that save the log.
status: string with the saved status action.
process_name: string name of the entity that tried do the action.
action: string with the action
"""
super(MacAppFirewallLogEvent, self).__init__(
timestamp, eventdata.EventTimestamp.ADDED_TIME)
self.timestamp = timestamp
self.computer_name = structure.computer_name
self.agent = structure.agent
self.status = structure.status
self.process_name = process_name
self.action = action
class MacAppFirewallParser(text_parser.PyparsingSingleLineTextParser):
"""Parse text based on appfirewall.log file."""
NAME = 'mac_appfirewall_log'
DESCRIPTION = u'Parser for appfirewall.log files.'
ENCODING = u'utf-8'
# Regular expressions for known actions.
# Define how a log line should look like.
# Example: 'Nov 2 04:07:35 DarkTemplar-2.local socketfilterfw[112] '
# '<Info>: Dropbox: Allow (in:0 out:2)'
# INFO: process_name is going to have a white space at the beginning.
FIREWALL_LINE = (
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME.setResultsName('time') +
pyparsing.Word(pyparsing.printables).setResultsName('computer_name') +
pyparsing.Word(pyparsing.printables).setResultsName('agent') +
pyparsing.Literal(u'<').suppress() +
pyparsing.CharsNotIn(u'>').setResultsName('status') +
pyparsing.Literal(u'>:').suppress() +
pyparsing.CharsNotIn(u':').setResultsName('process_name') +
pyparsing.Literal(u':') +
pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('action'))
# Repeated line.
# Example: Nov 29 22:18:29 --- last message repeated 1 time ---
REPEATED_LINE = (
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME.setResultsName('time') +
pyparsing.Literal(u'---').suppress() +
pyparsing.CharsNotIn(u'---').setResultsName('process_name') +
pyparsing.Literal(u'---').suppress())
# Define the available log line structures.
LINE_STRUCTURES = [
('logline', FIREWALL_LINE),
('repeated', REPEATED_LINE)]
def __init__(self):
"""Initializes a parser object."""
super(MacAppFirewallParser, self).__init__()
self._year_use = 0
self._last_month = None
self.previous_structure = None
def VerifyStructure(self, parser_context, line):
"""Verify that this file is a Mac AppFirewall log file.
Args:
parser_context: A parser context object (instance of ParserContext).
line: A single line from the text file.
Returns:
True if this is the correct parser, False otherwise.
"""
try:
line = self.FIREWALL_LINE.parseString(line)
except pyparsing.ParseException:
logging.debug(u'Not a Mac AppFirewall log file')
return False
if (line.action != 'creating /var/log/appfirewall.log' or
line.status != 'Error'):
return False
return True
def ParseRecord(self, parser_context, key, structure):
"""Parses each record structure and return an event object if applicable.
Args:
parser_context: A parser context object (instance of ParserContext).
key: An identification string indicating the name of the parsed
structure.
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
if key == 'logline' or key == 'repeated':
return self._ParseLogLine(parser_context, structure, key)
else:
logging.warning(
u'Unable to parse record, unknown structure: {0:s}'.format(key))
def _ParseLogLine(self, parser_context, structure, key):
"""Parse a logline and store appropriate attributes.
Args:
parser_context: A parser context object (instance of ParserContext).
structure: log line of structure.
key: type of line log (normal or repeated).
Returns:
Return an object MacAppFirewallLogEvent.
"""
# TODO: improve this to get a valid year.
if not self._year_use:
self._year_use = parser_context.year
if not self._year_use:
# Get from the creation time of the file.
self._year_use = self._GetYear(
self.file_entry.GetStat(), parser_context.timezone)
# If fail, get from the current time.
if not self._year_use:
self._year_use = timelib.GetCurrentYear()
# Gap detected between years.
month = timelib.MONTH_DICT.get(structure.month.lower())
if not self._last_month:
self._last_month = month
if month < self._last_month:
self._year_use += 1
timestamp = self._GetTimestamp(
structure.day,
month,
self._year_use,
structure.time)
if not timestamp:
logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp))
return
self._last_month = month
# If the actual entry is a repeated entry, we take the basic information
# from the previous entry, but using the timestmap from the actual entry.
if key == 'logline':
self.previous_structure = structure
else:
structure = self.previous_structure
# Pyparsing reads in RAW, but the text is in UTF8.
try:
action = structure.action.decode('utf-8')
except UnicodeDecodeError:
logging.warning(
u'Decode UTF8 failed, the message string may be cut short.')
action = structure.action.decode('utf-8', 'ignore')
# Due to the use of CharsNotIn pyparsing structure contains whitespaces
# that need to be removed.
process_name = structure.process_name.strip()
event_object = MacAppFirewallLogEvent(
timestamp, structure, process_name, action)
return event_object
def _GetTimestamp(self, day, month, year, time):
"""Gets a timestamp from a pyparsing ParseResults timestamp.
This is a timestamp_string as returned by using
text_parser.PyparsingConstants structures:
08, Nov, [20, 36, 37]
Args:
timestamp_string: The pyparsing ParseResults object
Returns:
day: An integer representing the day.
month: An integer representing the month.
year: An integer representing the year.
timestamp: A plaso timelib timestamp event or 0.
"""
try:
hour, minute, second = time
timestamp = timelib.Timestamp.FromTimeParts(
year, month, day, hour, minute, second)
except ValueError:
timestamp = 0
return timestamp
def _GetYear(self, stat, timezone):
"""Retrieves the year either from the input file or from the settings."""
time = getattr(stat, 'crtime', 0)
if not time:
time = getattr(stat, 'ctime', 0)
if not time:
logging.error(
u'Unable to determine correct year of log file, defaulting to '
u'current year.')
return timelib.GetCurrentYear()
try:
timestamp = datetime.datetime.fromtimestamp(time, timezone)
except ValueError as exception:
logging.error((
u'Unable to determine correct year of log file with error: {0:s}, '
u'defaulting to current year.').format(exception))
return timelib.GetCurrentYear()
return timestamp.year
manager.ParsersManager.RegisterParser(MacAppFirewallParser)
+118
View File
@@ -0,0 +1,118 @@
#!/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 Mac AppFirewall log file parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mac_appfirewall as mac_appfirewall_formatter
from plaso.lib import timelib_test
from plaso.parsers import mac_appfirewall
from plaso.parsers import test_lib
class MacAppFirewallUnitTest(test_lib.ParserTestCase):
"""Tests for Mac AppFirewall log file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mac_appfirewall.MacAppFirewallParser()
def testParseFile(self):
"""Test parsing of a Mac Wifi log file."""
knowledge_base_values = {'year': 2013}
test_file = self._GetTestFilePath(['appfirewall.log'])
event_queue_consumer = self._ParseFile(
self._parser, test_file, knowledge_base_values=knowledge_base_values)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 47)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-02 04:07:35')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.agent, u'socketfilterfw[112]')
self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local')
self.assertEqual(event_object.status, u'Error')
self.assertEqual(event_object.process_name, u'Logging')
self.assertEqual(event_object.action, u'creating /var/log/appfirewall.log')
expected_msg = (
u'Computer: DarkTemplar-2.local '
u'Agent: socketfilterfw[112] '
u'Status: Error '
u'Process name: Logging '
u'Log: creating /var/log/appfirewall.log')
expected_msg_short = (
u'Process name: Logging '
u'Status: Error')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[9]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-03 13:25:15')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.agent, u'socketfilterfw[87]')
self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local')
self.assertEqual(event_object.status, u'Info')
self.assertEqual(event_object.process_name, u'Dropbox')
self.assertEqual(event_object.action, u'Allow TCP LISTEN (in:0 out:1)')
expected_msg = (
u'Computer: DarkTemplar-2.local '
u'Agent: socketfilterfw[87] '
u'Status: Info '
u'Process name: Dropbox '
u'Log: Allow TCP LISTEN (in:0 out:1)')
expected_msg_short = (
u'Process name: Dropbox '
u'Status: Info')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
# Check repeated lines.
event_object = event_objects[38]
repeated_event_object = event_objects[39]
self.assertEqual(event_object.agent, repeated_event_object.agent)
self.assertEqual(
event_object.computer_name, repeated_event_object.computer_name)
self.assertEqual(event_object.status, repeated_event_object.status)
self.assertEqual(
event_object.process_name, repeated_event_object.process_name)
self.assertEqual(event_object.action, repeated_event_object.action)
# Year changes.
event_object = event_objects[45]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-31 23:59:23')
self.assertEqual(event_object.timestamp, expected_timestamp)
event_object = event_objects[46]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-01 01:13:23')
self.assertEqual(event_object.timestamp, expected_timestamp)
if __name__ == '__main__':
unittest.main()
+507
View File
@@ -0,0 +1,507 @@
#!/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.
"""Parser for Mac OS X Keychain files."""
# INFO: Only supports internet and application passwords,
# because it is the only data that contains timestamp events.
# Keychain can also store "secret notes". These notes are stored
# in the same type than the application format, then, they are already
# supported. The stored wifi are also application passwords.
# TODO: the AccessControl for each entry has not been implemented. Until now,
# I know that the AccessControl from Internet and App password are stored
# using other tables (Symmetric, certificates, etc). Access Control
# indicates which specific tool, or all, is able to use this entry.
import binascii
import construct
import logging
import os
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class KeychainInternetRecordEvent(event.EventObject):
"""Convenience class for an keychain internet record event."""
DATA_TYPE = 'mac:keychain:internet'
def __init__(
self, timestamp, timestamp_desc, entry_name, account_name,
text_description, comments, where, protocol, type_protocol, ssgp_hash):
"""Initializes the event object.
Args:
timestamp: Description of the timestamp value.
timestamp_desc: Timelib type of the timestamp.
entry_name: Name of the entry.
account_name: Name of the account.
text_description: Short description about the entry.
comments: String that contains the comments added by the user.
where: The domain name or IP where the password is used.
protocol: The internet protocol used (eg. https).
type_protocol: The sub-protocol used (eg. form).
ssgp_hash: String with hexadecimal values from the password / cert hash.
"""
super(KeychainInternetRecordEvent, self).__init__()
self.timestamp = timestamp
self.timestamp_desc = timestamp_desc
self.entry_name = entry_name
self.account_name = account_name
self.text_description = text_description
self.where = where
self.protocol = protocol
self.type_protocol = type_protocol
self.comments = comments
self.ssgp_hash = ssgp_hash
class KeychainApplicationRecordEvent(event.EventObject):
"""Convenience class for an keychain application password record event."""
DATA_TYPE = 'mac:keychain:application'
def __init__(
self, timestamp, timestamp_desc, entry_name,
account_name, text_description, comments, ssgp_hash):
"""Initializes the event object.
Args:
timestamp: Description of the timestamp value.
timestamp_desc: Timelib type of the timestamp.
entry_name: Name of the entry.
account_name: Name of the account.
text_description: Short description about the entry.
comments: String that contains the comments added by the user.
ssgp_hash: String with hexadecimal values from the password / cert hash.
"""
super(KeychainApplicationRecordEvent, self).__init__()
self.timestamp = timestamp
self.timestamp_desc = timestamp_desc
self.entry_name = entry_name
self.account_name = account_name
self.text_description = text_description
self.comments = comments
self.ssgp_hash = ssgp_hash
class KeychainParser(interface.BaseParser):
"""Parser for Keychain files."""
NAME = 'mac_keychain'
DESCRIPTION = u'Parser for Mac OS X Keychain files.'
KEYCHAIN_MAGIC_HEADER = 'kych'
KEYCHAIN_MAJOR_VERSION = 1
KEYCHAIN_MINOR_VERSION = 0
RECORD_TYPE_APPLICATION = 0x80000000
RECORD_TYPE_INTERNET = 0x80000001
# DB HEADER.
KEYCHAIN_DB_HEADER = construct.Struct(
'db_header',
construct.String('magic', 4),
construct.UBInt16('major_version'),
construct.UBInt16('minor_version'),
construct.UBInt32('header_size'),
construct.UBInt32('schema_offset'),
construct.Padding(4))
# DB SCHEMA.
KEYCHAIN_DB_SCHEMA = construct.Struct(
'db_schema',
construct.UBInt32('size'),
construct.UBInt32('number_of_tables'))
# For each number_of_tables, the schema has a TABLE_OFFSET with the
# offset starting in the DB_SCHEMA.
TABLE_OFFSET = construct.UBInt32('table_offset')
TABLE_HEADER = construct.Struct(
'table_header',
construct.UBInt32('table_size'),
construct.UBInt32('record_type'),
construct.UBInt32('number_of_records'),
construct.UBInt32('first_record'),
construct.UBInt32('index_offset'),
construct.Padding(4),
construct.UBInt32('recordnumbercount'))
RECORD_HEADER = construct.Struct(
'record_header',
construct.UBInt32('entry_length'),
construct.Padding(12),
construct.UBInt32('ssgp_length'),
construct.Padding(4),
construct.UBInt32('creation_time'),
construct.UBInt32('last_mod_time'),
construct.UBInt32('text_description'),
construct.Padding(4),
construct.UBInt32('comments'),
construct.Padding(8),
construct.UBInt32('entry_name'),
construct.Padding(20),
construct.UBInt32('account_name'),
construct.Padding(4))
RECORD_HEADER_APP = construct.Struct(
'record_entry_app',
RECORD_HEADER,
construct.Padding(4))
RECORD_HEADER_INET = construct.Struct(
'record_entry_inet',
RECORD_HEADER,
construct.UBInt32('where'),
construct.UBInt32('protocol'),
construct.UBInt32('type'),
construct.Padding(4),
construct.UBInt32('url'))
TEXT = construct.PascalString(
'text', length_field=construct.UBInt32('length'))
TIME = construct.Struct(
'timestamp',
construct.String('year', 4),
construct.String('month', 2),
construct.String('day', 2),
construct.String('hour', 2),
construct.String('minute', 2),
construct.String('second', 2),
construct.Padding(2))
TYPE_TEXT = construct.String('type', 4)
# TODO: add more protocols.
_PROTOCOL_TRANSLATION_DICT = {
u'htps': u'https',
u'smtp': u'smtp',
u'imap': u'imap',
u'http': u'http'}
def _GetTimestampFromEntry(self, parser_context, file_entry, structure):
"""Parse a time entry structure into a microseconds since Epoch in UTC.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
structure: TIME entry structure:
year: String with the number of the year.
month: String with the number of the month.
day: String with the number of the day.
hour: String with the number of the month.
minute: String with the number of the minute.
second: String with the number of the second.
Returns:
Microseconds since Epoch in UTC.
"""
try:
return timelib.Timestamp.FromTimeParts(
int(structure.year, 10), int(structure.month, 10),
int(structure.day, 10), int(structure.hour, 10),
int(structure.minute, 10), int(structure.second, 10))
except ValueError:
logging.warning(
u'[{0:s}] Invalid keychain time {1!s} in file: {2:s}'.format(
self.NAME, parser_context.GetDisplayName(file_entry), structure))
return 0
def _ReadEntryApplication(
self, parser_context, file_object, file_entry=None, parser_chain=None):
"""Extracts the information from an application password entry.
Args:
parser_context: A parser context object (instance of ParserContext).
file_object: A file-like object that points to an Keychain file.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
offset = file_object.tell()
try:
record = self.RECORD_HEADER_APP.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning((
u'[{0:s}] Unsupported record header at 0x{1:08x} in file: '
u'{2:s}').format(
self.NAME, offset, parser_context.GetDisplayName(file_entry)))
return
(ssgp_hash, creation_time, last_mod_time, text_description,
comments, entry_name, account_name) = self._ReadEntryHeader(
parser_context, file_entry, file_object, record.record_header, offset)
# Move to the end of the record, and then, prepared for the next record.
file_object.seek(
record.record_header.entry_length + offset - file_object.tell(),
os.SEEK_CUR)
event_object = KeychainApplicationRecordEvent(
creation_time, eventdata.EventTimestamp.CREATION_TIME,
entry_name, account_name, text_description, comments, ssgp_hash)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if creation_time != last_mod_time:
event_object = KeychainApplicationRecordEvent(
last_mod_time, eventdata.EventTimestamp.MODIFICATION_TIME,
entry_name, account_name, text_description, comments, ssgp_hash)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def _ReadEntryHeader(
self, parser_context, file_entry, file_object, record, offset):
"""Read the common record attributes.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
file_object: A file-like object that points to an Keychain file.
record: Structure with the header of the record.
offset: First byte of the record.
Returns:
A list of:
ssgp_hash: Hash of the encrypted data (passwd, cert, note).
creation_time: When the entry was created.
last_mod_time: Last time the entry was updated.
text_description: A brief description of the entry.
entry_name: Name of the entry
account_name: Name of the account.
"""
# Info: The hash header always start with the string ssgp follow by
# the hash. Furthermore The fields are always a multiple of four.
# Then if it is not multiple the value is padded by 0x00.
ssgp_hash = binascii.hexlify(file_object.read(record.ssgp_length)[4:])
file_object.seek(
record.creation_time - file_object.tell() + offset - 1, os.SEEK_CUR)
creation_time = self._GetTimestampFromEntry(
parser_context, file_entry, self.TIME.parse_stream(file_object))
file_object.seek(
record.last_mod_time - file_object.tell() + offset - 1, os.SEEK_CUR)
last_mod_time = self._GetTimestampFromEntry(
parser_context, file_entry, self.TIME.parse_stream(file_object))
# The comment field does not always contain data.
if record.text_description:
file_object.seek(
record.text_description - file_object.tell() + offset -1,
os.SEEK_CUR)
try:
text_description = self.TEXT.parse_stream(file_object)
except construct.FieldError:
text_description = u'N/A (error)'
else:
text_description = u'N/A'
# The comment field does not always contain data.
if record.comments:
file_object.seek(
record.text_description - file_object.tell() + offset -1,
os.SEEK_CUR)
try:
comments = self.TEXT.parse_stream(file_object)
except construct.FieldError:
comments = u'N/A (error)'
else:
comments = u'N/A'
file_object.seek(
record.entry_name - file_object.tell() + offset - 1, os.SEEK_CUR)
try:
entry_name = self.TEXT.parse_stream(file_object)
except construct.FieldError:
entry_name = u'N/A (error)'
file_object.seek(
record.account_name - file_object.tell() + offset - 1, os.SEEK_CUR)
try:
account_name = self.TEXT.parse_stream(file_object)
except construct.FieldError:
account_name = u'N/A (error)'
return (
ssgp_hash, creation_time, last_mod_time,
text_description, comments, entry_name, account_name)
def _ReadEntryInternet(
self, parser_context, file_object, file_entry=None, parser_chain=None):
"""Extracts the information from an Internet password entry.
Args:
parser_context: A parser context object (instance of ParserContext).
file_object: A file-like object that points to an Keychain file.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
offset = file_object.tell()
try:
record = self.RECORD_HEADER_INET.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning((
u'[{0:s}] Unsupported record header at 0x{1:08x} in file: '
u'{2:s}').format(
self.NAME, offset, parser_context.GetDisplayName(file_entry)))
return
(ssgp_hash, creation_time, last_mod_time, text_description,
comments, entry_name, account_name) = self._ReadEntryHeader(
parser_context, file_entry, file_object, record.record_header, offset)
if not record.where:
where = u'N/A'
protocol = u'N/A'
type_protocol = u'N/A'
else:
file_object.seek(
record.where - file_object.tell() + offset - 1, os.SEEK_CUR)
where = self.TEXT.parse_stream(file_object)
file_object.seek(
record.protocol - file_object.tell() + offset - 1, os.SEEK_CUR)
protocol = self.TYPE_TEXT.parse_stream(file_object)
file_object.seek(
record.type - file_object.tell() + offset - 1, os.SEEK_CUR)
type_protocol = self.TEXT.parse_stream(file_object)
type_protocol = self._PROTOCOL_TRANSLATION_DICT.get(
type_protocol, type_protocol)
if record.url:
file_object.seek(
record.url - file_object.tell() + offset - 1, os.SEEK_CUR)
url = self.TEXT.parse_stream(file_object)
where = u'{0:s}{1:s}'.format(where, url)
# Move to the end of the record, and then, prepared for the next record.
file_object.seek(
record.record_header.entry_length + offset - file_object.tell(),
os.SEEK_CUR)
event_object = KeychainInternetRecordEvent(
creation_time, eventdata.EventTimestamp.CREATION_TIME,
entry_name, account_name, text_description,
comments, where, protocol, type_protocol, ssgp_hash)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if creation_time != last_mod_time:
event_object = KeychainInternetRecordEvent(
last_mod_time, eventdata.EventTimestamp.MODIFICATION_TIME,
entry_name, account_name, text_description,
comments, where, protocol, type_protocol, ssgp_hash)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def _VerifyStructure(self, file_object):
"""Verify that we are dealing with an Keychain entry.
Args:
file_object: A file-like object that points to an Keychain file.
Returns:
A list of table positions if it is a keychain, None otherwise.
"""
# INFO: The HEADER KEYCHAIN:
# [DBHEADER] + [DBSCHEMA] + [OFFSET TABLE A] + ... + [OFFSET TABLE Z]
# Where the table offset is relative to the first byte of the DB Schema,
# then we must add to this offset the size of the [DBHEADER].
try:
db_header = self.KEYCHAIN_DB_HEADER.parse_stream(file_object)
except (IOError, construct.FieldError):
return
if (db_header.minor_version != self.KEYCHAIN_MINOR_VERSION or
db_header.major_version != self.KEYCHAIN_MAJOR_VERSION or
db_header.magic != self.KEYCHAIN_MAGIC_HEADER):
return
# Read the database schema and extract the offset for all the tables.
# They are ordered by file position from the top to the bottom of the file.
try:
db_schema = self.KEYCHAIN_DB_SCHEMA.parse_stream(file_object)
except (IOError, construct.FieldError):
return
table_offsets = []
for _ in range(db_schema.number_of_tables):
try:
table_offset = self.TABLE_OFFSET.parse_stream(file_object)
except (IOError, construct.FieldError):
return
table_offsets.append(table_offset + self.KEYCHAIN_DB_HEADER.sizeof())
return table_offsets
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from a Keychain file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
table_offsets = self._VerifyStructure(file_object)
if not table_offsets:
file_object.close()
raise errors.UnableToParseFile(u'The file is not a Keychain file.')
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
for table_offset in table_offsets:
# Skipping X bytes, unknown data at this point.
file_object.seek(table_offset - file_object.tell(), os.SEEK_CUR)
try:
table = self.TABLE_HEADER.parse_stream(file_object)
except construct.FieldError as exception:
logging.warning((
u'[{0:s}] Unable to parse table header in file: {1:s} '
u'with error: {2:s}.').format(
self.NAME, parser_context.GetDisplayName(file_entry),
exception))
continue
# Table_offset: absolute byte in the file where the table starts.
# table.first_record: first record in the table, relative to the
# first byte of the table.
file_object.seek(
table_offset + table.first_record - file_object.tell(), os.SEEK_CUR)
if table.record_type == self.RECORD_TYPE_INTERNET:
for _ in range(table.number_of_records):
self._ReadEntryInternet(
parser_context, file_object, file_entry=file_entry,
parser_chain=parser_chain)
elif table.record_type == self.RECORD_TYPE_APPLICATION:
for _ in range(table.number_of_records):
self._ReadEntryApplication(
parser_context, file_object, file_entry=file_entry,
parser_chain=parser_chain)
file_object.close()
manager.ParsersManager.RegisterParser(KeychainParser)
+107
View File
@@ -0,0 +1,107 @@
#!/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 Keychain password database parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mac_keychain as mac_keychain_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import test_lib
from plaso.parsers import mac_keychain
class MacKeychainParserTest(test_lib.ParserTestCase):
"""Tests for keychain file parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mac_keychain.KeychainParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['login.keychain'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 5)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-26 14:51:48')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(
event_object.timestamp_desc,
eventdata.EventTimestamp.CREATION_TIME)
self.assertEqual(event_object.entry_name, u'Secret Application')
self.assertEqual(event_object.account_name, u'moxilo')
expected_ssgp = (u'b8e44863af1cb0785b89681d22e2721997cc'
u'fb8adb8853e726aff94c8830b05a')
self.assertEqual(event_object.ssgp_hash, expected_ssgp)
self.assertEqual(event_object.text_description, u'N/A')
expected_msg = u'Name: Secret Application Account: moxilo'
expected_msg_short = u'Secret Application'
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[1]
self.assertEqual(
event_object.timestamp_desc,
eventdata.EventTimestamp.MODIFICATION_TIME)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-26 14:52:29')
self.assertEqual(event_object.timestamp, expected_timestamp)
event_object = event_objects[2]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-26 14:53:29')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.entry_name, u'Secret Note')
self.assertEqual(event_object.text_description, u'secure note')
self.assertEqual(len(event_object.ssgp_hash), 1696)
expected_msg = u'Name: Secret Note'
expected_msg_short = u'Secret Note'
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[3]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-26 14:54:33')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.entry_name, u'plaso.kiddaland.net')
self.assertEqual(event_object.account_name, u'MrMoreno')
expected_ssgp = (u'83ccacf55a8cb656d340ec405e9d8b308f'
u'ac54bb79c5c9b0219bd0d700c3c521')
self.assertEqual(event_object.ssgp_hash, expected_ssgp)
self.assertEqual(event_object.where, u'plaso.kiddaland.net')
self.assertEqual(event_object.protocol, u'http')
self.assertEqual(event_object.type_protocol, u'dflt')
self.assertEqual(event_object.text_description, u'N/A')
expected_msg = (u'Name: plaso.kiddaland.net Account: MrMoreno Where: '
u'plaso.kiddaland.net Protocol: http (dflt)')
expected_msg_short = u'plaso.kiddaland.net'
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+276
View File
@@ -0,0 +1,276 @@
#!/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.
"""This file contains the ASL securityd log plaintext parser."""
import datetime
import logging
import pyparsing
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
# INFO:
# http://opensource.apple.com/source/Security/Security-55471/sec/securityd/
class MacSecuritydLogEvent(time_events.TimestampEvent):
"""Convenience class for a ASL securityd line event."""
DATA_TYPE = 'mac:asl:securityd:line'
def __init__(
self, timestamp, structure, sender, sender_pid,
security_api, caller, message):
"""Initializes the event object.
Args:
timestamp: The timestamp time value, epoch.
structure: Structure with the parse fields.
level: String with the text representation of the priority level.
facility: String with the ASL facility.
sender: String with the name of the sender.
sender_pid: Process id of the sender.
security_api: Securityd function name.
caller: The caller field, a string containing two hex numbers.
message: String with the ASL message.
"""
super(MacSecuritydLogEvent, self).__init__(
timestamp,
eventdata.EventTimestamp.ADDED_TIME)
self.timestamp = timestamp
self.level = structure.level
self.sender_pid = sender_pid
self.facility = structure.facility
self.sender = sender
self.security_api = security_api
self.caller = caller
self.message = message
class MacSecuritydLogParser(text_parser.PyparsingSingleLineTextParser):
"""Parses the securityd file that contains logs from the security daemon."""
NAME = 'mac_securityd'
DESCRIPTION = u'Parser for Mac OS X securityd log files.'
ENCODING = u'utf-8'
# Default ASL Securityd log.
SECURITYD_LINE = (
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME.setResultsName('time') +
pyparsing.CharsNotIn(u'[').setResultsName('sender') +
pyparsing.Literal(u'[').suppress() +
text_parser.PyparsingConstants.PID.setResultsName('sender_pid') +
pyparsing.Literal(u']').suppress() +
pyparsing.Literal(u'<').suppress() +
pyparsing.CharsNotIn(u'>').setResultsName('level') +
pyparsing.Literal(u'>').suppress() +
pyparsing.Literal(u'[').suppress() +
pyparsing.CharsNotIn(u'{').setResultsName('facility') +
pyparsing.Literal(u'{').suppress() +
pyparsing.Optional(pyparsing.CharsNotIn(
u'}').setResultsName('security_api')) +
pyparsing.Literal(u'}').suppress() +
pyparsing.Optional(pyparsing.CharsNotIn(u']:').setResultsName('caller')) +
pyparsing.Literal(u']:').suppress() +
pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('message'))
# Repeated line.
REPEATED_LINE = (
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME.setResultsName('time') +
pyparsing.Literal(u'--- last message repeated').suppress() +
text_parser.PyparsingConstants.INTEGER.setResultsName('times') +
pyparsing.Literal(u'time ---').suppress())
# Define the available log line structures.
LINE_STRUCTURES = [
('logline', SECURITYD_LINE),
('repeated', REPEATED_LINE)]
def __init__(self):
"""Initializes a parser object."""
super(MacSecuritydLogParser, self).__init__()
self._year_use = 0
self._last_month = None
self.previous_structure = None
def VerifyStructure(self, parser_context, line):
"""Verify that this file is a ASL securityd log file.
Args:
parser_context: A parser context object (instance of ParserContext).
line: A single line from the text file.
Returns:
True if this is the correct parser, False otherwise.
"""
try:
line = self.SECURITYD_LINE.parseString(line)
except pyparsing.ParseException:
logging.debug(u'Not a ASL securityd log file')
return False
# Check if the day, month and time is valid taking a random year.
month = timelib.MONTH_DICT.get(line.month.lower())
if not month:
return False
if self._GetTimestamp(line.day, month, 2012, line.time) == 0:
return False
return True
def ParseRecord(self, parser_context, key, structure):
"""Parse each record structure and return an EventObject if applicable.
Args:
parser_context: A parser context object (instance of ParserContext).
key: An identification string indicating the name of the parsed
structure.
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
if key == 'repeated' or key == 'logline':
return self._ParseLogLine(parser_context, structure, key)
else:
logging.warning(
u'Unable to parse record, unknown structure: {0:s}'.format(key))
def _ParseLogLine(self, parser_context, structure, key):
"""Parse a logline and store appropriate attributes.
Args:
parser_context: A parser context object (instance of ParserContext).
key: An identification string indicating the name of the parsed
structure.
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
# TODO: improving this to get a valid year.
if not self._year_use:
self._year_use = parser_context.year
if not self._year_use:
# Get from the creation time of the file.
self._year_use = self._GetYear(
self.file_entry.GetStat(), parser_context.timezone)
# If fail, get from the current time.
if not self._year_use:
self._year_use = timelib.GetCurrentYear()
# Gap detected between years.
month = timelib.MONTH_DICT.get(structure.month.lower())
if not self._last_month:
self._last_month = month
if month < self._last_month:
self._year_use += 1
timestamp = self._GetTimestamp(
structure.day,
month,
self._year_use,
structure.time)
if not timestamp:
logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp))
return
self._last_month = month
if key == 'logline':
self.previous_structure = structure
message = structure.message
else:
times = structure.times
structure = self.previous_structure
message = u'Repeated {0:d} times: {1:s}'.format(
times, structure.message)
# It uses CarsNotIn structure which leaves whitespaces
# at the beginning of the sender and the caller.
sender = structure.sender.strip()
caller = structure.caller.strip()
if not caller:
caller = 'unknown'
if not structure.security_api:
security_api = u'unknown'
else:
security_api = structure.security_api
return MacSecuritydLogEvent(
timestamp, structure, sender, structure.sender_pid, security_api,
caller, message)
def _GetTimestamp(self, day, month, year, time):
"""Gets a timestamp from a pyparsing ParseResults timestamp.
This is a timestamp_string as returned by using
text_parser.PyparsingConstants structures:
08, Nov, [20, 36, 37]
Args:
day: An integer representing the day.
month: An integer representing the month.
year: An integer representing the year.
time: A list containing the hours, minutes, seconds.
Returns:
timestamp: A plaso timestamp.
"""
hours, minutes, seconds = time
return timelib.Timestamp.FromTimeParts(
year, month, day, hours, minutes, seconds)
def _GetYear(self, stat, zone):
"""Retrieves the year either from the input file or from the settings."""
time = getattr(stat, 'crtime', 0)
if not time:
time = getattr(stat, 'ctime', 0)
if not time:
current_year = timelib.GetCurrentYear()
logging.error((
u'Unable to determine year of log file.\nDefaulting to: '
u'{0:d}').format(current_year))
return current_year
try:
timestamp = datetime.datetime.fromtimestamp(time, zone)
except ValueError:
current_year = timelib.GetCurrentYear()
logging.error((
u'Unable to determine year of log file.\nDefaulting to: '
u'{0:d}').format(current_year))
return current_year
return timestamp.year
manager.ParsersManager.RegisterParser(MacSecuritydLogParser)
+159
View File
@@ -0,0 +1,159 @@
#!/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.
"""This file contains a unit test for ASL securityd log parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mac_securityd as mac_securityd_formatter
from plaso.lib import timelib_test
from plaso.parsers import mac_securityd as mac_securityd_parser
from plaso.parsers import test_lib
class MacSecurityUnitTest(test_lib.ParserTestCase):
"""A unit test for the ASL securityd log parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mac_securityd_parser.MacSecuritydLogParser()
def testParseFile(self):
"""Test parsing of a ASL securityd log file."""
knowledge_base_values = {'year': 2013}
test_file = self._GetTestFilePath(['security.log'])
events = self._ParseFile(
self._parser, test_file, knowledge_base_values=knowledge_base_values)
event_objects = self._GetEventObjectsFromQueue(events)
self.assertEqual(len(event_objects), 9)
event_object = event_objects[0]
expected_msg = (
u'Sender: secd (1) Level: Error Facility: user '
u'Text: securityd_xpc_dictionary_handler EscrowSecurityAl'
u'[3273] DeviceInCircle \xdeetta \xe6tti a\xf0 '
u'virka l\xedka, setja \xedslensku inn.')
expected_msg_short = (
u'Text: securityd_xpc_dictionary_handler '
u'EscrowSecurityAl[3273] DeviceInCircle ...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-02-26 19:11:56')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 1)
self.assertEqual(event_object.facility, u'user')
self.assertEqual(event_object.security_api, u'unknown')
self.assertEqual(event_object.caller, u'unknown')
self.assertEqual(event_object.level, u'Error')
expected_msg = (
u'securityd_xpc_dictionary_handler EscrowSecurityAl'
u'[3273] DeviceInCircle \xdeetta \xe6tti a\xf0 virka '
u'l\xedka, setja \xedslensku inn.')
self.assertEqual(event_object.message, expected_msg)
event_object = event_objects[1]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-26 19:11:57')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 11)
self.assertEqual(event_object.facility, u'serverxpc')
self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle')
self.assertEqual(event_object.caller, u'unknown')
self.assertEqual(event_object.level, u'Notice')
event_object = event_objects[2]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-26 19:11:58')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 111)
self.assertEqual(event_object.facility, u'user')
self.assertEqual(event_object.security_api, u'unknown')
self.assertEqual(event_object.caller, u'unknown')
self.assertEqual(event_object.level, u'Debug')
event_object = event_objects[3]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-26 19:11:59')
self.assertEqual(event_object.timestamp, 1388085119000000)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 1111)
self.assertEqual(event_object.facility, u'user')
self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle')
self.assertEqual(event_object.caller, u'C0x7fff872fa482')
self.assertEqual(event_object.level, u'Error')
event_object = event_objects[4]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-06 19:11:01')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 1)
self.assertEqual(event_object.facility, u'user')
self.assertEqual(event_object.security_api, u'unknown')
self.assertEqual(event_object.caller, u'unknown')
self.assertEqual(event_object.level, u'Error')
self.assertEqual(event_object.message, u'')
event_object = event_objects[5]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-06 19:11:02')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.sender, u'secd')
self.assertEqual(event_object.sender_pid, 11111)
self.assertEqual(event_object.facility, u'user')
self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle')
self.assertEqual(event_object.caller, u'C0x7fff872fa482 F0x106080db0')
self.assertEqual(event_object.level, u'Error')
self.assertEqual(event_object.message, u'')
event_object = event_objects[6]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-31 23:59:59')
self.assertEqual(event_object.timestamp, expected_timestamp)
event_object = event_objects[7]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-03-01 00:00:01')
self.assertEqual(event_object.timestamp, expected_timestamp)
# Repeated line.
event_object = event_objects[8]
expected_msg = u'Repeated 3 times: Happy new year!'
self.assertEqual(event_object.message, expected_msg)
if __name__ == '__main__':
unittest.main()
+280
View File
@@ -0,0 +1,280 @@
#!/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 the wifi.log (Mac OS X) parser."""
import datetime
import logging
import re
import pyparsing
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser
__author__ = 'Joaquin Moreno Garijo (bastionado@gmail.com)'
class MacWifiLogEvent(time_events.TimestampEvent):
"""Convenience class for a Mac Wifi log line event."""
DATA_TYPE = 'mac:wifilog:line'
def __init__(self, timestamp, agent, function, text, action):
"""Initializes the event object.
Args:
timestamp: The timestamp time value, epoch.
source_code: Details of the source code file generating the event.
log_level: The log level used for the event.
text: The log message
action: A string containing known WiFI actions, eg: connected to
an AP, configured, etc. If the action is not known,
the value is the message of the log (text variable).
"""
super(MacWifiLogEvent, self).__init__(
timestamp, eventdata.EventTimestamp.ADDED_TIME)
self.agent = agent
self.function = function
self.text = text
self.action = action
class MacWifiLogParser(text_parser.PyparsingSingleLineTextParser):
"""Parse text based on wifi.log file."""
NAME = 'macwifi'
DESCRIPTION = u'Parser for Mac OS X wifi.log files.'
ENCODING = u'utf-8'
# Regular expressions for known actions.
RE_CONNECTED = re.compile(r'Already\sassociated\sto\s(.*)\.\sBailing')
RE_WIFI_PARAMETERS = re.compile(
r'\[ssid=(.*?), bssid=(.*?), security=(.*?), rssi=')
# Define how a log line should look like.
WIFI_LINE = (
text_parser.PyparsingConstants.MONTH.setResultsName('day_of_week') +
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME_MSEC.setResultsName('time') +
pyparsing.Literal(u'<') +
pyparsing.CharsNotIn(u'>').setResultsName('agent') +
pyparsing.Literal(u'>') +
pyparsing.CharsNotIn(u':').setResultsName('function') +
pyparsing.Literal(u':') +
pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('text'))
WIFI_HEADER = (
text_parser.PyparsingConstants.MONTH.setResultsName('day_of_week') +
text_parser.PyparsingConstants.MONTH.setResultsName('month') +
text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') +
text_parser.PyparsingConstants.TIME_MSEC.setResultsName('time') +
pyparsing.Literal(u'***Starting Up***'))
# Define the available log line structures.
LINE_STRUCTURES = [
('logline', WIFI_LINE),
('header', WIFI_HEADER)]
def __init__(self):
"""Initializes a parser object."""
super(MacWifiLogParser, self).__init__()
self._year_use = 0
self._last_month = None
def _GetAction(self, agent, function, text):
"""Parse the well know actions for easy reading.
Args:
agent: The device that generate the entry.
function: The function or action called by the agent.
text: Mac Wifi log text.
Returns:
know_action: A formatted string representing the known (or common) action.
"""
if not agent.startswith('airportd'):
return text
if 'airportdProcessDLILEvent' in function:
interface = text.split()[0]
return u'Interface {0:s} turn up.'.format(interface)
if 'doAutoJoin' in function:
match = re.match(self.RE_CONNECTED, text)
if match:
ssid = match.group(1)[1:-1]
else:
ssid = 'Unknown'
return u'Wifi connected to SSID {0:s}'.format(ssid)
if 'processSystemPSKAssoc' in function:
wifi_parameters = self.RE_WIFI_PARAMETERS.search(text)
if wifi_parameters:
ssid = wifi_parameters.group(1)
bssid = wifi_parameters.group(2)
security = wifi_parameters.group(3)
if not ssid:
ssid = 'Unknown'
if not bssid:
bssid = 'Unknown'
if not security:
security = 'Unknown'
return (
u'New wifi configured. BSSID: {0:s}, SSID: {1:s}, '
u'Security: {2:s}.').format(bssid, ssid, security)
return text
def _GetTimestamp(self, day, month, year, time):
"""Gets a timestamp from a pyparsing ParseResults timestamp.
This is a timestamp_string as returned by using
text_parser.PyparsingConstants structures:
08, Nov, [20, 36, 37], 222]
Args:
timestamp_string: The pyparsing ParseResults object
Returns:
day: An integer representing the day.
month: An integer representing the month.
year: An integer representing the year.
timestamp: A plaso timelib timestamp event or 0.
"""
try:
time_part, millisecond = time
hour, minute, second = time_part
timestamp = timelib.Timestamp.FromTimeParts(
year, month, day, hour, minute, second,
microseconds=(millisecond * 1000))
except ValueError:
timestamp = 0
return timestamp
def _GetYear(self, stat, zone):
"""Retrieves the year either from the input file or from the settings."""
time = getattr(stat, 'crtime', 0)
if not time:
time = getattr(stat, 'ctime', 0)
if not time:
logging.error(
('Unable to determine correct year of syslog file, using current '
'year'))
return timelib.GetCurrentYear()
try:
timestamp = datetime.datetime.fromtimestamp(time, zone)
except ValueError as exception:
logging.error((
u'Unable to determine correct year of syslog file, using current '
u'one, with error: {0:s}').format(exception))
return timelib.GetCurrentYear()
return timestamp.year
def _ParseLogLine(self, parser_context, structure):
"""Parse a logline and store appropriate attributes.
Args:
parser_context: A parser context object (instance of ParserContext).
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
# TODO: improving this to get a valid year.
if not self._year_use:
self._year_use = parser_context.year
if not self._year_use:
# Get from the creation time of the file.
self._year_use = self._GetYear(
self.file_entry.GetStat(), parser_context.timezone)
# If fail, get from the current time.
if not self._year_use:
self._year_use = timelib.GetCurrentYear()
# Gap detected between years.
month = timelib.MONTH_DICT.get(structure.month.lower())
if not self._last_month:
self._last_month = month
if month < self._last_month:
self._year_use += 1
timestamp = self._GetTimestamp(
structure.day,
month,
self._year_use,
structure.time)
if not timestamp:
logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp))
return
self._last_month = month
text = structure.text
# Due to the use of CharsNotIn pyparsing structure contains whitespaces
# that need to be removed.
function = structure.function.strip()
action = self._GetAction(structure.agent, function, text)
return MacWifiLogEvent(
timestamp, structure.agent, function, text, action)
def ParseRecord(self, parser_context, key, structure):
"""Parse each record structure and return an EventObject if applicable.
Args:
parser_context: A parser context object (instance of ParserContext).
key: An identification string indicating the name of the parsed
structure.
structure: A pyparsing.ParseResults object from a line in the
log file.
Returns:
An event object (instance of EventObject) or None.
"""
if key == 'logline':
return self._ParseLogLine(parser_context, structure)
elif key != 'header':
logging.warning(
u'Unable to parse record, unknown structure: {0:s}'.format(key))
def VerifyStructure(self, parser_context, line):
"""Verify that this file is a Mac Wifi log file.
Args:
parser_context: A parser context object (instance of ParserContext).
line: A single line from the text file.
Returns:
True if this is the correct parser, False otherwise.
"""
try:
_ = self.WIFI_HEADER.parseString(line)
except pyparsing.ParseException:
logging.debug(u'Not a Mac Wifi log file')
return False
return True
manager.ParsersManager.RegisterParser(MacWifiLogParser)
+134
View File
@@ -0,0 +1,134 @@
#!/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 Mac wifi.log parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mac_wifi as mac_wifi_formatter
from plaso.lib import timelib_test
from plaso.parsers import mac_wifi
from plaso.parsers import test_lib
class MacWifiUnitTest(test_lib.ParserTestCase):
"""Tests for the Mac wifi.log parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mac_wifi.MacWifiLogParser()
def testParse(self):
"""Tests the Parse function."""
knowledge_base_values = {'year': 2013}
test_file = self._GetTestFilePath(['wifi.log'])
event_queue_consumer = self._ParseFile(
self._parser, test_file, knowledge_base_values=knowledge_base_values)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEqual(len(event_objects), 9)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-14 20:36:37.222')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.agent, u'airportd[88]')
self.assertEqual(event_object.function, u'airportdProcessDLILEvent')
self.assertEqual(event_object.action, u'Interface en0 turn up.')
self.assertEqual(event_object.text, u'en0 attached (up)')
expected_msg = (
u'Action: Interface en0 turn up. '
u'(airportdProcessDLILEvent) '
u'Log: en0 attached (up)')
expected_msg_short = (
u'Action: Interface en0 turn up.')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[1]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-14 20:36:43.818')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.agent, u'airportd[88]')
self.assertEqual(event_object.function, u'_doAutoJoin')
self.assertEqual(event_object.action, u'Wifi connected to SSID CampusNet')
expected_text = (
u'Already associated to \u201cCampusNet\u201d. Bailing on auto-join.')
self.assertEqual(event_object.text, expected_text)
event_object = event_objects[2]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-14 21:50:52.395')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.agent, u'airportd[88]')
self.assertEqual(event_object.function, u'_handleLinkEvent')
expected_string = (
u'Unable to process link event, op mode request returned -3903 '
u'(Operation not supported)')
self.assertEqual(event_object.action, expected_string)
self.assertEqual(event_object.text, expected_string)
event_object = event_objects[5]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-14 21:52:09.883')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(u'airportd[88]', event_object.agent)
self.assertEqual(u'_processSystemPSKAssoc', event_object.function)
expected_action = (
u'New wifi configured. BSSID: 88:30:8a:7a:61:88, SSID: AndroidAP, '
u'Security: WPA2 Personal.')
self.assertEqual(event_object.action, expected_action)
expected_text = (
u'No password for network <CWNetwork: 0x7fdfe970b250> '
u'[ssid=AndroidAP, bssid=88:30:8a:7a:61:88, security=WPA2 '
u'Personal, rssi=-21, channel=<CWChannel: 0x7fdfe9712870> '
u'[channelNumber=11(2GHz), channelWidth={20MHz}], ibss=0] '
u'in the system keychain')
self.assertEqual(event_object.text, expected_text)
event_object = event_objects[7]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-31 23:59:38.165')
self.assertEqual(event_object.timestamp, expected_timestamp)
event_object = event_objects[8]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2014-01-01 01:12:17.311')
self.assertEqual(event_object.timestamp, expected_timestamp)
if __name__ == '__main__':
unittest.main()
+153
View File
@@ -0,0 +1,153 @@
#!/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.
"""Parser for the Sleuthkit (TSK) bodyfile or mactime format.
The format specifications can be read here:
http://wiki.sleuthkit.org/index.php?title=Body_file
"""
import re
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import manager
from plaso.parsers import text_parser
class MactimeEvent(time_events.PosixTimeEvent):
"""Convenience class for a mactime-based event."""
DATA_TYPE = 'fs:mactime:line'
def __init__(self, posix_time, usage, row_offset, data):
"""Initializes a mactime-based event object.
Args:
posix_time: The POSIX time value.
usage: The description of the usage of the time value.
row_offset: The offset of the row.
data: A dict object containing extracted data from the body file.
"""
super(MactimeEvent, self).__init__(posix_time, usage)
self.offset = row_offset
self.user_sid = unicode(data.get('uid', u''))
self.user_gid = data.get('gid', None)
self.md5 = data.get('md5', None)
self.filename = data.get('name', 'N/A')
# Check if the filename field is not a string, eg in the instances where a
# filename only conists of numbers. In that case the self.filename field
# becomes an integer value instead of a string value. That causes issues
# later in the process, where we expect the filename value to be a string.
if not isinstance(self.filename, basestring):
self.filename = unicode(self.filename)
self.mode_as_string = data.get('mode_as_string', None)
self.size = data.get('size', None)
inode_number = data.get('inode', 0)
if isinstance(inode_number, basestring):
if '-' in inode_number:
inode_number, _, _ = inode_number.partition('-')
try:
inode_number = int(inode_number, 10)
except ValueError:
inode_number = 0
self.inode = inode_number
class MactimeParser(text_parser.TextCSVParser):
"""Parses SleuthKit's mactime bodyfiles."""
NAME = 'mactime'
DESCRIPTION = u'Parser for SleuthKit\'s mactime bodyfiles.'
COLUMNS = [
'md5', 'name', 'inode', 'mode_as_string', 'uid', 'gid', 'size',
'atime', 'mtime', 'ctime', 'crtime']
VALUE_SEPARATOR = '|'
MD5_RE = re.compile('^[0-9a-fA-F]+$')
_TIMESTAMP_DESC_MAP = {
'atime': eventdata.EventTimestamp.ACCESS_TIME,
'crtime': eventdata.EventTimestamp.CREATION_TIME,
'ctime': eventdata.EventTimestamp.CHANGE_TIME,
'mtime': eventdata.EventTimestamp.MODIFICATION_TIME,
}
def VerifyRow(self, unused_parser_context, row):
"""Verify we are dealing with a mactime bodyfile.
Args:
parser_context: A parser context object (instance of ParserContext).
row: A single row from the CSV file.
Returns:
True if this is the correct parser, False otherwise.
"""
if not self.MD5_RE.match(row['md5']):
return False
try:
# Verify that the "size" field is an integer, thus cast it to int
# and then back to string so it can be compared, if the value is
# not a string representation of an integer, eg: '12a' then this
# conversion will fail and we return a False value.
if str(int(row.get('size', '0'), 10)) != row.get('size', None):
return False
except ValueError:
return False
# TODO: Add additional verification.
return True
def ParseRow(
self, parser_context, row_offset, row, file_entry=None,
parser_chain=None):
"""Parses a row and extract event objects.
Args:
parser_context: A parser context object (instance of ParserContext).
row_offset: The offset of the row.
row: A dictionary containing all the fields as denoted in the
COLUMNS class list.
file_entry: optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
for key, value in row.iteritems():
if isinstance(row[key], basestring):
try:
row[key] = int(value, 10)
except ValueError:
pass
for key, timestamp_description in self._TIMESTAMP_DESC_MAP.iteritems():
value = row.get(key, None)
if not value:
continue
event_object = MactimeEvent(
value, timestamp_description, row_offset, row)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(MactimeParser)
+105
View File
@@ -0,0 +1,105 @@
#!/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 the for mactime parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mactime as mactime_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import mactime
from plaso.parsers import test_lib
from plaso.serializer import protobuf_serializer
class MactimeUnitTest(test_lib.ParserTestCase):
"""Tests the for mactime parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mactime.MactimeParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['mactime.body'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The file contains 13 lines x 4 timestamps per line, which should be
# 52 events in total. However several of these events have an empty
# timestamp value and are omitted.
# Total entries: 11 * 3 + 2 * 4 = 41
self.assertEquals(len(event_objects), 41)
# Test this entry:
# 0|/a_directory/another_file|16|r/rrw-------|151107|5000|22|1337961583|
# 1337961584|1337961585|0
event_object = event_objects[6]
expected_timestamp = timelib_test.CopyStringToTimestamp(
u'2012-05-25 15:59:43+00:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME)
self.assertEquals(event_object.inode, 16)
event_object = event_objects[6]
expected_timestamp = timelib_test.CopyStringToTimestamp(
u'2012-05-25 15:59:43+00:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME)
expected_string = u'/a_directory/another_file'
self._TestGetMessageStrings(event_object, expected_string, expected_string)
event_object = event_objects[8]
expected_timestamp = timelib_test.CopyStringToTimestamp(
u'2012-05-25 15:59:44+00:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME)
event_object = event_objects[7]
expected_timestamp = timelib_test.CopyStringToTimestamp(
u'2012-05-25 15:59:45+00:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.CHANGE_TIME)
self.assertEquals(event_object.filename, u'/a_directory/another_file')
self.assertEquals(event_object.mode_as_string, u'r/rrw-------')
event_object = event_objects[37]
self.assertEquals(event_object.inode, 4)
# Serialize the event objects.
serialized_events = []
serializer = protobuf_serializer.ProtobufEventObjectSerializer
for event_object in event_objects:
serialized_events.append(serializer.WriteSerialized(event_object))
self.assertEquals(len(serialized_events), len(event_objects))
if __name__ == '__main__':
unittest.main()
+205
View File
@@ -0,0 +1,205 @@
#!/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 parsers and plugins manager objects."""
from plaso.frontend import presets
class ParsersManager(object):
"""Class that implements the parsers manager."""
_parser_classes = {}
@classmethod
def DeregisterParser(cls, parser_class):
"""Deregisters a parser class.
The parser classes are identified based on their lower case name.
Args:
parser_class: the class object of the parser.
Raises:
KeyError: if parser class is not set for the corresponding name.
"""
parser_name = parser_class.NAME.lower()
if parser_name not in cls._parser_classes:
raise KeyError(
u'Parser class not set for name: {0:s}.'.format(
parser_class.NAME))
del cls._parser_classes[parser_name]
@classmethod
def GetFilterListsFromString(cls, parser_filter_string):
"""Determines an include and exclude list of parser and plugin names.
Takes a comma separated string and splits it up into two lists,
of parsers or plugins to include and to exclude from selection.
If a particular filter is prepended with a minus sign it will
be included in the exclude section, otherwise in the include.
Args:
parser_filter_string: The parser filter string.
Returns:
A tuple of two lists, include and exclude.
"""
includes = []
excludes = []
preset_categories = presets.categories.keys()
for filter_string in parser_filter_string.split(','):
filter_string = filter_string.strip()
if not filter_string:
continue
if filter_string.startswith('-'):
active_list = excludes
filter_string = filter_string[1:]
else:
active_list = includes
filter_string = filter_string.lower()
if filter_string in cls._parser_classes:
parser_class = cls._parser_classes[filter_string]
active_list.append(filter_string)
if parser_class.SupportsPlugins():
active_list.extend(parser_class.GetPluginNames())
elif filter_string in preset_categories:
active_list.extend(
presets.GetParsersFromCategory(filter_string))
else:
active_list.append(filter_string)
return includes, excludes
@classmethod
def GetParserNames(cls, parser_filter_string=None):
"""Retrieves the parser names.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Returns:
A list of parser names.
"""
parser_names = []
for parser_name, _ in cls.GetParsers(
parser_filter_string=parser_filter_string):
parser_names.append(parser_name)
return parser_names
@classmethod
def GetParserObjects(cls, parser_filter_string=None):
"""Retrieves the parser objects.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Returns:
A list of parser objects (instances of BaseParser).
"""
parser_objects = []
for _, parser_class in cls.GetParsers(
parser_filter_string=parser_filter_string):
parser_object = parser_class()
parser_objects.append(parser_object)
return parser_objects
@classmethod
def GetParsers(cls, parser_filter_string=None):
"""Retrieves the registered parsers.
Args:
parser_filter_string: Optional parser filter string. The default is None.
Yields:
A tuple that contains the uniquely identifying name of the parser
and the parser class (subclass of BaseParser).
"""
if parser_filter_string:
includes, excludes = cls.GetFilterListsFromString(parser_filter_string)
else:
includes = None
excludes = None
for parser_name, parser_class in cls._parser_classes.iteritems():
if excludes and parser_name in excludes:
continue
if includes and parser_name not in includes:
continue
yield parser_name, parser_class
@classmethod
def GetWindowsRegistryPlugins(cls):
"""Build a list of all available Windows Registry plugins.
Returns:
A plugins list (instance of PluginList).
"""
parser_class = cls._parser_classes.get('winreg', None)
if not parser_class:
return
return parser_class.GetPluginList()
@classmethod
def RegisterParser(cls, parser_class):
"""Registers a parser class.
The parser classes are identified based on their lower case name.
Args:
parser_class: the class object of the parser.
Raises:
KeyError: if parser class is already set for the corresponding name.
"""
parser_name = parser_class.NAME.lower()
if parser_name in cls._parser_classes:
raise KeyError((
u'Parser class already set for name: {0:s}.').format(
parser_class.NAME))
cls._parser_classes[parser_name] = parser_class
@classmethod
def RegisterParsers(cls, parser_classes):
"""Registers parser classes.
The parser classes are identified based on their lower case name.
Args:
parser_classes: a list of class objects of the parsers.
Raises:
KeyError: if parser class is already set for the corresponding name.
"""
for parser_class in parser_classes:
cls.RegisterParser(parser_class)
+152
View File
@@ -0,0 +1,152 @@
#!/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 parsers manager."""
import unittest
from plaso.parsers import interface
from plaso.parsers import manager
from plaso.parsers import plugins
class TestParser(interface.BaseParser):
"""Test parser."""
NAME = 'test_parser'
DESCRIPTION = u'Test parser.'
def Parse(self, unused_parser_context, unused_file_entry, parser_chain=None):
"""Parsers the file entry and extracts event objects.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
return
class TestParserWithPlugins(interface.BasePluginsParser):
"""Test parser with plugins."""
NAME = 'test_parser_with_plugins'
DESCRIPTION = u'Test parser with plugins.'
_plugin_classes = {}
def Parse(self, unused_parser_context, unused_file_entry, parser_chain=None):
"""Parsers the file entry and extracts event objects.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
return
class TestPlugin(plugins.BasePlugin):
"""Test plugin."""
NAME = 'test_plugin'
DESCRIPTION = u'Test plugin.'
def Process(self, unused_parser_context, unused_parser_chain=None, **kwargs):
"""Evaluates if this is the correct plugin and processes data accordingly.
Args:
parser_context: A parser context object (instance of ParserContext).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
kwargs: Depending on the plugin they may require different sets of
arguments to be able to evaluate whether or not this is
the correct plugin.
Raises:
ValueError: When there are unused keyword arguments.
"""
return
class ParsersManagerTest(unittest.TestCase):
"""Tests for the parsers manager."""
def testParserRegistration(self):
"""Tests the RegisterParser and DeregisterParser functions."""
# pylint: disable=protected-access
number_of_parsers = len(manager.ParsersManager._parser_classes)
manager.ParsersManager.RegisterParser(TestParser)
self.assertEquals(
len(manager.ParsersManager._parser_classes),
number_of_parsers + 1)
with self.assertRaises(KeyError):
manager.ParsersManager.RegisterParser(TestParser)
manager.ParsersManager.DeregisterParser(TestParser)
self.assertEquals(
len(manager.ParsersManager._parser_classes),
number_of_parsers)
def testPluginRegistration(self):
"""Tests the RegisterPlugin and DeregisterPlugin functions."""
TestParserWithPlugins.RegisterPlugin(TestPlugin)
# pylint: disable=protected-access
self.assertEquals(
len(TestParserWithPlugins._plugin_classes), 1)
with self.assertRaises(KeyError):
TestParserWithPlugins.RegisterPlugin(TestPlugin)
TestParserWithPlugins.DeregisterPlugin(TestPlugin)
self.assertEquals(
len(TestParserWithPlugins._plugin_classes), 0)
def testGetFilterListsFromString(self):
"""Tests the GetFilterListsFromString function."""
TestParserWithPlugins.RegisterPlugin(TestPlugin)
manager.ParsersManager.RegisterParser(TestParserWithPlugins)
manager.ParsersManager.RegisterParser(TestParser)
includes, excludes = manager.ParsersManager.GetFilterListsFromString(
'test_parser')
self.assertEquals(includes, ['test_parser'])
self.assertEquals(excludes, [])
includes, excludes = manager.ParsersManager.GetFilterListsFromString(
'-test_parser')
self.assertEquals(includes, [])
self.assertEquals(excludes, ['test_parser'])
includes, excludes = manager.ParsersManager.GetFilterListsFromString(
'test_parser_with_plugins')
self.assertEquals(includes, ['test_parser_with_plugins', 'test_plugin'])
TestParserWithPlugins.DeregisterPlugin(TestPlugin)
manager.ParsersManager.DeregisterParser(TestParserWithPlugins)
manager.ParsersManager.DeregisterParser(TestParser)
if __name__ == '__main__':
unittest.main()
+141
View File
@@ -0,0 +1,141 @@
#!/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.
"""Parser for McAfee Anti-Virus Logs.
McAfee AV uses 4 logs to track when scans were run, when virus databases were
updated, and when files match the virus database."""
import logging
from plaso.events import text_events
from plaso.lib import timelib
from plaso.parsers import manager
from plaso.parsers import text_parser
class McafeeAVEvent(text_events.TextEvent):
"""Convenience class for McAfee AV Log events """
DATA_TYPE = 'av:mcafee:accessprotectionlog'
def __init__(self, timestamp, offset, attributes):
"""Initializes a McAfee AV Log Event.
Args:
timestamp: The timestamp time value. The timestamp contains the
number of seconds since Jan 1, 1970 00:00:00 UTC.
offset: The offset of the attributes.
attributes: Dict of elements from the AV log line.
"""
del attributes['time']
del attributes['date']
super(McafeeAVEvent, self).__init__(timestamp, offset, attributes)
self.full_path = attributes['filename']
class McafeeAccessProtectionParser(text_parser.TextCSVParser):
"""Parses the McAfee AV Access Protection Log."""
NAME = 'mcafee_protection'
DESCRIPTION = u'Parser for McAfee AV Access Protection log files.'
VALUE_SEPARATOR = '\t'
# Define the columns of the McAfee AV Access Protection Log.
COLUMNS = ['date', 'time', 'status', 'username', 'filename',
'trigger_location', 'rule', 'action']
def _GetTimestamp(self, date, time, timezone):
"""Return a 64-bit signed timestamp in microseconds since Epoch.
The timestamp is made up of two strings, the date and the time, separated
by a tab. The time is in local time. The month and day can be either 1 or 2
characters long. E.g.: 7/30/2013\t10:22:48 AM
Args:
date: The string representing the date.
time: The string representing the time.
timezone: The timezone object.
Returns:
A plaso timestamp value, microseconds since Epoch in UTC or None.
"""
if not (date and time):
logging.warning('Unable to extract timestamp from McAfee AV logline.')
return
# TODO: Figure out how McAfee sets Day First and use that here.
# The in-file time format is '07/30/2013\t10:22:48 AM'.
return timelib.Timestamp.FromTimeString(
u'{0:s} {1:s}'.format(date, time), timezone=timezone)
def VerifyRow(self, parser_context, row):
"""Verify that this is a McAfee AV Access Protection Log file.
Args:
parser_context: A parser context object (instance of ParserContext).
row: A single row from the CSV file.
Returns:
True if this is the correct parser, False otherwise.
"""
if len(row) != 8:
return False
# This file can have a UTF-8 byte-order-marker at the beginning of
# the first row.
# TODO: Find out all the code pages this can have. Asked McAfee 10/31.
if row['date'][0:3] == '\xef\xbb\xbf':
row['date'] = row['date'][3:]
# Check the date format!
# If it doesn't pass, then this isn't a McAfee AV Access Protection Log
try:
self._GetTimestamp(row['date'], row['time'], parser_context.timezone)
except (TypeError, ValueError):
return False
# Use the presence of these strings as a backup or in case of partial file.
if (not 'Access Protection' in row['status'] and
not 'Would be blocked' in row['status']):
return False
return True
def ParseRow(
self, parser_context, row_offset, row, file_entry=None,
parser_chain=None):
"""Parses a row and extract event objects.
Args:
parser_context: A parser context object (instance of ParserContext).
row_offset: The offset of the row.
row: A dictionary containing all the fields as denoted in the
COLUMNS class list.
file_entry: optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
timestamp = self._GetTimestamp(
row['date'], row['time'], parser_context.timezone)
event_object = McafeeAVEvent(timestamp, row_offset, row)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(McafeeAccessProtectionParser)
+79
View File
@@ -0,0 +1,79 @@
#!/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 McAfee AV Log parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import mcafeeav as mcafeeav_formatter
from plaso.parsers import mcafeeav
from plaso.parsers import test_lib
class McafeeAccessProtectionUnitTest(test_lib.ParserTestCase):
"""Tests for the McAfee AV Log parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = mcafeeav.McafeeAccessProtectionParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['AccessProtectionLog.txt'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# The file contains 14 lines which results in 14 event objects.
self.assertEquals(len(event_objects), 14)
# Test that the UTF-8 byte order mark gets removed from the first line.
event_object = event_objects[0]
self.assertEquals(event_object.timestamp, 1380292946000000)
# Test this entry:
# 9/27/2013 2:42:26 PM Blocked by Access Protection rule
# SOMEDOMAIN\someUser C:\Windows\System32\procexp64.exe C:\Program Files
# (x86)\McAfee\Common Framework\UdaterUI.exe Common Standard
# Protection:Prevent termination of McAfee processes Action blocked :
# Terminate
event_object = event_objects[1]
self.assertEquals(event_object.timestamp, 1380292959000000)
self.assertEquals(event_object.username, u'SOMEDOMAIN\\someUser')
self.assertEquals(
event_object.full_path, u'C:\\Windows\\System32\\procexp64.exe')
expected_msg = (
u'File Name: C:\\Windows\\System32\\procexp64.exe '
u'User: SOMEDOMAIN\\someUser '
u'C:\\Program Files (x86)\\McAfee\\Common Framework\\Frame'
u'workService.exe '
u'Blocked by Access Protection rule '
u'Common Standard Protection:Prevent termination of McAfee processes '
u'Action blocked : Terminate')
expected_msg_short = (
u'C:\\Windows\\System32\\procexp64.exe '
u'Action blocked : Terminate')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+233
View File
@@ -0,0 +1,233 @@
#!/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.
"""Parser for Microsoft Internet Explorer (MSIE) Cache Files (CF)."""
import logging
import pymsiecf
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
if pymsiecf.get_version() < '20130317':
raise ImportWarning(u'MsiecfParser requires at least pymsiecf 20130317.')
class MsiecfUrlEvent(time_events.TimestampEvent):
"""Convenience class for an MSIECF URL event."""
DATA_TYPE = 'msiecf:url'
def __init__(
self, timestamp, timestamp_description, msiecf_item, recovered=False):
"""Initializes the event.
Args:
timestamp: The timestamp value.
timestamp_desc: The usage string describing the timestamp.
msiecf_item: The MSIECF item (pymsiecf.url).
recovered: Boolean value to indicate the item was recovered, False
by default.
"""
super(MsiecfUrlEvent, self).__init__(timestamp, timestamp_description)
self.recovered = recovered
self.offset = msiecf_item.offset
self.url = msiecf_item.location
self.number_of_hits = msiecf_item.number_of_hits
self.cache_directory_index = msiecf_item.cache_directory_index
self.filename = msiecf_item.filename
self.cached_file_size = msiecf_item.cached_file_size
if msiecf_item.type and msiecf_item.data:
if msiecf_item.type == u'cache':
if msiecf_item.data[:4] == 'HTTP':
self.http_headers = msiecf_item.data[:-1]
# TODO: parse data of other URL item type like history which requires
# OLE VT parsing.
class MsiecfParser(interface.BaseParser):
"""Parses MSIE Cache Files (MSIECF)."""
NAME = 'msiecf'
DESCRIPTION = u'Parser for MSIE Cache Files (MSIECF) also known as index.dat.'
def _ParseUrl(
self, parser_context, msiecf_item, file_entry=None, parser_chain=None,
recovered=False):
"""Extract data from a MSIE Cache Files (MSIECF) URL item.
Every item is stored as an event object, one for each timestamp.
Args:
parser_context: A parser context object (instance of ParserContext).
msiecf_item: An item (pymsiecf.url).
file_entry: optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
recovered: Boolean value to indicate the item was recovered, False
by default.
"""
# The secondary timestamp can be stored in either UTC or local time
# this is dependent on what the index.dat file is used for.
# Either the file path of the location string can be used to distinguish
# between the different type of files.
primary_timestamp = timelib.Timestamp.FromFiletime(
msiecf_item.get_primary_time_as_integer())
primary_timestamp_desc = 'Primary Time'
# Need to convert the FILETIME to the internal timestamp here to
# do the from localtime conversion.
secondary_timestamp = timelib.Timestamp.FromFiletime(
msiecf_item.get_secondary_time_as_integer())
secondary_timestamp_desc = 'Secondary Time'
if msiecf_item.type:
if msiecf_item.type == u'cache':
primary_timestamp_desc = eventdata.EventTimestamp.ACCESS_TIME
secondary_timestamp_desc = eventdata.EventTimestamp.MODIFICATION_TIME
elif msiecf_item.type == u'cookie':
primary_timestamp_desc = eventdata.EventTimestamp.ACCESS_TIME
secondary_timestamp_desc = eventdata.EventTimestamp.MODIFICATION_TIME
elif msiecf_item.type == u'history':
primary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
elif msiecf_item.type == u'history-daily':
primary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
# The secondary_timestamp is in localtime normalize it to be in UTC.
secondary_timestamp = timelib.Timestamp.LocaltimeToUTC(
secondary_timestamp, parser_context.timezone)
elif msiecf_item.type == u'history-weekly':
primary_timestamp_desc = eventdata.EventTimestamp.CREATION_TIME
secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
# The secondary_timestamp is in localtime normalize it to be in UTC.
secondary_timestamp = timelib.Timestamp.LocaltimeToUTC(
secondary_timestamp, parser_context.timezone)
event_object = MsiecfUrlEvent(
primary_timestamp, primary_timestamp_desc, msiecf_item, recovered)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if secondary_timestamp > 0:
event_object = MsiecfUrlEvent(
secondary_timestamp, secondary_timestamp_desc, msiecf_item,
recovered)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
expiration_timestamp = msiecf_item.get_expiration_time_as_integer()
if expiration_timestamp > 0:
# The expiration time in MSIECF version 4.7 is stored as a FILETIME value
# in version 5.2 it is stored as a FAT date time value.
# Since the as_integer function returns the raw integer value we need to
# apply the right conversion here.
if self.version == u'4.7':
event_object = MsiecfUrlEvent(
timelib.Timestamp.FromFiletime(expiration_timestamp),
eventdata.EventTimestamp.EXPIRATION_TIME, msiecf_item, recovered)
else:
event_object = MsiecfUrlEvent(
timelib.Timestamp.FromFatDateTime(expiration_timestamp),
eventdata.EventTimestamp.EXPIRATION_TIME, msiecf_item, recovered)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
last_checked_timestamp = msiecf_item.get_last_checked_time_as_integer()
if last_checked_timestamp > 0:
event_object = MsiecfUrlEvent(
timelib.Timestamp.FromFatDateTime(last_checked_timestamp),
eventdata.EventTimestamp.LAST_CHECKED_TIME, msiecf_item, recovered)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from a MSIE Cache File (MSIECF).
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
msiecf_file = pymsiecf.file()
msiecf_file.set_ascii_codepage(parser_context.codepage)
try:
msiecf_file.open_file_object(file_object)
self.version = msiecf_file.format_version
except IOError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
for item_index in range(0, msiecf_file.number_of_items):
try:
msiecf_item = msiecf_file.get_item(item_index)
if isinstance(msiecf_item, pymsiecf.url):
self._ParseUrl(
parser_context, msiecf_item, file_entry=file_entry,
parser_chain=parser_chain)
# TODO: implement support for pymsiecf.leak, pymsiecf.redirected,
# pymsiecf.item.
except IOError as exception:
logging.warning(
u'[{0:s}] unable to parse item: {1:d} in file: {2:s}: {3:s}'.format(
self.NAME, item_index, file_entry.name, exception))
for item_index in range(0, msiecf_file.number_of_recovered_items):
try:
msiecf_item = msiecf_file.get_recovered_item(item_index)
if isinstance(msiecf_item, pymsiecf.url):
self._ParseUrl(
parser_context, msiecf_item, file_entry=file_entry,
parser_chain=parser_chain, recovered=True)
# TODO: implement support for pymsiecf.leak, pymsiecf.redirected,
# pymsiecf.item.
except IOError as exception:
logging.info((
u'[{0:s}] unable to parse recovered item: {1:d} in file: {2:s}: '
u'{3:s}').format(
self.NAME, item_index, file_entry.name, exception))
file_object.close()
manager.ParsersManager.RegisterParser(MsiecfParser)
+113
View File
@@ -0,0 +1,113 @@
#!/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 Microsoft Internet Explorer (MSIE) Cache Files (CF) parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import msiecf as msiecf_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import msiecf
from plaso.parsers import test_lib
class MsiecfParserTest(test_lib.ParserTestCase):
"""Tests for the MSIE Cache Files (MSIECF) parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = msiecf.MsiecfParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['index.dat'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# MSIE Cache File information:
# File size: 32768 bytes
# Number of items: 7
# Number of recovered items: 11
# 7 + 11 records, each with 4 records.
self.assertEquals(len(event_objects), (7 + 11) * 4)
# Record type : URL
# Offset range : 21376 - 21632 (256)
# Location : Visited: testing@http://www.trafficfusionx.com
# /download/tfscrn2/funnycats.exe
# Primary time : Jun 23, 2011 18:02:10.066000000
# Secondary time : Jun 23, 2011 18:02:10.066000000
# Expiration time : Jun 29, 2011 17:55:02
# Last checked time : Jun 23, 2011 18:02:12
# Cache directory index : -2 (0xfe)
event_object = event_objects[8]
expected_location = (
u'Visited: testing@http://www.trafficfusionx.com/download/tfscrn2'
u'/funnycats.exe')
self.assertEquals(event_object.offset, 21376)
self.assertEquals(event_object.url, expected_location)
self.assertEquals(event_object.cache_directory_index, -2)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2011-06-23 18:02:10.066')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME)
event_object = event_objects[9]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2011-06-23 18:02:10.066')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME)
event_object = event_objects[10]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2011-06-29 17:55:02')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.EXPIRATION_TIME)
event_object = event_objects[11]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2011-06-23 18:02:12')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.LAST_CHECKED_TIME)
expected_msg = (
u'Location: Visited: testing@http://www.trafficfusionx.com/download'
u'/tfscrn2/funnycats.exe '
u'Number of hits: 6 '
u'Cached file size: 0')
expected_msg_short = (
u'Location: Visited: testing@http://www.trafficfusionx.com/download'
u'/tfscrn2/fun...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+109
View File
@@ -0,0 +1,109 @@
#!/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.
"""Parser for OLE Compound Files (OLECF)."""
import logging
import pyolecf
from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager
if pyolecf.get_version() < '20131012':
raise ImportWarning('OleCfParser requires at least pyolecf 20131012.')
class OleCfParser(interface.BasePluginsParser):
"""Parses OLE Compound Files (OLECF)."""
NAME = 'olecf'
DESCRIPTION = u'Parser for OLE Compound Files (OLECF).'
_plugin_classes = {}
def __init__(self):
"""Initializes a parser object."""
super(OleCfParser, self).__init__()
self._plugins = OleCfParser.GetPluginObjects()
for list_index, plugin_object in enumerate(self._plugins):
if plugin_object.NAME == 'olecf_default':
self._default_plugin = self._plugins.pop(list_index)
break
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extracts data from an OLE Compound File (OLECF).
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
UnableToParseFile: when the file cannot be parsed.
"""
file_object = file_entry.GetFileObject()
olecf_file = pyolecf.file()
olecf_file.set_ascii_codepage(parser_context.codepage)
try:
olecf_file.open_file_object(file_object)
except IOError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file {1:s}: {2:s}'.format(
self.NAME, file_entry.name, exception))
# Get a list of all root items from the OLE CF file.
root_item = olecf_file.root_item
item_names = [item.name for item in root_item.sub_items]
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
# Compare the list of available plugins.
# We will try to use every plugin against the file (except
# the default plugin) and run it. Only if none of the plugins
# works will we use the default plugin.
parsed = False
for plugin_object in self._plugins:
try:
plugin_object.Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
root_item=root_item, item_names=item_names)
except errors.WrongPlugin:
logging.debug(
u'[{0:s}] plugin: {1:s} cannot parse the OLECF file: {2:s}'.format(
self.NAME, plugin_object.NAME, file_entry.name))
# Check if we still haven't parsed the file, and if so we will use
# the default OLECF plugin.
if not parsed and self._default_plugin:
self._default_plugin.Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
root_item=root_item, item_names=item_names)
olecf_file.close()
file_object.close()
manager.ParsersManager.RegisterParser(OleCfParser)
+22
View File
@@ -0,0 +1,22 @@
#!/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.
"""This file contains an import statement for each OLECF plugin."""
from plaso.parsers.olecf_plugins import automatic_destinations
from plaso.parsers.olecf_plugins import default
from plaso.parsers.olecf_plugins import summary
@@ -0,0 +1,204 @@
#!/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.
"""Plugin to parse .automaticDestinations-ms OLECF files."""
import logging
import re
import construct
from plaso.events import time_events
from plaso.lib import binary
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers import olecf
from plaso.parsers import winlnk
from plaso.parsers.olecf_plugins import interface
class AutomaticDestinationsDestListEntryEvent(time_events.FiletimeEvent):
"""Convenience class for an .automaticDestinations-ms DestList entry event."""
DATA_TYPE = 'olecf:dest_list:entry'
def __init__(
self, timestamp, timestamp_description, entry_offset, dest_list_entry):
"""Initializes the event object.
Args:
timestamp: The FILETIME value for the timestamp.
timestamp_description: The usage string for the timestamp value.
entry_offset: The offset of the DestList entry relative to the start of
the DestList stream.
dest_list_entry: The DestList entry (instance of construct.Struct).
"""
super(AutomaticDestinationsDestListEntryEvent, self).__init__(
timestamp, timestamp_description)
self.offset = entry_offset
self.entry_number = dest_list_entry.entry_number
self.hostname = binary.ByteStreamCopyToString(
dest_list_entry.hostname, codepage='ascii')
self.path = binary.Ut16StreamCopyToString(dest_list_entry.path)
self.pin_status = dest_list_entry.pin_status
self.droid_volume_identifier = binary.ByteStreamCopyToGuid(
dest_list_entry.droid_volume_identifier)
self.droid_file_identifier = binary.ByteStreamCopyToGuid(
dest_list_entry.droid_file_identifier)
self.birth_droid_volume_identifier = binary.ByteStreamCopyToGuid(
dest_list_entry.birth_droid_volume_identifier)
self.birth_droid_file_identifier = binary.ByteStreamCopyToGuid(
dest_list_entry.birth_droid_file_identifier)
class AutomaticDestinationsOlecfPlugin(interface.OlecfPlugin):
"""Plugin that parses an .automaticDestinations-ms OLECF file."""
NAME = 'olecf_automatic_destinations'
DESCRIPTION = u'Parser for *.automaticDestinations-ms OLECF files.'
REQUIRED_ITEMS = frozenset([u'DestList'])
_RE_LNK_ITEM_NAME = re.compile(r'^[1-9a-f][0-9a-f]*$')
# We cannot use the parser registry here since winlnk could be disabled.
# TODO: see if there is a more elegant solution for this.
_WINLNK_PARSER = winlnk.WinLnkParser()
_DEST_LIST_STREAM_HEADER = construct.Struct(
'dest_list_stream_header',
construct.ULInt32('unknown1'),
construct.ULInt32('number_of_entries'),
construct.ULInt32('number_of_pinned_entries'),
construct.LFloat32('unknown2'),
construct.ULInt32('last_entry_number'),
construct.Padding(4),
construct.ULInt32('last_revision_number'),
construct.Padding(4))
_DEST_LIST_STREAM_HEADER_SIZE = _DEST_LIST_STREAM_HEADER.sizeof()
# Using Construct's utf-16 encoding here will create strings with their
# end-of-string characters exposed. Instead the strings are read as
# binary strings and converted using ReadUtf16().
_DEST_LIST_STREAM_ENTRY = construct.Struct(
'dest_list_stream_entry',
construct.ULInt64('unknown1'),
construct.Array(16, construct.Byte('droid_volume_identifier')),
construct.Array(16, construct.Byte('droid_file_identifier')),
construct.Array(16, construct.Byte('birth_droid_volume_identifier')),
construct.Array(16, construct.Byte('birth_droid_file_identifier')),
construct.String('hostname', 16),
construct.ULInt32('entry_number'),
construct.ULInt32('unknown2'),
construct.LFloat32('unknown3'),
construct.ULInt64('last_modification_time'),
construct.ULInt32('pin_status'),
construct.ULInt16('path_size'),
construct.String('path', lambda ctx: ctx.path_size * 2))
def ParseDestList(
self, parser_context, file_entry=None, parser_chain=None,
olecf_item=None):
"""Parses the DestList OLECF item.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
olecf_item: An optional OLECF item (instance of pyolecf.item).
"""
if not olecf_item:
return
try:
header = self._DEST_LIST_STREAM_HEADER.parse_stream(olecf_item)
except (IOError, construct.FieldError) as exception:
raise errors.UnableToParseFile(
u'Unable to parse DestList header with error: {0:s}'.format(
exception))
if header.unknown1 != 1:
# TODO: add format debugging notes to parser context.
logging.debug(u'[{0:s}] unknown1 value: {1:d}.'.format(
self.NAME, header.unknown1))
entry_offset = olecf_item.get_offset()
while entry_offset < olecf_item.size:
try:
entry = self._DEST_LIST_STREAM_ENTRY.parse_stream(olecf_item)
except (IOError, construct.FieldError) as exception:
raise errors.UnableToParseFile(
u'Unable to parse DestList entry with error: {0:s}'.format(
exception))
if not entry:
break
event_object = AutomaticDestinationsDestListEntryEvent(
entry.last_modification_time,
eventdata.EventTimestamp.MODIFICATION_TIME, entry_offset, entry)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
entry_offset = olecf_item.get_offset()
def ParseItems(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
**unused_kwargs):
"""Parses OLECF items.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
Raises:
ValueError: If the root_item is not set.
"""
if root_item is None:
raise ValueError(u'Root item not set.')
for item in root_item.sub_items:
if item.name == u'DestList':
self.ParseDestList(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
olecf_item=item)
elif self._RE_LNK_ITEM_NAME.match(item.name):
if file_entry:
display_name = u'{0:s} # {1:s}'.format(
parser_context.GetDisplayName(file_entry), item.name)
else:
display_name = u'# {0:s}'.format(item.name)
self._WINLNK_PARSER.ParseFileObject(
parser_context, item, file_entry=file_entry,
parser_chain=parser_chain, display_name=display_name)
# TODO: check for trailing data?
olecf.OleCfParser.RegisterPlugin(AutomaticDestinationsOlecfPlugin)
@@ -0,0 +1,101 @@
#!/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 .automaticDestinations-ms OLECF file plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import olecf as olecf_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers.olecf_plugins import automatic_destinations
from plaso.parsers.olecf_plugins import test_lib
class TestAutomaticDestinationsOlecfPlugin(test_lib.OleCfPluginTestCase):
"""Tests for the .automaticDestinations-ms OLECF file plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = automatic_destinations.AutomaticDestinationsOlecfPlugin()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath([
u'1b4dd67f29cb1962.automaticDestinations-ms'])
event_queue_consumer = self._ParseOleCfFileWithPlugin(
test_file, self._plugin)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 44)
# Check a AutomaticDestinationsDestListEntryEvent.
event_object = event_objects[3]
self.assertEquals(event_object.offset, 32)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-04-01 13:52:38.997538')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'Entry: 11 '
u'Pin status: Unpinned '
u'Hostname: wks-win764bitb '
u'Path: C:\\Users\\nfury\\Pictures\\The SHIELD '
u'Droid volume identifier: {cf6619c2-66a8-44a6-8849-1582fcd3a338} '
u'Droid file identifier: {63eea867-7b85-11e1-8950-005056a50b40} '
u'Birth droid volume identifier: '
u'{cf6619c2-66a8-44a6-8849-1582fcd3a338} '
u'Birth droid file identifier: {63eea867-7b85-11e1-8950-005056a50b40}')
expected_msg_short = (
u'Entry: 11 '
u'Pin status: Unpinned '
u'Path: C:\\Users\\nfury\\Pictures\\The SHIELD')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
# Check a WinLnkLinkEvent.
event_object = event_objects[1]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2010-11-10 07:51:16.749125')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'File size: 3545 '
u'File attribute flags: 0x00002020 '
u'Drive type: 3 '
u'Drive serial number: 0x24ba718b '
u'Local path: C:\\Users\\nfury\\AppData\\Roaming\\Microsoft\\Windows\\'
u'Libraries\\Documents.library-ms '
u'Link target: [Users Libraries, UNKNOWN: 0x00]')
expected_msg_short = (
u'C:\\Users\\nfury\\AppData\\Roaming\\Microsoft\\Windows\\Libraries\\'
u'Documents.library-ms')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+162
View File
@@ -0,0 +1,162 @@
#!/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.
"""The default plugin for parsing OLE Compound Files (OLECF)."""
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import olecf
from plaso.parsers.olecf_plugins import interface
class OleCfItemEvent(time_events.FiletimeEvent):
"""Convenience class for an OLECF item event."""
DATA_TYPE = 'olecf:item'
def __init__(self, timestamp, usage, olecf_item):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: A string describing the timestamp value.
olecf_item: The OLECF item (pyolecf.item).
"""
super(OleCfItemEvent, self).__init__(timestamp, usage)
# TODO: need a better way to express the original location of the
# original data.
self.offset = 0
self.name = olecf_item.name
# TODO: have pyolecf return the item type here.
# self.type = olecf_item.type
self.size = olecf_item.size
class DefaultOleCFPlugin(interface.OlecfPlugin):
"""Class to define the default OLECF file plugin."""
NAME = 'olecf_default'
DESCRIPTION = u'Parser for a generic OLECF item.'
def _ParseItem(
self, parser_context, file_entry=None, parser_chain=None,
olecf_item=None):
"""Parses an OLECF item.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
olecf_item: An optional OLECF item (instance of pyolecf.item).
Returns:
A boolean value indicating if an event object was produced.
"""
event_object = None
result = False
creation_time, modification_time = self.GetTimestamps(olecf_item)
if creation_time:
event_object = OleCfItemEvent(
creation_time, eventdata.EventTimestamp.CREATION_TIME,
olecf_item)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if modification_time:
event_object = OleCfItemEvent(
modification_time, eventdata.EventTimestamp.MODIFICATION_TIME,
olecf_item)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if event_object:
result = True
for sub_item in olecf_item.sub_items:
if self._ParseItem(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
olecf_item=sub_item):
result = True
return result
def ParseItems(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
**unused_kwargs):
"""Parses OLECF items.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
"""
if not self._ParseItem(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
olecf_item=root_item):
# If no event object was produced, produce at least one for
# the root item.
event_object = OleCfItemEvent(
0, eventdata.EventTimestamp.CREATION_TIME, root_item)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
def Process(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
item_names=None, **kwargs):
"""Determine if this is the right plugin for this OLECF file.
This function takes a list of sub items found in the root of a
OLECF file and compares that to a list of required items defined
in this plugin.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
item_names: Optional list of all items discovered in the root.
The default is None.
Raises:
errors.WrongPlugin: If the set of required items is not a subset
of the available items.
ValueError: If the root_item or items are not set.
"""
if root_item is None or item_names is None:
raise ValueError(u'Root item or items are not set.')
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
self.ParseItems(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
root_item=root_item)
olecf.OleCfParser.RegisterPlugin(DefaultOleCFPlugin)
@@ -0,0 +1,75 @@
#!/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 OLE Compound File (OLECF) default plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import olecf as olecf_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers.olecf_plugins import default
from plaso.parsers.olecf_plugins import test_lib
class TestDefaultPluginOleCf(test_lib.OleCfPluginTestCase):
"""Tests for the OLECF default plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = default.DefaultOleCFPlugin()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['Document.doc'])
event_queue_consumer = self._ParseOleCfFileWithPlugin(
test_file, self._plugin)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 5)
# Check the Root Entry event.
event_object = event_objects[0]
self.assertEquals(event_object.name, u'Root Entry')
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-05-16 02:29:49.795')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_string = (
u'Name: Root Entry')
self._TestGetMessageStrings(event_object, expected_string, expected_string)
# Check one other entry.
event_object = event_objects[1]
expected_string = u'Name: MsoDataStore'
self._TestGetMessageStrings(event_object, expected_string, expected_string)
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-05-16 02:29:49.704')
self.assertEquals(event_object.timestamp, expected_timestamp)
if __name__ == '__main__':
unittest.main()
+150
View File
@@ -0,0 +1,150 @@
#!/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.
"""This file contains the necessary interface for OLECF plugins."""
import abc
import logging
from plaso.lib import errors
from plaso.parsers import plugins
class OlecfPlugin(plugins.BasePlugin):
"""An OLECF plugin for Plaso."""
NAME = 'olecf'
# List of tables that should be present in the database, for verification.
REQUIRED_ITEMS = frozenset([])
def GetTimestamps(self, olecf_item):
"""Takes an OLECF object and returns extracted timestamps.
Args:
olecf_item: A OLECF item (instance of pyolecf.item).
Returns:
A tuple of two timestamps: created and modified.
"""
if not olecf_item:
return None, None
try:
creation_time = olecf_item.get_creation_time_as_integer()
except OverflowError as exception:
logging.warning(
u'Unable to read the creation time with error: {0:s}'.format(
exception))
creation_time = 0
try:
modification_time = olecf_item.get_modification_time_as_integer()
except OverflowError as exception:
logging.warning(
u'Unable to read the modification time with error: {0:s}'.format(
exception))
modification_time = 0
# If no useful events, return early.
if not creation_time and not modification_time:
return None, None
# Office template documents sometimes contain a creation time
# of -1 (0xffffffffffffffff).
if creation_time == 0xffffffffffffffffL:
creation_time = 0
return creation_time, modification_time
@abc.abstractmethod
def ParseItems(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
items=None, **kwargs):
"""Parses OLECF items.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
item_names: Optional list of all items discovered in the root.
The default is None.
"""
def Process(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
item_names=None, **kwargs):
"""Determine if this is the right plugin for this OLECF file.
This function takes a list of sub items found in the root of a
OLECF file and compares that to a list of required items defined
in this plugin.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
item_names: Optional list of all items discovered in the root.
The default is None.
Raises:
errors.WrongPlugin: If the set of required items is not a subset
of the available items.
ValueError: If the root_item or items are not set.
"""
if root_item is None or item_names is None:
raise ValueError(u'Root item or items are not set.')
if not frozenset(item_names) >= self.REQUIRED_ITEMS:
raise errors.WrongPlugin(
u'Not the correct items for: {0:s}'.format(self.NAME))
# This will raise if unhandled keyword arguments are passed.
super(OlecfPlugin, self).Process(parser_context, **kwargs)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
items = []
for item_string in self.REQUIRED_ITEMS:
item = root_item.get_sub_item_by_name(item_string)
if item:
items.append(item)
self.ParseItems(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
root_item=root_item, items=items)
class OleDefinitions(object):
"""Convenience class for OLE definitions."""
VT_I2 = 0x0002
VT_I4 = 0x0003
VT_BOOL = 0x000b
VT_LPSTR = 0x001e
VT_LPWSTR = 0x001e
VT_FILETIME = 0x0040
VT_CF = 0x0047
+433
View File
@@ -0,0 +1,433 @@
#!/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.
"""Plugin to parse the OLECF summary/document summary information items."""
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import olecf
from plaso.parsers.olecf_plugins import interface
class OleCfSummaryInfoEvent(time_events.FiletimeEvent):
"""Convenience class for an OLECF Summary info event."""
DATA_TYPE = 'olecf:summary_info'
def __init__(self, timestamp, usage, attributes):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
attributes: A dict object containing all extracted attributes.
"""
super(OleCfSummaryInfoEvent, self).__init__(
timestamp, usage)
self.name = u'Summary Information'
for attribute_name, attribute_value in attributes.iteritems():
setattr(self, attribute_name, attribute_value)
# TODO: Move this class to a higher level (to the interface)
# so the these functions can be shared by other plugins.
class OleCfSummaryInfo(object):
"""An OLECF Summary Info object."""
_CLASS_IDENTIFIER = 'f29f85e0-4ff9-1068-ab91-08002b27b3d9'
_PROPERTY_NAMES_INT32 = {
0x000e: 'number_of_pages', # PIDSI_PAGECOUNT
0x000f: 'number_of_words', # PIDSI_WORDCOUNT
0x0010: 'number_of_characters', # PIDSI_CHARCOUNT
0x0013: 'security', # PIDSI_SECURITY
}
_PROPERTY_NAMES_STRING = {
0x0002: 'title', # PIDSI_TITLE
0x0003: 'subject', # PIDSI_SUBJECT
0x0004: 'author', # PIDSI_AUTHOR
0x0005: 'keywords', # PIDSI_KEYWORDS
0x0006: 'comments', # PIDSI_COMMENTS
0x0007: 'template', # PIDSI_TEMPLATE
0x0008: 'last_saved_by', # PIDSI_LASTAUTHOR
0x0009: 'revision_number', # PIDSI_REVNUMBER
0x0012: 'application', # PIDSI_APPNAME
}
PIDSI_CODEPAGE = 0x0001
PIDSI_EDITTIME = 0x000a
PIDSI_LASTPRINTED = 0x000b
PIDSI_CREATE_DTM = 0x000c
PIDSI_LASTSAVE_DTM = 0x000d
PIDSI_THUMBNAIL = 0x0011
def __init__(self, olecf_item):
"""Initialize the OLECF summary object.
Args:
olecf_item: The OLECF item (instance of pyolecf.property_set_stream).
"""
super(OleCfSummaryInfo, self).__init__()
self.attributes = {}
self.events = []
self._InitFromPropertySet(olecf_item.set)
def _InitFromPropertySet(self, property_set):
"""Initializes the object from a property set.
Args:
property_set: The OLECF property set (pyolecf.property_set).
"""
# Combine the values of multiple property sections
# but do not override properties that are already set.
for property_section in property_set.sections:
if property_section.class_identifier != self._CLASS_IDENTIFIER:
continue
for property_value in property_section.properties:
self._InitFromPropertyValue(property_value)
def _InitFromPropertyValue(self, property_value):
"""Initializes the object from a property value.
Args:
property_value: The OLECF property value (pyolecf.property_value).
"""
if property_value.type == interface.OleDefinitions.VT_I2:
self._InitFromPropertyValueTypeInt16(property_value)
elif property_value.type == interface.OleDefinitions.VT_I4:
self._InitFromPropertyValueTypeInt32(property_value)
elif (property_value.type == interface.OleDefinitions.VT_LPSTR or
property_value.type == interface.OleDefinitions.VT_LPWSTR):
self._InitFromPropertyValueTypeString(property_value)
elif property_value.type == interface.OleDefinitions.VT_FILETIME:
self._InitFromPropertyValueTypeFiletime(property_value)
def _InitFromPropertyValueTypeInt16(self, property_value):
"""Initializes the object from a 16-bit int type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_I2).
"""
if property_value.identifier == self.PIDSI_CODEPAGE:
# TODO: can the codepage vary per property section?
# And is it needed to interpret the ASCII strings?
# codepage = property_value.data_as_integer
pass
def _InitFromPropertyValueTypeInt32(self, property_value):
"""Initializes the object from a 32-bit int type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_I4).
"""
property_name = self._PROPERTY_NAMES_INT32.get(
property_value.identifier, None)
if property_name and not property_name in self.attributes:
self.attributes[property_name] = property_value.data_as_integer
def _InitFromPropertyValueTypeString(self, property_value):
"""Initializes the object from a string type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_LPSTR or VT_LPWSTR).
"""
property_name = self._PROPERTY_NAMES_STRING.get(
property_value.identifier, None)
if property_name and not property_name in self.attributes:
self.attributes[property_name] = property_value.data_as_string
def _InitFromPropertyValueTypeFiletime(self, property_value):
"""Initializes the object from a filetime type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_FILETIME).
"""
if property_value.identifier == self.PIDSI_LASTPRINTED:
self.events.append(
(property_value.data_as_integer, 'Document Last Printed Time'))
elif property_value.identifier == self.PIDSI_CREATE_DTM:
self.events.append(
(property_value.data_as_integer, 'Document Creation Time'))
elif property_value.identifier == self.PIDSI_LASTSAVE_DTM:
self.events.append(
(property_value.data_as_integer, 'Document Last Save Time'))
elif property_value.identifier == self.PIDSI_EDITTIME:
# property_name = 'total_edit_time'
# TODO: handle duration.
pass
class OleCfDocumentSummaryInfoEvent(time_events.FiletimeEvent):
"""Convenience class for an OLECF Document Summary info event."""
DATA_TYPE = 'olecf:document_summary_info'
_CLASS_IDENTIFIER = 'd5cdd502-2e9c-101b-9397-08002b2cf9ae'
_PROPERTY_NAMES_BOOL = {
0x0013: 'shared_document', # PIDDSI_SHAREDDOC
}
_PROPERTY_NAMES_INT32 = {
0x0004: 'number_of_bytes', # PIDDSI_BYTECOUNT
0x0005: 'number_of_lines', # PIDDSI_LINECOUNT
0x0006: 'number_of_paragraphs', # PIDDSI_PARCOUNT
0x0007: 'number_of_slides', # PIDDSI_SLIDECOUNT
0x0008: 'number_of_notes', # PIDDSI_NOTECOUNT
0x0009: 'number_of_hidden_slides', # PIDDSI_HIDDENCOUNT
0x000a: 'number_of_clips', # PIDDSI_MMCLIPCOUNT
0x0011: 'number_of_characters_with_white_space', # PIDDSI_CCHWITHSPACES
0x0017: 'application_version', # PIDDSI_VERSION
}
_PROPERTY_NAMES_STRING = {
0x000e: 'manager', # PIDDSI_MANAGER
0x000f: 'company', # PIDDSI_COMPANY
0x001a: 'content_type', # PIDDSI_CONTENTTYPE
0x001b: 'content_status', # PIDDSI_CONTENTSTATUS
0x001c: 'language', # PIDDSI_LANGUAGE
0x001d: 'document_version', # PIDDSI_DOCVERSION
}
PIDDSI_CODEPAGE = 0x0001
PIDDSI_CATEGORY = 0x0002
PIDDSI_PRESFORMAT = 0x0003
PIDDSI_SCALE = 0x000b
PIDDSI_HEADINGPAIR = 0x000c
PIDDSI_DOCPARTS = 0x000d
PIDDSI_LINKSDIRTY = 0x0010
PIDDSI_VERSION = 0x0017
def __init__(self, timestamp, usage, olecf_item):
"""Initializes the event.
Args:
timestamp: The FILETIME timestamp value.
usage: The usage string, describing the timestamp value.
olecf_item: The OLECF item (pyolecf.property_set_stream).
"""
super(OleCfDocumentSummaryInfoEvent, self).__init__(
timestamp, usage)
self.name = u'Document Summary Information'
self._InitFromPropertySet(olecf_item.set)
def _InitFromPropertySet(self, property_set):
"""Initializes the event from a property set.
Args:
property_set: The OLECF property set (pyolecf.property_set).
"""
# Combine the values of multiple property sections
# but do not override properties that are already set.
for property_section in property_set.sections:
if property_section.class_identifier != self._CLASS_IDENTIFIER:
continue
for property_value in property_section.properties:
self._InitFromPropertyValue(property_value)
def _InitFromPropertyValue(self, property_value):
"""Initializes the event from a property value.
Args:
property_value: The OLECF property value (pyolecf.property_value).
"""
if property_value.type == interface.OleDefinitions.VT_I2:
self._InitFromPropertyValueTypeInt16(property_value)
elif property_value.type == interface.OleDefinitions.VT_I4:
self._InitFromPropertyValueTypeInt32(property_value)
elif property_value.type == interface.OleDefinitions.VT_BOOL:
self._InitFromPropertyValueTypeBool(property_value)
elif (property_value.type == interface.OleDefinitions.VT_LPSTR or
property_value.type == interface.OleDefinitions.VT_LPWSTR):
self._InitFromPropertyValueTypeString(property_value)
def _InitFromPropertyValueTypeInt16(self, property_value):
"""Initializes the event from a 16-bit int type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_I2).
"""
if property_value.identifier == self.PIDDSI_CODEPAGE:
# TODO: can the codepage vary per property section?
# And is it needed to interpret the ASCII strings?
# codepage = property_value.data_as_integer
pass
def _InitFromPropertyValueTypeInt32(self, property_value):
"""Initializes the event from a 32-bit int type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_I4).
"""
property_name = self._PROPERTY_NAMES_INT32.get(
property_value.identifier, None)
# The application version consists of 2 16-bit values that make up
# the version number. Where the upper 16-bit is the major number
# and the lower 16-bit the minor number.
if property_value.identifier == self.PIDDSI_VERSION:
application_version = property_value.data_as_integer
setattr(self, property_name, u'{0:d}.{1:d}'.format(
application_version >> 16, application_version & 0xffff))
elif property_name and not hasattr(self, property_name):
setattr(self, property_name, property_value.data_as_integer)
def _InitFromPropertyValueTypeBool(self, property_value):
"""Initializes the event from a boolean type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_BOOL).
"""
property_name = self._PROPERTY_NAMES_BOOL.get(
property_value.identifier, None)
if property_name and not hasattr(self, property_name):
setattr(self, property_name, property_value.data_as_boolean)
def _InitFromPropertyValueTypeString(self, property_value):
"""Initializes the event from a string type property value.
Args:
property_value: The OLECF property value (pyolecf.property_value
of type VT_LPSTR or VT_LPWSTR).
"""
property_name = self._PROPERTY_NAMES_STRING.get(
property_value.identifier, None)
if property_name and not hasattr(self, property_name):
setattr(self, property_name, property_value.data_as_string)
class DocumentSummaryOlecfPlugin(interface.OlecfPlugin):
"""Plugin that parses DocumentSummaryInformation item from an OLECF file."""
NAME = 'olecf_document_summary'
DESCRIPTION = u'Parser for a DocumentSummaryInformation OLECF stream.'
# pylint: disable=anomalous-backslash-in-string
REQUIRED_ITEMS = frozenset([u'\005DocumentSummaryInformation'])
def ParseItems(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
items=None, **unused_kwargs):
"""Parses a document summary information OLECF item.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
item_names: Optional list of all items discovered in the root.
The default is None.
"""
root_creation_time, root_modification_time = self.GetTimestamps(root_item)
for item in items:
if root_creation_time:
event_object = OleCfDocumentSummaryInfoEvent(
root_creation_time, eventdata.EventTimestamp.CREATION_TIME, item)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if root_modification_time:
event_object = OleCfDocumentSummaryInfoEvent(
root_modification_time, eventdata.EventTimestamp.MODIFICATION_TIME,
item)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
class SummaryInfoOlecfPlugin(interface.OlecfPlugin):
"""Plugin that parses the SummaryInformation item from an OLECF file."""
NAME = 'olecf_summary'
DESCRIPTION = u'Parser for a SummaryInformation OLECF stream.'
# pylint: disable=anomalous-backslash-in-string
REQUIRED_ITEMS = frozenset([u'\005SummaryInformation'])
def ParseItems(
self, parser_context, file_entry=None, parser_chain=None, root_item=None,
items=None, **unused_kwargs):
"""Parses a summary information OLECF item.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
root_item: Optional root item of the OLECF file. The default is None.
item_names: Optional list of all items discovered in the root.
The default is None.
"""
root_creation_time, root_modification_time = self.GetTimestamps(root_item)
for item in items:
summary_information_object = OleCfSummaryInfo(item)
for timestamp, timestamp_description in summary_information_object.events:
event_object = OleCfSummaryInfoEvent(
timestamp, timestamp_description,
summary_information_object.attributes)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if root_creation_time:
event_object = OleCfSummaryInfoEvent(
root_creation_time, eventdata.EventTimestamp.CREATION_TIME,
summary_information_object.attributes)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if root_modification_time:
event_object = OleCfSummaryInfoEvent(
root_modification_time, eventdata.EventTimestamp.MODIFICATION_TIME,
summary_information_object.attributes)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
olecf.OleCfParser.RegisterPlugins(
[DocumentSummaryOlecfPlugin, SummaryInfoOlecfPlugin])
+129
View File
@@ -0,0 +1,129 @@
#!/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 OLE Compound File summary and document summary plugins."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import olecf as olecf_formatter
from plaso.lib import timelib_test
from plaso.parsers.olecf_plugins import summary
from plaso.parsers.olecf_plugins import test_lib
class TestSummaryInfoOlecfPlugin(test_lib.OleCfPluginTestCase):
"""Tests for the OLECF summary information plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._summary_plugin = summary.SummaryInfoOlecfPlugin()
self._test_file = self._GetTestFilePath(['Document.doc'])
def testProcess(self):
"""Tests the Process function on a SummaryInformation stream."""
event_queue_consumer = self._ParseOleCfFileWithPlugin(
self._test_file, self._summary_plugin)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# There is one summary info stream with three event objects.
self.assertEquals(len(event_objects), 3)
event_object = event_objects[0]
self.assertEquals(event_object.name, u'Summary Information')
self.assertEquals(event_object.title, u'Table of Context')
self.assertEquals(event_object.author, u'DAVID NIDES')
self.assertEquals(event_object.template, u'Normal.dotm')
self.assertEquals(event_object.last_saved_by, u'Nides')
self.assertEquals(event_object.revision_number, u'4')
self.assertEquals(event_object.number_of_characters, 18)
self.assertEquals(event_object.application, u'Microsoft Office Word')
self.assertEquals(event_object.security, 0)
self.assertEquals(event_object.timestamp_desc, u'Document Creation Time')
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-12-10 18:38:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'Title: Table of Context '
u'Author: DAVID NIDES '
u'Template: Normal.dotm '
u'Revision number: 4 '
u'Last saved by: Nides '
u'Number of pages: 1 '
u'Number of words: 3 '
u'Number of characters: 18 '
u'Application: Microsoft Office Word '
u'Security: 0')
expected_msg_short = (
u'Title: Table of Context '
u'Author: DAVID NIDES '
u'Revision number: 4')
# TODO: add support for:
# u'Total edit time (secs): 0 '
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
class TestDocumentSummaryInfoOlecfPlugin(test_lib.OleCfPluginTestCase):
"""Tests for the OLECF document summary information plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._document_summary_plugin = summary.DocumentSummaryOlecfPlugin()
self._test_file = self._GetTestFilePath(['Document.doc'])
def testProcess(self):
"""Tests the Process function on a DocumentSummaryInformation stream."""
event_queue_consumer = self._ParseOleCfFileWithPlugin(
self._test_file, self._document_summary_plugin)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# There should only be one summary info stream with one event.
self.assertEquals(len(event_objects), 1)
event_object = event_objects[0]
self.assertEquals(event_object.name, u'Document Summary Information')
self.assertEquals(event_object.number_of_lines, 1)
self.assertEquals(event_object.number_of_paragraphs, 1)
self.assertEquals(event_object.company, u'KPMG')
self.assertFalse(event_object.shared_document)
self.assertEquals(event_object.application_version, u'14.0')
# TODO: add support for:
# self.assertEquals(event_object.is_shared, False)
expected_msg = (
u'Number of lines: 1 '
u'Number of paragraphs: 1 '
u'Company: KPMG '
u'Shared document: False '
u'Application version: 14.0')
expected_msg_short = (
u'Company: KPMG')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+84
View File
@@ -0,0 +1,84 @@
#!/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.
"""OLECF plugin related functions and classes for testing."""
from dfvfs.lib import definitions
from dfvfs.path import factory as path_spec_factory
from dfvfs.resolver import resolver as path_spec_resolver
import pyolecf
from plaso.engine import single_process
from plaso.parsers import test_lib
class OleCfPluginTestCase(test_lib.ParserTestCase):
"""The unit test case for OLE CF based plugins."""
def _OpenOleCfFile(self, path, codepage='cp1252'):
"""Opens an OLE compound file and returns back a pyolecf.file object.
Args:
path: The path to the OLE CF test file.
codepate: Optional codepage. The default is cp1252.
"""
path_spec = path_spec_factory.Factory.NewPathSpec(
definitions.TYPE_INDICATOR_OS, location=path)
file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec)
file_object = file_entry.GetFileObject()
olecf_file = pyolecf.file()
olecf_file.set_ascii_codepage(codepage)
olecf_file.open_file_object(file_object)
return olecf_file
def _ParseOleCfFileWithPlugin(
self, path, plugin_object, knowledge_base_values=None):
"""Parses a file as an OLE compound file and returns an event generator.
Args:
path: The path to the OLE CF test file.
plugin_object: The plugin object that is used to extract an event
generator.
knowledge_base_values: optional dict containing the knowledge base
values. The default is None.
Returns:
An event object queue consumer object (instance of
TestEventObjectQueueConsumer).
"""
event_queue = single_process.SingleProcessQueue()
event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue)
parse_error_queue = single_process.SingleProcessQueue()
parser_context = self._GetParserContext(
event_queue, parse_error_queue,
knowledge_base_values=knowledge_base_values)
olecf_file = self._OpenOleCfFile(path)
# Get a list of all root items from the OLE CF file.
root_item = olecf_file.root_item
item_names = [item.name for item in root_item.sub_items]
plugin_object.Process(
parser_context, root_item=root_item, item_names=item_names)
return event_queue_consumer
+326
View File
@@ -0,0 +1,326 @@
#!/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.
"""Parsers for Opera Browser history files."""
import logging
import os
import urllib2
from dfvfs.helpers import text_file
from xml.etree import ElementTree
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.lib import utils
from plaso.parsers import interface
from plaso.parsers import manager
class OperaTypedHistoryEvent(event.EventObject):
"""An EventObject for an Opera typed history entry."""
DATA_TYPE = 'opera:history:typed_entry'
def __init__(self, last_typed_time, url, entry_type):
"""A constructor for the typed history event.
Args:
last_typed_time: A ISO 8601 string denoting the last time
the URL was typed into a browser.
url: The url, or the typed hostname.
entry_type: A string indicating whether the URL was directly
typed in or the result of the user choosing from the
auto complete (based on prior history).
"""
super(OperaTypedHistoryEvent, self).__init__()
self.url = url
self.entry_type = entry_type
if entry_type == 'selected':
self.entry_selection = 'Filled from autocomplete.'
elif entry_type == 'text':
self.entry_selection = 'Manually typed.'
self.timestamp = timelib.Timestamp.FromTimeString(last_typed_time)
self.timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME
class OperaGlobalHistoryEvent(time_events.PosixTimeEvent):
"""An EventObject for an Opera global history entry."""
DATA_TYPE = 'opera:history:entry'
def __init__(self, timestamp, url, title, popularity_index):
"""Initialize the event object."""
super(OperaGlobalHistoryEvent, self).__init__(
timestamp, eventdata.EventTimestamp.PAGE_VISITED, self.DATA_TYPE)
self.url = url
if title != url:
self.title = title
self.popularity_index = popularity_index
if popularity_index < 0:
self.description = 'First and Only Visit'
else:
self.description = 'Last Visit'
class OperaTypedHistoryParser(interface.BaseParser):
"""Parses the Opera typed_history.xml file."""
NAME = 'opera_typed_history'
DESCRIPTION = u'Parser for Opera typed_history.xml files.'
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from an Opera typed history file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
text_file_object = text_file.TextFile(file_object)
# Need to verify the first line to make sure this is a) XML and
# b) the right XML.
first_line = text_file_object.readline(90)
if not first_line.startswith('<?xml version="1.0'):
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera typed history file [not a XML]')
# We read in the second line due to the fact that ElementTree
# reads the entire file in memory to parse the XML string and
# we only care about the XML file with the correct root key,
# which denotes a typed_history.xml file.
second_line = text_file_object.readline(50).strip()
if second_line != '<typed_history>':
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera typed history file [wrong XML root key]')
# For ElementTree to work we need to work on a file object seeked
# to the beginning.
file_object.seek(0, os.SEEK_SET)
xml = ElementTree.parse(file_object)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
for history_item in xml.iterfind('typed_history_item'):
content = history_item.get('content', '')
last_typed = history_item.get('last_typed', '')
entry_type = history_item.get('type', '')
event_object = OperaTypedHistoryEvent(last_typed, content, entry_type)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
file_object.close()
class OperaGlobalHistoryParser(interface.BaseParser):
"""Parses the Opera global_history.dat file."""
NAME = 'opera_global'
DESCRIPTION = u'Parser for Opera global_history.dat files.'
_SUPPORTED_URL_SCHEMES = frozenset(['file', 'http', 'https', 'ftp'])
def _IsValidUrl(self, url):
"""A simple test to see if an URL is considered valid."""
parsed_url = urllib2.urlparse.urlparse(url)
# Few supported first URL entries.
if parsed_url.scheme in self._SUPPORTED_URL_SCHEMES:
return True
return False
def _ReadRecord(self, text_file_object, max_line_length=0):
"""Return a single record from an Opera global_history file.
A single record consists of four lines, with each line as:
Title of page (or the URL if not there).
Website URL.
Timestamp in POSIX time.
Popularity index (-1 if first time visited).
Args:
text_file_object: A text file object (instance of dfvfs.TextFile).
max_line_length: An integer that denotes the maximum byte
length for each line read.
Returns:
A tuple of: title, url, timestamp, popularity_index.
Raises:
errors.NotAText: If the file being read is not a text file.
"""
if max_line_length:
title_raw = text_file_object.readline(max_line_length)
if len(title_raw) == max_line_length and not title_raw.endswith('\n'):
return None, None, None, None
if not utils.IsText(title_raw):
raise errors.NotAText(u'Title line is not a text.')
title = title_raw.strip()
else:
title = text_file_object.readline().strip()
if not title:
return None, None, None, None
url = text_file_object.readline().strip()
if not url:
return None, None, None, None
timestamp_line = text_file_object.readline().strip()
popularity_line = text_file_object.readline().strip()
try:
timestamp = int(timestamp_line, 10)
except ValueError:
if len(timestamp_line) > 30:
timestamp_line = timestamp_line[0:30]
logging.debug(u'Unable to read in timestamp [{!r}]'.format(
timestamp_line))
return None, None, None, None
try:
popularity_index = int(popularity_line, 10)
except ValueError:
try:
logging.debug(u'Unable to read in popularity index[{}]'.format(
popularity_line))
except UnicodeDecodeError:
logging.debug(
u'Unable to read in popularity index [unable to print '
u'bad line]')
return None, None, None, None
# Try to get the data into unicode.
try:
title_unicode = title.decode('utf-8')
except UnicodeDecodeError:
partial_title = title.decode('utf-8', 'ignore')
title_unicode = u'Warning: partial line, starts with: {}'.format(
partial_title)
return title_unicode, url, timestamp, popularity_index
def _ReadRecords(self, text_file_object):
"""Yield records read from an Opera global_history file.
A single record consists of four lines, with each line as:
Title of page (or the URL if not there).
Website URL.
Timestamp in POSIX time.
Popularity index (-1 if first time visited).
Args:
text_file_object: A text file object (instance of dfvfs.TextFile).
Yields:
A tuple of: title, url, timestamp, popularity_index.
"""
while True:
title, url, timestamp, popularity_index = self._ReadRecord(
text_file_object)
if not title:
raise StopIteration
if not url:
raise StopIteration
if not popularity_index:
raise StopIteration
yield title, url, timestamp, popularity_index
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from an Opera global history file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
text_file_object = text_file.TextFile(file_object)
try:
title, url, timestamp, popularity_index = self._ReadRecord(
text_file_object, 400)
except errors.NotAText:
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera history file [not a text file].')
if not title:
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera history file [no title present].')
if not self._IsValidUrl(url):
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera history file [not a valid URL].')
if not timestamp:
file_object.close()
raise errors.UnableToParseFile(
u'Not an Opera history file [timestamp does not exist].')
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
event_object = OperaGlobalHistoryEvent(
timestamp, url, title, popularity_index)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
# Read in the rest of the history file.
for title, url, timestamp, popularity_index in self._ReadRecords(
text_file_object):
event_object = OperaGlobalHistoryEvent(
timestamp, url, title, popularity_index)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
file_object.close()
manager.ParsersManager.RegisterParsers([
OperaTypedHistoryParser, OperaGlobalHistoryParser])
+118
View File
@@ -0,0 +1,118 @@
#!/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 Opera browser history parsers."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import opera as opera_formatter
from plaso.lib import timelib_test
from plaso.parsers import opera
from plaso.parsers import test_lib
class OperaTypedParserTest(test_lib.ParserTestCase):
"""Tests for the Opera Typed History parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = opera.OperaTypedHistoryParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['typed_history.xml'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 4)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-11 23:45:27')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.entry_selection, 'Filled from autocomplete.')
expected_string = u'plaso.kiddaland.net (Filled from autocomplete.)'
self._TestGetMessageStrings(event_object, expected_string, expected_string)
event_object = event_objects[3]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-11 22:46:07')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.entry_selection, 'Manually typed.')
expected_string = u'theonion.com (Manually typed.)'
self._TestGetMessageStrings(event_object, expected_string, expected_string)
class OperaGlobalParserTest(test_lib.ParserTestCase):
"""Tests for the Opera Global History parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = opera.OperaGlobalHistoryParser()
def testParseFile(self):
"""Read a history file and run a few tests."""
test_file = self._GetTestFilePath(['global_history.dat'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 37)
event_object = event_objects[4]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-11 22:45:46')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_msg = (
u'http://www.mbl.is/frettir/erlent/2013/11/11/'
u'karl_bretaprins_faer_ellilifeyri/ (Karl Bretaprins fær ellilífeyri'
u' - mbl.is) [First and Only Visit]')
expected_msg_short = (
u'http://www.mbl.is/frettir/erlent/2013/11/11/'
u'karl_bretaprins_faer_ellilifeyri/...')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
event_object = event_objects[10]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-11 22:45:55')
self.assertEquals(event_object.timestamp, expected_timestamp)
event_object = event_objects[16]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-11-11 22:46:16')
self.assertEquals(event_object.timestamp, expected_timestamp)
expected_title = (
u'10 Celebrities You Never Knew Were Abducted And Murdered '
u'By Andie MacDowell | The Onion - America\'s Finest News Source')
self.assertEquals(event_object.title, expected_title)
if __name__ == '__main__':
unittest.main()
+183
View File
@@ -0,0 +1,183 @@
#!/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 a parser for OXML files (i.e. MS Office 2007+)."""
import logging
import re
import struct
import zipfile
from xml.etree import ElementTree
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'David Nides (david.nides@gmail.com)'
class OpenXMLParserEvent(time_events.TimestampEvent):
"""Process timestamps from MS Office XML Events."""
DATA_TYPE = 'metadata:openxml'
def __init__(self, timestamp_string, usage, metadata):
"""Initializes the event object.
Args:
timestamp_string: An ISO 8601 representation of a timestamp.
usage: The description of the usage of the time value.
metadata: A dict object containing extracted metadata.
"""
timestamp = timelib.Timestamp.FromTimeString(timestamp_string)
super(OpenXMLParserEvent, self).__init__(timestamp, usage, self.DATA_TYPE)
for key, value in metadata.iteritems():
setattr(self, key, value)
class OpenXMLParser(interface.BaseParser):
"""Parse metadata from OXML files."""
NAME = 'openxml'
DESCRIPTION = u'Parser for OpenXML (OXML) files.'
_METAKEY_TRANSLATE = {
'creator': 'author',
'lastModifiedBy': 'last_saved_by',
'Total_Time': 'total_edit_time',
'Pages': 'num_pages',
'Characters_with_spaces': 'num_chars_w_spaces',
'Paragraphs': 'num_paragraphs',
'Characters': 'num_chars',
'Lines': 'num_lines',
'revision': 'revision_num',
'Words': 'num_words',
'Application': 'creating_app',
'Shared_Doc': 'shared',
}
_FILES_REQUIRED = frozenset([
'[Content_Types].xml', '_rels/.rels', 'docProps/core.xml'])
def _FixString(self, key):
"""Convert CamelCase to lower_with_underscore."""
# TODO: Add unicode support.
fix_key = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', fix_key).lower()
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract data from an OXML file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
if not zipfile.is_zipfile(file_object):
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, 'Not a Zip file.'))
try:
zip_container = zipfile.ZipFile(file_object, 'r')
except (zipfile.BadZipfile, struct.error, zipfile.LargeZipFile):
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, 'Bad Zip file.'))
zip_name_list = set(zip_container.namelist())
if not self._FILES_REQUIRED.issubset(zip_name_list):
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, 'OXML element(s) missing.'))
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
metadata = {}
timestamps = {}
try:
rels_xml = zip_container.read('_rels/.rels')
except zipfile.BadZipfile as exception:
logging.error(
u'Unable to parse file {0:s} with error: {1:s}'.format(
file_entry.name, exception))
return
rels_root = ElementTree.fromstring(rels_xml)
for properties in rels_root.iter():
if 'properties' in repr(properties.get('Type')):
try:
xml = zip_container.read(properties.get('Target'))
root = ElementTree.fromstring(xml)
except (
OverflowError, IndexError, KeyError, ValueError,
zipfile.BadZipfile) as exception:
logging.warning(
u'[{0:s}] unable to read property with error: {1:s}.'.format(
self.NAME, exception))
continue
for element in root.iter():
if element.text:
_, _, tag = element.tag.partition('}')
# Not including the 'lpstr' attribute because it is
# very verbose.
if tag == 'lpstr':
continue
if tag in ('created', 'modified', 'lastPrinted'):
timestamps[tag] = element.text
else:
tag_name = self._METAKEY_TRANSLATE.get(tag, self._FixString(tag))
metadata[tag_name] = element.text
if timestamps.get('created', None):
event_object = OpenXMLParserEvent(
timestamps.get('created'), eventdata.EventTimestamp.CREATION_TIME,
metadata)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if timestamps.get('modified', None):
event_object = OpenXMLParserEvent(
timestamps.get('modified'),
eventdata.EventTimestamp.MODIFICATION_TIME, metadata)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if timestamps.get('lastPrinted', None):
event_object = OpenXMLParserEvent(
timestamps.get('lastPrinted'), eventdata.EventTimestamp.LAST_PRINTED,
metadata)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(OpenXMLParser)
+96
View File
@@ -0,0 +1,96 @@
#!/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 OXML parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import oxml as oxml_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import oxml
from plaso.parsers import test_lib
class OXMLTest(test_lib.ParserTestCase):
"""Tests for the OXML parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = oxml.OpenXMLParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['Document.docx'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 2)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-11-07 23:29:00')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME)
event_object = event_objects[1]
self.assertEquals(event_object.num_chars, u'13')
self.assertEquals(event_object.total_time, u'1385')
self.assertEquals(event_object.characters_with_spaces, u'14')
self.assertEquals(event_object.i4, u'1')
self.assertEquals(event_object.app_version, u'14.0000')
self.assertEquals(event_object.num_lines, u'1')
self.assertEquals(event_object.scale_crop, u'false')
self.assertEquals(event_object.num_pages, u'1')
self.assertEquals(event_object.num_words, u'2')
self.assertEquals(event_object.links_up_to_date, u'false')
self.assertEquals(event_object.num_paragraphs, u'1')
self.assertEquals(event_object.doc_security, u'0')
self.assertEquals(event_object.hyperlinks_changed, u'false')
self.assertEquals(event_object.revision_num, u'3')
self.assertEquals(event_object.last_saved_by, u'Nides')
self.assertEquals(event_object.author, u'Nides')
self.assertEquals(
event_object.creating_app, u'Microsoft Office Word')
self.assertEquals(event_object.template, u'Normal.dotm')
expected_msg = (
u'Creating App: Microsoft Office Word '
u'App version: 14.0000 '
u'Last saved by: Nides '
u'Author: Nides '
u'Revision Num: 3 '
u'Template: Normal.dotm '
u'Num pages: 1 '
u'Num words: 2 '
u'Num chars: 13 '
u'Num lines: 1 '
u'Hyperlinks changed: false '
u'Links up to date: false '
u'Scale crop: false')
expected_msg_short = (
u'Author: Nides')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+843
View File
@@ -0,0 +1,843 @@
#!/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.
"""Parser for PCAP files."""
import binascii
import operator
import socket
import dpkt
from plaso.events import time_events
from plaso.lib import errors
from plaso.lib import eventdata
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Dominique Kilman (lexistar97@gmail.com)'
def ParseDNS(dns_packet_data):
"""Parse DNS packets and return a string with relevant details.
Args:
dns_packet_data: DNS packet data.
Returns:
Formatted DNS details.
"""
dns_data = []
try:
dns = dpkt.dns.DNS(dns_packet_data)
if dns.rcode is dpkt.dns.DNS_RCODE_NOERR:
if dns.get_qr() == 1:
if not dns.an:
dns_data.append('DNS Response: No answer for ')
dns_data.append(dns.qd[0].name)
else:
# Type of DNS answer.
for answer in dns.an:
if answer.type == 5:
dns_data.append('DNS-CNAME request ')
dns_data.append(answer.name)
dns_data.append(' response: ')
dns_data.append(answer.cname)
elif answer.type == 1:
dns_data.append('DNS-A request ')
dns_data.append(answer.name)
dns_data.append(' response: ')
dns_data.append(socket.inet_ntoa(answer.rdata))
elif answer.type == 12:
dns_data.append('DNS-PTR request ')
dns_data.append(answer.name)
dns_data.append(' response: ')
dns_data.append(answer.ptrname)
elif not dns.get_qr():
dns_data.append('DNS Query for ')
dns_data.append(dns.qd[0].name)
else:
dns_data.append('DNS error code ')
dns_data.append(str(dns.rcode))
except dpkt.UnpackError as exception:
dns_data.append('DNS Unpack Error: {0:s}. First 20 of data {1:s}'.format(
exception, repr(dns_packet_data[:20])))
except IndexError as exception:
dns_data.append('DNS Index Error: {0:s}'.format(exception))
return u' '.join(dns_data)
def ParseNetBios(netbios_packet):
"""Parse the netBIOS stream details.
Args:
netbios_packet: NetBIOS packet.
Returns:
Formatted netBIOS details.
"""
netbios_data = []
for query in netbios_packet.qd:
netbios_data.append('NETBIOS qd:')
netbios_data.append(repr(dpkt.netbios.decode_name(query.name)))
for answer in netbios_packet.an:
netbios_data.append('NETBIOS an:')
netbios_data.append(repr(dpkt.netbios.decode_name(answer.name)))
for name in netbios_packet.ns:
netbios_data.append('NETBIOS ns:')
netbios_data.append(repr(dpkt.netbios.decode_name(name.name)))
return u' '.join(netbios_data)
def TCPFlags(flag):
"""Check the tcp flags for a packet for future use.
Args:
flag: Flag value from TCP packet.
Returns:
String with printable flags for specific packet.
"""
res = []
if flag & dpkt.tcp.TH_FIN:
res.append('FIN')
if flag & dpkt.tcp.TH_SYN:
res.append('SYN')
if flag & dpkt.tcp.TH_RST:
res.append('RST')
if flag & dpkt.tcp.TH_PUSH:
res.append('PUSH')
if flag & dpkt.tcp.TH_ACK:
res.append('ACK')
if flag & dpkt.tcp.TH_URG:
res.append('URG')
if flag & dpkt.tcp.TH_ECE:
res.append('ECN')
if flag & dpkt.tcp.TH_CWR:
res.append('CWR')
return '|'.join(res)
def ICMPTypes(packet):
"""Parse the type information for the icmp packets.
Args:
packet: ICMP packet data.
Returns:
Formatted ICMP details.
"""
icmp_type = packet.type
icmp_code = packet.code
icmp_data = []
icmp_data.append('ICMP')
# TODO: Make the below code more readable.
# Possible to use lookup dict? Or method
# calls?
if icmp_type is dpkt.icmp.ICMP_CODE_NONE:
icmp_data.append('ICMP without codes')
elif icmp_type is dpkt.icmp.ICMP_ECHOREPLY:
icmp_data.append('echo reply')
elif icmp_type is dpkt.icmp.ICMP_UNREACH:
icmp_data.append('ICMP dest unreachable')
if icmp_code is dpkt.icmp.ICMP_UNREACH_NET:
icmp_data.append(': bad net')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST:
icmp_data.append(': host unreachable')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_PROTO:
icmp_data.append(': bad protocol')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_PORT:
icmp_data.append(': port unreachable')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_NEEDFRAG:
icmp_data.append(': IP_DF caused drop')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_SRCFAIL:
icmp_data.append(': src route failed')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_NET_UNKNOWN:
icmp_data.append(': unknown net')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_UNKNOWN:
icmp_data.append(': unknown host')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_ISOLATED:
icmp_data.append(': src host isolated')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_NET_PROHIB:
icmp_data.append(': for crypto devs')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_PROHIB:
icmp_data.append(': for cypto devs')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_TOSNET:
icmp_data.append(': bad tos for net')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_TOSHOST:
icmp_data.append(': bad tos for host')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_FILTER_PROHIB:
icmp_data.append(': prohibited access')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_PRECEDENCE:
icmp_data.append(': precedence error')
elif icmp_code is dpkt.icmp.ICMP_UNREACH_PRECEDENCE_CUTOFF:
icmp_data.append(': precedence cutoff')
elif icmp_type is dpkt.icmp.ICMP_SRCQUENCH:
icmp_data.append('ICMP source quench')
elif icmp_type is dpkt.icmp.ICMP_REDIRECT:
icmp_data.append('ICMP Redirect')
if icmp_code is dpkt.icmp.ICMP_REDIRECT_NET:
icmp_data.append(' for network')
elif icmp_code is dpkt.icmp.ICMP_REDIRECT_HOST:
icmp_data.append(' for host')
elif icmp_code is dpkt.icmp.ICMP_REDIRECT_TOSNET:
icmp_data.append(' for tos and net')
elif icmp_code is dpkt.icmp.ICMP_REDIRECT_TOSHOST:
icmp_data.append(' for tos and host')
elif icmp_type is dpkt.icmp.ICMP_ALTHOSTADDR:
icmp_data.append('ICMP alternate host address')
elif icmp_type is dpkt.icmp.ICMP_ECHO:
icmp_data.append('ICMP echo')
elif icmp_type is dpkt.icmp.ICMP_RTRADVERT:
icmp_data.append('ICMP Route advertisement')
if icmp_code is dpkt.icmp.ICMP_RTRADVERT_NORMAL:
icmp_data.append(': normal')
elif icmp_code is dpkt.icmp.ICMP_RTRADVERT_NOROUTE_COMMON:
icmp_data.append(': selective routing')
elif icmp_type is dpkt.icmp.ICMP_RTRSOLICIT:
icmp_data.append('ICMP Router solicitation')
elif icmp_type is dpkt.icmp.ICMP_TIMEXCEED:
icmp_data.append('ICMP time exceeded, code:')
if icmp_code is dpkt.icmp.ICMP_TIMEXCEED_INTRANS:
icmp_data.append(' ttl==0 in transit')
elif icmp_code is dpkt.icmp.ICMP_TIMEXCEED_REASS:
icmp_data.append('ttl==0 in reass')
elif icmp_type is dpkt.icmp.ICMP_PARAMPROB:
icmp_data.append('ICMP ip header bad')
if icmp_code is dpkt.icmp.ICMP_PARAMPROB_ERRATPTR:
icmp_data.append(':req. opt. absent')
elif icmp_code is dpkt.icmp.ICMP_PARAMPROB_OPTABSENT:
icmp_data.append(': req. opt. absent')
elif icmp_code is dpkt.icmp.ICMP_PARAMPROB_LENGTH:
icmp_data.append(': length')
elif icmp_type is dpkt.icmp.ICMP_TSTAMP:
icmp_data.append('ICMP timestamp request')
elif icmp_type is dpkt.icmp.ICMP_TSTAMPREPLY:
icmp_data.append('ICMP timestamp reply')
elif icmp_type is dpkt.icmp.ICMP_INFO:
icmp_data.append('ICMP information request')
elif icmp_type is dpkt.icmp.ICMP_INFOREPLY:
icmp_data.append('ICMP information reply')
elif icmp_type is dpkt.icmp.ICMP_MASK:
icmp_data.append('ICMP address mask request')
elif icmp_type is dpkt.icmp.ICMP_MASKREPLY:
icmp_data.append('ICMP address mask reply')
elif icmp_type is dpkt.icmp.ICMP_TRACEROUTE:
icmp_data.append('ICMP traceroute')
elif icmp_type is dpkt.icmp.ICMP_DATACONVERR:
icmp_data.append('ICMP data conversion error')
elif icmp_type is dpkt.icmp.ICMP_MOBILE_REDIRECT:
icmp_data.append('ICMP mobile host redirect')
elif icmp_type is dpkt.icmp.ICMP_IP6_WHEREAREYOU:
icmp_data.append('ICMP IPv6 where-are-you')
elif icmp_type is dpkt.icmp.ICMP_IP6_IAMHERE:
icmp_data.append('ICMP IPv6 i-am-here')
elif icmp_type is dpkt.icmp.ICMP_MOBILE_REG:
icmp_data.append('ICMP mobile registration req')
elif icmp_type is dpkt.icmp.ICMP_MOBILE_REGREPLY:
icmp_data.append('ICMP mobile registration reply')
elif icmp_type is dpkt.icmp.ICMP_DNS:
icmp_data.append('ICMP domain name request')
elif icmp_type is dpkt.icmp.ICMP_DNSREPLY:
icmp_data.append('ICMP domain name reply')
elif icmp_type is dpkt.icmp.ICMP_PHOTURIS:
icmp_data.append('ICMP Photuris')
if icmp_code is dpkt.icmp.ICMP_PHOTURIS_UNKNOWN_INDEX:
icmp_data.append(': unknown sec index')
elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_AUTH_FAILED:
icmp_data.append(': auth failed')
elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_DECOMPRESS_FAILED:
icmp_data.append(': decompress failed')
elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_DECRYPT_FAILED:
icmp_data.append(': decrypt failed')
elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_NEED_AUTHN:
icmp_data.append(': no authentication')
elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_NEED_AUTHZ:
icmp_data.append(': no authorization')
elif icmp_type is dpkt.icmp.ICMP_TYPE_MAX:
icmp_data.append('ICMP Type Max')
return u' '.join(icmp_data)
class Stream(object):
"""Used to store packet details on network streams parsed from a pcap file."""
def __init__(self, packet, prot_data, source_ip, dest_ip, prot):
"""Initialize new stream.
Args:
packet: Packet data.
prot_data: Protocol level data for ARP, UDP, RCP, ICMP.
other types of ether packets, this is just the ether.data.
source_ip: Source IP.
dest_ip: Dest IP.
prot: Protocol (TCP, UDP, ICMP, ARP).
"""
self.packet_id = [packet[1]]
self.timestamps = [packet[0]]
self.size = packet[3]
self.start_time = packet[0]
self.all_data = [prot_data]
self.protocol_data = ''
self.stream_data = []
if prot == 'TCP' or prot == 'UDP':
self.source_port = prot_data.sport
self.dest_port = prot_data.dport
else:
self.source_port = ''
self.dest_port = ''
self.source_ip = source_ip
self.dest_ip = dest_ip
self.protocol = prot
def AddPacket(self, packet, prot_data):
"""Add another packet to an existing stream.
Args:
packet: Packet data.
prot_data: Protocol level data for ARP, UDP, RCP, ICMP.
other types of ether packets, this is just the ether.data
"""
self.packet_id.append(packet[1])
self.timestamps.append(packet[0])
self.all_data.append(prot_data)
self.size += packet[3]
def SpecialTypes(self):
"""Checks for some special types of packets.
This method checks for some special packets and assembles usable data
currently works for: DNS (udp 53), http, netbios (udp 137), ICMP.
Returns:
A tuple consisting of a basic desctiption of the stream
(i.e. HTTP Request) and the prettyfied string for the protocols.
"""
packet_details = []
if self.stream_data[:4] == 'HTTP':
try:
http = dpkt.http.Response(self.stream_data)
packet_details.append('HTTP Response: status: ')
packet_details.append(http.status)
packet_details.append(' reason: ')
packet_details.append(http.reason)
packet_details.append(' version: ')
packet_details.append(http.version)
return 'HTTP Response', u' '.join(packet_details)
except dpkt.UnpackError as exception:
packet_details = (
u'HTTP Response Unpack Error: {0:s}. '
u'First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'HTTP Response', packet_details
except IndexError as exception:
packet_details = (
u'HTTP Response Index Error: {0:s}. First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'HTTP Response', packet_details
except ValueError as exception:
packet_details = (
u'HTTP Response parsing error: {0:s}. '
u'First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'HTTP Response', packet_details
elif self.stream_data[:3] == 'GET' or self.stream_data[:4] == 'POST':
try:
http = dpkt.http.Request(self.stream_data)
packet_details.append('HTTP Request: method: ')
packet_details.append(http.method)
packet_details.append(' uri: ')
packet_details.append(http.uri)
packet_details.append(' version: ')
packet_details.append(http.version)
packet_details.append(' headers: ')
packet_details.append(repr(http.headers))
return 'HTTP Request', u' '.join(packet_details)
except dpkt.UnpackError as exception:
packet_details = (
u'HTTP Request unpack error: {0:s}. First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'HTTP Request', packet_details
except ValueError as exception:
packet_details = (
u'HTTP Request parsing error: {0:s}. '
u'First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'HTTP Request', packet_details
elif self.protocol == 'UDP' and (
self.source_port == 53 or self.dest_port == 53):
# DNS request/replies.
# Check to see if the lengths are valid.
for packet in self.all_data:
if not packet.ulen == len(packet):
packet_details.append('Truncated DNS packets - unable to parse: ')
packet_details.append(repr(self.stream_data[15:40]))
return 'DNS', u' '.join(packet_details)
return 'DNS', ParseDNS(self.stream_data)
elif self.protocol == 'UDP' and (
self.source_port == 137 or self.dest_port == 137):
return 'NetBIOS', ParseNetBios(dpkt.netbios.NS(self.stream_data))
elif self.protocol == 'ICMP':
# ICMP packets all end up as 1 stream, so they need to be
# processed 1 by 1.
return 'ICMP', ICMPTypes(self.all_data[0])
elif '\x03\x01' in self.stream_data[1:3]:
# Some form of ssl3 data.
try:
ssl = dpkt.ssl.SSL2(self.stream_data)
packet_details.append('SSL data. Length: ')
packet_details.append(str(ssl.len))
return 'SSL', u' '.join(packet_details)
except dpkt.UnpackError as exception:
packet_details = (
u'SSL unpack error: {0:s}. First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'SSL', packet_details
elif '\x03\x00' in self.stream_data[1:3]:
# Some form of ssl3 data.
try:
ssl = dpkt.ssl.SSL2(self.stream_data)
packet_details.append('SSL data. Length: ')
packet_details.append(str(ssl.len))
return 'SSL', u' '.join(packet_details)
except dpkt.UnpackError as exception:
packet_details = (
u'SSL unpack error: {0:s}. First 20 of data {1:s}').format(
exception, repr(self.stream_data[:20]))
return 'SSL', packet_details
return 'other', self.protocol_data
def Clean(self):
"""Clean up stream data."""
clean_data = []
for packet in self.all_data:
try:
clean_data.append(packet.data)
except AttributeError:
pass
self.stream_data = ''.join(clean_data)
class PcapEvent(time_events.PosixTimeEvent):
"""Convenience class for a PCAP record event."""
DATA_TYPE = 'metadata:pcap'
def __init__(self, timestamp, usage, stream_object):
"""Initializes the event.
Args:
timestamp: The POSIX value of the timestamp.
usage: A usage description value.
stream_object: The stream object (instance of Stream).
"""
super(PcapEvent, self).__init__(timestamp, usage)
self.source_ip = stream_object.source_ip
self.dest_ip = stream_object.dest_ip
self.source_port = stream_object.source_port
self.dest_port = stream_object.dest_port
self.protocol = stream_object.protocol
self.size = stream_object.size
self.stream_type, self.protocol_data = stream_object.SpecialTypes()
self.first_packet_id = min(stream_object.packet_id)
self.last_packet_id = max(stream_object.packet_id)
self.packet_count = len(stream_object.packet_id)
self.stream_data = repr(stream_object.stream_data[:50])
class PcapParser(interface.BaseParser):
"""Parses PCAP files."""
NAME = 'pcap'
DESCRIPTION = u'Parser for PCAP files.'
def _ParseIPPacket(
self, connections, trunc_list, packet_number, timestamp,
packet_data_size, ip_packet):
"""Parses an IP packet.
Args:
connections: A dictionary object to track the IP connections.
trunc_list: A list of packets that truncated strangely and could
not be turned into a stream.
packet_number: The PCAP packet number, where 1 is the first packet.
timestamp: The PCAP packet timestamp.
packet_data_size: The packet data size.
ip_packet: The IP packet (instance of dpkt.ip.IP).
"""
packet_values = [timestamp, packet_number, ip_packet, packet_data_size]
source_ip_address = socket.inet_ntoa(ip_packet.src)
destination_ip_address = socket.inet_ntoa(ip_packet.dst)
if ip_packet.p == dpkt.ip.IP_PROTO_TCP:
# Later versions of dpkt seem to return a string instead of a TCP object.
if isinstance(ip_packet.data, str):
try:
tcp = dpkt.tcp.TCP(ip_packet.data)
except (dpkt.NeedData, dpkt.UnpackError):
trunc_list.append(packet_values)
return
else:
tcp = ip_packet.data
stream_key = 'tcp: {0:s}:{1:d} > {2:s}:{3:d}'.format(
source_ip_address, tcp.sport, destination_ip_address, tcp.dport)
if stream_key in connections:
connections[stream_key].AddPacket(packet_values, tcp)
else:
connections[stream_key] = Stream(
packet_values, tcp, source_ip_address, destination_ip_address,
'TCP')
elif ip_packet.p == dpkt.ip.IP_PROTO_UDP:
# Later versions of dpkt seem to return a string instead of an UDP object.
if isinstance(ip_packet.data, str):
try:
udp = dpkt.udp.UDP(ip_packet.data)
except (dpkt.NeedData, dpkt.UnpackError):
trunc_list.append(packet_values)
return
else:
udp = ip_packet.data
stream_key = 'udp: {0:s}:{1:d} > {2:s}:{3:d}'.format(
source_ip_address, udp.sport, destination_ip_address, udp.dport)
if stream_key in connections:
connections[stream_key].AddPacket(packet_values, udp)
else:
connections[stream_key] = Stream(
packet_values, udp, source_ip_address, destination_ip_address,
'UDP')
elif ip_packet.p == dpkt.ip.IP_PROTO_ICMP:
# Later versions of dpkt seem to return a string instead of
# an ICMP object.
if isinstance(ip_packet.data, str):
icmp = dpkt.icmp.ICMP(ip_packet.data)
else:
icmp = ip_packet.data
stream_key = 'icmp: {0:d} {1:s} > {2:s}'.format(
timestamp, source_ip_address, destination_ip_address)
if stream_key in connections:
connections[stream_key].AddPacket(packet_values, icmp)
else:
connections[stream_key] = Stream(
packet_values, icmp, source_ip_address, destination_ip_address,
'ICMP')
def _ParseOtherPacket(self, packet_values):
"""Parses a non-IP packet.
Args:
packet_values: list of packet values
Returns:
A stream object (instance of Stream) or None if the packet data
is not supported.
"""
ether = packet_values[2]
stream_object = None
if ether.type == dpkt.ethernet.ETH_TYPE_ARP:
arp = ether.data
arp_data = []
stream_object = Stream(
packet_values, arp, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'ARP')
if arp.op == dpkt.arp.ARP_OP_REQUEST:
arp_data.append('arp request: target IP = ')
arp_data.append(socket.inet_ntoa(arp.tpa))
stream_object.protocol_data = u' '.join(arp_data)
elif arp.op == dpkt.arp.ARP_OP_REPLY:
arp_data.append('arp reply: target IP = ')
arp_data.append(socket.inet_ntoa(arp.tpa))
arp_data.append(' target MAC = ')
arp_data.append(binascii.hexlify(arp.tha))
stream_object.protocol_data = u' '.join(arp_data)
elif arp.op == dpkt.arp.ARP_OP_REVREQUEST:
arp_data.append('arp protocol address request: target IP = ')
arp_data.append(socket.inet_ntoa(arp.tpa))
stream_object.protocol_data = u' '.join(arp_data)
elif arp.op == dpkt.arp.ARP_OP_REVREPLY:
arp_data.append('arp protocol address reply: target IP = ')
arp_data.append(socket.inet_ntoa(arp.tpa))
arp_data.append(' target MAC = ')
arp_data.append(binascii.hexlify(arp.tha))
stream_object.protocol_data = u' '.join(arp_data)
elif ether.type == dpkt.ethernet.ETH_TYPE_IP6:
ip6 = ether.data
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ip6.src),
binascii.hexlify(ip6.dst), 'IPv6')
stream_object.protocol_data = 'IPv6'
elif ether.type == dpkt.ethernet.ETH_TYPE_CDP:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'CDP')
stream_object.protocol_data = 'CDP'
elif ether.type == dpkt.ethernet.ETH_TYPE_DTP:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'DTP')
stream_object.protocol_data = 'DTP'
elif ether.type == dpkt.ethernet.ETH_TYPE_REVARP:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'RARP')
stream_object.protocol_data = 'Reverse ARP'
elif ether.type == dpkt.ethernet.ETH_TYPE_8021Q:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), '8021Q packet')
stream_object.protocol_data = '8021Q packet'
elif ether.type == dpkt.ethernet.ETH_TYPE_IPX:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'IPX')
stream_object.protocol_data = 'IPX'
elif ether.type == dpkt.ethernet.ETH_TYPE_PPP:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'PPP')
stream_object.protocol_data = 'PPP'
elif ether.type == dpkt.ethernet.ETH_TYPE_MPLS:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'MPLS')
stream_object.protocol_data = 'MPLS'
elif ether.type == dpkt.ethernet.ETH_TYPE_MPLS_MCAST:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'MPLS')
stream_object.protocol_data = 'MPLS MCAST'
elif ether.type == dpkt.ethernet.ETH_TYPE_PPPoE_DISC:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'PPOE')
stream_object.protocol_data = 'PPoE Disc packet'
elif ether.type == dpkt.ethernet.ETH_TYPE_PPPoE:
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), 'PPPoE')
stream_object.protocol_data = 'PPPoE'
elif str(hex(ether.type)) == '0x2452':
stream_object = Stream(
packet_values, ether.data, binascii.hexlify(ether.src),
binascii.hexlify(ether.dst), '802.11')
stream_object.protocol_data = '802.11'
return stream_object
def _ParseOtherStreams(self, other_list, trunc_list):
"""Process PCAP packets that are not IP packets.
For all packets that were not IP packets, create stream containers
depending on the type of packet.
Args:
other_list: List of non-ip packets.
trunc_list: A list of packets that truncated strangely and could
not be turned into a stream.
Returns:
A list of stream objects (instances of Stream).
"""
other_streams = []
for packet_values in other_list:
stream_object = self._ParseOtherPacket(packet_values)
if stream_object:
other_streams.append(stream_object)
for packet_values in trunc_list:
ip_packet = packet_values[2]
source_ip_address = socket.inet_ntoa(ip_packet.src)
destination_ip_address = socket.inet_ntoa(ip_packet.dst)
stream_object = Stream(
packet_values, ip_packet.data, source_ip_address,
destination_ip_address, 'BAD')
stream_object.protocolData = 'Bad truncated IP packet'
other_streams.append(stream_object)
return other_streams
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Parses a PCAP file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
file_object = file_entry.GetFileObject()
self.ParseFileObject(
parser_context, file_object, file_entry=file_entry,
parser_chain=parser_chain)
file_object.close()
def ParseFileObject(
self, parser_context, file_object, file_entry=None, parser_chain=None):
"""Parses a PCAP file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_object: A file-like object.
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
Raises:
UnableToParseFile: when the file cannot be parsed.
"""
data = file_object.read(dpkt.pcap.FileHdr.__hdr_len__)
try:
file_header = dpkt.pcap.FileHdr(data)
packet_header_class = dpkt.pcap.PktHdr
except (dpkt.NeedData, dpkt.UnpackError) as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, exception))
if file_header.magic == dpkt.pcap.PMUDPCT_MAGIC:
try:
file_header = dpkt.pcap.LEFileHdr(data)
packet_header_class = dpkt.pcap.LEPktHdr
except (dpkt.NeedData, dpkt.UnpackError) as exception:
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format(
self.NAME, file_entry.name, exception))
elif file_header.magic != dpkt.pcap.TCPDUMP_MAGIC:
raise errors.UnableToParseFile(u'Unsupported file signature')
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
packet_number = 1
connections = {}
other_list = []
trunc_list = []
data = file_object.read(dpkt.pcap.PktHdr.__hdr_len__)
while data:
packet_header = packet_header_class(data)
timestamp = packet_header.tv_sec + (packet_header.tv_usec / 1000000.0)
packet_data = file_object.read(packet_header.caplen)
ethernet_frame = dpkt.ethernet.Ethernet(packet_data)
if ethernet_frame.type == dpkt.ethernet.ETH_TYPE_IP:
self._ParseIPPacket(
connections, trunc_list, packet_number, timestamp,
len(ethernet_frame), ethernet_frame.data)
else:
packet_values = [
timestamp, packet_number, ethernet_frame, len(ethernet_frame)]
other_list.append(packet_values)
packet_number += 1
data = file_object.read(dpkt.pcap.PktHdr.__hdr_len__)
other_streams = self._ParseOtherStreams(other_list, trunc_list)
for stream_object in sorted(
connections.values(), key=operator.attrgetter('start_time')):
if not stream_object.protocol == 'ICMP':
stream_object.Clean()
event_objects = [
PcapEvent(
min(stream_object.timestamps),
eventdata.EventTimestamp.START_TIME, stream_object),
PcapEvent(
max(stream_object.timestamps),
eventdata.EventTimestamp.END_TIME, stream_object)]
parser_context.ProduceEvents(
event_objects, parser_chain=parser_chain, file_entry=file_entry)
for stream_object in other_streams:
event_objects = [
PcapEvent(
min(stream_object.timestamps),
eventdata.EventTimestamp.START_TIME, stream_object),
PcapEvent(
max(stream_object.timestamps),
eventdata.EventTimestamp.END_TIME, stream_object)]
parser_context.ProduceEvents(
event_objects, parser_chain=parser_chain, file_entry=file_entry)
manager.ParsersManager.RegisterParser(PcapParser)
+117
View File
@@ -0,0 +1,117 @@
#!/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 PCAP parser."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import pcap as pcap_formatter
from plaso.parsers import pcap
from plaso.parsers import test_lib
class PcapParserTest(test_lib.ParserTestCase):
"""Tests for the PCAP parser."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._parser = pcap.PcapParser()
def testParse(self):
"""Tests the Parse function."""
test_file = self._GetTestFilePath(['test.pcap'])
event_queue_consumer = self._ParseFile(self._parser, test_file)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# PCAP information:
# Number of streams: 96 (TCP: 47, UDP: 39, ICMP: 0, Other: 10)
#
# For each stream 2 event objects are generated one for the start
# and one for the end time.
self.assertEquals(len(event_objects), 192)
# Test stream 3 (event object 6).
# Protocol: TCP
# Source IP: 192.168.195.130
# Dest IP: 63.245.217.43
# Source Port: 1038
# Dest Port: 443
# Stream Type: SSL
# Starting Packet: 4
# Ending Packet: 6
event_object = event_objects[6]
self.assertEquals(event_object.packet_count, 3)
self.assertEquals(event_object.protocol, u'TCP')
self.assertEquals(event_object.source_ip, u'192.168.195.130')
self.assertEquals(event_object.dest_ip, u'63.245.217.43')
self.assertEquals(event_object.dest_port, 443)
self.assertEquals(event_object.source_port, 1038)
self.assertEquals(event_object.stream_type, u'SSL')
self.assertEquals(event_object.first_packet_id, 4)
self.assertEquals(event_object.last_packet_id, 6)
# Test stream 6 (event object 12).
# Protocol: UDP
# Source IP: 192.168.195.130
# Dest IP: 192.168.195.2
# Source Port: 55679
# Dest Port: 53
# Stream Type: DNS
# Starting Packet: 4
# Ending Packet: 6
# Protocol Data: DNS Query for wpad.localdomain
event_object = event_objects[12]
self.assertEquals(event_object.packet_count, 5)
self.assertEquals(event_object.protocol, u'UDP')
self.assertEquals(event_object.source_ip, u'192.168.195.130')
self.assertEquals(event_object.dest_ip, u'192.168.195.2')
self.assertEquals(event_object.dest_port, 53)
self.assertEquals(event_object.source_port, 55679)
self.assertEquals(event_object.stream_type, u'DNS')
self.assertEquals(event_object.first_packet_id, 11)
self.assertEquals(event_object.last_packet_id, 1307)
self.assertEquals(
event_object.protocol_data, u'DNS Query for wpad.localdomain')
expected_msg = (
u'Source IP: 192.168.195.130 '
u'Destination IP: 192.168.195.2 '
u'Source Port: 55679 '
u'Destination Port: 53 '
u'Protocol: UDP '
u'Type: DNS '
u'Size: 380 '
u'Protocol Data: DNS Query for wpad.localdomain '
u'Stream Data: \'\\xb8\\x9c\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00'
u'\\x00\\x00\\x04wpad\\x0blocaldomain\\x00\\x00\\x01\\x00\\x01\\xb8'
u'\\x9c\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x04wpa\' '
u'First Packet ID: 11 '
u'Last Packet ID: 1307 '
u'Packet Count: 5')
expected_msg_short = (
u'Type: DNS '
u'First Packet ID: 11')
self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short)
if __name__ == '__main__':
unittest.main()
+159
View File
@@ -0,0 +1,159 @@
#!/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 the Property List (Plist) Parser.
Plaso's engine calls PlistParser when it encounters Plist files to be processed.
"""
import binascii
import logging
from binplist import binplist
from plaso.lib import errors
from plaso.lib import utils
from plaso.parsers import interface
from plaso.parsers import manager
class PlistParser(interface.BasePluginsParser):
"""De-serializes and parses plists the event objects are generated by plist.
The Plaso engine calls parsers by their Parse() method. This parser's
Parse() has GetTopLevel() which deserializes plist files using the binplist
library and calls plugins (PlistPlugin) registered through the
interface by their Process() to produce event objects.
Plugins are how this parser understands the content inside a plist file,
each plugin holds logic specific to a particular plist file. See the
interface and plist_plugins/ directory for examples of how plist plugins are
implemented.
"""
NAME = 'plist'
DESCRIPTION = u'Parser for binary and text plist files.'
_plugin_classes = {}
def __init__(self):
"""Initializes a parser object."""
super(PlistParser, self).__init__()
self._plugins = PlistParser.GetPluginObjects()
def GetTopLevel(self, file_object, file_name=''):
"""Returns the deserialized content of a plist as a dictionary object.
Args:
file_object: A file-like object to parse.
file_name: The name of the file-like object.
Returns:
A dictionary object representing the contents of the plist.
"""
try:
top_level_object = binplist.readPlist(file_object)
except binplist.FormatError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] File is not a plist file: {1:s}'.format(
self.NAME, utils.GetUnicodeString(exception)))
except (
LookupError, binascii.Error, ValueError, AttributeError) as exception:
raise errors.UnableToParseFile(
u'[{0:s}] Unable to parse XML file, reason: {1:s}'.format(
self.NAME, exception))
except OverflowError as exception:
raise errors.UnableToParseFile(
u'[{0:s}] Unable to parse: {1:s} with error: {2:s}'.format(
self.NAME, file_name, exception))
if not top_level_object:
raise errors.UnableToParseFile(
u'[{0:s}] File is not a plist: {1:s}'.format(
self.NAME, utils.GetUnicodeString(file_name)))
# Since we are using readPlist from binplist now instead of manually
# opening up the BinarPlist file we loose this option. Keep it commented
# out for now but this needs to be tested a bit more.
# TODO: Re-evaluate if we can delete this or still require it.
#if bpl.is_corrupt:
# logging.warning(
# u'[{0:s}] corruption detected in binary plist: {1:s}'.format(
# self.NAME, file_name))
return top_level_object
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Parse and extract values from a plist file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
# TODO: Should we rather query the stats object to get the size here?
file_object = file_entry.GetFileObject()
file_size = file_object.get_size()
if file_size <= 0:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] file size: {1:d} bytes is less equal 0.'.format(
self.NAME, file_size))
# 50MB is 10x larger than any plist seen to date.
if file_size > 50000000:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] file size: {1:d} bytes is larger than 50 MB.'.format(
self.NAME, file_size))
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
top_level_object = None
try:
top_level_object = self.GetTopLevel(file_object, file_entry.name)
except errors.UnableToParseFile:
file_object.close()
raise
if not top_level_object:
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] unable to parse: {1:s} skipping.'.format(
self.NAME, file_entry.name))
file_system = file_entry.GetFileSystem()
plist_name = file_system.BasenamePath(file_entry.name)
for plugin_object in self._plugins:
try:
plugin_object.Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
plist_name=plist_name, top_level=top_level_object)
except errors.WrongPlistPlugin as exception:
logging.debug(u'[{0:s}] Wrong plugin: {1:s} for: {2:s}'.format(
self.NAME, exception[0], exception[1]))
file_object.close()
manager.ParsersManager.RegisterParser(PlistParser)
+30
View File
@@ -0,0 +1,30 @@
#!/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 an import statement for each plist related plugin."""
from plaso.parsers.plist_plugins import airport
from plaso.parsers.plist_plugins import appleaccount
from plaso.parsers.plist_plugins import bluetooth
from plaso.parsers.plist_plugins import ipod
from plaso.parsers.plist_plugins import install_history
from plaso.parsers.plist_plugins import macuser
from plaso.parsers.plist_plugins import safari
from plaso.parsers.plist_plugins import softwareupdate
from plaso.parsers.plist_plugins import spotlight
from plaso.parsers.plist_plugins import spotlight_volume
from plaso.parsers.plist_plugins import timemachine
from plaso.parsers.plist_plugins import default
+63
View File
@@ -0,0 +1,63 @@
#!/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.
"""This file contains the airport plist plugin in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class AirportPlugin(interface.PlistPlugin):
"""Plist plugin that extracts WiFi information."""
NAME = 'plist_airport'
DESCRIPTION = u'Parser for Airport plist files.'
PLIST_PATH = 'com.apple.airport.preferences.plist'
PLIST_KEYS = frozenset(['RememberedNetworks'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant Airport entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
for wifi in match['RememberedNetworks']:
description = (
u'[WiFi] Connected to network: <{0:s}> using security {1:s}').format(
wifi.get('SSIDString', u'no SSID string'),
wifi.get('SecurityType', u'N/A'))
last_connected = wifi.get('LastConnected')
event_object = plist_event.PlistEvent(
u'/RememberedNetworks', u'item', last_connected, description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(AirportPlugin)
@@ -0,0 +1,69 @@
#!/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 airport plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import airport
from plaso.parsers.plist_plugins import test_lib
class AirportPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the airport plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = airport.AirportPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['com.apple.airport.preferences.plist'])
plist_name = 'com.apple.airport.preferences.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 4)
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
expected_timestamps = frozenset([
1375144166000000, 1386874984000000, 1386949546000000,
1386950747000000])
self.assertTrue(set(timestamps) == expected_timestamps)
event_object = event_objects[0]
self.assertEqual(event_object.key, u'item')
self.assertEqual(event_object.root, u'/RememberedNetworks')
expected_desc = (
u'[WiFi] Connected to network: <europa> using security '
u'WPA/WPA2 Personal')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'/RememberedNetworks/item {0:s}'.format(expected_desc)
expected_short = expected_string[:77] + u'...'
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
if __name__ == '__main__':
unittest.main()
+111
View File
@@ -0,0 +1,111 @@
#!/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.
"""This file contains a Apple Account plist plugin in Plaso."""
from plaso.events import plist_event
from plaso.lib import errors
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class AppleAccountPlugin(interface.PlistPlugin):
"""Basic plugin to extract the apple account information."""
NAME = 'plist_appleaccount'
DESCRIPTION = u'Parser for Apple account information plist files.'
PLIST_PATH = u'com.apple.coreservices.appleidauthenticationinfo'
PLIST_KEYS = frozenset(['AuthCertificates', 'AccessorVersions', 'Accounts'])
def Process(
self, parser_context, file_entry=None, parser_chain=None, plist_name=None,
top_level=None, **kwargs):
"""Check if it is a valid Apple account plist file name.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
plist_name: name of the plist file.
top_level: dictionary with the plist file parsed.
"""
if not plist_name.startswith(self.PLIST_PATH):
raise errors.WrongPlistPlugin(self.NAME, plist_name)
super(AppleAccountPlugin, self).Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
plist_name=self.PLIST_PATH, top_level=top_level, **kwargs)
# Generated events:
# Accounts: account name.
# FirstName: first name associated with the account.
# LastName: family name associate with the account.
# CreationDate: timestamp when the account was configured in the system.
# LastSuccessfulConnect: last time when the account was connected.
# ValidationDate: last time when the account was validated.
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant Apple Account entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
root = '/Accounts'
for name_account, account in match['Accounts'].iteritems():
general_description = u'{0:s} ({1:s} {2:s})'.format(
name_account, account.get('FirstName', '<FirstName>'),
account.get('LastName', '<LastName>'))
key = name_account
description = u'Configured Apple account {0:s}'.format(
general_description)
event_object = plist_event.PlistEvent(
root, key, account['CreationDate'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if 'LastSuccessfulConnect' in account:
description = u'Connected Apple account {0:s}'.format(
general_description)
event_object = plist_event.PlistEvent(
root, key, account['LastSuccessfulConnect'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if 'ValidationDate' in account:
description = u'Last validation Apple account {0:s}'.format(
general_description)
event_object = plist_event.PlistEvent(
root, key, account['ValidationDate'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(AppleAccountPlugin)
@@ -0,0 +1,79 @@
#!/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 Apple account plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import appleaccount
from plaso.parsers.plist_plugins import test_lib
class AppleAccountPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the Apple account plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = appleaccount.AppleAccountPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
plist_file = (u'com.apple.coreservices.appleidauthenticationinfo.'
u'ABC0ABC1-ABC0-ABC0-ABC0-ABC0ABC1ABC2.plist')
test_file = self._GetTestFilePath([plist_file])
plist_name = plist_file
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 3)
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
expected_timestamps = frozenset([
1372106802000000, 1387980032000000, 1387980032000000])
self.assertTrue(set(timestamps) == expected_timestamps)
event_object = event_objects[0]
self.assertEqual(event_object.root, u'/Accounts')
self.assertEqual(event_object.key, u'email@domain.com')
expected_desc = (
u'Configured Apple account email@domain.com (Joaquin Moreno Garijo)')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'/Accounts/email@domain.com {0:s}'.format(expected_desc)
expected_short = expected_string[:77] + u'...'
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
event_object = event_objects[1]
expected_desc = (u'Connected Apple account '
u'email@domain.com (Joaquin Moreno Garijo)')
self.assertEqual(event_object.desc, expected_desc)
event_object = event_objects[2]
expected_desc = (u'Last validation Apple account '
u'email@domain.com (Joaquin Moreno Garijo)')
self.assertEqual(event_object.desc, expected_desc)
if __name__ == '__main__':
unittest.main()
+99
View File
@@ -0,0 +1,99 @@
#!/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 a default plist plugin in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
class BluetoothPlugin(interface.PlistPlugin):
"""Basic plugin to extract interesting Bluetooth related keys."""
NAME = 'plist_bluetooth'
DESCRIPTION = u'Parser for Bluetooth plist files.'
PLIST_PATH = 'com.apple.bluetooth.plist'
PLIST_KEYS = frozenset(['DeviceCache', 'PairedDevices'])
# LastInquiryUpdate = Device connected via Bluetooth Discovery. Updated
# when a device is detected in discovery mode. E.g. BT headphone power
# on. Pairing is not required for a device to be discovered and cached.
#
# LastNameUpdate = When the human name was last set. Usually done only once
# during initial setup.
#
# LastServicesUpdate = Time set when device was polled to determine what it
# is. Usually done at setup or manually requested via advanced menu.
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant BT entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing extracted keys from PLIST_KEYS.
The default is None.
"""
root = '/DeviceCache'
for device, value in match['DeviceCache'].items():
name = value.get('Name', '')
if name:
name = u''.join(('Name:', name))
if device in match['PairedDevices']:
desc = 'Paired:True {0:s}'.format(name)
key = device
if 'LastInquiryUpdate' in value:
event_object = plist_event.PlistEvent(
root, key, value['LastInquiryUpdate'], desc)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if value.get('LastInquiryUpdate'):
desc = u' '.join(filter(None, ('Bluetooth Discovery', name)))
key = u''.join((device, '/LastInquiryUpdate'))
event_object = plist_event.PlistEvent(
root, key, value['LastInquiryUpdate'], desc)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if value.get('LastNameUpdate'):
desc = u' '.join(filter(None, ('Device Name Set', name)))
key = u''.join((device, '/LastNameUpdate'))
event_object = plist_event.PlistEvent(
root, key, value['LastNameUpdate'], desc)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if value.get('LastServicesUpdate'):
desc = desc = u' '.join(filter(None, ('Services Updated', name)))
key = ''.join((device, '/LastServicesUpdate'))
event_object = plist_event.PlistEvent(
root, key, value['LastServicesUpdate'], desc)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(BluetoothPlugin)
@@ -0,0 +1,85 @@
#!/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 Bluetooth plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import bluetooth
from plaso.parsers.plist_plugins import test_lib
class TestBtPlugin(test_lib.PlistPluginTestCase):
"""Tests for the Bluetooth plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = bluetooth.BluetoothPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['plist_binary'])
plist_name = 'com.apple.bluetooth.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 14)
paired_event_objects = []
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
if event_object.desc.startswith(u'Paired'):
paired_event_objects.append(event_object)
# Ensure all 14 events and times from the plist are parsed correctly.
self.assertEquals(len(timestamps), 14)
expected_timestamps = frozenset([
1341957896010535, 1341957896010535, 1350666385239661, 1350666391557044,
1341957900020116, 1302199013524275, 1301012201414766, 1351818797324095,
1351818797324095, 1351819298997672, 1351818803000000, 1351827808261762,
1345251268370453, 1345251192528750])
self.assertTrue(set(timestamps) == expected_timestamps)
# Ensure two paired devices are matched.
self.assertEquals(len(paired_event_objects), 2)
# One of the paired event object descriptions should contain the string:
# Paired:True Name:Apple Magic Trackpad 2.
paired_descriptions = [
event_object.desc for event_object in paired_event_objects]
self.assertTrue(
'Paired:True Name:Apple Magic Trackpad 2' in paired_descriptions)
expected_string = (
u'/DeviceCache/44-00-00-00-00-04 '
u'Paired:True '
u'Name:Apple Magic Trackpad 2')
self._TestGetMessageStrings(event_object, expected_string, expected_string)
if __name__ == '__main__':
unittest.main()
+86
View File
@@ -0,0 +1,86 @@
#!/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 a default plist plugin in Plaso."""
import datetime
import logging
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
class DefaultPlugin(interface.PlistPlugin):
"""Basic plugin to extract keys with timestamps as values from plists."""
NAME = 'plist_default'
DESCRIPTION = u'Parser for plist files.'
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, top_level=None,
**unused_kwargs):
"""Simple method to exact date values from a Plist.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
top_level: Plist in dictionary form.
"""
for root, key, value in interface.RecurseKey(top_level):
if isinstance(value, datetime.datetime):
event_object = plist_event.PlistEvent(root, key, value)
parser_context.ProduceEvent(
event_object, file_entry=file_entry, parser_chain=parser_chain)
# TODO: Binplist keeps a list of offsets but not mapped to a key.
# adjust code when there is a way to map keys to offsets.
# TODO: move this into the parser as with the olecf plugins.
def Process(
self, parser_context, file_entry=None, parser_chain=None, plist_name=None,
top_level=None, **kwargs):
"""Overwrite the default Process function so it always triggers.
Process() checks if the current plist being processed is a match for a
plugin by comparing the PATH and KEY requirements defined by a plugin. If
both match processing continues; else raise WrongPlistPlugin.
The purpose of the default plugin is to always trigger on any given plist
file, thus it needs to overwrite the default behavior of comparing PATH
and KEY.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
plist_name: Name of the plist file.
top_level: Plist in dictionary form.
"""
logging.debug(u'Plist {0:s} plugin used for: {1:s}'.format(
self.NAME, plist_name))
self.GetEntries(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
top_level=top_level, **kwargs)
plist.PlistParser.RegisterPlugin(DefaultPlugin)
+110
View File
@@ -0,0 +1,110 @@
#!/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 default plist plugin."""
import datetime
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.lib import timelib_test
from plaso.parsers.plist_plugins import default
from plaso.parsers.plist_plugins import test_lib
import pytz
class TestDefaultPlist(test_lib.PlistPluginTestCase):
"""Tests for the default plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = default.DefaultPlugin()
def testProcessSingle(self):
"""Tests Process on a plist containing a root, value and timestamp."""
top_level_dict_single = {
'DE-00-AD-00-BE-EF': {
'Name': 'DBF Industries Slideshow Lazer', 'LastUsed':
datetime.datetime(
2012, 11, 2, 1, 21, 38, 997672, tzinfo=pytz.utc)}}
event_object_generator = self._ParsePlistWithPlugin(
self._plugin, 'single', top_level_dict_single)
event_objects = self._GetEventObjectsFromQueue(event_object_generator)
self.assertEquals(len(event_objects), 1)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2012-11-02 01:21:38.997672')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.root, u'/DE-00-AD-00-BE-EF')
self.assertEquals(event_object.key, u'LastUsed')
expected_string = (
u'/DE-00-AD-00-BE-EF/LastUsed')
self._TestGetMessageStrings(event_object, expected_string, expected_string)
def testProcessMulti(self):
"""Tests Process on a plist containing five keys with date values."""
top_level_dict_many_keys = {
'DeviceCache': {
'44-00-00-00-00-04': {
'Name': 'Apple Magic Trackpad 2', 'LMPSubversion': 796,
'LMPVersion': 3, 'PageScanMode': 0, 'ClassOfDevice': 9620,
'SupportedFeatures': '\x00\x00\x00\x00', 'Manufacturer': 76,
'PageScanPeriod': 0, 'ClockOffset': 17981, 'LastNameUpdate':
datetime.datetime(
2012, 11, 2, 1, 21, 38, 997672, tzinfo=pytz.utc),
'InquiryRSSI': 198, 'PageScanRepetitionMode': 1,
'LastServicesUpdate':
datetime.datetime(2012, 11, 2, 1, 13, 23, tzinfo=pytz.utc),
'displayName': 'Apple Magic Trackpad 2', 'LastInquiryUpdate':
datetime.datetime(
2012, 11, 2, 1, 13, 17, 324095, tzinfo=pytz.utc),
'Services': '', 'BatteryPercent': 0.61},
'44-00-00-00-00-02': {
'Name': 'test-macpro', 'ClockOffset': 28180, 'ClassOfDevice':
3670276, 'PageScanMode': 0, 'LastNameUpdate':
datetime.datetime(
2011, 4, 7, 17, 56, 53, 524275, tzinfo=pytz.utc),
'PageScanPeriod': 2, 'PageScanRepetitionMode': 1,
'LastInquiryUpdate':
datetime.datetime(
2012, 7, 10, 22, 5, 0, 20116, tzinfo=pytz.utc)}}}
event_queue_consumer = self._ParsePlistWithPlugin(
self._plugin, 'nested', top_level_dict_many_keys)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 5)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2011-04-07 17:56:53.524275')
self.assertEquals(event_object.timestamp, expected_timestamp)
self.assertEquals(event_object.root, u'/DeviceCache/44-00-00-00-00-02')
self.assertEquals(event_object.key, u'LastNameUpdate')
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,67 @@
#!/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.
"""This file contains the install history plist plugin in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class InstallHistoryPlugin(interface.PlistPlugin):
"""Plist plugin that extracts the installation history."""
NAME = 'plist_install_history'
DESCRIPTION = u'Parser for installation history plist files.'
PLIST_PATH = 'InstallHistory.plist'
PLIST_KEYS = frozenset([
'date', 'displayName', 'displayVersion',
'processName', 'packageIdentifiers'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, top_level=None,
**unused_kwargs):
"""Extracts relevant install history entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
top_level: Optional plist in dictionary form. The default is None.
"""
for entry in top_level:
packages = []
for package in entry.get('packageIdentifiers'):
packages.append(package)
description = (
u'Installation of [{0:s} {1:s}] using [{2:s}]. '
u'Packages: {3:s}.').format(
entry.get('displayName'), entry.get('displayVersion'),
entry.get('processName'), u', '.join(packages))
event_object = plist_event.PlistEvent(
u'/item', u'', entry.get('date'), description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(InstallHistoryPlugin)
@@ -0,0 +1,77 @@
#!/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 install history plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import install_history
from plaso.parsers.plist_plugins import test_lib
class InstallHistoryPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the install history plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = install_history.InstallHistoryPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['InstallHistory.plist'])
plist_name = 'InstallHistory.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 7)
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
expected_timestamps = frozenset([
1384225175000000, 1388205491000000, 1388232883000000, 1388232883000000,
1388232883000000, 1388232883000000, 1390941528000000])
self.assertTrue(set(timestamps) == expected_timestamps)
event_object = event_objects[0]
self.assertEqual(event_object.key, u'')
self.assertEqual(event_object.root, u'/item')
expected_desc = (
u'Installation of [OS X 10.9 (13A603)] using [OS X Installer]. '
u'Packages: com.apple.pkg.BaseSystemBinaries, '
u'com.apple.pkg.BaseSystemResources, '
u'com.apple.pkg.Essentials, com.apple.pkg.BSD, '
u'com.apple.pkg.JavaTools, com.apple.pkg.AdditionalEssentials, '
u'com.apple.pkg.AdditionalSpeechVoices, '
u'com.apple.pkg.AsianLanguagesSupport, com.apple.pkg.MediaFiles, '
u'com.apple.pkg.JavaEssentials, com.apple.pkg.OxfordDictionaries, '
u'com.apple.pkg.X11redirect, com.apple.pkg.OSInstall, '
u'com.apple.pkg.update.compatibility.2013.001.')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'/item/ {}'.format(expected_desc)
expected_short = expected_string[:77] + u'...'
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
if __name__ == '__main__':
unittest.main()
+323
View File
@@ -0,0 +1,323 @@
#!/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.
"""Plist_interface contains basic interface for plist plugins within Plaso.
Plist files are only one example of a type of object that the Plaso tool is
expected to encounter and process. There can be and are many other parsers
which are designed to process specific data types.
PlistPlugin defines the attributes necessary for registration, discovery
and operation of plugins for plist files which will be used by PlistParser.
"""
import abc
import logging
from plaso.lib import errors
from plaso.parsers import plugins
class PlistPlugin(plugins.BasePlugin):
"""This is an abstract class from which plugins should be based.
The following are the attributes and methods expected to be overridden by a
plugin.
Attributes:
PLIST_PATH - string of the filename the plugin is designed to process.
PLIST_KEY - list of keys holding values that are necessary for processing.
Please note, PLIST_KEY is cAse sensitive and for a plugin to match a
plist file needs to contain at minimum the number of keys needed for
processing or WrongPlistPlugin is raised.
For example if a Plist file contains the following keys,
{'foo': 1, 'bar': 2, 'opt': 3} with 'foo' and 'bar' being keys critical to
processing define PLIST_KEY as ['foo', 'bar']. If 'opt' is only optionally
defined it can still be accessed by manually processing self.top_level from
the plugin.
Methods:
GetEntries() - extract and format info from keys and yields event.PlistEvent.
"""
NAME = 'plist_plugin'
# PLIST_PATH is a string for the filename this parser is designed to process.
# This is expected to be overriden by the processing plugin.
# Ex. 'com.apple.bluetooth.plist'
PLIST_PATH = 'any'
# PLIST_KEYS is a list of keys required by a plugin.
# This is expected to be overriden by the processing plugin.
# Ex. frozenset(['DeviceCache', 'PairedDevices'])
PLIST_KEYS = frozenset(['any'])
# This is expected to be overriden by the processing plugin.
# URLS should contain a list of URLs with additional information about
# this key or value.
# Ex. ['http://www.forensicswiki.org/wiki/Property_list_(plist)']
URLS = []
@abc.abstractmethod
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, top_level=None,
match=None, **unused_kwargs):
"""Extracts event objects from the values of entries within a plist.
This is the main method that a plist plugin needs to implement.
The contents of the plist keys defined in PLIST_KEYS will be made available
to the plugin as self.matched{'KEY': 'value'}. The plugin should implement
logic to parse this into a useful event for incorporation into the Plaso
timeline.
For example if you want to note the timestamps of when devices were
LastInquiryUpdated you would need to examine the bluetooth config file
called 'com.apple.bluetooth' and need to look at devices under the key
'DeviceCache'. To do this the plugin needs to define
PLIST_PATH = 'com.apple.bluetooth' and PLIST_KEYS =
frozenset(['DeviceCache']). IMPORTANT: this interface requires exact names
and is case sensitive. A unit test based on a real world file is expected
for each plist plugin.
When a file with this key is encountered during processing self.matched is
populated and the plugin's GetEntries() is called. The plugin would have
self.matched = {'DeviceCache': [{'DE:AD:BE:EF:01': {'LastInquiryUpdate':
DateTime_Object}, 'DE:AD:BE:EF:01': {'LastInquiryUpdate':
DateTime_Object}'...}]} and needs to implement logic here to extract
values, format, and produce the data as a event.PlistEvent.
The attributes for a PlistEvent should include the following:
root = Root key this event was extracted from. E.g. DeviceCache/
key = Key the value resided in. E.g. 'DE:AD:BE:EF:01'
time = Date this artifact was created in microseconds(usec) from epoch.
desc = Short description. E.g. 'Device LastInquiryUpdated'
See plist/bluetooth.py for the implemented example plugin.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
top_level: Optional plist in dictionary form. The default is None.
match: Optional dictionary containing extracted keys from PLIST_KEYS.
The default is None.
"""
def Process(
self, parser_context, file_entry=None, parser_chain=None, plist_name=None,
top_level=None, **kwargs):
"""Determine if this is the correct plugin; if so proceed with processing.
Process() checks if the current plist being processed is a match for a
plugin by comparing the PATH and KEY requirements defined by a plugin. If
both match processing continues; else raise WrongPlistPlugin.
This function also extracts the required keys as defined in self.PLIST_KEYS
from the plist and stores the result in self.match[key] and calls
self.GetEntries() which holds the processing logic implemented by the
plugin.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
plist_name: Name of the plist file.
top_level: Plist in dictionary form.
Raises:
WrongPlistPlugin: If this plugin is not able to process the given file.
ValueError: If top_level or plist_name are not set.
"""
if plist_name is None or top_level is None:
raise ValueError(u'Top level or plist name are not set.')
if plist_name.lower() != self.PLIST_PATH.lower():
raise errors.WrongPlistPlugin(self.NAME, plist_name)
if isinstance(top_level, dict):
if not set(top_level.keys()).issuperset(self.PLIST_KEYS):
raise errors.WrongPlistPlugin(self.NAME, plist_name)
else:
# Make sure we are getting back an object that has an iterator.
if not hasattr(top_level, '__iter__'):
raise errors.WrongPlistPlugin(self.NAME, plist_name)
# This is a list and we need to just look at the first level
# of keys there.
keys = []
for top_level_entry in top_level:
if isinstance(top_level_entry, dict):
keys.extend(top_level_entry.keys())
# Compare this is a set, which removes possible duplicate entries
# in the list.
if not set(keys).issuperset(self.PLIST_KEYS):
raise errors.WrongPlistPlugin(self.NAME, plist_name)
# This will raise if unhandled keyword arguments are passed.
super(PlistPlugin, self).Process(parser_context, **kwargs)
logging.debug(u'Plist Plugin Used: {0:s} for: {1:s}'.format(
self.NAME, plist_name))
match = GetKeys(top_level, self.PLIST_KEYS)
# Add ourselves to the parser chain, which will be used in all subsequent
# event creation in this parser.
parser_chain = self._BuildParserChain(parser_chain)
self.GetEntries(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
top_level=top_level, match=match)
def RecurseKey(recur_item, root='', depth=15):
"""Flattens nested dictionaries and lists by yielding it's values.
The hierarchy of a plist file is a series of nested dictionaries and lists.
This is a helper function helps plugins navigate the structure without
having to reimplement their own recursive methods.
This method implements an overridable depth limit to prevent processing
extremely deeply nested plists. If the limit is reached a debug message is
logged indicating which key processing stopped on.
Example Input Plist:
recur_item = { DeviceRoot: { DeviceMAC1: [Value1, Value2, Value3],
DeviceMAC2: [Value1, Value2, Value3]}}
Example Output:
('', DeviceRoot, {DeviceMACs...})
(DeviceRoot, DeviceMAC1, [Value1, Value2, Value3])
(DeviceRoot, DeviceMAC2, [Value1, Value2, Value3])
Args:
recur_item: An object to be checked for additional nested items.
root: The pathname of the current working key.
depth: A counter to ensure we stop at the maximum recursion depth.
Yields:
A tuple of the root, key, and value from a plist.
"""
if depth < 1:
logging.debug(u'Recursion limit hit for key: {0:s}'.format(root))
return
if type(recur_item) in (list, tuple):
for recur in recur_item:
for key in RecurseKey(recur, root, depth):
yield key
return
if not hasattr(recur_item, 'iteritems'):
return
for key, value in recur_item.iteritems():
yield root, key, value
if isinstance(value, dict):
value = [value]
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
for keyval in RecurseKey(
item, root=root + u'/' + key, depth=depth - 1):
yield keyval
def GetKeys(top_level, keys, depth=1):
"""Helper function to return keys nested in a plist dict.
By default this function will return the values for the named keys requested
by a plugin in match dictonary objecte. The default setting is to look
a single layer down from the root (same as the check for plugin
applicability). This level is suitable for most cases.
For cases where there is varability in the name at the first level
(e.g. it is the MAC addresses of a device, or a UUID) it is possible to
override the depth limit and use GetKeys to fetch from a deeper level.
E.g.
Top_Level (root): # depth = 0
|-- Key_Name_is_UUID_Generated_At_Install 1234-5678-8 # depth = 1
| |-- Interesting_SubKey_with_value_to_Process: [Values, ...] # depth = 2
Args:
top_level: Plist in dictionary form.
keys: A list of keys that should be returned.
depth: Defines how many levels deep to check for a match.
Returns:
A dictionary with just the keys requested or an empty dict if the plist
is flat, eg. top_level is a list instead of a dict object.
"""
match = {}
if not isinstance(top_level, dict):
# Return an empty dict here if top_level is a list object, which happens
# if the plist file is flat.
return match
keys = set(keys)
if depth == 1:
for key in keys:
match[key] = top_level.get(key, None)
else:
for _, parsed_key, parsed_value in RecurseKey(top_level, depth=depth):
if parsed_key in keys:
match[parsed_key] = parsed_value
if set(match.keys()) == keys:
return match
return match
def GetKeysDefaultEmpty(top_level, keys, depth=1):
"""Return keys nested in a plist dict, defaulting to an empty value.
The method GetKeys fails if the supplied key does not exist within the
plist object. This alternate method behaves the same way as GetKeys
except that instead of raising an error if the key doesn't exist it will
assign a default empty value ('') to the field.
Args:
top_level: Plist in dictionary form.
keys: A list of keys that should be returned.
depth: Defines how many levels deep to check for a match.
Returns:
A dictionary with just the keys requested.
"""
keys = set(keys)
match = {}
if depth == 1:
for key in keys:
value = top_level.get(key, None)
if value is not None:
match[key] = value
else:
for _, parsed_key, parsed_value in RecurseKey(top_level, depth=depth):
if parsed_key in keys:
match[parsed_key] = parsed_value
if set(match.keys()) == keys:
return match
return match
@@ -0,0 +1,135 @@
#!/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 plist plugin interface."""
import unittest
from plaso.events import plist_event
from plaso.lib import errors
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
from plaso.parsers.plist_plugins import test_lib
class MockPlugin(interface.PlistPlugin):
"""Mock plugin."""
NAME = 'mock_plist_plugin'
DESCRIPTION = u'Parser for testing parsing plist files.'
PLIST_PATH = 'plist_binary'
PLIST_KEYS = frozenset(['DeviceCache', 'PairedDevices'])
def GetEntries(self, parser_context, **unused_kwargs):
event_object = plist_event.PlistEvent(
u'/DeviceCache/44-00-00-00-00-00', u'LastInquiryUpdate',
1351827808261762)
parser_context.ProduceEvent(event_object, parser_chain=self.NAME)
class TestPlistPlugin(test_lib.PlistPluginTestCase):
"""Tests for the plist plugin interface."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._top_level_dict = {
'DeviceCache': {
'44-00-00-00-00-04': {
'Name': 'Apple Magic Trackpad 2', 'LMPSubversion': 796,
'Services': '', 'BatteryPercent': 0.61},
'44-00-00-00-00-02': {
'Name': 'test-macpro', 'ClockOffset': 28180,
'PageScanPeriod': 2, 'PageScanRepetitionMode': 1}}}
def testGetPluginNames(self):
"""Tests the GetPluginNames function."""
plugin_names = plist.PlistParser.GetPluginNames()
self.assertNotEquals(plugin_names, [])
self.assertTrue('plist_default' in plugin_names)
def testProcess(self):
"""Tests the Process function."""
# Ensure the plugin only processes if both filename and keys exist.
plugin_object = MockPlugin()
# Test correct filename and keys.
top_level = {'DeviceCache': 1, 'PairedDevices': 1}
event_object_generator = self._ParsePlistWithPlugin(
plugin_object, 'plist_binary', top_level)
event_objects = self._GetEventObjectsFromQueue(event_object_generator)
self.assertEquals(len(event_objects), 1)
# Correct filename with odd filename cAsinG. Adding an extra useless key.
top_level = {'DeviceCache': 1, 'PairedDevices': 1, 'R@ndomExtraKey': 1}
event_object_generator = self._ParsePlistWithPlugin(
plugin_object, 'pLiSt_BinAry', top_level)
event_objects = self._GetEventObjectsFromQueue(event_object_generator)
self.assertEquals(len(event_objects), 1)
# Test wrong filename.
top_level = {'DeviceCache': 1, 'PairedDevices': 1}
with self.assertRaises(errors.WrongPlistPlugin):
_ = self._ParsePlistWithPlugin(
plugin_object, 'wrong_file.plist', top_level)
# Test not enough required keys.
top_level = {'Useless_Key': 0, 'PairedDevices': 1}
with self.assertRaises(errors.WrongPlistPlugin):
_ = self._ParsePlistWithPlugin(
plugin_object, 'plist_binary.plist', top_level)
def testRecurseKey(self):
"""Tests the RecurseKey function."""
# Ensure with a depth of 1 we only return the root key.
result = list(interface.RecurseKey(self._top_level_dict, depth=1))
self.assertEquals(len(result), 1)
# Trying again with depth limit of 2 this time.
result = list(interface.RecurseKey(self._top_level_dict, depth=2))
self.assertEquals(len(result), 3)
# A depth of two should gives us root plus the two devices. Let's check.
my_keys = []
for unused_root, key, unused_value in result:
my_keys.append(key)
expected = set(['DeviceCache', '44-00-00-00-00-04', '44-00-00-00-00-02'])
self.assertTrue(expected == set(my_keys))
def testGetKeys(self):
"""Tests the GetKeys function."""
# Match DeviceCache from the root level.
key = ['DeviceCache']
result = interface.GetKeys(self._top_level_dict, key)
self.assertEquals(len(result), 1)
# Look for a key nested a layer beneath DeviceCache from root level.
# Note: overriding the default depth to look deeper.
key = ['44-00-00-00-00-02']
result = interface.GetKeys(self._top_level_dict, key, depth=2)
self.assertEquals(len(result), 1)
# Check the value of the result was extracted as expected.
self.assertTrue('test-macpro' == result[key[0]]['Name'])
if __name__ == '__main__':
unittest.main()
+91
View File
@@ -0,0 +1,91 @@
#!/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.
"""This file contains a plist plugin for the iPod/iPhone storage plist."""
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
class IPodPlistEvent(time_events.PythonDatetimeEvent):
"""An event object for an entry in the iPod plist file."""
DATA_TYPE = 'ipod:device:entry'
def __init__(self, datetime_timestamp, device_id, device_info):
"""Initialize the event.
Args:
datetime_timestamp: The timestamp for the event as a datetime object.
device_id: The device ID.
device_info: A dict that contains extracted information from the plist.
"""
super(IPodPlistEvent, self).__init__(
datetime_timestamp, eventdata.EventTimestamp.LAST_CONNECTED)
self.device_id = device_id
# Save the other attributes.
for key, value in device_info.iteritems():
if key == 'Connected':
continue
attribute_name = key.lower().replace(u' ', u'_')
setattr(self, attribute_name, value)
class IPodPlugin(interface.PlistPlugin):
"""Plugin to extract iPod/iPad/iPhone device information."""
NAME = 'ipod_device'
DESCRIPTION = u'Parser for iPod, iPad and iPhone plist files.'
PLIST_PATH = 'com.apple.iPod.plist'
PLIST_KEYS = frozenset(['Devices'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extract device information from the iPod plist.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
if not 'Devices' in match:
return
devices = match['Devices']
if not devices:
return
for device, device_info in devices.iteritems():
if 'Connected' not in device_info:
continue
event_object = IPodPlistEvent(
device_info.get('Connected'), device, device_info)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(IPodPlugin)
+77
View File
@@ -0,0 +1,77 @@
#!/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 iPod plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import ipod as ipod_formatter
from plaso.lib import eventdata
from plaso.lib import timelib_test
from plaso.parsers import plist
from plaso.parsers.plist_plugins import ipod
from plaso.parsers.plist_plugins import test_lib
class TestIPodPlugin(test_lib.PlistPluginTestCase):
"""Tests for the iPod plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = ipod.IPodPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
plist_name = 'com.apple.iPod.plist'
test_file = self._GetTestFilePath([plist_name])
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 4)
event_object = event_objects[1]
timestamp = timelib_test.CopyStringToTimestamp('2013-10-09 19:27:54')
self.assertEquals(event_object.timestamp, timestamp)
expected_string = (
u'Device ID: 4C6F6F6E65000000 Type: iPhone [10016] Connected 1 times '
u'Serial nr: 526F676572 IMEI [012345678901234]')
self._TestGetMessageStrings(
event_object, expected_string, expected_string[0:77] + '...')
self.assertEquals(
event_object.timestamp_desc, eventdata.EventTimestamp.LAST_CONNECTED)
self.assertEquals(event_object.device_class, u'iPhone')
self.assertEquals(event_object.device_id, u'4C6F6F6E65000000')
self.assertEquals(event_object.firmware_version, 256)
self.assertEquals(event_object.imei, u'012345678901234')
self.assertEquals(event_object.use_count, 1)
event_object = event_objects[3]
timestamp = timelib_test.CopyStringToTimestamp('1995-11-22 18:25:07')
self.assertEquals(event_object.timestamp, timestamp)
self.assertEquals(event_object.device_id, u'0000A11300000000')
if __name__ == '__main__':
unittest.main()
+172
View File
@@ -0,0 +1,172 @@
#!/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.
"""This file contains the Mac OS X user plist plugin."""
# TODO: Only plists from Mac OS X 10.8 and 10.9 were tested. Look at other
# versions as well.
import binascii
from binplist import binplist
from dfvfs.file_io import fake_file_io
from dfvfs.path import fake_path_spec
from dfvfs.resolver import context
from xml.etree import ElementTree
from plaso.events import plist_event
from plaso.lib import timelib
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class MacUserPlugin(interface.PlistPlugin):
"""Basic plugin to extract timestamp Mac user information."""
NAME = 'plist_macuser'
DESCRIPTION = u'Parser for Mac OS X user plist files.'
# The PLIST_PATH is dynamic, "user".plist is the name of the
# Mac OS X user.
PLIST_KEYS = frozenset([
'name', 'uid', 'home',
'passwordpolicyoptions', 'ShadowHashData'])
_ROOT = u'/'
def Process(
self, parser_context, file_entry=None, parser_chain=None, plist_name=None,
top_level=None, **kwargs):
"""Check if it is a valid Mac OS X system account plist file name.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
plist_name: name of the plist file.
top_level: dictionary with the plist file parsed.
"""
super(MacUserPlugin, self).Process(
parser_context, file_entry=file_entry, parser_chain=parser_chain,
plist_name=self.PLIST_PATH, top_level=top_level, **kwargs)
# Generated events:
# name: string with the system user.
# uid: user ID.
# passwordpolicyoptions: XML Plist structures with the timestamp.
# passwordLastSetTime: last time the password was changed.
# lastLoginTimestamp: last time the user was authenticated (*).
# failedLoginTimestamp: last time the user passwd was incorrectly(*).
# failedLoginCount: times of incorrect passwords.
# (*): depending on the situation, these timestamps are reset (0 value).
# It is translated by the library as a 2001-01-01 00:00:00 (COCAO
# zero time representation). If this happens, the event is not yield.
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant user timestamp entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
account = match['name'][0]
uid = match['uid'][0]
cocoa_zero = (
timelib.Timestamp.COCOA_TIME_TO_POSIX_BASE *
timelib.Timestamp.MICRO_SECONDS_PER_SECOND)
# INFO: binplist return a string with the Plist XML.
for policy in match['passwordpolicyoptions']:
xml_policy = ElementTree.fromstring(policy)
for dict_elements in xml_policy.iterfind('dict'):
key_values = [value.text for value in dict_elements.getchildren()]
policy_dict = dict(zip(key_values[0::2], key_values[1::2]))
if policy_dict.get('passwordLastSetTime', 0):
timestamp = timelib.Timestamp.FromTimeString(
policy_dict.get('passwordLastSetTime', '0'))
if timestamp > cocoa_zero:
# Extract the hash password information.
# It is store in the attribure ShadowHasData which is
# a binary plist data; However binplist only extract one
# level of binary plist, then it returns this information
# as a string.
# TODO: change this into a DataRange instead. For this we
# need the file offset and size of the ShadowHashData value data.
resolver_context = context.Context()
fake_file = fake_file_io.FakeFile(
resolver_context, match['ShadowHashData'][0])
fake_file.open(path_spec=fake_path_spec.FakePathSpec(
location=u'ShadowHashData'))
try:
plist_file = binplist.BinaryPlist(file_obj=fake_file)
top_level = plist_file.Parse()
except binplist.FormatError:
top_level = dict()
salted_hash = top_level.get('SALTED-SHA512-PBKDF2', None)
if salted_hash:
password_hash = u'$ml${0:d}${1:s}${2:s}'.format(
salted_hash['iterations'],
binascii.hexlify(salted_hash['salt']),
binascii.hexlify(salted_hash['entropy']))
else:
password_hash = u'N/A'
description = (
u'Last time {0:s} ({1!s}) changed the password: {2!s}').format(
account, uid, password_hash)
event_object = plist_event.PlistTimeEvent(
self._ROOT, u'passwordLastSetTime', timestamp, description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if policy_dict.get('lastLoginTimestamp', 0):
timestamp = timelib.Timestamp.FromTimeString(
policy_dict.get('lastLoginTimestamp', '0'))
description = u'Last login from {0:s} ({1!s})'.format(account, uid)
if timestamp > cocoa_zero:
event_object = plist_event.PlistTimeEvent(
self._ROOT, u'lastLoginTimestamp', timestamp, description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if policy_dict.get('failedLoginTimestamp', 0):
timestamp = timelib.Timestamp.FromTimeString(
policy_dict.get('failedLoginTimestamp', '0'))
description = (
u'Last failed login from {0:s} ({1!s}) ({2!s} times)').format(
account, uid, policy_dict['failedLoginCount'])
if timestamp > cocoa_zero:
event_object = plist_event.PlistTimeEvent(
self._ROOT, u'failedLoginTimestamp', timestamp, description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(MacUserPlugin)
@@ -0,0 +1,73 @@
#!/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 Mac OS X local users plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.lib import timelib_test
from plaso.parsers import plist
from plaso.parsers.plist_plugins import macuser
from plaso.parsers.plist_plugins import test_lib
class MacUserPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the Mac OS X local user plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = macuser.MacUserPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
plist_name = u'user.plist'
test_file = self._GetTestFilePath([plist_name])
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 1)
event_object = event_objects[0]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-12-28 04:35:47')
self.assertEqual(event_object.timestamp, expected_timestamp)
self.assertEqual(event_object.key, u'passwordLastSetTime')
self.assertEqual(event_object.root, u'/')
expected_desc = (
u'Last time user (501) changed the password: '
u'$ml$37313$fa6cac1869263baa85cffc5e77a3d4ee164b7'
u'5536cae26ce8547108f60e3f554$a731dbb0e386b169af8'
u'9fbb33c255ceafc083c6bc5194853f72f11c550c42e4625'
u'ef113b66f3f8b51fc3cd39106bad5067db3f7f1491758ff'
u'e0d819a1b0aba20646fd61345d98c0c9a411bfd1144dd4b'
u'3c40ec0f148b66d5b9ab014449f9b2e103928ef21db6e25'
u'b536a60ff17a84e985be3aa7ba3a4c16b34e0d1d2066ae178')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'//passwordLastSetTime {}'.format(expected_desc)
expected_short = u'{}...'.format(expected_string[:77])
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
if __name__ == '__main__':
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
#!/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 a default plist plugin in Plaso."""
import logging
from plaso.events import time_events
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
class SafariHistoryEvent(time_events.TimestampEvent):
"""An EventObject for Safari history entries."""
def __init__(self, timestamp, history_entry):
"""Initialize the event.
Args:
timestamp: The timestamp of the Event, in microseconds since Unix Epoch.
history_entry: A dict object read from the Safari history plist.
"""
super(SafariHistoryEvent, self).__init__(
timestamp, eventdata.EventTimestamp.LAST_VISITED_TIME)
self.data_type = 'safari:history:visit'
self.url = history_entry.get('', None)
self.title = history_entry.get('title', None)
display_title = history_entry.get('displayTitle', None)
if display_title != self.title:
self.display_title = display_title
self.visit_count = history_entry.get('visitCount', None)
self.was_http_non_get = history_entry.get('lastVisitWasHTTPNonGet', None)
class SafariHistoryPlugin(interface.PlistPlugin):
"""Plugin to extract Safari history timestamps."""
NAME = 'safari_history'
DESCRIPTION = u'Parser for Safari history plist files.'
PLIST_PATH = 'History.plist'
PLIST_KEYS = frozenset(['WebHistoryDates', 'WebHistoryFileVersion'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts Safari history items.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
if match.get('WebHistoryFileVersion', 0) != 1:
logging.warning(u'Unable to parse Safari version: {0:s}'.format(
match.get('WebHistoryFileVersion', 0)))
return
for history_entry in match.get('WebHistoryDates', {}):
try:
time = timelib.Timestamp.FromCocoaTime(float(
history_entry.get('lastVisitedDate', 0)))
except ValueError:
logging.warning(u'Unable to translate timestamp: {0:s}'.format(
history_entry.get('lastVisitedDate', 0)))
continue
if not time:
logging.debug('No timestamp set, skipping record.')
continue
event_object = SafariHistoryEvent(time, history_entry)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(SafariHistoryPlugin)
@@ -0,0 +1,65 @@
#!/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 Safari history plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.lib import timelib_test
from plaso.parsers import plist
from plaso.parsers.plist_plugins import safari
from plaso.parsers.plist_plugins import test_lib
class SafariPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the Safari history plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = safari.SafariHistoryPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['History.plist'])
plist_name = 'History.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
# 18 entries in timeline.
self.assertEquals(len(event_objects), 18)
event_object = event_objects[8]
expected_timestamp = timelib_test.CopyStringToTimestamp(
'2013-07-08 17:31:00')
self.assertEquals(event_objects[10].timestamp, expected_timestamp)
expected_url = u'http://netverslun.sci-mx.is/aminosyrur'
self.assertEquals(event_object.url, expected_url)
expected_string = (
u'Visited: {0:s} (Am\xedn\xf3s\xfdrur ) Visit Count: 1').format(
expected_url)
self._TestGetMessageStrings(event_object, expected_string, expected_string)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,83 @@
#!/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.
"""This file contains a default plist plugin in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class SoftwareUpdatePlugin(interface.PlistPlugin):
"""Basic plugin to extract the Mac OS X update status."""
NAME = 'plist_softwareupdate'
DESCRIPTION = u'Parser for Mac OS X software update plist files.'
PLIST_PATH = 'com.apple.SoftwareUpdate.plist'
PLIST_KEYS = frozenset([
'LastFullSuccessfulDate', 'LastSuccessfulDate',
'LastAttemptSystemVersion', 'LastUpdatesAvailable',
'LastRecommendedUpdatesAvailable', 'RecommendedUpdates'])
# Generated events:
# LastFullSuccessfulDate: timestamp when Mac OS X was full update.
# LastSuccessfulDate: timestamp when Mac OS X was partially update.
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant Mac OS X update entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
root = '/'
key = ''
version = match.get('LastAttemptSystemVersion', u'N/A')
pending = match['LastUpdatesAvailable']
description = u'Last Mac OS X {0:s} full update.'.format(version)
event_object = plist_event.PlistEvent(
root, key, match['LastFullSuccessfulDate'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if pending:
software = []
for update in match['RecommendedUpdates']:
software.append(u'{0:s}({1:s})'.format(
update['Identifier'], update['Product Key']))
description = (
u'Last Mac OS {0!s} partially update, pending {1!s}: {2:s}.').format(
version, pending, u','.join(software))
event_object = plist_event.PlistEvent(
root, key, match['LastSuccessfulDate'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(SoftwareUpdatePlugin)
@@ -0,0 +1,65 @@
#!/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 Software Update plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import softwareupdate
from plaso.parsers.plist_plugins import test_lib
class SoftwareUpdatePluginTest(test_lib.PlistPluginTestCase):
"""Tests for the SoftwareUpdate plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = softwareupdate.SoftwareUpdatePlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
plist_name = u'com.apple.SoftwareUpdate.plist'
test_file = self._GetTestFilePath([plist_name])
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 2)
event_object = event_objects[0]
self.assertEqual(event_object.key, u'')
self.assertEqual(event_object.root, u'/')
expected_desc = u'Last Mac OS X 10.9.1 (13B42) full update.'
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'// {}'.format(expected_desc)
self._TestGetMessageStrings(
event_object, expected_string, expected_string)
event_object = event_objects[1]
self.assertEqual(event_object.key, u'')
self.assertEqual(event_object.root, u'/')
expected_desc = (
u'Last Mac OS 10.9.1 (13B42) partially '
u'update, pending 1: RAWCameraUpdate5.03(031-2664).')
self.assertEqual(event_object.desc, expected_desc)
if __name__ == '__main__':
unittest.main()
+67
View File
@@ -0,0 +1,67 @@
#!/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.
"""This file contains the Spotlight searched terms plugin in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class SpotlightPlugin(interface.PlistPlugin):
"""Basic plugin to extract Spotlight."""
NAME = 'plist_spotlight'
DESCRIPTION = u'Parser for Spotlight plist files.'
PLIST_PATH = 'com.apple.spotlight.plist'
PLIST_KEYS = frozenset(['UserShortcuts'])
# Generated events:
# name of the item: searched term.
# PATH: path of the program associated to the term.
# LAST_USED: last time when it was executed.
# DISPLAY_NAME: the display name of the program associated.
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant Spotlight entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
for search_text, data in match['UserShortcuts'].iteritems():
description = (
u'Spotlight term searched "{0:s}" associate to {1:s} '
u'({2:s})').format(search_text, data['DISPLAY_NAME'], data['PATH'])
event_object = plist_event.PlistEvent(
u'/UserShortcuts', search_text, data['LAST_USED'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(SpotlightPlugin)
@@ -0,0 +1,69 @@
#!/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 spotlight plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import spotlight
from plaso.parsers.plist_plugins import test_lib
class SpotlightPluginTest(test_lib.PlistPluginTestCase):
"""Tests for the spotlight plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = spotlight.SpotlightPlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['com.apple.spotlight.plist'])
plist_name = 'com.apple.spotlight.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 9)
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
expected_timestamps = frozenset([
1379937262090906, 1387822901900937, 1375236414408299, 1388331212005129,
1376696381196456, 1386951868185477, 1380942616952359, 1389056477460443,
1386111811136093])
self.assertTrue(set(timestamps) == expected_timestamps)
event_object = event_objects[1]
self.assertEqual(event_object.key, u'gr')
self.assertEqual(event_object.root, u'/UserShortcuts')
expected_desc = (u'Spotlight term searched "gr" associate to '
u'Grab (/Applications/Utilities/Grab.app)')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'/UserShortcuts/gr {}'.format(expected_desc)
expected_short = expected_string[:77] + u'...'
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,60 @@
#!/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.
"""This file contains the Spotlight Volume Configuration plist in Plaso."""
from plaso.events import plist_event
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
class SpotlightVolumePlugin(interface.PlistPlugin):
"""Basic plugin to extract the Spotlight Volume Configuration."""
NAME = 'plist_spotlight_volume'
DESCRIPTION = u'Parser for Spotlight volume configuration plist files.'
PLIST_PATH = 'VolumeConfiguration.plist'
PLIST_KEYS = frozenset(['Stores'])
def GetEntries(
self, parser_context, file_entry=None, parser_chain=None, match=None,
**unused_kwargs):
"""Extracts relevant VolumeConfiguration Spotlight entries.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: Optional file entry object (instance of dfvfs.FileEntry).
The default is None.
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
match: Optional dictionary containing keys extracted from PLIST_KEYS.
The default is None.
"""
for volume_name, volume in match['Stores'].iteritems():
description = u'Spotlight Volume {0:s} ({1:s}) activated.'.format(
volume_name, volume['PartialPath'])
event_object = plist_event.PlistEvent(
u'/Stores', '', volume['CreationDate'], description)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
plist.PlistParser.RegisterPlugin(SpotlightVolumePlugin)
@@ -0,0 +1,67 @@
#!/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 Spotlight Volume configuration plist plugin."""
import unittest
# pylint: disable=unused-import
from plaso.formatters import plist as plist_formatter
from plaso.parsers import plist
from plaso.parsers.plist_plugins import spotlight_volume
from plaso.parsers.plist_plugins import test_lib
class SpotlightVolumePluginTest(test_lib.PlistPluginTestCase):
"""Tests for the Spotlight Volume configuration plist plugin."""
def setUp(self):
"""Sets up the needed objects used throughout the test."""
self._plugin = spotlight_volume.SpotlightVolumePlugin()
self._parser = plist.PlistParser()
def testProcess(self):
"""Tests the Process function."""
test_file = self._GetTestFilePath(['VolumeConfiguration.plist'])
plist_name = 'VolumeConfiguration.plist'
event_queue_consumer = self._ParsePlistFileWithPlugin(
self._parser, self._plugin, test_file, plist_name)
event_objects = self._GetEventObjectsFromQueue(event_queue_consumer)
self.assertEquals(len(event_objects), 2)
timestamps = []
for event_object in event_objects:
timestamps.append(event_object.timestamp)
expected_timestamps = frozenset([
1372139683000000, 1369657656000000])
self.assertTrue(set(timestamps) == expected_timestamps)
event_object = event_objects[0]
self.assertEqual(event_object.key, u'')
self.assertEqual(event_object.root, u'/Stores')
expected_desc = (u'Spotlight Volume 4D4BFEB5-7FE6-4033-AAAA-'
u'AAAABBBBCCCCDDDD (/.MobileBackups) activated.')
self.assertEqual(event_object.desc, expected_desc)
expected_string = u'/Stores/ {}'.format(expected_desc)
expected_short = expected_string[:77] + u'...'
self._TestGetMessageStrings(
event_object, expected_string, expected_short)
if __name__ == '__main__':
unittest.main()

Some files were not shown because too many files have changed in this diff Show More