diff --git a/plaso/engine/artifact_filters.py b/plaso/engine/artifact_filters.py new file mode 100644 index 0000000000..7a3d91ff78 --- /dev/null +++ b/plaso/engine/artifact_filters.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +"""Helper to create filters based on forensic artifact definitions.""" + +from __future__ import unicode_literals + +from artifacts import definitions as artifact_types + +from dfvfs.helpers import file_system_searcher +from dfwinreg import registry_searcher +from plaso.engine import logger +from plaso.engine import path_helper + + +class ArtifactDefinitionsFilterHelper(object): + """Helper to create filters based on artifact definitions. + + Builds extraction filters from forensic artifact definitions. + + For more information about Forensic Artifacts see: + https://github.com/ForensicArtifacts/artifacts/blob/master/docs/Artifacts%20definition%20format%20and%20style%20guide.asciidoc + """ + + _KNOWLEDGE_BASE_VALUE = 'ARTIFACT_FILTERS' + + _COMPATIBLE_REGISTRY_KEY_PATH_PREFIXES = ['HKEY_LOCAL_MACHINE'] + + def __init__(self, artifacts_registry, artifact_definitions, knowledge_base): + """Initializes an artifact definitions filter helper. + + Args: + artifacts_registry (artifacts.ArtifactDefinitionsRegistry]): artifact + definitions registry. + artifact_definitions (list[str]): artifact definition names to filter. + path (str): path to a file that contains one or more artifact definitions. + knowledge_base (KnowledgeBase): contains information from the source + data needed for filtering. + """ + super(ArtifactDefinitionsFilterHelper, self).__init__() + self._artifacts = artifact_definitions + self._artifacts_registry = artifacts_registry + self._knowledge_base = knowledge_base + + def _CheckKeyCompatibility(self, key_path): + """Checks if a Windows Registry key path is supported by dfWinReg. + + Args: + key_path (str): path of the Windows Registry key. + + Returns: + bool: True if key is compatible or False if not. + """ + for key_path_prefix in self._COMPATIBLE_REGISTRY_KEY_PATH_PREFIXES: + if key_path.startswith(key_path_prefix): + return True + + logger.warning( + 'Prefix of key "{0:s}" is currently not supported'.format(key_path)) + return False + + def BuildFindSpecs(self, environment_variables=None): + """Builds find specifications from artifact definitions. + + The resulting find specifications are set in the knowledge base. + + Args: + environment_variables (Optional[list[EnvironmentVariableArtifact]]): + environment variables. + """ + find_specs_per_source_type = { + artifact_types.TYPE_INDICATOR_FILE: [], + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY: []} + + for name in self._artifacts: + definition = self._artifacts_registry.GetDefinitionByName(name) + if not definition: + continue + + for source in definition.sources: + if source.type_indicator == artifact_types.TYPE_INDICATOR_FILE: + # TODO: move source.paths iteration into + # BuildFindSpecsFromFileArtifact. + for path_entry in set(source.paths): + find_specs = self.BuildFindSpecsFromFileArtifact( + path_entry, source.separator, environment_variables, + self._knowledge_base.user_accounts) + find_specs_per_source_type[ + artifact_types.TYPE_INDICATOR_FILE].extend(find_specs) + + elif (source.type_indicator == + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY): + # TODO: move source.keys iteration into + # BuildFindSpecsFromRegistryArtifact. + for key_path in set(source.keys): + if self._CheckKeyCompatibility(key_path): + find_specs = self.BuildFindSpecsFromRegistryArtifact(key_path) + find_specs_per_source_type[ + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY].extend( + find_specs) + + elif (source.type_indicator == + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_VALUE): + # TODO: Handle Registry Values Once Supported in dfwinreg. + # https://github.com/log2timeline/dfwinreg/issues/98 + logger.warning(( + 'Windows Registry values are not supported, extracting key: ' + '"{0!s}"').format(source.key_value_pairs)) + + # TODO: move source.key_value_pairs iteration into + # BuildFindSpecsFromRegistryArtifact. + for key_path in set([ + key_path for key_path, _ in source.key_value_pairs]): + if self._CheckKeyCompatibility(key_path): + find_specs = self.BuildFindSpecsFromRegistryArtifact(key_path) + find_specs_per_source_type[ + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY].extend( + find_specs) + + else: + logger.warning( + 'Unsupported artifact definition source type: "{0:s}"'.format( + source.type_indicator)) + + self._knowledge_base.SetValue( + self._KNOWLEDGE_BASE_VALUE, find_specs_per_source_type) + + def BuildFindSpecsFromFileArtifact( + self, source_path, path_separator, environment_variables, user_accounts): + """Builds find specifications from a file source type. + + Args: + source_path (str): file system path defined by the source. + path_separator (str): file system path segment separator. + environment_variables list(str): environment variable attributes used to + dynamically populate environment variables in key. + user_accounts (list[str]): identified user accounts stored in the + knowledge base. + + Returns: + list[dfvfs.FindSpec]: find specifications for the file source type. + """ + find_specs = [] + for glob_path in path_helper.PathHelper.ExpandRecursiveGlobs( + source_path, path_separator): + for path in path_helper.PathHelper.ExpandUsersHomeDirectoryPath( + glob_path, user_accounts): + if '%' in path: + path = path_helper.PathHelper.ExpandWindowsPath( + path, environment_variables) + + if not path.startswith(path_separator): + logger.warning(( + 'The path filter must be defined as an absolute path: ' + '"{0:s}"').format(path)) + continue + + # Convert the path filters into a list of path segments and + # strip the root path segment. + path_segments = path.split(path_separator) + + # Remove initial root entry + path_segments.pop(0) + + if not path_segments[-1]: + logger.warning( + 'Empty last path segment in path filter: "{0:s}"'.format(path)) + path_segments.pop(-1) + + try: + find_spec = file_system_searcher.FindSpec( + location_glob=path_segments, case_sensitive=False) + except ValueError as exception: + logger.error(( + 'Unable to build find specification for path: "{0:s}" with ' + 'error: {1!s}').format(path, exception)) + continue + + find_specs.append(find_spec) + + return find_specs + + def BuildFindSpecsFromRegistryArtifact(self, source_key_path): + """Build find specifications from a Windows Registry source type. + + Args: + source_key_path (str): Windows Registry key path defined by the source. + + Returns: + list[dfwinreg.FindSpec]: find specifications for the Windows Registry + source type. + """ + find_specs = [] + for key_path in path_helper.PathHelper.ExpandRecursiveGlobs( + source_key_path, '\\'): + if '%%' in key_path: + logger.error('Unable to expand key path: "{0:s}"'.format(key_path)) + continue + + find_spec = registry_searcher.FindSpec(key_path_glob=key_path) + find_specs.append(find_spec) + + return find_specs diff --git a/plaso/engine/path_helper.py b/plaso/engine/path_helper.py index b3867cd5b8..190c86bcfc 100644 --- a/plaso/engine/path_helper.py +++ b/plaso/engine/path_helper.py @@ -3,14 +3,121 @@ from __future__ import unicode_literals +import re + from dfvfs.lib import definitions as dfvfs_definitions +from plaso.engine import logger from plaso.lib import py2to3 class PathHelper(object): """Class that implements the path helper.""" + _RECURSIVE_GLOB_LIMIT = 10 + + @classmethod + def AppendPathEntries(cls, path, path_separator, count, skip_first): + """Appends wildcard entries to end of path. + + Will append wildcard * to given path building a list of strings for "count" + iterations, skipping the first directory if skip_first is true. + + Args: + path (str): Path to append wildcards to. + path_separator (str): path segment separator. + count (int): Number of entries to be appended. + skip_first (bool): Whether or not to skip first entry to append. + + Returns: + list[str]: Paths that were expanded from the path with wildcards. + """ + paths = [] + replacement = '{0:s}*'.format(path_separator) + + iteration = 0 + while iteration < count: + if skip_first and iteration == 0: + path += replacement + else: + path += replacement + paths.append(path) + iteration += 1 + + return paths + + @classmethod + def ExpandRecursiveGlobs(cls, path, path_separator): + """Expands recursive like globs present in an artifact path. + + If a path ends in '**', with up to two optional digits such as '**10', + the '**' will recursively match all files and zero or more directories + from the specified path. The optional digits indicate the recursion depth. + By default recursion depth is 10 directories. + + If the glob is followed by the specified path segment separator, only + directories and subdirectories will be matched. + + Args: + path (str): path to be expanded. + path_separator (str): path segment separator. + + Returns: + list[str]: String path expanded for each glob. + """ + glob_regex = r'(.*)?{0}\*\*(\d{{1,2}})?({0})?$'.format( + re.escape(path_separator)) + + match = re.search(glob_regex, path) + if not match: + return [path] + + skip_first = False + if match.group(3): + skip_first = True + if match.group(2): + iterations = int(match.group(2)) + else: + iterations = cls._RECURSIVE_GLOB_LIMIT + logger.warning(( + 'Path "{0:s}" contains fully recursive glob, limiting to 10 ' + 'levels').format(path)) + + return cls.AppendPathEntries( + match.group(1), path_separator, iterations, skip_first) + + @classmethod + def ExpandUsersHomeDirectoryPath(cls, path, user_accounts): + """Expands a path to contain all users home or profile directories. + + Expands the GRR artifacts path variable "%%users.homedir%%". + + Args: + path (str): Windows path with environment variables. + user_accounts (list[UserAccountArtifact]): user accounts. + + Returns: + list [str]: paths returned for user accounts without a drive letter. + """ + path_upper_case = path.upper() + if not path_upper_case.startswith('%%USERS.HOMEDIR%%'): + user_paths = [path] + else: + regex = re.compile(re.escape('%%users.homedir%%')) + + user_paths = [] + for user_account in user_accounts: + user_path = regex.sub(user_account.user_directory, path, re.IGNORECASE) + user_paths.append(user_path) + + # Remove the drive letter, if it exists. + for path_index, user_path in enumerate(user_paths): + if len(user_path) > 2 and user_path[1] == ':': + _, _, user_path = user_path.rpartition(':') + user_paths[path_index] = user_path + + return user_paths + @classmethod def ExpandWindowsPath(cls, path, environment_variables): """Expands a Windows path containing environment variables. @@ -23,6 +130,8 @@ def ExpandWindowsPath(cls, path, environment_variables): Returns: str: expanded Windows path. """ + # TODO: Add support for items such as %%users.localappdata%% + if environment_variables is None: environment_variables = [] @@ -42,9 +151,20 @@ def ExpandWindowsPath(cls, path, environment_variables): not path_segment.endswith('%')): continue - lookup_key = path_segment.upper()[1:-1] + check_for_drive_letter = False + path_segment_upper_case = path_segment.upper() + if path_segment_upper_case.startswith('%%ENVIRON_'): + lookup_key = path_segment_upper_case[10:-2] + check_for_drive_letter = True + else: + lookup_key = path_segment_upper_case[1:-1] path_segments[index] = lookup_table.get(lookup_key, path_segment) + if check_for_drive_letter: + # Remove the drive letter. + if len(path_segments[index]) >= 2 and path_segments[index][1] == ':': + _, _, path_segments[index] = path_segments[index].rpartition(':') + return '\\'.join(path_segments) @classmethod diff --git a/test_data/artifacts/artifacts_filters.yaml b/test_data/artifacts/artifacts_filters.yaml new file mode 100644 index 0000000000..7de1896d7f --- /dev/null +++ b/test_data/artifacts/artifacts_filters.yaml @@ -0,0 +1,67 @@ +# Artifact definitions. + +name: TestFiles +doc: Test Doc +sources: +- type: FILE + attributes: + paths: ['%%environ_systemdrive%%\AUTHORS'] + separator: '\' +labels: [System] +supported_os: [Windows] +--- +name: TestFiles2 +doc: Test Doc2 +sources: +- type: FILE + attributes: + paths: + - '%%environ_systemdrive%%\test_data\*.evtx' + - '%%users.homedir%%\Documents\WindowsPowerShell\profile.ps1' + - '\test_data\testdir\filter_*.txt' + - '\does_not_exist\some_file_*.txt' + - '\globbed\test\path\**\' + - 'failing' + separator: '\' +labels: [System] +supported_os: [Windows] +--- +name: TestRegistry +doc: Test Registry Doc +sources: +- type: REGISTRY_KEY + attributes: + keys: ['HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\*'] +supported_os: [Windows] +--- +name: TestRegistryKey +doc: Test Registry Doc Key +sources: +- type: REGISTRY_KEY + attributes: + keys: + - 'HKEY_LOCAL_MACHINE\System\ControlSet001\services\**\' + - 'HKEY_LOCAL_MACHINE\System\ControlSet002\services\**\' + - 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum\USBSTOR' + - 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum\USBSTOR\**' +supported_os: [Windows] +--- +name: TestRegistryValue +doc: Test Registry Doc Value +sources: +- type: REGISTRY_VALUE + attributes: + key_value_pairs: + - {key: 'HKEY_LOCAL_MACHINE\System\ControlSet001\Control\Session Manager', value: 'BootExecute'} + - {key: 'HKEY_LOCAL_MACHINE\System\ControlSet002\Control\Session Manager', value: 'BootExecute'} +supported_os: [Windows] +--- +name: TestFilesImageExport +doc: Test Doc +sources: +- type: FILE + attributes: + paths: ['\a_directory\*_file'] + separator: '\' +labels: [System] +supported_os: [Windows] \ No newline at end of file diff --git a/tests/engine/artifact_filters.py b/tests/engine/artifact_filters.py new file mode 100644 index 0000000000..41a43e0e88 --- /dev/null +++ b/tests/engine/artifact_filters.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Tests for the artifacts file filter functions.""" + +from __future__ import unicode_literals + +import unittest + +from artifacts import definitions as artifact_types +from artifacts import reader as artifacts_reader +from artifacts import registry as artifacts_registry + +from dfwinreg import registry as dfwinreg_registry +from dfwinreg import registry_searcher as dfwinreg_registry_searcher + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.containers import artifacts +from plaso.engine import artifact_filters +from plaso.engine import knowledge_base as knowledge_base_engine +from plaso.parsers import winreg as windows_registry_parser + +from tests import test_lib as shared_test_lib + + +class ArtifactDefinitionsFilterHelperTest(shared_test_lib.BaseTestCase): + """Tests for artifact definitions filter helper.""" + + # pylint: disable=protected-access + + def _CreateTestArtifactDefinitionsFilterHelper( + self, artifact_definitions, knowledge_base): + """Creates an artifact definitions filter helper for testing. + + Args: + artifact_definitions (list[str]): artifact definition names to filter. + knowledge_base (KnowledgeBase): contains information from the source + data needed for filtering. + + Returns: + ArtifactDefinitionsFilterHelper: artifact definitions filter helper. + """ + registry = artifacts_registry.ArtifactDefinitionsRegistry() + reader = artifacts_reader.YamlArtifactsReader() + + test_artifacts_path = self._GetTestFilePath(['artifacts']) + registry.ReadFromDirectory(reader, test_artifacts_path) + + return artifact_filters.ArtifactDefinitionsFilterHelper( + registry, artifact_definitions, knowledge_base) + + @shared_test_lib.skipUnlessHasTestFile(['artifacts']) + @shared_test_lib.skipUnlessHasTestFile(['System.evtx']) + @shared_test_lib.skipUnlessHasTestFile(['testdir', 'filter_1.txt']) + @shared_test_lib.skipUnlessHasTestFile(['testdir', 'filter_3.txt']) + def testBuildFindSpecsWithFileSystem(self): + """Tests the BuildFindSpecs function for file type artifacts.""" + knowledge_base = knowledge_base_engine.KnowledgeBase() + + testuser1 = artifacts.UserAccountArtifact( + identifier='1000', + user_directory='C:\\\\Users\\\\testuser1', + username='testuser1') + knowledge_base.AddUserAccount(testuser1) + + testuser2 = artifacts.UserAccountArtifact( + identifier='1001', + user_directory='C:\\\\Users\\\\testuser2', + username='testuser2') + knowledge_base.AddUserAccount(testuser2) + + test_filter_file = self._CreateTestArtifactDefinitionsFilterHelper( + ['TestFiles', 'TestFiles2'], knowledge_base) + + environment_variable = artifacts.EnvironmentVariableArtifact( + case_sensitive=False, name='SystemDrive', value='C:') + + test_filter_file.BuildFindSpecs( + environment_variables=[environment_variable]) + find_specs_per_source_type = knowledge_base.GetValue( + test_filter_file._KNOWLEDGE_BASE_VALUE) + find_specs = find_specs_per_source_type.get( + artifact_types.TYPE_INDICATOR_FILE, []) + + # Should build 15 FindSpec entries. + self.assertEqual(len(find_specs), 15) + + # Last find_spec should contain the testuser2 profile path. + location_segments = sorted([ + find_spec._location_segments for find_spec in find_specs]) + path_segments = [ + 'Users', 'testuser2', 'Documents', 'WindowsPowerShell', 'profile\\.ps1'] + self.assertEqual(location_segments[2], path_segments) + + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location='.') + file_system = path_spec_resolver.Resolver.OpenFileSystem(path_spec) + searcher = file_system_searcher.FileSystemSearcher( + file_system, path_spec) + + path_spec_generator = searcher.Find(find_specs=find_specs) + self.assertIsNotNone(path_spec_generator) + + path_specs = list(path_spec_generator) + + # Two evtx, one symbolic link to evtx, one AUTHORS, two filter_*.txt files, + # total 6 path specifications. + self.assertEqual(len(path_specs), 6) + + file_system.Close() + + @shared_test_lib.skipUnlessHasTestFile(['artifacts']) + @shared_test_lib.skipUnlessHasTestFile(['SYSTEM']) + def testBuildFindSpecsWithRegistry(self): + """Tests the BuildFindSpecs function on Windows Registry artifacts.""" + knowledge_base = knowledge_base_engine.KnowledgeBase() + test_filter_file = self._CreateTestArtifactDefinitionsFilterHelper( + ['TestRegistry'], knowledge_base) + + test_filter_file.BuildFindSpecs(environment_variables=None) + find_specs_per_source_type = knowledge_base.GetValue( + test_filter_file._KNOWLEDGE_BASE_VALUE) + find_specs = find_specs_per_source_type.get( + artifact_types.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY, []) + + self.assertEqual(len(find_specs), 1) + + win_registry_reader = ( + windows_registry_parser.FileObjectWinRegistryFileReader()) + + file_entry = self._GetTestFileEntry(['SYSTEM']) + file_object = file_entry.GetFileObject() + + registry_file = win_registry_reader.Open(file_object) + + win_registry = dfwinreg_registry.WinRegistry() + key_path_prefix = win_registry.GetRegistryFileMapping(registry_file) + registry_file.SetKeyPathPrefix(key_path_prefix) + win_registry.MapFile(key_path_prefix, registry_file) + + searcher = dfwinreg_registry_searcher.WinRegistrySearcher(win_registry) + key_paths = list(searcher.Find(find_specs=find_specs)) + + self.assertIsNotNone(key_paths) + + # Three key paths found. + self.assertEqual(len(key_paths), 3) + + def testCheckKeyCompatibility(self): + """Tests the _CheckKeyCompatibility function""" + knowledge_base = knowledge_base_engine.KnowledgeBase() + test_filter_file = self._CreateTestArtifactDefinitionsFilterHelper( + [], knowledge_base) + + # Compatible Key. + key_path = 'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control' + compatible_key = test_filter_file._CheckKeyCompatibility(key_path) + self.assertTrue(compatible_key) + + # NOT a Compatible Key. + key_path = 'HKEY_USERS\\S-1-5-18' + compatible_key = test_filter_file._CheckKeyCompatibility(key_path) + self.assertFalse(compatible_key) + + def testBuildFindSpecsFromFileArtifact(self): + """Tests the BuildFindSpecsFromFileArtifact function for file artifacts.""" + knowledge_base = knowledge_base_engine.KnowledgeBase() + test_filter_file = self._CreateTestArtifactDefinitionsFilterHelper( + [], knowledge_base) + + separator = '\\' + user_accounts = [] + + # Test expansion of environment variables. + path_entry = '%%environ_systemroot%%\\test_data\\*.evtx' + environment_variable = [artifacts.EnvironmentVariableArtifact( + case_sensitive=False, name='SystemRoot', value='C:\\Windows')] + + find_specs = test_filter_file.BuildFindSpecsFromFileArtifact( + path_entry, separator, environment_variable, user_accounts) + + # Should build 1 find_spec. + self.assertEqual(len(find_specs), 1) + + # Location segments should be equivalent to \Windows\test_data\*.evtx. + path_segments = ['Windows', 'test\\_data', '.*\\.evtx'] + self.assertEqual(find_specs[0]._location_segments, path_segments) + + # Test expansion of globs. + path_entry = '\\test_data\\**' + find_specs = test_filter_file.BuildFindSpecsFromFileArtifact( + path_entry, separator, environment_variable, user_accounts) + + # Glob expansion should by default recurse ten levels. + self.assertEqual(len(find_specs), 10) + + # Last entry in find_specs list should be 10 levels of depth. + path_segments = [ + 'test\\_data', '.*', '.*', '.*', '.*', '.*', '.*', '.*', '.*', '.*', + '.*'] + self.assertEqual(find_specs[9]._location_segments, path_segments) + + # Test expansion of user home directories + separator = '/' + testuser1 = artifacts.UserAccountArtifact( + user_directory='/homes/testuser1', username='testuser1') + testuser2 = artifacts.UserAccountArtifact( + user_directory='/home/testuser2', username='testuser2') + user_accounts = [testuser1, testuser2] + path_entry = '%%users.homedir%%/.thumbnails/**3' + + find_specs = test_filter_file.BuildFindSpecsFromFileArtifact( + path_entry, separator, environment_variable, user_accounts) + + # Six total find specs should be created for testuser1 and testuser2. + self.assertEqual(len(find_specs), 6) + + # Last entry in find_specs list should be testuser2 with a depth of 3 + path_segments = ['home', 'testuser2', '\\.thumbnails', '.*', '.*', '.*'] + self.assertEqual(find_specs[5]._location_segments, path_segments) + + # Test Windows path with profile directories and globs with a depth of 4. + separator = '\\' + testuser1 = artifacts.UserAccountArtifact( + user_directory='\\Users\\\\testuser1', username='testuser1') + testuser2 = artifacts.UserAccountArtifact( + user_directory='\\Users\\\\testuser2', username='testuser2') + user_accounts = [testuser1, testuser2] + path_entry = '%%users.homedir%%\\AppData\\**4' + + find_specs = test_filter_file.BuildFindSpecsFromFileArtifact( + path_entry, separator, environment_variable, user_accounts) + + # Eight find specs should be created for testuser1 and testuser2. + self.assertEqual(len(find_specs), 8) + + # Last entry in find_specs list should be testuser2, with a depth of 4. + path_segments = ['Users', 'testuser2', 'AppData', '.*', '.*', '.*', '.*'] + self.assertEqual(find_specs[7]._location_segments, path_segments) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/engine/path_helper.py b/tests/engine/path_helper.py index 13717d9a73..b89aafa2c7 100644 --- a/tests/engine/path_helper.py +++ b/tests/engine/path_helper.py @@ -19,6 +19,125 @@ class PathHelperTest(shared_test_lib.BaseTestCase): """Tests for the path helper.""" + def testAppendPathEntries(self): + """Tests the AppendPathEntries function.""" + separator = '\\' + path = '\\Windows\\Test' + + # Test depth of ten skipping first entry. + # The path will have 9 entries as the default depth for ** is 10, but the + # first entry is being skipped. + count = 10 + skip_first = True + paths = path_helper.PathHelper.AppendPathEntries( + path, separator, count, skip_first) + + # Nine paths returned + self.assertEqual(len(paths), 9) + + # Nine paths in total, each one level deeper than the previous. + check_paths = sorted([ + '\\Windows\\Test\\*\\*', + '\\Windows\\Test\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*\\*\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*']) + self.assertEqual(sorted(paths), check_paths) + + # Now test with skip_first set to False, but only a depth of 4. + # the path will have a total of 4 entries. + count = 4 + skip_first = False + paths = path_helper.PathHelper.AppendPathEntries( + path, separator, count, skip_first) + + # Four paths returned + self.assertEqual(len(paths), 4) + + # Four paths in total, each one level deeper than the previous. + check_paths = sorted([ + '\\Windows\\Test\\*', + '\\Windows\\Test\\*\\*', + '\\Windows\\Test\\*\\*\\*', + '\\Windows\\Test\\*\\*\\*\\*']) + self.assertEqual(sorted(paths), check_paths) + + def testExpandRecursiveGlobs(self): + """Tests the _ExpandRecursiveGlobs function.""" + separator = '/' + + # Test a path with a trailing /, which means first directory is skipped. + # The path will have 9 entries as the default depth for ** is 10, but the + # first entry is being skipped. + path = '/etc/sysconfig/**/' + paths = path_helper.PathHelper.ExpandRecursiveGlobs(path, separator) + + # Nine paths returned + self.assertEqual(len(paths), 9) + + # Nine paths in total, each one level deeper than the previous. + check_paths = sorted([ + '/etc/sysconfig/*/*', + '/etc/sysconfig/*/*/*', + '/etc/sysconfig/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*/*/*/*/*', + '/etc/sysconfig/*/*/*/*/*/*/*/*/*/*']) + self.assertEqual(sorted(paths), check_paths) + + # Now test with no trailing separator, but only a depth of 4. + # the path will have a total of 4 entries. + path = '/etc/sysconfig/**4' + paths = path_helper.PathHelper.ExpandRecursiveGlobs(path, separator) + + # Four paths returned + self.assertEqual(len(paths), 4) + + # Four paths in total, each one level deeper than the previous. + check_paths = sorted([ + '/etc/sysconfig/*', + '/etc/sysconfig/*/*', + '/etc/sysconfig/*/*/*', + '/etc/sysconfig/*/*/*/*']) + self.assertEqual(sorted(paths), check_paths) + + def testExpandUsersHomeDirectoryPath(self): + """Tests the ExpandUsersHomeDirectoryPath function.""" + user_account_artifact1 = artifacts.UserAccountArtifact( + user_directory='C:\\Users\\Test1', username='Test1') + user_account_artifact2 = artifacts.UserAccountArtifact( + user_directory='C:\\Users\\Test2', username='Test2') + + path = '%%users.homedir%%\\Profile' + expanded_paths = path_helper.PathHelper.ExpandUsersHomeDirectoryPath( + path, [user_account_artifact1, user_account_artifact2]) + + expected_expanded_paths = [ + '\\Users\\Test1\\Profile', + '\\Users\\Test2\\Profile'] + self.assertEqual(expanded_paths, expected_expanded_paths) + + path = 'C:\\Temp' + expanded_paths = path_helper.PathHelper.ExpandUsersHomeDirectoryPath( + path, [user_account_artifact1, user_account_artifact2]) + + expected_expanded_paths = ['\\Temp'] + self.assertEqual(expanded_paths, expected_expanded_paths) + + path = 'C:\\Temp\\%%users.homedir%%' + expanded_paths = path_helper.PathHelper.ExpandUsersHomeDirectoryPath( + path, [user_account_artifact1, user_account_artifact2]) + + expected_expanded_paths = ['\\Temp\\%%users.homedir%%'] + self.assertEqual(expanded_paths, expected_expanded_paths) + def testExpandWindowsPath(self): """Tests the ExpandWindowsPath function.""" environment_variable = artifacts.EnvironmentVariableArtifact( @@ -40,6 +159,10 @@ def testExpandWindowsPath(self): '%Bogus%\\System32', [environment_variable]) self.assertEqual(expanded_path, '%Bogus%\\System32') + expanded_path = path_helper.PathHelper.ExpandWindowsPath( + '%%environ_systemroot%%\\System32', [environment_variable]) + self.assertEqual(expanded_path, '\\Windows\\System32') + # Test non-string environment variable. environment_variable = artifacts.EnvironmentVariableArtifact( case_sensitive=False, name='SystemRoot', value=('bogus', 0))