"""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().__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().__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(
f"Unable to parse record at offset: 0x{record_offset:08x} "
f"with error: {exception!s}"
)
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(
f"Unsupported record additional data size: {additional_data_size:d}."
)
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(
[f"{name:s}: {value!s}" 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(
f"Unable to parse record extra field at offset: 0x{file_offset:08x} "
f"with error: {exception!s}"
)
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(
f"Unable to decode inline record string with error: {exception!s}."
)
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(
f"Unable to parse record string at offset: 0x{string_offset:08x} "
f"with error: {exception!s}"
)
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(
f"Unable to parse file header with error: {exception!s}"
)
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(
f"unable to parse record with error: {exception!s}"
)
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)