Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created parser for Apple IPS files. #4688

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
46 changes: 46 additions & 0 deletions plaso/data/formatters/macos.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,52 @@
# Plaso Mac OS related event formatters.
---
type: 'conditional'
data_type: 'apple:ips_recovery_logd'
message:
- 'Application Name: {application_name}'
- 'Application Version: {application_version}'
- 'Bug Type: {bug_type}'
- 'Crash Reporter_key: {crash_reporter_key}'
- 'Device Model: {device_model}'
- 'Exception Type: {exception_type}'
- 'Incident Identifier: {incident_identifier}'
- 'Operating system version: {operating_system_version}'
- 'Parent Process: {parent_process}'
- 'Parent Process Identifier: {parent_process_identifier}'
- 'Process Identifier: {process_identifier}'
- 'Process Launch Time: {process_launch_time}'
- 'User Identifier: {user_identifier}'
short_message:
- 'Bug Type: {bug_type}'
- 'Device Model: {device_model}'
- 'Incident Identifier: {incident_identifier}'
- 'Operating system version: {operating_system_version}'
short_source: 'RecoveryLogd'
source: 'Apple Recovery IPS'
---
type: 'conditional'
data_type: 'apple:ips_stacks'
message:
- 'Bug Type: {bug_type}'
- 'Crash Reporter_key: {crash_reporter_key}'
- 'Device Model: {device_model}'
- 'Event Time: {event_time}'
- 'Kernel Version: {kernel_version}'
- 'Incident Identifier: {incident_identifier}'
- 'Process List: {process_list}'
- 'Operating system version: {operating_system_version}'
- 'Reason: {reason}'
short_message:
- 'Bug Type: {bug_type}'
- 'Crash Reporter_key: {crash_reporter_key}'
- 'Device Model: {device_model}'
- 'Incident Identifier: {incident_identifier}'
- 'Operating system version: {operating_system_version}'
- 'Reason: {reason}'
short_source: 'StacksIPS'
source: 'Apple Stacks IPS'
---
type: 'conditional'
data_type: 'imessage:event:chat'
enumeration_helpers:
- input_attribute: 'message_type'
Expand Down
14 changes: 14 additions & 0 deletions plaso/data/timeliner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ attribute_mappings:
description: 'Recorded Time'
place_holder_event: true
---
data_type: 'apple:ips_recovery_logd'
attribute_mappings:
- name: 'event_time'
description: 'Event Time'
- name: 'process_launch_time'
description: 'Process Launch Time'
place_holder_event: true
---
data_type: 'apple:ips_stacks'
attribute_mappings:
- name: 'event_time'
description: 'Event Time'
place_holder_event: true
---
data_type: 'av:defender:detection_history'
attribute_mappings:
- name: 'recorded_time'
Expand Down
2 changes: 2 additions & 0 deletions plaso/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from plaso.parsers import firefox_cache
from plaso.parsers import fish_history
from plaso.parsers import fseventsd
from plaso.parsers import ips_parser
from plaso.parsers import java_idx
from plaso.parsers import jsonl_parser
from plaso.parsers import locate
Expand Down Expand Up @@ -56,6 +57,7 @@
from plaso.parsers import bencode_plugins
from plaso.parsers import czip_plugins
from plaso.parsers import esedb_plugins
from plaso.parsers import ips_plugins
from plaso.parsers import jsonl_plugins
from plaso.parsers import olecf_plugins
from plaso.parsers import plist_plugins
Expand Down
98 changes: 98 additions & 0 deletions plaso/parsers/ips_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
"""Parser for IPS formatted log files."""

import json

from dfvfs.helpers import text_file

from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager


class IPSFile(object):
"""IPS log file.

Attributes:
content (dict[str, object]): JSON serialized IPS log file content.
header (dict[str, object]): JSON serialized IPS log file header.
"""

def __init__(self):
"""Initializes an IPS log file."""
super(IPSFile, self).__init__()
self.content = None
self.header = None

def Open(self, text_file_object):
"""Opens an IPS log file.

Args:
text_file_object (text_file.TextFile): text file object.
Raises:
ValueError: if the file object is missing.
"""
if not text_file_object:
raise ValueError('Missing text file object.')

self.header = json.loads(text_file_object.readline())
self.content = json.loads('\n'.join(text_file_object.readlines()))


class IPSParser(interface.FileEntryParser):
"""Parses IPS formatted log files."""

NAME = 'ips'
DATA_FORMAT = 'IPS log file'

_plugin_classes = {}

def ParseFileEntry(self, parser_mediator, file_entry):
"""Parses an IPS formatted log file entry.

Args:
parser_mediator (ParserMediator): parser mediator.
file_entry (dfvfs.FileEntry): file entry to be parsed.
"""
file_object = file_entry.GetFileObject()
text_file_object = text_file.TextFile(file_object)

try:
ips_file_object = IPSFile()
ips_file_object.Open(text_file_object)

except ValueError:
raise errors.WrongParser('Invalid IPS file.')

for plugin_name, plugin in self._plugins_per_name.items():
if parser_mediator.abort:
break

profiling_name = '/'.join([self.NAME, plugin.NAME])

parser_mediator.SampleFormatCheckStartTiming(profiling_name)

try:
result = plugin.CheckRequiredKeys(ips_file_object)
finally:
parser_mediator.SampleFormatCheckStopTiming(profiling_name)

if not result:
continue

parser_mediator.SampleStartTiming(profiling_name)

try:
plugin.UpdateChainAndProcess(
parser_mediator, ips_file=ips_file_object)

except Exception as exception: # pylint: disable=broad-except
parser_mediator.ProduceExtractionWarning((
'plugin: {0:s} unable to parse IPS file with error: {1!s}').format(
plugin_name, exception))

finally:
parser_mediator.SampleStopTiming(profiling_name)


manager.ParsersManager.RegisterParser(IPSParser)
5 changes: 5 additions & 0 deletions plaso/parsers/ips_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""Imports for the ips log parser plugins."""

from plaso.parsers.ips_plugins import recovery_logd
from plaso.parsers.ips_plugins import stacks_ips
128 changes: 128 additions & 0 deletions plaso/parsers/ips_plugins/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
"""Interface for IPS log file parser plugins."""

import abc
import pyparsing

from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.parsers import plugins


class IPSPlugin(plugins.BasePlugin):
"""IPS file parser plugin."""

NAME = 'ips_plugin'
DATA_FORMAT = 'ips log file'

ENCODING = 'utf-8'

REQUIRED_HEADER_KEYS = frozenset()
REQUIRED_CONTENT_KEYS = frozenset()

_TWO_DIGITS = pyparsing.Word(pyparsing.nums, exact=2).set_parse_action(
lambda tokens: int(tokens[0], 10))

_FOUR_DIGITS = pyparsing.Word(pyparsing.nums, exact=4).set_parse_action(
lambda tokens: int(tokens[0], 10))

_VARYING_DIGITS = pyparsing.Word(pyparsing.nums)

TIMESTAMP_GRAMMAR = (
_FOUR_DIGITS.set_results_name('year') + pyparsing.Suppress('-') +
_TWO_DIGITS.set_results_name('month') + pyparsing.Suppress('-') +
_TWO_DIGITS.set_results_name('day') +
_TWO_DIGITS.set_results_name('hours') + pyparsing.Suppress(':') +
_TWO_DIGITS.set_results_name('minutes') + pyparsing.Suppress(':') +
_TWO_DIGITS.set_results_name('seconds') + pyparsing.Suppress('.') +
_VARYING_DIGITS.set_results_name('fraction') +
pyparsing.Word(
pyparsing.nums + '+' + '-').set_results_name('time_zone_delta'))

def _ParseTimestampValue(self, parser_mediator, timestamp_text):
"""Parses a timestamp string.

Args:
parser_mediator (ParserMediator): parser mediator.
timestamp_text (str): the timestamp to parse.

Returns:
dfdatetime.TimeElements: date and time or None if not available.
"""
# dfDateTime takes the time zone offset as number of minutes relative from
# UTC. So for Easter Standard Time (EST), which is UTC-5:00 the sign needs
# to be converted, to +300 minutes.

parsed_timestamp = self.TIMESTAMP_GRAMMAR.parseString(timestamp_text)

try:
time_delta_hours = int(parsed_timestamp['time_zone_delta'][:3], 10)
time_delta_minutes = int(parsed_timestamp['time_zone_delta'][3:], 10)
except (TypeError, ValueError):
parser_mediator.ProduceExtractionWarning(
'unsupported time zone offset value')
return None

time_zone_offset = (time_delta_hours * 60) + time_delta_minutes

try:
fraction = parsed_timestamp['fraction']
fraction_float = float(f'0.{fraction:s}')
milliseconds = round(fraction_float * 1000)

time_elements_tuple = (
parsed_timestamp['year'], parsed_timestamp['month'],
parsed_timestamp['day'], parsed_timestamp['hours'],
parsed_timestamp['minutes'], parsed_timestamp['seconds'],
milliseconds)

time_element_object = dfdatetime_time_elements.TimeElementsInMilliseconds(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to self see if TimeElementsWithFractionOfSecond can be used instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the fraction of second value is either 2 or 4 digits. @rick-slin can you confirm this based on other samples ?

I recommend we preserve the precision here to prevent misrepresentation, also see https://osdfir.blogspot.com/2021/10/pearls-and-pitfalls-of-timeline-analysis.html

When time permits I'll make some tweaks to dfDateTime to support 10 ms and 100 us intervals

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find an ips file with a timestamp with something other than 10 ms precision (hundredth of seconds). Where did you encounter one with 100 us precision? I checked on three iPhones (iOS 14, 15, and 16) and a MacBook.

I see your point about preserving granularity.

time_elements_tuple=time_elements_tuple,
time_zone_offset=time_zone_offset)

except (TypeError, ValueError):
parser_mediator.ProduceExtractionWarning('unsupported date time value')
return None

return time_element_object

def CheckRequiredKeys(self, ips_file):
"""Checks the IPS header and content have the keys required for the plugin.

Args:
ips_file (IPSFile): the file for which the structure is checked.

Returns:
bool: True if the file has the required keys defined by the plugin, or
False if it does not, or if the plugin does not define required
keys. The header and content can have more keys than the minimum
required and still return True.
"""
if not self.REQUIRED_HEADER_KEYS or not self.REQUIRED_CONTENT_KEYS:
return False

has_required_keys = True
for required_header_key in self.REQUIRED_HEADER_KEYS:
if required_header_key not in ips_file.header.keys():
has_required_keys = False
break

for required_content_key in self.REQUIRED_CONTENT_KEYS:
if required_content_key not in ips_file.content.keys():
has_required_keys = False
break

return has_required_keys

# pylint: disable=arguments-differ
@abc.abstractmethod
def Process(self, parser_mediator, ips_file=None, **unused_kwargs):
"""Extracts events from an IPS log file.

Args:
parser_mediator (ParserMediator): parser mediator.
ips_file (Optional[IPSFile]): database.

Raises:
ValueError: If the file value is missing.
"""
Loading
Loading