# -*- coding: utf-8 -*-
"""The Apple System Log (ASL) file parser."""
import os
from dfdatetime import posix_time as dfdatetime_posix_time
from plaso.containers import events
from plaso.lib import definitions
from plaso.lib import dtfabric_helper
from plaso.lib import errors
from plaso.lib import specification
from plaso.parsers import interface
from plaso.parsers import manager
[docs]
class ASLEventData(events.EventData):
"""Apple System Log (ASL) event data.
Attributes:
computer_name (str): name of the host.
extra_information (str): extra fields associated to the event.
facility (str): facility.
group_identifier (int): group identifier (GID).
level (str): level of criticality of the event.
message (str): message of the event.
message_identifier (int): message identifier.
process_identifier (int): process identifier (PID).
read_group_identifier (int): the group identifier that can read this file,
where -1 represents all.
read_user_identifier (int): user identifier that can read this file,
where -1 represents all.
record_position (int): position of the event record.
sender (str): sender or process that created the event.
user_identifier (int): user identifier (UID).
written_time (dfdatetime.DateTimeValues): entry written date and time.
"""
DATA_TYPE = 'macos:asl:entry'
[docs]
def __init__(self):
"""Initializes event data."""
super(ASLEventData, self).__init__(data_type=self.DATA_TYPE)
self.computer_name = None
self.extra_information = None
self.facility = None
self.group_identifier = None
self.level = None
self.message = None
self.message_identifier = None
self.process_identifier = None
self.read_group_identifier = None
self.read_user_identifier = None
self.record_position = None
self.sender = None
self.user_identifier = None
self.written_time = None
[docs]
class ASLFileEventData(events.EventData):
"""Apple System Log (ASL) file event data.
Attributes:
creation_time (dfdatetime.DateTimeValues): creation date and time.
format_version (int): ASL file format version.
is_dirty (bool): True if the last log entry offset does not match value
in file header and the file is considered dirty.
"""
DATA_TYPE = 'macos:asl:file'
[docs]
def __init__(self):
"""Initializes event data."""
super(ASLFileEventData, self).__init__(data_type=self.DATA_TYPE)
self.creation_time = None
self.format_version = None
self.is_dirty = None
[docs]
class ASLParser(interface.FileObjectParser, dtfabric_helper.DtFabricHelper):
"""Parser for Apple System Log (ASL) files."""
NAME = 'asl_log'
DATA_FORMAT = 'Apple System Log (ASL) file'
_DEFINITION_FILE = os.path.join(
os.path.dirname(__file__), 'asl.yaml')
# Most significant bit of a 64-bit string offset.
_STRING_OFFSET_MSB = 1 << 63
def _ParseRecord(self, parser_mediator, file_object, record_offset):
"""Parses a record and produces events.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
file_object (file): file-like object.
record_offset (int): offset of the record relative to the start of
the file.
Returns:
int: next record offset.
Raises:
ParseError: if the record cannot be parsed.
"""
record_map = self._GetDataTypeMap('asl_record')
try:
record, record_data_size = self._ReadStructureFromFileObject(
file_object, record_offset, record_map)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError((
'Unable to parse record at offset: 0x{0:08x} with error: '
'{1!s}').format(record_offset, exception))
hostname = self._ParseRecordString(
file_object, record.hostname_string_offset)
sender = self._ParseRecordString(
file_object, record.sender_string_offset)
facility = self._ParseRecordString(
file_object, record.facility_string_offset)
message = self._ParseRecordString(
file_object, record.message_string_offset)
file_offset = record_offset + record_data_size
additional_data_size = record.data_size + 6 - record_data_size
if additional_data_size % 8 != 0:
raise errors.ParseError(
'Invalid record additional data size: {0:d}.'.format(
additional_data_size))
additional_data = self._ReadData(
file_object, file_offset, additional_data_size)
extra_fields = {}
for additional_data_offset in range(0, additional_data_size - 8, 16):
record_extra_field = self._ParseRecordExtraField(
additional_data[additional_data_offset:], file_offset)
file_offset += 16
name = self._ParseRecordString(
file_object, record_extra_field.name_string_offset)
value = self._ParseRecordString(
file_object, record_extra_field.value_string_offset)
if name is not None:
extra_fields[name] = value
# TODO: implement determine previous record offset
timestamp = ((record.written_time * definitions.NANOSECONDS_PER_SECOND) +
record.written_time_nanoseconds)
event_data = ASLEventData()
event_data.computer_name = hostname
event_data.extra_information = ', '.join([
'{0:s}: {1!s}'.format(name, value)
for name, value in sorted(extra_fields.items())])
event_data.facility = facility
event_data.group_identifier = record.group_identifier
event_data.level = record.alert_level
event_data.message = message
event_data.message_identifier = record.message_identifier
event_data.process_identifier = record.process_identifier
event_data.read_group_identifier = record.read_group_identifier
event_data.read_user_identifier = record.read_user_identifier
event_data.record_position = record_offset
event_data.sender = sender
event_data.user_identifier = record.user_identifier
event_data.written_time = dfdatetime_posix_time.PosixTimeInNanoseconds(
timestamp=timestamp)
parser_mediator.ProduceEventData(event_data)
return record.next_record_offset
def _ParseRecordExtraField(self, byte_stream, file_offset):
"""Parses a record extra field.
Args:
byte_stream (bytes): byte stream.
file_offset (int): offset of the record extra field relative to
the start of the file.
Returns:
asl_record_extra_field: record extra field.
Raises:
ParseError: if the record extra field cannot be parsed.
"""
extra_field_map = self._GetDataTypeMap('asl_record_extra_field')
try:
record_extra_field = self._ReadStructureFromByteStream(
byte_stream, file_offset, extra_field_map)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError((
'Unable to parse record extra field at offset: 0x{0:08x} with error: '
'{1!s}').format(file_offset, exception))
return record_extra_field
def _ParseRecordString(self, file_object, string_offset):
"""Parses a record string.
Args:
file_object (file): file-like object.
string_offset (int): offset of the string relative to the start of
the file.
Returns:
str: record string or None if string offset is 0.
Raises:
ParseError: if the record string cannot be parsed.
"""
if string_offset == 0:
return None
if string_offset & self._STRING_OFFSET_MSB:
if (string_offset >> 60) != 8:
raise errors.ParseError('Invalid inline record string flag.')
string_size = (string_offset >> 56) & 0x0f
if string_size >= 8:
raise errors.ParseError('Invalid inline record string size.')
string_data = bytes(bytearray([
string_offset >> (8 * byte_index) & 0xff
for byte_index in range(6, -1, -1)]))
try:
return string_data[:string_size].decode('utf-8')
except UnicodeDecodeError as exception:
raise errors.ParseError(
'Unable to decode inline record string with error: {0!s}.'.format(
exception))
record_string_map = self._GetDataTypeMap('asl_record_string')
try:
record_string, _ = self._ReadStructureFromFileObject(
file_object, string_offset, record_string_map)
except (ValueError, errors.ParseError) as exception:
raise errors.ParseError((
'Unable to parse record string at offset: 0x{0:08x} with error: '
'{1!s}').format(string_offset, exception))
return record_string.string.rstrip('\x00')
[docs]
def ParseFileObject(self, parser_mediator, file_object):
"""Parses an ASL file-like object.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
file_object (dfvfs.FileIO): file-like object.
Raises:
WrongParser: when the file cannot be parsed.
"""
file_header_map = self._GetDataTypeMap('asl_file_header')
try:
file_header, _ = self._ReadStructureFromFileObject(
file_object, 0, file_header_map)
except (ValueError, errors.ParseError) as exception:
raise errors.WrongParser(
'Unable to parse file header with error: {0!s}'.format(
exception))
is_dirty = False
file_size = file_object.get_size()
if file_header.first_log_entry_offset > 0:
last_log_entry_offset = 0
file_offset = file_header.first_log_entry_offset
while file_offset < file_size:
last_log_entry_offset = file_offset
try:
file_offset = self._ParseRecord(
parser_mediator, file_object, file_offset)
except errors.ParseError as exception:
parser_mediator.ProduceExtractionWarning(
'unable to parse record with error: {0!s}'.format(exception))
return
if file_offset == 0:
break
if last_log_entry_offset != file_header.last_log_entry_offset:
is_dirty = True
parser_mediator.ProduceRecoveryWarning(
'last log entry offset does not match value in file header.')
event_data = ASLFileEventData()
event_data.format_version = file_header.format_version
event_data.is_dirty = is_dirty
if file_header.creation_time:
event_data.creation_time = dfdatetime_posix_time.PosixTime(
timestamp=file_header.creation_time)
parser_mediator.ProduceEventData(event_data)
manager.ParsersManager.RegisterParser(ASLParser)