Import from old repository
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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."""
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
Reference in New Issue
Block a user