Skip to content

Commit

Permalink
Changes to launchd plists parser plugin (log2timeline#4803)
Browse files Browse the repository at this point in the history
  • Loading branch information
Spferical authored Apr 1, 2024
1 parent 8eb3267 commit dab1e79
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 21 deletions.
17 changes: 7 additions & 10 deletions plaso/parsers/plist.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,6 @@ def ParseFileObject(self, parser_mediator, file_object):
display_name = parser_mediator.GetDisplayName()
filename_lower_case = filename.lower()

try:
top_level_keys = set(top_level_object.keys())
except AttributeError as exception:
raise errors.WrongParser(
'Unable to parse top level keys of: {0:s} with error: {1!s}.'.format(
filename, exception))

found_matching_plugin = False
for plugin_name, plugin in self._plugins_per_name.items():
if parser_mediator.abort:
Expand All @@ -170,13 +163,17 @@ def ParseFileObject(self, parser_mediator, file_object):
if path_filter.Match(filename_lower_case):
path_filter_match = True

result = (path_filter_match and
top_level_keys.issuperset(plugin.PLIST_KEYS))
try:
required_format = plugin.CheckRequiredFormat(top_level_object)
except Exception as exception: # pylint: disable=broad-except
parser_mediator.ProduceExtractionWarning((
'plugin: {0:s} unable to parse plist file with error: '
'{1!s}').format(plugin_name, exception))

finally:
parser_mediator.SampleFormatCheckStopTiming(profiling_name)

if not result:
if not path_filter_match or not required_format:
logger.debug('Skipped parsing file: {0:s} with plugin: {1:s}'.format(
display_name, plugin_name))
continue
Expand Down
22 changes: 17 additions & 5 deletions plaso/parsers/plist_plugins/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,13 +690,13 @@ class PlistPlugin(plugins.BasePlugin):
NAME = 'plist_plugin'

# This is expected to be overridden by the processing plugin, for example:
# frozenset(PlistPathFilter('com.apple.bluetooth.plist'))
# PLIST_PATH_FILTERS = frozenset(PlistPathFilter('com.apple.bluetooth.plist'))
PLIST_PATH_FILTERS = frozenset()

# PLIST_KEYS is a list of keys required by a plugin.
# This is expected to be overridden by the processing plugin.
# Ex. frozenset(['DeviceCache', 'PairedDevices'])
PLIST_KEYS = frozenset(['any'])
# PLIST_KEYS is a list of keys required by a plugin. This is expected to be
# overridden by the processing plugin, for example:
# PLIST_KEYS = frozenset(['DeviceCache', 'PairedDevices'])
PLIST_KEYS = frozenset()

def _GetDateTimeValueFromPlistKey(self, plist_key, plist_value_name):
"""Retrieves a date and time value from a specific value in a plist key.
Expand Down Expand Up @@ -750,6 +750,7 @@ def _GetKeys(self, top_level, keys, depth=1):
# 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:
Expand Down Expand Up @@ -857,6 +858,17 @@ def _ParsePlist(
top_level (Optional[dict[str, object]]): plist top-level item.
"""

def CheckRequiredFormat(self, top_level):
"""Check if the plist has the minimal structure required by the plugin.
Args:
top_level (dict[str, object]): plist top-level item.
Returns:
bool: True if this is the correct plugin, False otherwise.
"""
return set(top_level.keys()).issuperset(self.PLIST_KEYS)

def Process(self, parser_mediator, top_level=None, **kwargs):
"""Extracts events from a plist file.
Expand Down
29 changes: 23 additions & 6 deletions plaso/parsers/plist_plugins/launchd.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,25 @@ class MacOSLaunchdPlistPlugin(interface.PlistPlugin):
# /Library/LaunchAgents/*.plist
# ~/Library/LaunchAgents

PLIST_KEYS = frozenset([
'GroupName',
'Label',
'Program',
'ProgramArguments',
'UserName'])
# The Mac OS documentation indicates that Label and # ProgramArguments are
# required keys, lauchd plists have been observed that contain Label and
# Program keys.

PLIST_KEYS = frozenset(['Label'])

def CheckRequiredFormat(self, top_level):
"""Check if the plist has the minimal structure required by the plugin.
Args:
top_level (dict[str, object]): plist top-level item.
Returns:
bool: True if this is the correct plugin, False otherwise.
"""
if not super(MacOSLaunchdPlistPlugin, self).CheckRequiredFormat(top_level):
return False

return 'Program' in top_level or 'ProgramArguments' in top_level

# pylint: disable=arguments-differ
def _ParsePlist(self, parser_mediator, top_level=None, **unused_kwargs):
Expand All @@ -74,8 +87,12 @@ def _ParsePlist(self, parser_mediator, top_level=None, **unused_kwargs):
"""
program = top_level.get('Program', None)
program_arguments = top_level.get('ProgramArguments', None)
if not (program or program_arguments):
return
if program and program_arguments:
program = ' '.join([program, ' '.join(program_arguments)])
elif program_arguments:
program = ' '.join(program_arguments)

event_data = MacOSLaunchdEventData()
event_data.group_name = top_level.get('GroupName')
Expand Down
10 changes: 10 additions & 0 deletions test_data/launchd.minimal.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>foo</string>
<key>Program</key>
<string>/usr/bin/true</string>
</dict>
</plist>
12 changes: 12 additions & 0 deletions test_data/launchd.noprogram.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>foo</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/true</string>
</array>
</dict>
</plist>
56 changes: 56 additions & 0 deletions tests/parsers/plist_plugins/launchd.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,62 @@ def testProcess(self):
event_data = storage_writer.GetAttributeContainerByIndex('event_data', 0)
self.CheckEventData(event_data, expected_event_values)

def testProcessMinimal(self):
"""Tests the Process function."""
plist_name = 'launchd.minimal.plist'

plugin = launchd.MacOSLaunchdPlistPlugin()
storage_writer = self._ParsePlistFileWithPlugin(
plugin, [plist_name], plist_name)

number_of_event_data = storage_writer.GetNumberOfAttributeContainers(
'event_data')
self.assertEqual(number_of_event_data, 1)

number_of_warnings = storage_writer.GetNumberOfAttributeContainers(
'extraction_warning')
self.assertEqual(number_of_warnings, 0)

number_of_warnings = storage_writer.GetNumberOfAttributeContainers(
'recovery_warning')
self.assertEqual(number_of_warnings, 0)

expected_event_values = {
'data_type': 'macos:launchd:entry',
'name': 'foo',
'program': '/usr/bin/true'}

event_data = storage_writer.GetAttributeContainerByIndex('event_data', 0)
self.CheckEventData(event_data, expected_event_values)

def testProcessNoProgram(self):
"""Tests the Process function."""
plist_name = 'launchd.noprogram.plist'

plugin = launchd.MacOSLaunchdPlistPlugin()
storage_writer = self._ParsePlistFileWithPlugin(
plugin, [plist_name], plist_name)

number_of_event_data = storage_writer.GetNumberOfAttributeContainers(
'event_data')
self.assertEqual(number_of_event_data, 1)

number_of_warnings = storage_writer.GetNumberOfAttributeContainers(
'extraction_warning')
self.assertEqual(number_of_warnings, 0)

number_of_warnings = storage_writer.GetNumberOfAttributeContainers(
'recovery_warning')
self.assertEqual(number_of_warnings, 0)

expected_event_values = {
'data_type': 'macos:launchd:entry',
'name': 'foo',
'program': '/usr/bin/true'}

event_data = storage_writer.GetAttributeContainerByIndex('event_data', 0)
self.CheckEventData(event_data, expected_event_values)


if __name__ == '__main__':
unittest.main()

0 comments on commit dab1e79

Please sign in to comment.