Import from old repository
This commit is contained in:
@@ -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()
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/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.
|
||||
"""Plist 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
|
||||
|
||||
from plaso.engine import single_process
|
||||
from plaso.parsers import test_lib
|
||||
|
||||
|
||||
class PlistPluginTestCase(test_lib.ParserTestCase):
|
||||
"""The unit test case for a plist plugin."""
|
||||
|
||||
def _ParsePlistFileWithPlugin(
|
||||
self, parser_object, plugin_object, path, plist_name,
|
||||
knowledge_base_values=None):
|
||||
"""Parses a file using the parser and plugin object.
|
||||
|
||||
Args:
|
||||
parser_object: the parser object.
|
||||
plugin_object: the plugin object.
|
||||
path: the path of the file to parse.
|
||||
plist_name: the name of the plist to parse.
|
||||
knowledge_base_values: optional dict containing the knowledge base
|
||||
values. The default is None.
|
||||
|
||||
Returns:
|
||||
An event object queue consumer object (instance of
|
||||
TestEventObjectQueueConsumer).
|
||||
"""
|
||||
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()
|
||||
top_level_object = parser_object.GetTopLevel(file_object)
|
||||
self.assertNotEquals(top_level_object, None)
|
||||
|
||||
return self._ParsePlistWithPlugin(
|
||||
plugin_object, plist_name, top_level_object,
|
||||
knowledge_base_values=knowledge_base_values)
|
||||
|
||||
def _ParsePlistWithPlugin(
|
||||
self, plugin_object, plist_name, top_level_object,
|
||||
knowledge_base_values=None):
|
||||
"""Parses a plist using the plugin object.
|
||||
|
||||
Args:
|
||||
plugin_object: the plugin object.
|
||||
plist_name: the name of the plist to parse.
|
||||
top_level_object: the top-level plist object.
|
||||
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)
|
||||
plugin_object.Process(
|
||||
parser_context, plist_name=plist_name, top_level=top_level_object)
|
||||
|
||||
return event_queue_consumer
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/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 TimeMachine plist plugin in Plaso."""
|
||||
|
||||
import construct
|
||||
|
||||
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 TimeMachinePlugin(interface.PlistPlugin):
|
||||
"""Basic plugin to extract time machine hardisk and the backups."""
|
||||
|
||||
NAME = 'plist_timemachine'
|
||||
DESCRIPTION = u'Parser for TimeMachine plist files.'
|
||||
|
||||
PLIST_PATH = 'com.apple.TimeMachine.plist'
|
||||
PLIST_KEYS = frozenset(['Destinations', 'RootVolumeUUID'])
|
||||
|
||||
# Generated events:
|
||||
# DestinationID: remote UUID hard disk where the backup is done.
|
||||
# BackupAlias: structure that contains the extra information from the
|
||||
# destinationID.
|
||||
# SnapshotDates: list of the backup dates.
|
||||
|
||||
TM_BACKUP_ALIAS = construct.Struct(
|
||||
'tm_backup_alias',
|
||||
construct.Padding(10),
|
||||
construct.PascalString('value', length_field=construct.UBInt8('length')))
|
||||
|
||||
def GetEntries(
|
||||
self, parser_context, file_entry=None, parser_chain=None, match=None,
|
||||
**unused_kwargs):
|
||||
"""Extracts relevant TimeMachine 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 = '/Destinations'
|
||||
key = 'item/SnapshotDates'
|
||||
|
||||
# For each TimeMachine devices.
|
||||
for destination in match['Destinations']:
|
||||
hd_uuid = destination['DestinationID']
|
||||
if not hd_uuid:
|
||||
hd_uuid = u'Unknown device'
|
||||
alias = destination['BackupAlias']
|
||||
try:
|
||||
alias = self.TM_BACKUP_ALIAS.parse(alias).value
|
||||
except construct.FieldError:
|
||||
alias = u'Unknown alias'
|
||||
# For each Backup.
|
||||
for timestamp in destination['SnapshotDates']:
|
||||
description = u'TimeMachine Backup in {0:s} ({1:s})'.format(
|
||||
alias, hd_uuid)
|
||||
event_object = plist_event.PlistEvent(root, key, timestamp, description)
|
||||
parser_context.ProduceEvent(
|
||||
event_object, parser_chain=parser_chain, file_entry=file_entry)
|
||||
|
||||
|
||||
plist.PlistParser.RegisterPlugin(TimeMachinePlugin)
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/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 timemachine 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 timemachine
|
||||
from plaso.parsers.plist_plugins import test_lib
|
||||
|
||||
|
||||
class TimeMachinePluginTest(test_lib.PlistPluginTestCase):
|
||||
"""Tests for the timemachine plist plugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Sets up the needed objects used throughout the test."""
|
||||
self._plugin = timemachine.TimeMachinePlugin()
|
||||
self._parser = plist.PlistParser()
|
||||
|
||||
def testProcess(self):
|
||||
"""Tests the Process function."""
|
||||
test_file = self._GetTestFilePath(['com.apple.TimeMachine.plist'])
|
||||
plist_name = 'com.apple.timemachine.plist'
|
||||
event_object_generator = self._ParsePlistFileWithPlugin(
|
||||
self._parser, self._plugin, test_file, plist_name)
|
||||
event_objects = self._GetEventObjectsFromQueue(event_object_generator)
|
||||
|
||||
self.assertEquals(len(event_objects), 13)
|
||||
|
||||
timestamps = []
|
||||
for event_object in event_objects:
|
||||
timestamps.append(event_object.timestamp)
|
||||
expected_timestamps = frozenset([
|
||||
1379165051000000, 1380098455000000, 1380810276000000, 1381883538000000,
|
||||
1382647890000000, 1383351739000000, 1384090020000000, 1385130914000000,
|
||||
1386265911000000, 1386689852000000, 1387723091000000, 1388840950000000,
|
||||
1388842718000000])
|
||||
self.assertTrue(set(timestamps) == expected_timestamps)
|
||||
|
||||
event_object = event_objects[0]
|
||||
self.assertEqual(event_object.root, u'/Destinations')
|
||||
self.assertEqual(event_object.key, u'item/SnapshotDates')
|
||||
expected_desc = (
|
||||
u'TimeMachine Backup in BackUpFast '
|
||||
u'(5B33C22B-A4A1-4024-A2F5-C9979C4AAAAA)')
|
||||
self.assertEqual(event_object.desc, expected_desc)
|
||||
expected_string = u'/Destinations/item/SnapshotDates {}'.format(
|
||||
expected_desc)
|
||||
expected_short = expected_string[:77] + u'...'
|
||||
self._TestGetMessageStrings(
|
||||
event_object, expected_string, expected_short)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user