#!/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 preprocessors for Mac OS X.""" import logging from binplist import binplist from dfvfs.helpers import file_system_searcher from xml.etree import ElementTree from plaso.lib import errors from plaso.lib import utils from plaso.parsers.plist_plugins import interface as plist_interface from plaso.preprocessors import interface from plaso.preprocessors import manager class PlistPreprocessPlugin(interface.PreprocessPlugin): """Class that defines the plist preprocess plugin object.""" SUPPORTED_OS = ['MacOSX'] WEIGHT = 2 # Path to the plist file to be parsed, can depend on paths discovered # in previous preprocessors. PLIST_PATH = '' # The key that's value should be returned back. It is an ordered list # of preference. If the first value is found it will be returned and no # others will be searched. PLIST_KEYS = [''] def GetValue(self, searcher, unused_knowledge_base): """Returns a value retrieved from keys within a plist file. Where the name of the keys are defined in PLIST_KEYS. Args: searcher: The file system searcher object (instance of dfvfs.FileSystemSearcher). knowledge_base: A knowledge base object (instance of KnowledgeBase), which contains information from the source data needed for parsing. Returns: The value of the first key that is found. Raises: errors.PreProcessFail: if the preprocessing fails. """ file_entry = self._FindFileEntry(searcher, self.PLIST_PATH) if not file_entry: raise errors.PreProcessFail( u'Unable to open file: {0:s}'.format(self.PLIST_PATH)) file_object = file_entry.GetFileObject() value = self.ParseFile(file_entry, file_object) file_object.close() return value def ParseFile(self, file_entry, file_object): """Parses the plist file and returns the parsed key. Args: file_entry: The file entry (instance of dfvfs.FileEntry). file_object: The file-like object. Returns: The value of the first key defined by PLIST_KEYS that is found. Raises: errors.PreProcessFail: if the preprocessing fails. """ try: plist_file = binplist.BinaryPlist(file_object) top_level_object = plist_file.Parse() except binplist.FormatError as exception: raise errors.PreProcessFail( u'File is not a plist: {0:s} with error: {1:s}'.format( file_entry.path_spec.comparable, exception)) except OverflowError as exception: raise errors.PreProcessFail( u'Unable to process plist: {0:s} with error: {1:s}'.format( file_entry.path_spec.comparable, exception)) if not plist_file: raise errors.PreProcessFail( u'File is not a plist: {0:s}'.format(file_entry.path_spec.comparable)) match = None key_name = '' for plist_key in self.PLIST_KEYS: try: match = plist_interface.GetKeys( top_level_object, frozenset([plist_key])) except KeyError: continue if match: key_name = plist_key break if not match: raise errors.PreProcessFail( u'Keys not found inside plist file: {0:s}.'.format( u','.join(self.PLIST_KEYS))) return self.ParseKey(match, key_name) def ParseKey(self, key, key_name): """Retrieves a specific value from the key. Args: key: The key object (instance of dict). key_name: The name of the key. Returns: The value of the key defined by key_name. Raises: errors.PreProcessFail: if the preprocessing fails. """ value = key.get(key_name, None) if not value: raise errors.PreProcessFail( u'Value of key: {0:s} not found.'.format(key_name)) return value class XMLPlistPreprocessPlugin(PlistPreprocessPlugin): """Class that defines the Mac OS X XML plist preprocess plugin object.""" def _GetKeys(self, xml_root, key_name): """Return a dict with the requested keys.""" match = {} generator = xml_root.iter() for key in generator: if 'key' in key.tag and key_name in key.text: value_key = generator.next() value = '' for subkey in value_key.iter(): if 'string' in subkey.tag: value = subkey.text match[key.text] = value # Now we need to go over the match dict and retrieve values. return match def ParseFile(self, file_entry, file_object): """Parse the file and return parsed key. Args: file_entry: The file entry (instance of dfvfs.FileEntry). file_object: The file-like object. Returns: The value of the first key defined by PLIST_KEYS that is found. Raises: errors.PreProcessFail: if the preprocessing fails. """ # TODO: Move to defusedxml for safer XML parsing. try: xml = ElementTree.parse(file_object) except ElementTree.ParseError: raise errors.PreProcessFail(u'File is not a XML file.') except IOError: raise errors.PreProcessFail(u'File is not a XML file.') xml_root = xml.getroot() key_name = '' match = None for key in self.PLIST_KEYS: match = self._GetKeys(xml_root, key) if match: key_name = key break if not match: raise errors.PreProcessFail( u'Keys not found inside plist file: {0:s}.'.format( u','.join(self.PLIST_KEYS))) return self.ParseKey(match, key_name) class MacOSXBuild(XMLPlistPreprocessPlugin): """Fetches build information about a Mac OS X system.""" ATTRIBUTE = 'build' PLIST_PATH = '/System/Library/CoreServices/SystemVersion.plist' PLIST_KEYS = ['ProductUserVisibleVersion'] class MacOSXHostname(XMLPlistPreprocessPlugin): """Fetches hostname information about a Mac OS X system.""" ATTRIBUTE = 'hostname' PLIST_PATH = '/Library/Preferences/SystemConfiguration/preferences.plist' PLIST_KEYS = ['ComputerName', 'LocalHostName'] class MacOSXKeyboard(PlistPreprocessPlugin): """Fetches keyboard information from a Mac OS X system.""" ATTRIBUTE = 'keyboard_layout' PLIST_PATH = '/Library/Preferences/com.apple.HIToolbox.plist' PLIST_KEYS = ['AppleCurrentKeyboardLayoutInputSourceID'] def ParseKey(self, key, key_name): """Determines the keyboard layout.""" value = super(MacOSXKeyboard, self).ParseKey(key, key_name) if type(value) in (list, tuple): value = value[0] _, _, keyboard_layout = value.rpartition('.') return keyboard_layout class MacOSXTimeZone(interface.PreprocessPlugin): """Gather timezone information from a Mac OS X system.""" ATTRIBUTE = 'time_zone_str' SUPPORTED_OS = ['MacOSX'] WEIGHT = 1 ZONE_FILE_PATH = u'/private/etc/localtime' def GetValue(self, searcher, unused_knowledge_base): """Determines the local time zone settings. Args: searcher: The file system searcher object (instance of dfvfs.FileSystemSearcher). knowledge_base: A knowledge base object (instance of KnowledgeBase), which contains information from the source data needed for parsing. Returns: The local timezone settings. Raises: errors.PreProcessFail: if the preprocessing fails. """ path = self.ZONE_FILE_PATH file_entry = self._FindFileEntry(searcher, path) if not file_entry: raise errors.PreProcessFail( u'Unable to find file: {0:s}'.format(path)) if not file_entry.link: raise errors.PreProcessFail( u'Unable to retrieve timezone information from: {0:s}.'.format(path)) _, _, zone = file_entry.link.partition(u'zoneinfo/') return zone class MacOSXUsers(interface.PreprocessPlugin): """Get information about user accounts on a Mac OS X system.""" SUPPORTED_OS = ['MacOSX'] ATTRIBUTE = 'users' WEIGHT = 1 # Define the path to the user account information. USER_PATH = '/private/var/db/dslocal/nodes/Default/users/[^_].+.plist' _KEYS = frozenset(['name', 'uid', 'home', 'realname']) def _OpenPlistFile(self, searcher, path_spec): """Open a Plist file given a path and returns a plist top level object. Args: searcher: The file system searcher object (instance of dfvfs.FileSystemSearcher). path_spec: The path specification (instance of dfvfs.PathSpec) of the plist file. Raises: errors.PreProcessFail: if the preprocessing fails. """ plist_file_location = getattr(path_spec, 'location', u'') file_entry = searcher.GetFileEntryByPathSpec(path_spec) file_object = file_entry.GetFileObject() try: plist_file = binplist.BinaryPlist(file_object) top_level_object = plist_file.Parse() except binplist.FormatError as exception: exception = utils.GetUnicodeString(exception) raise errors.PreProcessFail( u'File is not a plist: {0:s}'.format(exception)) except OverflowError as exception: raise errors.PreProcessFail( u'Error processing: {0:s} with error: {1:s}'.format( plist_file_location, exception)) if not plist_file: raise errors.PreProcessFail( u'File is not a plist: {0:s}'.format(plist_file_location)) return top_level_object def GetValue(self, searcher, unused_knowledge_base): """Determines the user accounts. Args: searcher: The file system searcher object (instance of dfvfs.FileSystemSearcher). knowledge_base: A knowledge base object (instance of KnowledgeBase), which contains information from the source data needed for parsing. Returns: A list containing username information dicts. Raises: errors.PreProcessFail: if the preprocessing fails. """ find_spec = file_system_searcher.FindSpec( location_regex=self.USER_PATH, case_sensitive=False) path_specs = list(searcher.Find(find_specs=[find_spec])) if not path_specs: raise errors.PreProcessFail(u'Unable to find user plist files.') users = [] for path_spec in path_specs: plist_file_location = getattr(path_spec, 'location', u'') if not plist_file_location: raise errors.PreProcessFail(u'Missing user plist file location.') try: top_level_object = self._OpenPlistFile(searcher, path_spec) except IOError: logging.warning(u'Unable to parse user plist file: {0:s}'.format( plist_file_location)) continue try: match = plist_interface.GetKeysDefaultEmpty( top_level_object, self._KEYS) except KeyError as exception: logging.warning( u'Unable to read user plist file: {0:s} with error: {1:s}'.format( plist_file_location, exception)) continue # TODO: as part of artifacts, create a proper object for this. user = { 'uid': match.get('uid', [-1])[0], 'path': match.get('home', [u''])[0], 'name': match.get('name', [u''])[0], 'realname': match.get('realname', [u'N/A'])[0]} users.append(user) if not users: raise errors.PreProcessFail(u'Unable to find any users on the system.') return users manager.PreprocessPluginsManager.RegisterPlugins([ MacOSXBuild, MacOSXHostname, MacOSXKeyboard, MacOSXTimeZone, MacOSXUsers])