From 118f94cceb41f2b65fd3a92526e09d57a0c292e5 Mon Sep 17 00:00:00 2001 From: Joachim Metz Date: Sat, 16 May 2026 06:01:52 +0200 Subject: [PATCH 1/3] Changes to use black Python formatter --- .github/workflows/test_tox.yml | 31 -- .pylintrc | 7 +- docs/conf.py | 147 ++++--- pyproject.toml | 12 +- tox.ini | 53 ++- utils/check_dependencies.py | 11 +- utils/dependencies.py | 687 +++++++++++++++++---------------- utils/update_release.sh | 2 +- 8 files changed, 498 insertions(+), 452 deletions(-) diff --git a/.github/workflows/test_tox.yml b/.github/workflows/test_tox.yml index aae8d80..3893832 100644 --- a/.github/workflows/test_tox.yml +++ b/.github/workflows/test_tox.yml @@ -85,34 +85,3 @@ jobs: uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} - lint: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.14'] - container: - image: ubuntu:26.04 - steps: - - uses: actions/checkout@v6 - - name: Set up container - env: - DEBIAN_FRONTEND: noninteractive - run: | - apt-get update -q - apt-get install -y libterm-readline-gnu-perl locales software-properties-common - locale-gen en_US.UTF-8 - ln -f -s /usr/share/zoneinfo/UTC /etc/localtime - - name: Install dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: | - add-apt-repository -y universe - add-apt-repository -y ppa:deadsnakes/ppa - add-apt-repository -y ppa:gift/dev - apt-get update -q - apt-get install -y build-essential git libffi-dev pkg-config python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libcaes-python3 libcreg-python3 libewf-python3 libexe-python3 libfcrypto-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libregf-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsapm-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 libwrc-python3 python3-artifacts python3-cffi-backend python3-dfdatetime python3-dfimagetools python3-dfvfs python3-dfwinreg python3-dtfabric python3-idna python3-pip python3-pytsk3 python3-setuptools python3-xattr python3-yaml tox - - name: Run linter - env: - LANG: en_US.UTF-8 - run: | - tox -e lint diff --git a/.pylintrc b/.pylintrc index bbedb09..235c1de 100644 --- a/.pylintrc +++ b/.pylintrc @@ -358,12 +358,11 @@ indent-after-paren=4 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). -# indent-string=' ' -indent-string=' ' +indent-string=' ' # Maximum number of characters on a single line. # max-line-length=100 -max-line-length=80 +max-line-length=88 # Maximum number of lines in a module. max-module-lines=1000 @@ -599,7 +598,7 @@ spelling-store-unknown-words=no # This flag controls whether inconsistent-quotes generates a warning when the # character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no +check-quote-consistency=yes # This flag controls whether the implicit-str-concat should generate a warning # on implicit string concatenation in sequences defined over several lines. diff --git a/docs/conf.py b/docs/conf.py index bcd2c2d..0d78e69 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,30 +9,29 @@ from docutils import transforms # Change PYTHONPATH to include artifactsrc module and dependencies. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) import artifactsrc # pylint: disable=wrong-import-position import utils.dependencies # pylint: disable=wrong-import-position - # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '2.0.1' +needs_sphinx = "2.0.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'recommonmark', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.doctest', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx_markdown_tables', - 'sphinx_rtd_theme', + "recommonmark", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_markdown_tables", + "sphinx_rtd_theme", ] # We cannot install architecture dependent Python modules on readthedocs, @@ -40,8 +39,9 @@ pip_installed_modules = set() dependency_helper = utils.dependencies.DependencyHelper( - dependencies_file=os.path.join('..', 'dependencies.ini'), - test_dependencies_file=os.path.join('..', 'test_dependencies.ini')) + dependencies_file=os.path.join("..", "dependencies.ini"), + test_dependencies_file=os.path.join("..", "test_dependencies.ini"), +) modules_to_mock = set(dependency_helper.dependencies.keys()) modules_to_mock = modules_to_mock.difference(pip_installed_modules) @@ -57,39 +57,38 @@ # General information about the project. # pylint: disable=redefined-builtin -project = 'Digital Forensics Artifact knowledge base' -copyright = 'The Digital Forensics Artifact knowledge base authors' +project = "Digital Forensics Artifact knowledge base" +copyright = "The Digital Forensics Artifact knowledge base authors" version = artifactsrc.__version__ release = artifactsrc.__version__ # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The master toctree document. -master_doc = 'index' +master_doc = "index" # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Output file base name for HTML help builder. -htmlhelp_basename = 'artifactskbdoc' +htmlhelp_basename = "artifactskbdoc" # -- Options linkcheck ---------------------------------------------------- -linkcheck_ignore = [ -] +linkcheck_ignore = [] # -- Code to rewrite links for readthedocs -------------------------------- @@ -97,74 +96,74 @@ # This function is a Sphinx core event callback, the format of which is detailed # here: https://www.sphinx-doc.org/en/master/extdev/appapi.html#events + # pylint: disable=unused-argument def RunSphinxAPIDoc(app): - """Runs sphinx-apidoc to auto-generate documentation. + """Runs sphinx-apidoc to auto-generate documentation. - Args: - app (sphinx.application.Sphinx): Sphinx application. Required by the - the Sphinx event callback API. - """ - current_directory = os.path.abspath(os.path.dirname(__file__)) - module_path = os.path.join(current_directory, '..', 'artifactsrc') - api_directory = os.path.join(current_directory, 'sources', 'api') - apidoc.main(['-o', api_directory, module_path, '--force']) + Args: + app (sphinx.application.Sphinx): Sphinx application. Required by the + the Sphinx event callback API. + """ + current_directory = os.path.abspath(os.path.dirname(__file__)) + module_path = os.path.join(current_directory, "..", "artifactsrc") + api_directory = os.path.join(current_directory, "sources", "api") + apidoc.main(["-o", api_directory, module_path, "--force"]) class MarkdownLinkFixer(transforms.Transform): - """Transform definition to parse .md references to internal pages.""" + """Transform definition to parse .md references to internal pages.""" - default_priority = 1000 + default_priority = 1000 - _URI_PREFIXES = [] + _URI_PREFIXES = [] - def _FixLinks(self, node): - """Corrects links to .md files not part of the documentation. + def _FixLinks(self, node): + """Corrects links to .md files not part of the documentation. - Args: - node (docutils.nodes.Node): docutils node. + Args: + node (docutils.nodes.Node): docutils node. - Returns: - docutils.nodes.Node: docutils node, with correct URIs outside - of Markdown pages outside the documentation. - """ - if isinstance(node, nodes.reference) and 'refuri' in node: - reference_uri = node['refuri'] - for uri_prefix in self._URI_PREFIXES: - if (reference_uri.startswith(uri_prefix) and not ( - reference_uri.endswith('.asciidoc') or - reference_uri.endswith('.md'))): - node['refuri'] = reference_uri + '.md' - break + Returns: + docutils.nodes.Node: docutils node, with correct URIs outside + of Markdown pages outside the documentation. + """ + if isinstance(node, nodes.reference) and "refuri" in node: + reference_uri = node["refuri"] + for uri_prefix in self._URI_PREFIXES: + if reference_uri.startswith(uri_prefix) and not ( + reference_uri.endswith(".asciidoc") or reference_uri.endswith(".md") + ): + node["refuri"] = reference_uri + ".md" + break - return node + return node - def _Traverse(self, node): - """Traverses the document tree rooted at node. + def _Traverse(self, node): + """Traverses the document tree rooted at node. - Args: - node (docutils.nodes.Node): docutils node. - """ - self._FixLinks(node) + Args: + node (docutils.nodes.Node): docutils node. + """ + self._FixLinks(node) - for child_node in node.children: - self._Traverse(child_node) + for child_node in node.children: + self._Traverse(child_node) - # pylint: disable=arguments-differ - def apply(self): - """Applies this transform on document tree.""" - self._Traverse(self.document) + # pylint: disable=arguments-differ + def apply(self): + """Applies this transform on document tree.""" + self._Traverse(self.document) # pylint: invalid-name def setup(app): - """Called at Sphinx initialization. - - Args: - app (sphinx.application.Sphinx): Sphinx application. - """ - # Triggers sphinx-apidoc to generate API documentation. - app.connect('builder-inited', RunSphinxAPIDoc) - app.add_config_value( - 'recommonmark_config', {'enable_auto_toc_tree': True}, True) - app.add_transform(MarkdownLinkFixer) + """Called at Sphinx initialization. + + Args: + app (sphinx.application.Sphinx): Sphinx application. + """ + # Triggers sphinx-apidoc to generate API documentation. + app.connect("builder-inited", RunSphinxAPIDoc) + app.add_config_value("recommonmark_config", {"enable_auto_toc_tree": True}, True) + app.add_transform(MarkdownLinkFixer) diff --git a/pyproject.toml b/pyproject.toml index c31e278..766854c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "artifactsrc" -version = "20260510" +version = "20260516" description = "Digital Forensics Artifact knowledge base" maintainers = [ { name = "Joachim Metz", email = "joachim.metz@gmail.com" }, @@ -64,6 +64,16 @@ Documentation = "https://artifactsrc.readthedocs.io/en/latest" Homepage = "https://github.com/ForensicArtifacts/artifacts-kb" Repository = "https://github.com/ForensicArtifacts/artifacts-kb" +[tool.black] +line-length = 88 +target-version = ["py310"] +include = "\\.pyi?$" + +[tool.docformatter] +black = true +non-cap = [] +non-strict = true + [tool.setuptools] package-dir = {"artifactsrc" = "artifactsrc"} diff --git a/tox.ini b/tox.ini index b92077a..4fada63 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py3{10,11,12,13,14},coverage,docs,lint,wheel +envlist = py3{10,11,12,13,14},black,coverage,docformatter,docs,pylint,wheel,yamllint [testenv] allowlist_externals = ./run_tests.py @@ -23,6 +23,38 @@ commands = coverage: coverage xml wheel: python -m build --no-isolation --wheel +[testenv:black] +skipsdist = True +pip_pre = True +passenv = + CFLAGS + CPPFLAGS + LDFLAGS +setenv = + PYTHONPATH = {toxinidir} +deps = + black + setuptools >= 65 +commands = + black --version + black . + +[testenv:docformatter] +skipsdist = True +pip_pre = True +passenv = + CFLAGS + CPPFLAGS + LDFLAGS +setenv = + PYTHONPATH = {toxinidir} +deps = + docformatter + setuptools >= 65 +commands = + docformatter --version + docformatter --in-place --recursive artifactsrc tests tools + [testenv:docs] usedevelop = True deps = @@ -31,7 +63,7 @@ commands = sphinx-build -b html -d build/doctrees docs dist/docs sphinx-build -b linkcheck docs dist/docs -[testenv:lint] +[testenv:pylint] skipsdist = True pip_pre = True passenv = @@ -43,9 +75,22 @@ setenv = deps = pylint >= 3.3.0, < 3.4.0 setuptools >= 65 - yamllint >= 1.26.0 commands = pylint --version - yamllint -v pylint --rcfile=.pylintrc artifactsrc tests tools + +[testenv:yamllint] +skipsdist = True +pip_pre = True +passenv = + CFLAGS + CPPFLAGS + LDFLAGS +setenv = + PYTHONPATH = {toxinidir} +deps = + setuptools >= 65 + yamllint >= 1.26.0 +commands = + yamllint -v yamllint -c .yamllint.yaml artifactsrc diff --git a/utils/check_dependencies.py b/utils/check_dependencies.py index 97e95d3..6814c13 100755 --- a/utils/check_dependencies.py +++ b/utils/check_dependencies.py @@ -4,13 +4,12 @@ import sys # Change PYTHONPATH to include dependencies. -sys.path.insert(0, '.') +sys.path.insert(0, ".") import utils.dependencies # pylint: disable=wrong-import-position +if __name__ == "__main__": + dependency_helper = utils.dependencies.DependencyHelper() -if __name__ == '__main__': - dependency_helper = utils.dependencies.DependencyHelper() - - if not dependency_helper.CheckDependencies(): - sys.exit(1) + if not dependency_helper.CheckDependencies(): + sys.exit(1) diff --git a/utils/dependencies.py b/utils/dependencies.py index c94b92e..9e733f8 100644 --- a/utils/dependencies.py +++ b/utils/dependencies.py @@ -6,350 +6,375 @@ class DependencyDefinition: - """Dependency definition. - - Attributes: - dpkg_name (str): name of the dpkg package that provides the dependency. - is_optional (bool): True if the dependency is optional. - l2tbinaries_name (str): name of the l2tbinaries package that provides - the dependency. - maximum_version (str): maximum supported version, a greater or equal - version is not supported. - minimum_version (str): minimum supported version, a lesser version is - not supported. - name (str): name of (the Python module that provides) the dependency. - pypi_name (str): name of the PyPI package that provides the dependency. - python2_only (bool): True if the dependency is only supported by Python 2. - python3_only (bool): True if the dependency is only supported by Python 3. - rpm_name (str): name of the rpm package that provides the dependency. - skip_check (bool): True if the dependency should be skipped by the - CheckDependencies or CheckTestDependencies methods of DependencyHelper. - skip_requires (bool): True if the dependency should be excluded from - pyproject.toml dependencies. - version_property (str): name of the version attribute or function. - """ - - def __init__(self, name): - """Initializes a dependency configuration. - - Args: - name (str): name of the dependency. + """Dependency definition. + + Attributes: + dpkg_name (str): name of the dpkg package that provides the dependency. + is_optional (bool): True if the dependency is optional. + l2tbinaries_name (str): name of the l2tbinaries package that provides + the dependency. + maximum_version (str): maximum supported version, a greater or equal + version is not supported. + minimum_version (str): minimum supported version, a lesser version is + not supported. + name (str): name of (the Python module that provides) the dependency. + pypi_name (str): name of the PyPI package that provides the dependency. + python2_only (bool): True if the dependency is only supported by Python 2. + python3_only (bool): True if the dependency is only supported by Python 3. + rpm_name (str): name of the rpm package that provides the dependency. + skip_check (bool): True if the dependency should be skipped by the + CheckDependencies or CheckTestDependencies methods of DependencyHelper. + skip_requires (bool): True if the dependency should be excluded from + pyproject.toml dependencies. + version_property (str): name of the version attribute or function. """ - super().__init__() - self.dpkg_name = None - self.is_optional = False - self.l2tbinaries_name = None - self.maximum_version = None - self.minimum_version = None - self.name = name - self.pypi_name = None - self.python2_only = False - self.python3_only = False - self.rpm_name = None - self.skip_check = None - self.skip_requires = None - self.version_property = None + def __init__(self, name): + """Initializes a dependency configuration. + + Args: + name (str): name of the dependency. + """ + super().__init__() + self.dpkg_name = None + self.is_optional = False + self.l2tbinaries_name = None + self.maximum_version = None + self.minimum_version = None + self.name = name + self.pypi_name = None + self.python2_only = False + self.python3_only = False + self.rpm_name = None + self.skip_check = None + self.skip_requires = None + self.version_property = None -class DependencyDefinitionReader: - """Dependency definition reader.""" - - _VALUE_NAMES = frozenset([ - 'dpkg_name', - 'is_optional', - 'l2tbinaries_name', - 'maximum_version', - 'minimum_version', - 'pypi_name', - 'python2_only', - 'python3_only', - 'rpm_name', - 'skip_check', - 'skip_requires', - 'version_property']) - - def _GetConfigValue(self, config_parser, section_name, value_name): - """Retrieves a value from the config parser. - - Args: - config_parser (ConfigParser): configuration parser. - section_name (str): name of the section that contains the value. - value_name (str): name of the value. - - Returns: - object: configuration value or None if the value does not exists. - """ - try: - return config_parser.get(section_name, value_name) - except configparser.NoOptionError: - return None - - def Read(self, file_object): - """Reads dependency definitions. - - Args: - file_object (file): file-like object to read from. - - Yields: - DependencyDefinition: dependency definition. - """ - config_parser = configparser.ConfigParser(interpolation=None) - config_parser.read_file(file_object) - - for section_name in config_parser.sections(): - dependency_definition = DependencyDefinition(section_name) - for value_name in self._VALUE_NAMES: - value = self._GetConfigValue(config_parser, section_name, value_name) - setattr(dependency_definition, value_name, value) - yield dependency_definition +class DependencyDefinitionReader: + """Dependency definition reader.""" + + _VALUE_NAMES = frozenset( + [ + "dpkg_name", + "is_optional", + "l2tbinaries_name", + "maximum_version", + "minimum_version", + "pypi_name", + "python2_only", + "python3_only", + "rpm_name", + "skip_check", + "skip_requires", + "version_property", + ] + ) + + def _GetConfigValue(self, config_parser, section_name, value_name): + """Retrieves a value from the config parser. + + Args: + config_parser (ConfigParser): configuration parser. + section_name (str): name of the section that contains the value. + value_name (str): name of the value. + + Returns: + object: configuration value or None if the value does not exists. + """ + try: + return config_parser.get(section_name, value_name) + except configparser.NoOptionError: + return None + + def Read(self, file_object): + """Reads dependency definitions. + + Args: + file_object (file): file-like object to read from. + + Yields: + DependencyDefinition: dependency definition. + """ + config_parser = configparser.ConfigParser(interpolation=None) + config_parser.read_file(file_object) + + for section_name in config_parser.sections(): + dependency_definition = DependencyDefinition(section_name) + for value_name in self._VALUE_NAMES: + value = self._GetConfigValue(config_parser, section_name, value_name) + setattr(dependency_definition, value_name, value) + + yield dependency_definition class DependencyHelper: - """Dependency helper. - - Attributes: - dependencies (dict[str, DependencyDefinition]): dependencies. - """ - - _VERSION_NUMBERS_REGEX = re.compile(r'[0-9.]+') - _VERSION_SPLIT_REGEX = re.compile(r'\.|\-') - - def __init__( - self, dependencies_file='dependencies.ini', - test_dependencies_file='test_dependencies.ini'): - """Initializes a dependency helper. - - Args: - dependencies_file (Optional[str]): path to the dependencies configuration - file. - test_dependencies_file (Optional[str]): path to the test dependencies - configuration file. - """ - super().__init__() - self._test_dependencies = {} - self.dependencies = {} - - dependency_reader = DependencyDefinitionReader() - - with open(dependencies_file, 'r', encoding='utf-8') as file_object: - for dependency in dependency_reader.Read(file_object): - self.dependencies[dependency.name] = dependency - - if os.path.exists(test_dependencies_file): - with open(test_dependencies_file, 'r', encoding='utf-8') as file_object: - for dependency in dependency_reader.Read(file_object): - self._test_dependencies[dependency.name] = dependency - - def _CheckPythonModule(self, dependency): - """Checks the availability of a Python module. - - Args: - dependency (DependencyDefinition): dependency definition. - - Returns: - tuple: containing: - - bool: True if the Python module is available and conforms to - the minimum required version, False otherwise. - str: status message. - """ - module_object = self._ImportPythonModule(dependency.name) - if not module_object: - return False, f'missing: {dependency.name:s}' - - if not dependency.version_property: - return True, dependency.name - - return self._CheckPythonModuleVersion( - dependency.name, module_object, dependency.version_property, - dependency.minimum_version, dependency.maximum_version) - - def _CheckPythonModuleVersion( - self, module_name, module_object, version_property, minimum_version, - maximum_version): - """Checks the version of a Python module. - - Args: - module_object (module): Python module. - module_name (str): name of the Python module. - version_property (str): version attribute or function. - minimum_version (str): minimum version. - maximum_version (str): maximum version. - - Returns: - tuple: containing: - - bool: True if the Python module is available and conforms to - the minimum required version, False otherwise. - str: status message. - """ - module_version = None - if not version_property.endswith('()'): - module_version = getattr(module_object, version_property, None) - else: - version_method = getattr( - module_object, version_property[:-2], None) - if version_method: - module_version = version_method() - - if not module_version: - return False, ( - f'unable to determine version information for: {module_name:s}') - - # Make sure the module version is a string. - module_version = f'{module_version!s}' - - # Split the version string and convert every digit into an integer. - # A string compare of both version strings will yield an incorrect result. - - # Strip any semantic suffixes such as a1, b1, pre, post, rc, dev. - module_version = self._VERSION_NUMBERS_REGEX.findall(module_version)[0] - - if module_version[-1] == '.': - module_version = module_version[:-1] - - try: - module_version_map = list( - map(int, self._VERSION_SPLIT_REGEX.split(module_version))) - except ValueError: - return False, ( - f'unable to parse module version: {module_name:s} {module_version:s}') - - if minimum_version: - try: - minimum_version_map = list( - map(int, self._VERSION_SPLIT_REGEX.split(minimum_version))) - except ValueError: - return False, ( - f'unable to parse minimum version: {module_name:s} ' - f'{minimum_version:s}') - - if module_version_map < minimum_version_map: - return False, ( - f'{module_name:s} version: {module_version!s} is too old, ' - f'{minimum_version!s} or later required') - - if maximum_version: - try: - maximum_version_map = list( - map(int, self._VERSION_SPLIT_REGEX.split(maximum_version))) - except ValueError: - return False, ( - f'unable to parse maximum version: {module_name:s} ' - f'{maximum_version:s}') - - if module_version_map > maximum_version_map: - return False, ( - f'{module_name:s} version: {module_version!s} is too recent, ' - f'{maximum_version!s} or earlier required') - - return True, f'{module_name:s} version: {module_version!s}' - - def _ImportPythonModule(self, module_name): - """Imports a Python module. - - Args: - module_name (str): name of the module. - - Returns: - module: Python module or None if the module cannot be imported. - """ - try: - module_object = list(map(__import__, [module_name]))[0] - except ImportError: - return None - - # If the module name contains dots get the upper most module object. - if '.' in module_name: - for submodule_name in module_name.split('.')[1:]: - module_object = getattr(module_object, submodule_name, None) - - return module_object - - def _PrintCheckDependencyStatus( - self, dependency, result, status_message, verbose_output=True): - """Prints the check dependency status. - - Args: - dependency (DependencyDefinition): dependency definition. - result (bool): True if the Python module is available and conforms to - the minimum required version, False otherwise. - status_message (str): status message. - verbose_output (Optional[bool]): True if output should be verbose. - """ - if not result or dependency.is_optional: - if dependency.is_optional: - status_indicator = '[OPTIONAL]' - else: - status_indicator = '[FAILURE]' - - print(f'{status_indicator:s}\t{status_message:s}') - - elif verbose_output: - print(f'[OK]\t\t{status_message:s}') - - def CheckDependencies(self, verbose_output=True): - """Checks the availability of the dependencies. - - Args: - verbose_output (Optional[bool]): True if output should be verbose. - - Returns: - bool: True if the dependencies are available, False otherwise. - """ - print('Checking availability and versions of dependencies.') - check_result = True - - for _, dependency in sorted(self.dependencies.items()): - if dependency.skip_check: - continue - - result, status_message = self._CheckPythonModule(dependency) - - if not result and not dependency.is_optional: - check_result = False - - self._PrintCheckDependencyStatus( - dependency, result, status_message, verbose_output=verbose_output) - - if check_result and not verbose_output: - print('[OK]') - - print('') - return check_result - - def CheckTestDependencies(self, verbose_output=True): - """Checks the availability of the dependencies when running tests. - - Args: - verbose_output (Optional[bool]): True if output should be verbose. + """Dependency helper. - Returns: - bool: True if the dependencies are available, False otherwise. + Attributes: + dependencies (dict[str, DependencyDefinition]): dependencies. """ - if not self.CheckDependencies(verbose_output=verbose_output): - return False - print('Checking availability and versions of test dependencies.') - check_result = True + _VERSION_NUMBERS_REGEX = re.compile(r"[0-9.]+") + _VERSION_SPLIT_REGEX = re.compile(r"\.|\-") + + def __init__( + self, + dependencies_file="dependencies.ini", + test_dependencies_file="test_dependencies.ini", + ): + """Initializes a dependency helper. + + Args: + dependencies_file (Optional[str]): path to the dependencies configuration + file. + test_dependencies_file (Optional[str]): path to the test dependencies + configuration file. + """ + super().__init__() + self._test_dependencies = {} + self.dependencies = {} + + dependency_reader = DependencyDefinitionReader() + + with open(dependencies_file, "r", encoding="utf-8") as file_object: + for dependency in dependency_reader.Read(file_object): + self.dependencies[dependency.name] = dependency + + if os.path.exists(test_dependencies_file): + with open(test_dependencies_file, "r", encoding="utf-8") as file_object: + for dependency in dependency_reader.Read(file_object): + self._test_dependencies[dependency.name] = dependency + + def _CheckPythonModule(self, dependency): + """Checks the availability of a Python module. + + Args: + dependency (DependencyDefinition): dependency definition. + + Returns: + tuple: containing: + + bool: True if the Python module is available and conforms to + the minimum required version, False otherwise. + str: status message. + """ + module_object = self._ImportPythonModule(dependency.name) + if not module_object: + return False, f"missing: {dependency.name:s}" + + if not dependency.version_property: + return True, dependency.name + + return self._CheckPythonModuleVersion( + dependency.name, + module_object, + dependency.version_property, + dependency.minimum_version, + dependency.maximum_version, + ) + + def _CheckPythonModuleVersion( + self, + module_name, + module_object, + version_property, + minimum_version, + maximum_version, + ): + """Checks the version of a Python module. + + Args: + module_object (module): Python module. + module_name (str): name of the Python module. + version_property (str): version attribute or function. + minimum_version (str): minimum version. + maximum_version (str): maximum version. + + Returns: + tuple: containing: + + bool: True if the Python module is available and conforms to + the minimum required version, False otherwise. + str: status message. + """ + module_version = None + if not version_property.endswith("()"): + module_version = getattr(module_object, version_property, None) + else: + version_method = getattr(module_object, version_property[:-2], None) + if version_method: + module_version = version_method() + + if not module_version: + return False, ( + f"unable to determine version information for: {module_name:s}" + ) + + # Make sure the module version is a string. + module_version = f"{module_version!s}" + + # Split the version string and convert every digit into an integer. + # A string compare of both version strings will yield an incorrect result. + + # Strip any semantic suffixes such as a1, b1, pre, post, rc, dev. + module_version = self._VERSION_NUMBERS_REGEX.findall(module_version)[0] + + if module_version[-1] == ".": + module_version = module_version[:-1] + + try: + module_version_map = list( + map(int, self._VERSION_SPLIT_REGEX.split(module_version)) + ) + except ValueError: + return False, ( + f"unable to parse module version: {module_name:s} {module_version:s}" + ) + + if minimum_version: + try: + minimum_version_map = list( + map(int, self._VERSION_SPLIT_REGEX.split(minimum_version)) + ) + except ValueError: + return False, ( + f"unable to parse minimum version: {module_name:s} " + f"{minimum_version:s}" + ) + + if module_version_map < minimum_version_map: + return False, ( + f"{module_name:s} version: {module_version!s} is too old, " + f"{minimum_version!s} or later required" + ) + + if maximum_version: + try: + maximum_version_map = list( + map(int, self._VERSION_SPLIT_REGEX.split(maximum_version)) + ) + except ValueError: + return False, ( + f"unable to parse maximum version: {module_name:s} " + f"{maximum_version:s}" + ) + + if module_version_map > maximum_version_map: + return False, ( + f"{module_name:s} version: {module_version!s} is too recent, " + f"{maximum_version!s} or earlier required" + ) + + return True, f"{module_name:s} version: {module_version!s}" + + def _ImportPythonModule(self, module_name): + """Imports a Python module. + + Args: + module_name (str): name of the module. + + Returns: + module: Python module or None if the module cannot be imported. + """ + try: + module_object = list(map(__import__, [module_name]))[0] + except ImportError: + return None + + # If the module name contains dots get the upper most module object. + if "." in module_name: + for submodule_name in module_name.split(".")[1:]: + module_object = getattr(module_object, submodule_name, None) + + return module_object + + def _PrintCheckDependencyStatus( + self, dependency, result, status_message, verbose_output=True + ): + """Prints the check dependency status. + + Args: + dependency (DependencyDefinition): dependency definition. + result (bool): True if the Python module is available and conforms to + the minimum required version, False otherwise. + status_message (str): status message. + verbose_output (Optional[bool]): True if output should be verbose. + """ + if not result or dependency.is_optional: + if dependency.is_optional: + status_indicator = "[OPTIONAL]" + else: + status_indicator = "[FAILURE]" + + print(f"{status_indicator:s}\t{status_message:s}") + + elif verbose_output: + print(f"[OK]\t\t{status_message:s}") + + def CheckDependencies(self, verbose_output=True): + """Checks the availability of the dependencies. + + Args: + verbose_output (Optional[bool]): True if output should be verbose. + + Returns: + bool: True if the dependencies are available, False otherwise. + """ + print("Checking availability and versions of dependencies.") + check_result = True + + for _, dependency in sorted(self.dependencies.items()): + if dependency.skip_check: + continue + + result, status_message = self._CheckPythonModule(dependency) + + if not result and not dependency.is_optional: + check_result = False + + self._PrintCheckDependencyStatus( + dependency, result, status_message, verbose_output=verbose_output + ) + + if check_result and not verbose_output: + print("[OK]") + + print("") + return check_result + + def CheckTestDependencies(self, verbose_output=True): + """Checks the availability of the dependencies when running tests. + + Args: + verbose_output (Optional[bool]): True if output should be verbose. + + Returns: + bool: True if the dependencies are available, False otherwise. + """ + if not self.CheckDependencies(verbose_output=verbose_output): + return False + + print("Checking availability and versions of test dependencies.") + check_result = True - for dependency in sorted( - self._test_dependencies.values(), - key=lambda dependency: dependency.name): - if dependency.skip_check: - continue + for dependency in sorted( + self._test_dependencies.values(), key=lambda dependency: dependency.name + ): + if dependency.skip_check: + continue - result, status_message = self._CheckPythonModule(dependency) + result, status_message = self._CheckPythonModule(dependency) - if not result and not dependency.is_optional: - check_result = False + if not result and not dependency.is_optional: + check_result = False - self._PrintCheckDependencyStatus( - dependency, result, status_message, verbose_output=verbose_output) + self._PrintCheckDependencyStatus( + dependency, result, status_message, verbose_output=verbose_output + ) - if check_result and not verbose_output: - print('[OK]') + if check_result and not verbose_output: + print("[OK]") - print('') - return check_result + print("") + return check_result diff --git a/utils/update_release.sh b/utils/update_release.sh index 0684061..8819234 100755 --- a/utils/update_release.sh +++ b/utils/update_release.sh @@ -9,7 +9,7 @@ EXIT_SUCCESS=0; VERSION=$(date -u +"%Y%m%d") # Update the Python module version. -sed "s/__version__ = '[0-9]*'/__version__ = '${VERSION}'/" -i artifactsrc/__init__.py +sed "s/__version__ = \"[0-9]*\"/__version__ = \"${VERSION}\"/" -i artifactsrc/__init__.py # Update the version in the pyproject configuration. sed "s/version = \"[0-9]*\"/version = \"${VERSION}\"/" -i pyproject.toml From 1b00d48b51150866e7b64a8342670edde455dc3f Mon Sep 17 00:00:00 2001 From: Joachim Metz Date: Sat, 16 May 2026 06:03:17 +0200 Subject: [PATCH 2/3] Changes to use black Python formatter --- .github/workflows/lint.yml | 68 ++ artifactsrc/__init__.py | 2 +- artifactsrc/resource_file.py | 423 +++++----- artifactsrc/volume_scanner.py | 845 +++++++++---------- run_tests.py | 33 +- tests/resource_file.py | 1426 +++++++++++++++++++++++++-------- tests/test_lib.py | 77 +- tools/check_artifacts.py | 355 ++++---- tools/generate_docs.py | 332 ++++---- 9 files changed, 2224 insertions(+), 1337 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..b7508c7 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,68 @@ +# Check source. +name: lint +on: + pull_request: + branches: + - main + push: + branches: + - main +permissions: read-all +jobs: + black: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check format of Python code + uses: psf/black@stable + with: + options: "--check" + src: "." + pylint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.14'] + container: + image: ubuntu:26.04 + steps: + - uses: actions/checkout@v6 + - name: Set up container + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update -q + apt-get install -y libterm-readline-gnu-perl locales software-properties-common + locale-gen en_US.UTF-8 + ln -f -s /usr/share/zoneinfo/UTC /etc/localtime + - name: Install dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + add-apt-repository -y universe + add-apt-repository -y ppa:deadsnakes/ppa + add-apt-repository -y ppa:gift/dev + apt-get update -q + apt-get install -y build-essential git libffi-dev pkg-config python${{ matrix.python-version }} python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv libbde-python3 libcaes-python3 libcreg-python3 libewf-python3 libexe-python3 libfcrypto-python3 libfsapfs-python3 libfsext-python3 libfsfat-python3 libfshfs-python3 libfsntfs-python3 libfsxfs-python3 libfvde-python3 libfwnt-python3 libluksde-python3 libmodi-python3 libphdi-python3 libqcow-python3 libregf-python3 libsigscan-python3 libsmdev-python3 libsmraw-python3 libvhdi-python3 libvmdk-python3 libvsapm-python3 libvsgpt-python3 libvshadow-python3 libvslvm-python3 libwrc-python3 python3-artifacts python3-cffi-backend python3-dfdatetime python3-dfimagetools python3-dfvfs python3-dfwinreg python3-dtfabric python3-idna python3-pip python3-pytsk3 python3-setuptools python3-xattr python3-yaml tox + - name: Run linter + env: + LANG: en_US.UTF-8 + run: | + tox -e pylint + yamllint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.12'] + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install tox + run: | + python -m pip install tox + - name: Run linter + run: | + tox -e yamllint diff --git a/artifactsrc/__init__.py b/artifactsrc/__init__.py index 9ca8285..75032ee 100644 --- a/artifactsrc/__init__.py +++ b/artifactsrc/__init__.py @@ -1,3 +1,3 @@ """ForensicArtifacts.com Artifact Repository resources.""" -__version__ = '20260411' +__version__ = "20260411" diff --git a/artifactsrc/resource_file.py b/artifactsrc/resource_file.py index a9503d9..9136343 100644 --- a/artifactsrc/resource_file.py +++ b/artifactsrc/resource_file.py @@ -7,231 +7,238 @@ class MessageResourceFile: - """Windows Message Resource file. + """Windows Message Resource file. - Attributes: - windows_path (str): Windows path of the message resource file. - """ - - _MESSAGE_TABLE_RESOURCE_IDENTIFIER = 0x0b - _VERSION_INFORMATION_RESOURCE_IDENTIFIER = 0x10 - - def __init__( - self, windows_path, ascii_codepage='cp1252', - preferred_language_identifier=0x0409): - """Initializes the Windows Message Resource file. - - Args: - windows_path (str): normalized version of the Windows path. - ascii_codepage (Optional[str]): ASCII string codepage. - preferred_language_identifier (Optional[int]): preferred language - identifier (LCID). - """ - super().__init__() - self._ascii_codepage = ascii_codepage - self._exe_file = pyexe.file() - self._exe_file.set_ascii_codepage(self._ascii_codepage) - self._exe_section = None - self._file_object = None - self._file_version = None - self._is_open = False - self._preferred_language_identifier = preferred_language_identifier - self._product_version = None - # TODO: wrc stream set codepage? - self._wrc_stream = pywrc.stream() - - self.windows_path = windows_path - - def _GetVersionInformation(self): - """Determines the file and product version.""" - version_information_resource = self._GetVersionInformationResource() - if not version_information_resource: - return - - file_version = version_information_resource.file_version - major_version = (file_version >> 48) & 0xffff - minor_version = (file_version >> 32) & 0xffff - build_number = (file_version >> 16) & 0xffff - revision_number = file_version & 0xffff - - self._file_version = ( - f'{major_version:d}.{minor_version:d}.{build_number:d}.' - f'{revision_number:d}') - - product_version = version_information_resource.product_version - major_version = (product_version >> 48) & 0xffff - minor_version = (product_version >> 32) & 0xffff - build_number = (product_version >> 16) & 0xffff - revision_number = product_version & 0xffff - - self._product_version = ( - f'{major_version:d}.{minor_version:d}.{build_number:d}.' - f'{revision_number:d}') - - if file_version != product_version: - logging.warning(( - f'Mismatch between file version: {self._file_version:s} and product ' - f'version: {self._product_version:s} in message file: ' - f'{self.windows_path:s}.')) - - def _GetVersionInformationResource(self): - """Retrieves the version information resource. - - Returns: - pywrc.version_information_resource: version information resource or None - if not available. - """ - preferred_wrc_resource_sub_item = None - - wrc_resource = self._wrc_stream.get_resource_by_identifier( - self._VERSION_INFORMATION_RESOURCE_IDENTIFIER) - if wrc_resource: - first_wrc_resource_sub_item = None - for wrc_resource_item in wrc_resource.items: - for wrc_resource_sub_item in wrc_resource_item.sub_items: - if not first_wrc_resource_sub_item: - first_wrc_resource_sub_item = wrc_resource_sub_item - - language_identifier = wrc_resource_sub_item.identifier - if language_identifier == self._preferred_language_identifier: - if not preferred_wrc_resource_sub_item: - preferred_wrc_resource_sub_item = wrc_resource_sub_item - - if not preferred_wrc_resource_sub_item: - preferred_wrc_resource_sub_item = first_wrc_resource_sub_item - - if not preferred_wrc_resource_sub_item: - return None - - resource_data = preferred_wrc_resource_sub_item.read() - - version_information_resource = pywrc.version_information_resource() - version_information_resource.copy_from_byte_stream(resource_data) - - return version_information_resource - - @property - def file_version(self): - """str: the file version.""" - if self._file_version is None: - self._GetVersionInformation() - return self._file_version - - @property - def product_version(self): - """str: the product version.""" - if self._product_version is None: - self._GetVersionInformation() - return self._product_version - - def Close(self): - """Closes the Windows Message Resource file. - - Raises: - IOError: if not open. - OSError: if not open. + Attributes: + windows_path (str): Windows path of the message resource file. """ - if not self._is_open: - raise IOError('Not opened.') - if self._exe_section: - self._wrc_stream.close() + _MESSAGE_TABLE_RESOURCE_IDENTIFIER = 0x0B + _VERSION_INFORMATION_RESOURCE_IDENTIFIER = 0x10 + + def __init__( + self, + windows_path, + ascii_codepage="cp1252", + preferred_language_identifier=0x0409, + ): + """Initializes the Windows Message Resource file. + + Args: + windows_path (str): normalized version of the Windows path. + ascii_codepage (Optional[str]): ASCII string codepage. + preferred_language_identifier (Optional[int]): preferred language + identifier (LCID). + """ + super().__init__() + self._ascii_codepage = ascii_codepage + self._exe_file = pyexe.file() + self._exe_file.set_ascii_codepage(self._ascii_codepage) + self._exe_section = None + self._file_object = None + self._file_version = None + self._is_open = False + self._preferred_language_identifier = preferred_language_identifier + self._product_version = None + # TODO: wrc stream set codepage? + self._wrc_stream = pywrc.stream() + + self.windows_path = windows_path + + def _GetVersionInformation(self): + """Determines the file and product version.""" + version_information_resource = self._GetVersionInformationResource() + if not version_information_resource: + return + + file_version = version_information_resource.file_version + major_version = (file_version >> 48) & 0xFFFF + minor_version = (file_version >> 32) & 0xFFFF + build_number = (file_version >> 16) & 0xFFFF + revision_number = file_version & 0xFFFF + + self._file_version = ( + f"{major_version:d}.{minor_version:d}.{build_number:d}." + f"{revision_number:d}" + ) + product_version = version_information_resource.product_version + major_version = (product_version >> 48) & 0xFFFF + minor_version = (product_version >> 32) & 0xFFFF + build_number = (product_version >> 16) & 0xFFFF + revision_number = product_version & 0xFFFF + + self._product_version = ( + f"{major_version:d}.{minor_version:d}.{build_number:d}." + f"{revision_number:d}" + ) + if file_version != product_version: + logging.warning( + f"Mismatch between file version: {self._file_version:s} and product " + f"version: {self._product_version:s} in message file: " + f"{self.windows_path:s}." + ) + + def _GetVersionInformationResource(self): + """Retrieves the version information resource. + + Returns: + pywrc.version_information_resource: version information resource or None + if not available. + """ + preferred_wrc_resource_sub_item = None - self._exe_file.close() - self._file_object = None - self._is_open = False - - def GetMessageTableResource(self): - """Retrieves the message table resource. - - Returns: - pywrc.resource: resource containing the message table resource or None - if not available. - """ - return self._wrc_stream.get_resource_by_identifier( - self._MESSAGE_TABLE_RESOURCE_IDENTIFIER) - - def GetMUILanguage(self): - """Retrieves the MUI language. - - Returns: - str: MUI language or None if not available. - """ - mui_resource = self.GetMUIResource() - if not mui_resource: - return None - - return mui_resource.language + wrc_resource = self._wrc_stream.get_resource_by_identifier( + self._VERSION_INFORMATION_RESOURCE_IDENTIFIER + ) + if wrc_resource: + first_wrc_resource_sub_item = None + for wrc_resource_item in wrc_resource.items: + for wrc_resource_sub_item in wrc_resource_item.sub_items: + if not first_wrc_resource_sub_item: + first_wrc_resource_sub_item = wrc_resource_sub_item + + language_identifier = wrc_resource_sub_item.identifier + if language_identifier == self._preferred_language_identifier: + if not preferred_wrc_resource_sub_item: + preferred_wrc_resource_sub_item = wrc_resource_sub_item - def GetMUIResource(self): - """Retrieves the MUI resource. + if not preferred_wrc_resource_sub_item: + preferred_wrc_resource_sub_item = first_wrc_resource_sub_item + + if not preferred_wrc_resource_sub_item: + return None + + resource_data = preferred_wrc_resource_sub_item.read() + + version_information_resource = pywrc.version_information_resource() + version_information_resource.copy_from_byte_stream(resource_data) + + return version_information_resource + + @property + def file_version(self): + """str: the file version.""" + if self._file_version is None: + self._GetVersionInformation() + return self._file_version + + @property + def product_version(self): + """str: the product version.""" + if self._product_version is None: + self._GetVersionInformation() + return self._product_version + + def Close(self): + """Closes the Windows Message Resource file. + + Raises: + IOError: if not open. + OSError: if not open. + """ + if not self._is_open: + raise IOError("Not opened.") + + if self._exe_section: + self._wrc_stream.close() + + self._exe_file.close() + self._file_object = None + self._is_open = False + + def GetMessageTableResource(self): + """Retrieves the message table resource. + + Returns: + pywrc.resource: resource containing the message table resource or None + if not available. + """ + return self._wrc_stream.get_resource_by_identifier( + self._MESSAGE_TABLE_RESOURCE_IDENTIFIER + ) + + def GetMUILanguage(self): + """Retrieves the MUI language. + + Returns: + str: MUI language or None if not available. + """ + mui_resource = self.GetMUIResource() + if not mui_resource: + return None + + return mui_resource.language + + def GetMUIResource(self): + """Retrieves the MUI resource. + + Returns: + pywrc.mui_resource: MUI resource or None if not available. + """ + preferred_wrc_resource_sub_item = None + + wrc_resource = self._wrc_stream.get_resource_by_name("MUI") + if wrc_resource: + first_wrc_resource_sub_item = None + for wrc_resource_item in wrc_resource.items: + for wrc_resource_sub_item in wrc_resource_item.sub_items: + if not first_wrc_resource_sub_item: + first_wrc_resource_sub_item = wrc_resource_sub_item + + language_identifier = wrc_resource_sub_item.identifier + if language_identifier == self._preferred_language_identifier: + if not preferred_wrc_resource_sub_item: + preferred_wrc_resource_sub_item = wrc_resource_sub_item - Returns: - pywrc.mui_resource: MUI resource or None if not available. - """ - preferred_wrc_resource_sub_item = None - - wrc_resource = self._wrc_stream.get_resource_by_name('MUI') - if wrc_resource: - first_wrc_resource_sub_item = None - for wrc_resource_item in wrc_resource.items: - for wrc_resource_sub_item in wrc_resource_item.sub_items: - if not first_wrc_resource_sub_item: - first_wrc_resource_sub_item = wrc_resource_sub_item - - language_identifier = wrc_resource_sub_item.identifier - if language_identifier == self._preferred_language_identifier: if not preferred_wrc_resource_sub_item: - preferred_wrc_resource_sub_item = wrc_resource_sub_item + preferred_wrc_resource_sub_item = first_wrc_resource_sub_item - if not preferred_wrc_resource_sub_item: - preferred_wrc_resource_sub_item = first_wrc_resource_sub_item + if not preferred_wrc_resource_sub_item: + return None - if not preferred_wrc_resource_sub_item: - return None + resource_data = preferred_wrc_resource_sub_item.read() - resource_data = preferred_wrc_resource_sub_item.read() + mui_resource = pywrc.mui_resource() + mui_resource.copy_from_byte_stream(resource_data) - mui_resource = pywrc.mui_resource() - mui_resource.copy_from_byte_stream(resource_data) + return mui_resource - return mui_resource + def HasMessageTableResource(self): + """Determines if the resource file as a message table resource. - def HasMessageTableResource(self): - """Determines if the resource file as a message table resource. + Returns: + bool: True if the resource file as a message table resource. + """ + wrc_resource = None + if self._wrc_stream: + try: + wrc_resource = self._wrc_stream.get_resource_by_identifier( + self._MESSAGE_TABLE_RESOURCE_IDENTIFIER + ) + except IOError: + pass - Returns: - bool: True if the resource file as a message table resource. - """ - wrc_resource = None - if self._wrc_stream: - try: - wrc_resource = self._wrc_stream.get_resource_by_identifier( - self._MESSAGE_TABLE_RESOURCE_IDENTIFIER) - except IOError: - pass - - return bool(wrc_resource) + return bool(wrc_resource) - def OpenFileObject(self, file_object): - """Opens the Windows Message Resource file using a file-like object. + def OpenFileObject(self, file_object): + """Opens the Windows Message Resource file using a file-like object. - Args: - file_object (file): file-like object. + Args: + file_object (file): file-like object. - Raises: - IOError: if already open. - OSError: if already open. - """ - if self._is_open: - raise IOError('Already open.') + Raises: + IOError: if already open. + OSError: if already open. + """ + if self._is_open: + raise IOError("Already open.") - self._exe_file.open_file_object(file_object) - self._exe_section = self._exe_file.get_section_by_name('.rsrc') + self._exe_file.open_file_object(file_object) + self._exe_section = self._exe_file.get_section_by_name(".rsrc") - if self._exe_section: - self._wrc_stream.set_virtual_address(self._exe_section.virtual_address) - self._wrc_stream.open_file_object(self._exe_section) + if self._exe_section: + self._wrc_stream.set_virtual_address(self._exe_section.virtual_address) + self._wrc_stream.open_file_object(self._exe_section) - self._file_object = file_object - self._is_open = True + self._file_object = file_object + self._is_open = True diff --git a/artifactsrc/volume_scanner.py b/artifactsrc/volume_scanner.py index b73e059..b79725a 100644 --- a/artifactsrc/volume_scanner.py +++ b/artifactsrc/volume_scanner.py @@ -23,413 +23,448 @@ class CheckResults: - """Check results. + """Check results. - Attributes: - data_formats (set[str]): data formats that were found. - number_of_file_entries (int): number of file entries that were found. - """ - - def __init__(self): - """Initializes check results.""" - super().__init__() - self.data_formats = set() - self.number_of_file_entries = 0 - - -class ArtifactDefinitionsVolumeScanner(dfvfs_volume_scanner.VolumeScanner): - """Artifact definitions volume scanner.""" - - # Preserve the absolute path value of __file__ in case it is changed - # at run-time. - _CHECKS_DEFINITIONS_FILE = ( - os.path.join(os.path.dirname(__file__), 'data', 'checks.yaml')) - - _DEFINITION_FILES_PATH = os.path.dirname(__file__) - - _SYSTEM_DIRECTORY_FIND_SPECS = [ - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='/sbin', - location_separator='/'), - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='/System/Library', - location_separator='/'), - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='\\Windows\\System32', - location_separator='\\'), - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='\\WINNT\\System32', - location_separator='\\'), - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='\\WINNT35\\System32', - location_separator='\\'), - dfvfs_file_system_searcher.FindSpec( - case_sensitive=False, location='\\WTSRV\\System32', - location_separator='\\')] - - # We need to check for both forward and backward slashes since the path - # specification will be dfVFS back-end dependent. - _WINDOWS_SYSTEM_DIRECTORIES = set([ - '/windows/system32', '\\windows\\system32', - '/winnt/system32', '\\winnt\\system32', - '/winnt35/system32', '\\winnt35\\system32', - '/wtsrv/system32', '\\wtsrv\\system32']) - - _WINDOWS_DIRECTORIES = frozenset([ - 'C:\\Windows', - 'C:\\WINNT', - 'C:\\WTSRV', - 'C:\\WINNT35', - ]) - - _FORMAT_VERSION_STRING = { - 'bplist': 'bplist 0x{format_version:s}', - 'esedb': 'esedb 0x{format_version:x}', - 'evt': 'evt {major_format_version:d}.{minor_format_version:d}', - 'evtx': 'evtx {major_format_version:d}.{minor_format_version:d}', - 'job': 'job {format_version:d}', - 'regf': 'regf {major_format_version:d}.{minor_format_version:d}', - 'scca': 'scca {format_version:d}', - } - - def __init__(self, artifacts_registry, mediator=None): - """Initializes an artifact definitions volume scanner. - - Args: - artifacts_registry (artifacts.ArtifactDefinitionsRegistry): artifact - definitions registry. - mediator (Optional[dfvfs.VolumeScannerMediator]): a volume scanner - mediator. - """ - super().__init__(mediator=mediator) - self._ascii_codepage = 'cp1252' - self._artifacts_registry = artifacts_registry - self._checks_definitions = None - self._data_location = os.path.join('data') - self._data_type_fabric = self._ReadDataTypeFabricDefinitionFile( - 'formats.yaml') - self._data_type_maps = {} - self._environment_variables = [] - self._file_system = None - self._file_system_searcher = None - self._filter_generator = None - self._mount_point = None - self._path_resolver = None - self._preferred_language_identifier = 'en-US' - self._windows_directory = None - self._windows_registry = None - - def _DetermineDataFormat(self, names, file_object): - """Determines the data format. - - Args: - names (list[str]): names of data formats to check. - file_object (file): file-like object. - - Returns: - str: data format identifier or None if the data format could not - be determined. - """ - for name in names: - format_data_type_map = self._GetDataTypeMap(name) - - layout = getattr(format_data_type_map, 'layout', None) - if not layout: - continue - - layout_element_definition = layout[0] - if layout_element_definition.offset is None: - continue - - data_type_map = self._GetDataTypeMap( - layout_element_definition.data_type) - - structure_values = self._ReadStructureFromFileObject( - file_object, layout_element_definition.offset, data_type_map) - if structure_values: - format_string = self._FORMAT_VERSION_STRING.get(name, name) - return format_string.format(**structure_values.__dict__) - - return None - - def _GetDataTypeMap(self, name): - """Retrieves a data type map defined by the definition file. - - The data type maps are cached for reuse. - - Args: - name (str): name of the data type as defined by the definition file. - - Returns: - dtfabric.DataTypeMap: data type map which contains a data type definition, - such as a structure, that can be mapped onto binary data. - """ - data_type_map = self._data_type_maps.get(name, None) - if not data_type_map: - data_type_map = self._data_type_fabric.CreateDataTypeMap(name) - self._data_type_maps[name] = data_type_map - - return data_type_map - - def _OpenMessageResourceFile(self, windows_path): - """Opens the message resource file specified by the Windows path. - - Args: - windows_path (str): Windows path containing the message resource - filename. - - Returns: - MessageResourceFile: message resource file or None. + Attributes: + data_formats (set[str]): data formats that were found. + number_of_file_entries (int): number of file entries that were found. """ - path_spec = self._path_resolver.ResolvePath(windows_path) - if path_spec is None: - return None - - return self._OpenMessageResourceFileByPathSpec(path_spec) - - def _OpenMessageResourceFileByPathSpec(self, path_spec): - """Opens the message resource file specified by the path specification. - - Args: - path_spec (dfvfs.PathSpec): path specification. - - Returns: - MessageResourceFile: message resource file or None. - """ - windows_path = self._path_resolver.GetWindowsPath(path_spec) - if windows_path is None: - logging.warning('Unable to retrieve Windows path.') - - try: - file_object = dfvfs_resolver.Resolver.OpenFileObject(path_spec) - except IOError as exception: - logging.warning( - f'Unable to open: {path_spec.comparable:s} with error: {exception!s}') - file_object = None - if file_object is None: - return None + def __init__(self): + """Initializes check results.""" + super().__init__() + self.data_formats = set() + self.number_of_file_entries = 0 - message_file = resource_file.MessageResourceFile( - windows_path, ascii_codepage=self._ascii_codepage, - preferred_language_identifier=self._preferred_language_identifier) - message_file.OpenFileObject(file_object) - return message_file - - def _ReadChecksDefinitions(self): - """Reads the checks definitions from checks.yaml. - - Returns: - list[dict[str, object]]: checks definitions. - """ - check_definitions = {} - - with open(self._CHECKS_DEFINITIONS_FILE, 'r', - encoding='utf-8') as file_object: - for check_definition in yaml.safe_load_all(file_object): - name = check_definition.get('name', None) - if name: - check_definitions[name.lower()] = check_definition - - return check_definitions - - def _ReadDataTypeFabricDefinitionFile(self, filename): - """Reads a dtFabric definition file. - - Args: - filename (str): name of the dtFabric definition file. - - Returns: - dtfabric.DataTypeFabric: data type fabric which contains the data format - data type maps of the data type definition, such as a structure, that - can be mapped onto binary data or None if no filename is provided. - """ - if not filename: - return None - - path = os.path.join(self._DEFINITION_FILES_PATH, filename) - with open(path, 'rb') as file_object: - definition = file_object.read() - - return dtfabric_fabric.DataTypeFabric(yaml_definition=definition) - - def _ReadStructureFromFileObject( - self, file_object, file_offset, data_type_map): - """Reads a structure from a file-like object. - - This method currently only supports fixed-size structures. - - Args: - file_object (file): a file-like object to parse. - file_offset (int): offset of the structure data relative to the start - of the file-like object. - data_type_map (dtfabric.DataTypeMap): data type map of the structure. - - Returns: - object: structure values object or None if the structure cannot be read. - """ - structure_values = None - - data_size = data_type_map.GetSizeHint() - if data_size: - file_object.seek(file_offset, os.SEEK_SET) - try: - data = file_object.read(data_size) - structure_values = data_type_map.MapByteStream(data) - except (dtfabric_errors.ByteStreamTooSmallError, - dtfabric_errors.MappingError): - pass - - return structure_values - - def CheckArtifactDefinition(self, artifact_definition): - """Checks if an artifact definition on a storage media image. - - Args: - artifact_definition (artifacts.ArtifactDefinition): artifact definition. - - Returns: - CheckResults: check results. - """ - check_result = CheckResults() - - if self._checks_definitions is None: - self._checks_definitions = self._ReadChecksDefinitions() - - find_specs = list(self._filter_generator.GetFindSpecs( - [artifact_definition.name])) - if find_specs: - path_specs = list(self._file_system_searcher.Find(find_specs=find_specs)) - check_result.number_of_file_entries = len(path_specs) - - check_definition = self._checks_definitions.get( - artifact_definition.name.lower(), None) - if check_definition: - for path_spec in path_specs: - file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) - if file_entry.size > 0: - file_object = file_entry.GetFileObject() - if file_object: - formats = check_definition.get('formats', []) - data_format = self._DetermineDataFormat(formats, file_object) - check_result.data_formats.add(data_format or 'unknown') - - return check_result - - def GetWindowsVersion(self): - """Determines the Windows version from kernel executable file. - - Returns: - str: Windows version or None otherwise. - """ - # Window NT variants. - kernel_executable_path = '\\'.join([ - self._windows_directory, 'System32', 'ntoskrnl.exe']) - message_file = self._OpenMessageResourceFile(kernel_executable_path) - - if not message_file: - # Window 9x variants. - kernel_executable_path = '\\'.join([ - self._windows_directory, 'System32', '\\kernel32.dll']) - message_file = self._OpenMessageResourceFile(kernel_executable_path) - - if not message_file: - return None - - return message_file.file_version - - def ScanForOperatingSystemVolumes(self, source_path, options=None): - """Scans for volumes containing an operating system. - - Args: - source_path (str): source path. - options (Optional[dfvfs.VolumeScannerOptions]): volume scanner options. - If None the default volume scanner options are used, which are defined - in the VolumeScannerOptions class. - - Returns: - bool: True if a volume with an operating system was found. - - Raises: - ScannerError: if the source path does not exists, or if the source path - is not a file or directory, or if the format of or within the source - file is not supported. - """ - if not options: - options = dfvfs_volume_scanner.VolumeScannerOptions() - - scan_context = self._ScanSource(source_path) - - self._source_path = source_path - self._source_type = scan_context.source_type - - base_path_specs = self._GetBasePathSpecs(scan_context, options) - - if (not base_path_specs or - scan_context.source_type == dfvfs_definitions.SOURCE_TYPE_FILE): - return False - - for path_spec in base_path_specs: - file_system = dfvfs_resolver.Resolver.OpenFileSystem(path_spec) - - if path_spec.type_indicator == dfvfs_definitions.TYPE_INDICATOR_OS: - mount_point = path_spec - else: - mount_point = path_spec.parent - - file_system_searcher = dfvfs_file_system_searcher.FileSystemSearcher( - file_system, mount_point) - - system_directories = [] - for system_directory_path_spec in file_system_searcher.Find( - find_specs=self._SYSTEM_DIRECTORY_FIND_SPECS): - relative_path = file_system_searcher.GetRelativePath( - system_directory_path_spec) - if relative_path: - system_directories.append(relative_path.lower()) - - if system_directories or len(base_path_specs) == 1: - self._file_system_searcher = file_system_searcher - self._file_system = file_system - self._mount_point = mount_point - - if self._WINDOWS_SYSTEM_DIRECTORIES.intersection(set(system_directories)): - path_resolver = dfvfs_windows_path_resolver.WindowsPathResolver( - file_system, mount_point) - - # TODO: determine Windows directory based on system directories. - windows_directory = None - for windows_path in self._WINDOWS_DIRECTORIES: - windows_path_spec = path_resolver.ResolvePath(windows_path) - if windows_path_spec is not None: - windows_directory = windows_path - break - - if windows_directory: - path_resolver.SetEnvironmentVariable('SystemRoot', windows_directory) - path_resolver.SetEnvironmentVariable('WinDir', windows_directory) - - registry_file_reader = ( - windows_registry.StorageMediaImageWindowsRegistryFileReader( - file_system, path_resolver)) - winregistry = dfwinreg_registry.WinRegistry( - registry_file_reader=registry_file_reader) - - collector = ( - environment_variables.WindowsEnvironmentVariablesCollector()) - - self._environment_variables = list(collector.Collect(winregistry)) - self._path_resolver = path_resolver - self._windows_directory = windows_directory - self._windows_registry = winregistry - - if system_directories: - # TODO: on Mac OS prevent detecting the Recovery volume. - break - - self._filter_generator = ( - artifact_filters.ArtifactDefinitionFiltersGenerator( - self._artifacts_registry, self._environment_variables, [])) - - return True +class ArtifactDefinitionsVolumeScanner(dfvfs_volume_scanner.VolumeScanner): + """Artifact definitions volume scanner.""" + + # Preserve the absolute path value of __file__ in case it is changed + # at run-time. + _CHECKS_DEFINITIONS_FILE = os.path.join( + os.path.dirname(__file__), "data", "checks.yaml" + ) + + _DEFINITION_FILES_PATH = os.path.dirname(__file__) + + _SYSTEM_DIRECTORY_FIND_SPECS = [ + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, location="/sbin", location_separator="/" + ), + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, location="/System/Library", location_separator="/" + ), + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, + location="\\Windows\\System32", + location_separator="\\", + ), + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, location="\\WINNT\\System32", location_separator="\\" + ), + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, + location="\\WINNT35\\System32", + location_separator="\\", + ), + dfvfs_file_system_searcher.FindSpec( + case_sensitive=False, location="\\WTSRV\\System32", location_separator="\\" + ), + ] + + # We need to check for both forward and backward slashes since the path + # specification will be dfVFS back-end dependent. + _WINDOWS_SYSTEM_DIRECTORIES = set( + [ + "/windows/system32", + "\\windows\\system32", + "/winnt/system32", + "\\winnt\\system32", + "/winnt35/system32", + "\\winnt35\\system32", + "/wtsrv/system32", + "\\wtsrv\\system32", + ] + ) + + _WINDOWS_DIRECTORIES = frozenset( + [ + "C:\\Windows", + "C:\\WINNT", + "C:\\WTSRV", + "C:\\WINNT35", + ] + ) + + _FORMAT_VERSION_STRING = { + "bplist": "bplist 0x{format_version:s}", + "esedb": "esedb 0x{format_version:x}", + "evt": "evt {major_format_version:d}.{minor_format_version:d}", + "evtx": "evtx {major_format_version:d}.{minor_format_version:d}", + "job": "job {format_version:d}", + "regf": "regf {major_format_version:d}.{minor_format_version:d}", + "scca": "scca {format_version:d}", + } + + def __init__(self, artifacts_registry, mediator=None): + """Initializes an artifact definitions volume scanner. + + Args: + artifacts_registry (artifacts.ArtifactDefinitionsRegistry): artifact + definitions registry. + mediator (Optional[dfvfs.VolumeScannerMediator]): a volume scanner + mediator. + """ + super().__init__(mediator=mediator) + self._ascii_codepage = "cp1252" + self._artifacts_registry = artifacts_registry + self._checks_definitions = None + self._data_location = os.path.join("data") + self._data_type_fabric = self._ReadDataTypeFabricDefinitionFile("formats.yaml") + self._data_type_maps = {} + self._environment_variables = [] + self._file_system = None + self._file_system_searcher = None + self._filter_generator = None + self._mount_point = None + self._path_resolver = None + self._preferred_language_identifier = "en-US" + self._windows_directory = None + self._windows_registry = None + + def _DetermineDataFormat(self, names, file_object): + """Determines the data format. + + Args: + names (list[str]): names of data formats to check. + file_object (file): file-like object. + + Returns: + str: data format identifier or None if the data format could not + be determined. + """ + for name in names: + format_data_type_map = self._GetDataTypeMap(name) + + layout = getattr(format_data_type_map, "layout", None) + if not layout: + continue + + layout_element_definition = layout[0] + if layout_element_definition.offset is None: + continue + + data_type_map = self._GetDataTypeMap(layout_element_definition.data_type) + + structure_values = self._ReadStructureFromFileObject( + file_object, layout_element_definition.offset, data_type_map + ) + if structure_values: + format_string = self._FORMAT_VERSION_STRING.get(name, name) + return format_string.format(**structure_values.__dict__) + + return None + + def _GetDataTypeMap(self, name): + """Retrieves a data type map defined by the definition file. + + The data type maps are cached for reuse. + + Args: + name (str): name of the data type as defined by the definition file. + + Returns: + dtfabric.DataTypeMap: data type map which contains a data type definition, + such as a structure, that can be mapped onto binary data. + """ + data_type_map = self._data_type_maps.get(name, None) + if not data_type_map: + data_type_map = self._data_type_fabric.CreateDataTypeMap(name) + self._data_type_maps[name] = data_type_map + + return data_type_map + + def _OpenMessageResourceFile(self, windows_path): + """Opens the message resource file specified by the Windows path. + + Args: + windows_path (str): Windows path containing the message resource + filename. + + Returns: + MessageResourceFile: message resource file or None. + """ + path_spec = self._path_resolver.ResolvePath(windows_path) + if path_spec is None: + return None + + return self._OpenMessageResourceFileByPathSpec(path_spec) + + def _OpenMessageResourceFileByPathSpec(self, path_spec): + """Opens the message resource file specified by the path specification. + + Args: + path_spec (dfvfs.PathSpec): path specification. + + Returns: + MessageResourceFile: message resource file or None. + """ + windows_path = self._path_resolver.GetWindowsPath(path_spec) + if windows_path is None: + logging.warning("Unable to retrieve Windows path.") + + try: + file_object = dfvfs_resolver.Resolver.OpenFileObject(path_spec) + except IOError as exception: + logging.warning( + f"Unable to open: {path_spec.comparable:s} with error: {exception!s}" + ) + file_object = None + + if file_object is None: + return None + + message_file = resource_file.MessageResourceFile( + windows_path, + ascii_codepage=self._ascii_codepage, + preferred_language_identifier=self._preferred_language_identifier, + ) + message_file.OpenFileObject(file_object) + + return message_file + + def _ReadChecksDefinitions(self): + """Reads the checks definitions from checks.yaml. + + Returns: + list[dict[str, object]]: checks definitions. + """ + check_definitions = {} + + with open(self._CHECKS_DEFINITIONS_FILE, "r", encoding="utf-8") as file_object: + for check_definition in yaml.safe_load_all(file_object): + name = check_definition.get("name", None) + if name: + check_definitions[name.lower()] = check_definition + + return check_definitions + + def _ReadDataTypeFabricDefinitionFile(self, filename): + """Reads a dtFabric definition file. + + Args: + filename (str): name of the dtFabric definition file. + + Returns: + dtfabric.DataTypeFabric: data type fabric which contains the data format + data type maps of the data type definition, such as a structure, that + can be mapped onto binary data or None if no filename is provided. + """ + if not filename: + return None + + path = os.path.join(self._DEFINITION_FILES_PATH, filename) + with open(path, "rb") as file_object: + definition = file_object.read() + + return dtfabric_fabric.DataTypeFabric(yaml_definition=definition) + + def _ReadStructureFromFileObject(self, file_object, file_offset, data_type_map): + """Reads a structure from a file-like object. + + This method currently only supports fixed-size structures. + + Args: + file_object (file): a file-like object to parse. + file_offset (int): offset of the structure data relative to the start + of the file-like object. + data_type_map (dtfabric.DataTypeMap): data type map of the structure. + + Returns: + object: structure values object or None if the structure cannot be read. + """ + structure_values = None + + data_size = data_type_map.GetSizeHint() + if data_size: + file_object.seek(file_offset, os.SEEK_SET) + try: + data = file_object.read(data_size) + structure_values = data_type_map.MapByteStream(data) + except ( + dtfabric_errors.ByteStreamTooSmallError, + dtfabric_errors.MappingError, + ): + pass + + return structure_values + + def CheckArtifactDefinition(self, artifact_definition): + """Checks if an artifact definition on a storage media image. + + Args: + artifact_definition (artifacts.ArtifactDefinition): artifact definition. + + Returns: + CheckResults: check results. + """ + check_result = CheckResults() + + if self._checks_definitions is None: + self._checks_definitions = self._ReadChecksDefinitions() + + find_specs = list( + self._filter_generator.GetFindSpecs([artifact_definition.name]) + ) + if find_specs: + path_specs = list(self._file_system_searcher.Find(find_specs=find_specs)) + check_result.number_of_file_entries = len(path_specs) + + check_definition = self._checks_definitions.get( + artifact_definition.name.lower(), None + ) + if check_definition: + for path_spec in path_specs: + file_entry = self._file_system.GetFileEntryByPathSpec(path_spec) + if file_entry.size > 0: + file_object = file_entry.GetFileObject() + if file_object: + formats = check_definition.get("formats", []) + data_format = self._DetermineDataFormat( + formats, file_object + ) + check_result.data_formats.add(data_format or "unknown") + + return check_result + + def GetWindowsVersion(self): + """Determines the Windows version from kernel executable file. + + Returns: + str: Windows version or None otherwise. + """ + # Window NT variants. + kernel_executable_path = "\\".join( + [self._windows_directory, "System32", "ntoskrnl.exe"] + ) + message_file = self._OpenMessageResourceFile(kernel_executable_path) + + if not message_file: + # Window 9x variants. + kernel_executable_path = "\\".join( + [self._windows_directory, "System32", "\\kernel32.dll"] + ) + message_file = self._OpenMessageResourceFile(kernel_executable_path) + + if not message_file: + return None + + return message_file.file_version + + def ScanForOperatingSystemVolumes(self, source_path, options=None): + """Scans for volumes containing an operating system. + + Args: + source_path (str): source path. + options (Optional[dfvfs.VolumeScannerOptions]): volume scanner options. + If None the default volume scanner options are used, which are defined + in the VolumeScannerOptions class. + + Returns: + bool: True if a volume with an operating system was found. + + Raises: + ScannerError: if the source path does not exists, or if the source path + is not a file or directory, or if the format of or within the source + file is not supported. + """ + if not options: + options = dfvfs_volume_scanner.VolumeScannerOptions() + + scan_context = self._ScanSource(source_path) + + self._source_path = source_path + self._source_type = scan_context.source_type + + base_path_specs = self._GetBasePathSpecs(scan_context, options) + + if ( + not base_path_specs + or scan_context.source_type == dfvfs_definitions.SOURCE_TYPE_FILE + ): + return False + + for path_spec in base_path_specs: + file_system = dfvfs_resolver.Resolver.OpenFileSystem(path_spec) + + if path_spec.type_indicator == dfvfs_definitions.TYPE_INDICATOR_OS: + mount_point = path_spec + else: + mount_point = path_spec.parent + + file_system_searcher = dfvfs_file_system_searcher.FileSystemSearcher( + file_system, mount_point + ) + + system_directories = [] + for system_directory_path_spec in file_system_searcher.Find( + find_specs=self._SYSTEM_DIRECTORY_FIND_SPECS + ): + relative_path = file_system_searcher.GetRelativePath( + system_directory_path_spec + ) + if relative_path: + system_directories.append(relative_path.lower()) + + if system_directories or len(base_path_specs) == 1: + self._file_system_searcher = file_system_searcher + self._file_system = file_system + self._mount_point = mount_point + + if self._WINDOWS_SYSTEM_DIRECTORIES.intersection(set(system_directories)): + path_resolver = dfvfs_windows_path_resolver.WindowsPathResolver( + file_system, mount_point + ) + + # TODO: determine Windows directory based on system directories. + windows_directory = None + for windows_path in self._WINDOWS_DIRECTORIES: + windows_path_spec = path_resolver.ResolvePath(windows_path) + if windows_path_spec is not None: + windows_directory = windows_path + break + + if windows_directory: + path_resolver.SetEnvironmentVariable( + "SystemRoot", windows_directory + ) + path_resolver.SetEnvironmentVariable("WinDir", windows_directory) + + registry_file_reader = ( + windows_registry.StorageMediaImageWindowsRegistryFileReader( + file_system, path_resolver + ) + ) + winregistry = dfwinreg_registry.WinRegistry( + registry_file_reader=registry_file_reader + ) + + collector = ( + environment_variables.WindowsEnvironmentVariablesCollector() + ) + + self._environment_variables = list(collector.Collect(winregistry)) + self._path_resolver = path_resolver + self._windows_directory = windows_directory + self._windows_registry = winregistry + + if system_directories: + # TODO: on Mac OS prevent detecting the Recovery volume. + break + + self._filter_generator = artifact_filters.ArtifactDefinitionFiltersGenerator( + self._artifacts_registry, self._environment_variables, [] + ) + + return True diff --git a/run_tests.py b/run_tests.py index 49164fd..b8e445e 100755 --- a/run_tests.py +++ b/run_tests.py @@ -7,27 +7,26 @@ import unittest # Change PYTHONPATH to include dependencies. -sys.path.insert(0, '.') +sys.path.insert(0, ".") import utils.dependencies # pylint: disable=wrong-import-position +if __name__ == "__main__": + print(f"Using Python version {sys.version!s}") -if __name__ == '__main__': - print(f'Using Python version {sys.version!s}') + fail_unless_has_test_file = "--fail-unless-has-test-file" in sys.argv + setattr(unittest, "fail_unless_has_test_file", fail_unless_has_test_file) + if fail_unless_has_test_file: + # Remove --fail-unless-has-test-file otherwise it will conflict with + # the argparse tests. + sys.argv.remove("--fail-unless-has-test-file") - fail_unless_has_test_file = '--fail-unless-has-test-file' in sys.argv - setattr(unittest, 'fail_unless_has_test_file', fail_unless_has_test_file) - if fail_unless_has_test_file: - # Remove --fail-unless-has-test-file otherwise it will conflict with - # the argparse tests. - sys.argv.remove('--fail-unless-has-test-file') + dependency_helper = utils.dependencies.DependencyHelper() - dependency_helper = utils.dependencies.DependencyHelper() + if not dependency_helper.CheckTestDependencies(): + sys.exit(1) - if not dependency_helper.CheckTestDependencies(): - sys.exit(1) - - test_suite = unittest.TestLoader().discover('tests', pattern='*.py') - test_results = unittest.TextTestRunner(verbosity=2).run(test_suite) - if not test_results.wasSuccessful(): - sys.exit(1) + test_suite = unittest.TestLoader().discover("tests", pattern="*.py") + test_results = unittest.TextTestRunner(verbosity=2).run(test_suite) + if not test_results.wasSuccessful(): + sys.exit(1) diff --git a/tests/resource_file.py b/tests/resource_file.py index 03338fa..2b84469 100644 --- a/tests/resource_file.py +++ b/tests/resource_file.py @@ -9,456 +9,1176 @@ class TestWrcResource: - """Windows Resource Compiler (WRC) resource for testing. + """Windows Resource Compiler (WRC) resource for testing. - Attributes: - items (list[TestWrcResourceItem]): resources items. - """ + Attributes: + items (list[TestWrcResourceItem]): resources items. + """ - def __init__(self): - """Initializes a resource.""" - super().__init__() - self.items = [] + def __init__(self): + """Initializes a resource.""" + super().__init__() + self.items = [] - # pylint: disable=invalid-name + # pylint: disable=invalid-name - def get_number_of_items(self): - """Retrieves the number of resource items. + def get_number_of_items(self): + """Retrieves the number of resource items. - Returns: - int: number of resource items. - """ - return len(self.items) + Returns: + int: number of resource items. + """ + return len(self.items) - def get_item_by_index(self, index): - """Retrieves a specific resource item. + def get_item_by_index(self, index): + """Retrieves a specific resource item. - Args: - index (int): index. + Args: + index (int): index. - Returns: - TestWrcResourceItem: resource item. - """ - return self.items[index] + Returns: + TestWrcResourceItem: resource item. + """ + return self.items[index] class TestWrcResourceItem: - """Windows Resource Compiler (WRC) resource item for testing. - - Attributes: - identifier (int]): identifier. - sub_items (list[TestWrcResourceItem]): resources sub items. - """ + """Windows Resource Compiler (WRC) resource item for testing. - def __init__(self, identifier): - """Initializes a resource item. - - Args: + Attributes: identifier (int]): identifier. + sub_items (list[TestWrcResourceItem]): resources sub items. """ - super().__init__() - self.identifier = identifier - self.resource_data = None - self.sub_items = [] - # pylint: disable=invalid-name + def __init__(self, identifier): + """Initializes a resource item. - def get_number_of_sub_items(self): - """Retrieves the number of resource sub items. + Args: + identifier (int]): identifier. + """ + super().__init__() + self.identifier = identifier + self.resource_data = None + self.sub_items = [] - Returns: - int: number of resource sub items. - """ - return len(self.sub_items) + # pylint: disable=invalid-name - def get_sub_item_by_index(self, index): - """Retrieves a specific resource sub item. + def get_number_of_sub_items(self): + """Retrieves the number of resource sub items. - Args: - index (int): index. + Returns: + int: number of resource sub items. + """ + return len(self.sub_items) - Returns: - TestWrcResourceItem: resource item. - """ - return self.sub_items[index] + def get_sub_item_by_index(self, index): + """Retrieves a specific resource sub item. - def read(self): - """Reads the resource data. + Args: + index (int): index. - Returns: - bytes: resource data. - """ - return self.resource_data + Returns: + TestWrcResourceItem: resource item. + """ + return self.sub_items[index] + def read(self): + """Reads the resource data. -class TestWrcStream: - """Windows Resource Compiler (WRC) stream for testing. + Returns: + bytes: resource data. + """ + return self.resource_data - Attributes: - resources (dict[int|str, object]): resources per identifier. - """ - def __init__(self): - """Initializes a stream.""" - super().__init__() - self.resources = {} +class TestWrcStream: + """Windows Resource Compiler (WRC) stream for testing. - # pylint: disable=invalid-name + Attributes: + resources (dict[int|str, object]): resources per identifier. + """ - def get_resource_by_identifier(self, identifier): - """Retrieves a specific resource by identifier. + def __init__(self): + """Initializes a stream.""" + super().__init__() + self.resources = {} - Args: - identifier (int): identifier. + # pylint: disable=invalid-name - Returns: - object: resource or None. - """ - return self.resources.get(identifier, None) + def get_resource_by_identifier(self, identifier): + """Retrieves a specific resource by identifier. - def get_resource_by_name(self, name): - """Retrieves a specific resource by name. + Args: + identifier (int): identifier. - Args: - name (str): name. + Returns: + object: resource or None. + """ + return self.resources.get(identifier, None) - Returns: - object: resource or None. - """ - return self.resources.get(name, None) + def get_resource_by_name(self, name): + """Retrieves a specific resource by name. + Args: + name (str): name. -class MessageResourceFileTest(test_lib.BaseTestCase): - """Tests for the Windows Message Resource file object.""" - - # pylint: disable=protected-access - - _VERSION_INFORMATION_RESOURCE_DATA = bytes(bytearray([ - 0xfc, 0x02, 0x34, 0x00, 0x00, 0x00, 0x56, 0x00, 0x53, 0x00, 0x5f, 0x00, - 0x56, 0x00, 0x45, 0x00, 0x52, 0x00, 0x53, 0x00, 0x49, 0x00, 0x4f, 0x00, - 0x4e, 0x00, 0x5f, 0x00, 0x49, 0x00, 0x4e, 0x00, 0x46, 0x00, 0x4f, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xbd, 0x04, 0xef, 0xfe, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x02, 0x00, 0x00, - 0x01, 0x00, 0x53, 0x00, 0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e, 0x00, - 0x67, 0x00, 0x46, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x49, 0x00, - 0x6e, 0x00, 0x66, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x38, 0x02, 0x00, 0x00, - 0x01, 0x00, 0x30, 0x00, 0x34, 0x00, 0x30, 0x00, 0x39, 0x00, 0x30, 0x00, - 0x34, 0x00, 0x65, 0x00, 0x34, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x09, 0x00, - 0x01, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x6d, 0x00, 0x65, 0x00, - 0x6e, 0x00, 0x74, 0x00, 0x73, 0x00, 0x00, 0x00, 0x43, 0x00, 0x6f, 0x00, - 0x6d, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x73, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x5e, 0x00, 0x1b, 0x00, 0x01, 0x00, 0x46, 0x00, - 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x44, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x63, 0x00, 0x72, 0x00, 0x69, 0x00, 0x70, 0x00, 0x74, 0x00, 0x69, 0x00, - 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, - 0x52, 0x00, 0x65, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x72, 0x00, - 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x20, 0x00, 0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x06, 0x00, 0x01, 0x00, 0x46, 0x00, - 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x56, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x73, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x3a, 0x00, 0x0d, 0x00, 0x01, 0x00, 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x4e, 0x00, - 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x77, 0x00, 0x72, 0x00, - 0x63, 0x00, 0x5f, 0x00, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x2e, 0x00, 0x64, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x84, 0x00, 0x30, 0x00, 0x01, 0x00, 0x4c, 0x00, 0x65, 0x00, 0x67, 0x00, - 0x61, 0x00, 0x6c, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x70, 0x00, 0x79, 0x00, - 0x72, 0x00, 0x69, 0x00, 0x67, 0x00, 0x68, 0x00, 0x74, 0x00, 0x00, 0x00, - 0x28, 0x00, 0x43, 0x00, 0x29, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, - 0x31, 0x00, 0x37, 0x00, 0x2c, 0x00, 0x20, 0x00, 0x4a, 0x00, 0x6f, 0x00, - 0x61, 0x00, 0x63, 0x00, 0x68, 0x00, 0x69, 0x00, 0x6d, 0x00, 0x20, 0x00, - 0x4d, 0x00, 0x65, 0x00, 0x74, 0x00, 0x7a, 0x00, 0x20, 0x00, 0x3c, 0x00, - 0x6a, 0x00, 0x6f, 0x00, 0x61, 0x00, 0x63, 0x00, 0x68, 0x00, 0x69, 0x00, - 0x6d, 0x00, 0x2e, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x74, 0x00, 0x7a, 0x00, - 0x40, 0x00, 0x67, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x69, 0x00, 0x6c, 0x00, - 0x2e, 0x00, 0x63, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x3e, 0x00, 0x00, 0x00, - 0x42, 0x00, 0x0d, 0x00, 0x01, 0x00, 0x4f, 0x00, 0x72, 0x00, 0x69, 0x00, - 0x67, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x46, 0x00, - 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x61, 0x00, 0x6d, 0x00, - 0x65, 0x00, 0x00, 0x00, 0x77, 0x00, 0x72, 0x00, 0x63, 0x00, 0x5f, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x64, 0x00, - 0x6c, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x09, 0x00, - 0x01, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x75, 0x00, - 0x63, 0x00, 0x74, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x77, 0x00, 0x72, 0x00, 0x63, 0x00, 0x5f, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x30, 0x00, 0x06, 0x00, 0x01, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00, - 0x64, 0x00, 0x75, 0x00, 0x63, 0x00, 0x74, 0x00, 0x56, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x73, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x00, 0x00, - 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x00, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x01, 0x00, 0x56, 0x00, 0x61, 0x00, 0x72, 0x00, - 0x46, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x49, 0x00, 0x6e, 0x00, - 0x66, 0x00, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x04, 0x00, - 0x00, 0x00, 0x54, 0x00, 0x72, 0x00, 0x61, 0x00, 0x6e, 0x00, 0x73, 0x00, - 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x09, 0x04, 0xe4, 0x04])) - - def testGetVersionInformationNoWrc(self): - """Tests the _GetVersionInformation function.""" - test_file_path = self._GetTestFilePath(['nowrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) - - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\nowrc_test.dll') - - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) - - try: - resource = message_resource_file._GetVersionInformation() - self.assertIsNone(resource) - - finally: - message_resource_file.Close() - - def testGetVersionInformationWrc(self): - """Tests the _GetVersionInformation function.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) - - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') - - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) - - message_resource_file._GetVersionInformation() - - self.assertEqual(message_resource_file.file_version, '1.0.0.0') - self.assertEqual(message_resource_file.product_version, '1.0.0.0') - - message_resource_file.Close() - - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\test.dll') - - # Test with an empty WRC stream. - wrc_stream = TestWrcStream() - message_resource_file._wrc_stream = wrc_stream - - message_resource_file._GetVersionInformation() - self.assertIsNone(message_resource_file.file_version) - self.assertIsNone(message_resource_file.product_version) - - # Test with empty version information. - wrc_resource = TestWrcResource() - wrc_stream.resources[0x10] = wrc_resource - - wrc_resource_item = TestWrcResourceItem(1) - wrc_resource.items.append(wrc_resource_item) - - wrc_resource_sub_item = TestWrcResourceItem(0x409) - wrc_resource_item.sub_items.append(wrc_resource_sub_item) + Returns: + object: resource or None. + """ + return self.resources.get(name, None) - wrc_resource_sub_item.resource_data = ( - self._VERSION_INFORMATION_RESOURCE_DATA) - message_resource_file._GetVersionInformation() - self.assertEqual(message_resource_file.file_version, '0.0.0.0') - self.assertEqual(message_resource_file.product_version, '2.0.0.0') +class MessageResourceFileTest(test_lib.BaseTestCase): + """Tests for the Windows Message Resource file object.""" + + # pylint: disable=protected-access + + _VERSION_INFORMATION_RESOURCE_DATA = bytes( + bytearray( + [ + 0xFC, + 0x02, + 0x34, + 0x00, + 0x00, + 0x00, + 0x56, + 0x00, + 0x53, + 0x00, + 0x5F, + 0x00, + 0x56, + 0x00, + 0x45, + 0x00, + 0x52, + 0x00, + 0x53, + 0x00, + 0x49, + 0x00, + 0x4F, + 0x00, + 0x4E, + 0x00, + 0x5F, + 0x00, + 0x49, + 0x00, + 0x4E, + 0x00, + 0x46, + 0x00, + 0x4F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xBD, + 0x04, + 0xEF, + 0xFE, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x04, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x5C, + 0x02, + 0x00, + 0x00, + 0x01, + 0x00, + 0x53, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x67, + 0x00, + 0x46, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x66, + 0x00, + 0x6F, + 0x00, + 0x00, + 0x00, + 0x38, + 0x02, + 0x00, + 0x00, + 0x01, + 0x00, + 0x30, + 0x00, + 0x34, + 0x00, + 0x30, + 0x00, + 0x39, + 0x00, + 0x30, + 0x00, + 0x34, + 0x00, + 0x65, + 0x00, + 0x34, + 0x00, + 0x00, + 0x00, + 0x2A, + 0x00, + 0x09, + 0x00, + 0x01, + 0x00, + 0x43, + 0x00, + 0x6F, + 0x00, + 0x6D, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x73, + 0x00, + 0x00, + 0x00, + 0x43, + 0x00, + 0x6F, + 0x00, + 0x6D, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x73, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x5E, + 0x00, + 0x1B, + 0x00, + 0x01, + 0x00, + 0x46, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x44, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x63, + 0x00, + 0x72, + 0x00, + 0x69, + 0x00, + 0x70, + 0x00, + 0x74, + 0x00, + 0x69, + 0x00, + 0x6F, + 0x00, + 0x6E, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x57, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x64, + 0x00, + 0x6F, + 0x00, + 0x77, + 0x00, + 0x73, + 0x00, + 0x20, + 0x00, + 0x52, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x6F, + 0x00, + 0x75, + 0x00, + 0x72, + 0x00, + 0x63, + 0x00, + 0x65, + 0x00, + 0x20, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x66, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2C, + 0x00, + 0x06, + 0x00, + 0x01, + 0x00, + 0x46, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x56, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x73, + 0x00, + 0x69, + 0x00, + 0x6F, + 0x00, + 0x6E, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x31, + 0x00, + 0x2E, + 0x00, + 0x30, + 0x00, + 0x2E, + 0x00, + 0x30, + 0x00, + 0x00, + 0x00, + 0x3A, + 0x00, + 0x0D, + 0x00, + 0x01, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x61, + 0x00, + 0x6C, + 0x00, + 0x4E, + 0x00, + 0x61, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x00, + 0x00, + 0x77, + 0x00, + 0x72, + 0x00, + 0x63, + 0x00, + 0x5F, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x2E, + 0x00, + 0x64, + 0x00, + 0x6C, + 0x00, + 0x6C, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x84, + 0x00, + 0x30, + 0x00, + 0x01, + 0x00, + 0x4C, + 0x00, + 0x65, + 0x00, + 0x67, + 0x00, + 0x61, + 0x00, + 0x6C, + 0x00, + 0x43, + 0x00, + 0x6F, + 0x00, + 0x70, + 0x00, + 0x79, + 0x00, + 0x72, + 0x00, + 0x69, + 0x00, + 0x67, + 0x00, + 0x68, + 0x00, + 0x74, + 0x00, + 0x00, + 0x00, + 0x28, + 0x00, + 0x43, + 0x00, + 0x29, + 0x00, + 0x20, + 0x00, + 0x32, + 0x00, + 0x30, + 0x00, + 0x31, + 0x00, + 0x37, + 0x00, + 0x2C, + 0x00, + 0x20, + 0x00, + 0x4A, + 0x00, + 0x6F, + 0x00, + 0x61, + 0x00, + 0x63, + 0x00, + 0x68, + 0x00, + 0x69, + 0x00, + 0x6D, + 0x00, + 0x20, + 0x00, + 0x4D, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x7A, + 0x00, + 0x20, + 0x00, + 0x3C, + 0x00, + 0x6A, + 0x00, + 0x6F, + 0x00, + 0x61, + 0x00, + 0x63, + 0x00, + 0x68, + 0x00, + 0x69, + 0x00, + 0x6D, + 0x00, + 0x2E, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x7A, + 0x00, + 0x40, + 0x00, + 0x67, + 0x00, + 0x6D, + 0x00, + 0x61, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x2E, + 0x00, + 0x63, + 0x00, + 0x6F, + 0x00, + 0x6D, + 0x00, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x42, + 0x00, + 0x0D, + 0x00, + 0x01, + 0x00, + 0x4F, + 0x00, + 0x72, + 0x00, + 0x69, + 0x00, + 0x67, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x61, + 0x00, + 0x6C, + 0x00, + 0x46, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x61, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x00, + 0x00, + 0x77, + 0x00, + 0x72, + 0x00, + 0x63, + 0x00, + 0x5F, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x2E, + 0x00, + 0x64, + 0x00, + 0x6C, + 0x00, + 0x6C, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x32, + 0x00, + 0x09, + 0x00, + 0x01, + 0x00, + 0x50, + 0x00, + 0x72, + 0x00, + 0x6F, + 0x00, + 0x64, + 0x00, + 0x75, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x4E, + 0x00, + 0x61, + 0x00, + 0x6D, + 0x00, + 0x65, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x77, + 0x00, + 0x72, + 0x00, + 0x63, + 0x00, + 0x5F, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x30, + 0x00, + 0x06, + 0x00, + 0x01, + 0x00, + 0x50, + 0x00, + 0x72, + 0x00, + 0x6F, + 0x00, + 0x64, + 0x00, + 0x75, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x56, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x73, + 0x00, + 0x69, + 0x00, + 0x6F, + 0x00, + 0x6E, + 0x00, + 0x00, + 0x00, + 0x31, + 0x00, + 0x2E, + 0x00, + 0x30, + 0x00, + 0x2E, + 0x00, + 0x30, + 0x00, + 0x00, + 0x00, + 0x44, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x56, + 0x00, + 0x61, + 0x00, + 0x72, + 0x00, + 0x46, + 0x00, + 0x69, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x66, + 0x00, + 0x6F, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x24, + 0x00, + 0x04, + 0x00, + 0x00, + 0x00, + 0x54, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x6E, + 0x00, + 0x73, + 0x00, + 0x6C, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x69, + 0x00, + 0x6F, + 0x00, + 0x6E, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x09, + 0x04, + 0xE4, + 0x04, + ] + ) + ) + + def testGetVersionInformationNoWrc(self): + """Tests the _GetVersionInformation function.""" + test_file_path = self._GetTestFilePath(["nowrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) + + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\nowrc_test.dll" + ) + + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) + + try: + resource = message_resource_file._GetVersionInformation() + self.assertIsNone(resource) + + finally: + message_resource_file.Close() + + def testGetVersionInformationWrc(self): + """Tests the _GetVersionInformation function.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) + + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) + + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) + + message_resource_file._GetVersionInformation() + + self.assertEqual(message_resource_file.file_version, "1.0.0.0") + self.assertEqual(message_resource_file.product_version, "1.0.0.0") + + message_resource_file.Close() + + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\test.dll" + ) + + # Test with an empty WRC stream. + wrc_stream = TestWrcStream() + message_resource_file._wrc_stream = wrc_stream + + message_resource_file._GetVersionInformation() + self.assertIsNone(message_resource_file.file_version) + self.assertIsNone(message_resource_file.product_version) + + # Test with empty version information. + wrc_resource = TestWrcResource() + wrc_stream.resources[0x10] = wrc_resource + + wrc_resource_item = TestWrcResourceItem(1) + wrc_resource.items.append(wrc_resource_item) + + wrc_resource_sub_item = TestWrcResourceItem(0x409) + wrc_resource_item.sub_items.append(wrc_resource_sub_item) + + wrc_resource_sub_item.resource_data = self._VERSION_INFORMATION_RESOURCE_DATA + + message_resource_file._GetVersionInformation() + self.assertEqual(message_resource_file.file_version, "0.0.0.0") + self.assertEqual(message_resource_file.product_version, "2.0.0.0") - def testGetVersionInformationResourceNoWrc(self): - """Tests the _GetVersionInformationResource function.""" - test_file_path = self._GetTestFilePath(['nowrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testGetVersionInformationResourceNoWrc(self): + """Tests the _GetVersionInformationResource function.""" + test_file_path = self._GetTestFilePath(["nowrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\nowrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\nowrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - version_information_resource = ( - message_resource_file._GetVersionInformationResource()) + version_information_resource = ( + message_resource_file._GetVersionInformationResource() + ) - self.assertIsNone(version_information_resource) + self.assertIsNone(version_information_resource) - message_resource_file.Close() + message_resource_file.Close() - def testGetVersionInformationResourceWrc(self): - """Tests the _GetVersionInformationResource function.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testGetVersionInformationResourceWrc(self): + """Tests the _GetVersionInformationResource function.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - version_information_resource = ( - message_resource_file._GetVersionInformationResource()) + version_information_resource = ( + message_resource_file._GetVersionInformationResource() + ) - self.assertIsNotNone(version_information_resource) + self.assertIsNotNone(version_information_resource) - message_resource_file.Close() + message_resource_file.Close() - def testFileVersionProperty(self): - """Tests the file_version property.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testFileVersionProperty(self): + """Tests the file_version property.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - self.assertEqual(message_resource_file.file_version, '1.0.0.0') + self.assertEqual(message_resource_file.file_version, "1.0.0.0") - message_resource_file.Close() + message_resource_file.Close() - def testProductVersionProperty(self): - """Tests the product_version property.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testProductVersionProperty(self): + """Tests the product_version property.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - self.assertEqual(message_resource_file.product_version, '1.0.0.0') + self.assertEqual(message_resource_file.product_version, "1.0.0.0") - message_resource_file.Close() + message_resource_file.Close() - # TODO: add open/close test on non PE/COFF file. + # TODO: add open/close test on non PE/COFF file. - def testOpenFileObjectAndCloseNoWrc(self): - """Tests the OpenFileObject and Close functions.""" - test_file_path = self._GetTestFilePath(['nowrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testOpenFileObjectAndCloseNoWrc(self): + """Tests the OpenFileObject and Close functions.""" + test_file_path = self._GetTestFilePath(["nowrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\nowrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\nowrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - with self.assertRaises(IOError): - message_resource_file.OpenFileObject(file_object) + with self.assertRaises(IOError): + message_resource_file.OpenFileObject(file_object) - message_resource_file.Close() + message_resource_file.Close() - def testOpenFileObjectAndCloseWrc(self): - """Tests the OpenFileObject and Close functions.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testOpenFileObjectAndCloseWrc(self): + """Tests the OpenFileObject and Close functions.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - with self.assertRaises(IOError): - message_resource_file.OpenFileObject(file_object) + with self.assertRaises(IOError): + message_resource_file.OpenFileObject(file_object) - message_resource_file.Close() + message_resource_file.Close() - def testGetMessageTableResourceNoWrc(self): - """Tests the GetMessageTableResource function.""" - test_file_path = self._GetTestFilePath(['nowrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testGetMessageTableResourceNoWrc(self): + """Tests the GetMessageTableResource function.""" + test_file_path = self._GetTestFilePath(["nowrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\nowrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\nowrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - try: - resource = message_resource_file.GetMessageTableResource() - self.assertIsNone(resource) + try: + resource = message_resource_file.GetMessageTableResource() + self.assertIsNone(resource) - finally: - message_resource_file.Close() + finally: + message_resource_file.Close() - def testGetMessageTableResourceWrc(self): - """Tests the GetMessageTableResource function.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testGetMessageTableResourceWrc(self): + """Tests the GetMessageTableResource function.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - try: - message_table_resource = message_resource_file.GetMessageTableResource() - self.assertIsNotNone(message_table_resource) + try: + message_table_resource = message_resource_file.GetMessageTableResource() + self.assertIsNotNone(message_table_resource) - finally: - message_resource_file.Close() + finally: + message_resource_file.Close() - def testGetMUILanguage(self): - """Tests the GetMUILanguage function.""" - test_file_path = self._GetTestFilePath(['wrc_test.mui.dll']) - self._SkipIfPathNotExists(test_file_path) + def testGetMUILanguage(self): + """Tests the GetMUILanguage function.""" + test_file_path = self._GetTestFilePath(["wrc_test.mui.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - mui_language = message_resource_file.GetMUILanguage() - self.assertEqual(mui_language, 'en-US') + mui_language = message_resource_file.GetMUILanguage() + self.assertEqual(mui_language, "en-US") - message_resource_file.Close() + message_resource_file.Close() - # Test with an empty WRC stream. - wrc_stream = TestWrcStream() + # Test with an empty WRC stream. + wrc_stream = TestWrcStream() - message_resource_file._wrc_stream = wrc_stream + message_resource_file._wrc_stream = wrc_stream - mui_language = message_resource_file.GetMUILanguage() - self.assertIsNone(mui_language) + mui_language = message_resource_file.GetMUILanguage() + self.assertIsNone(mui_language) - def testHasMessageTableResourceNoWrc(self): - """Tests the HasMessageTableResource function.""" - test_file_path = self._GetTestFilePath(['nowrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testHasMessageTableResourceNoWrc(self): + """Tests the HasMessageTableResource function.""" + test_file_path = self._GetTestFilePath(["nowrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\nowrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\nowrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - try: - result = message_resource_file.HasMessageTableResource() - self.assertFalse(result) + try: + result = message_resource_file.HasMessageTableResource() + self.assertFalse(result) - finally: - message_resource_file.Close() + finally: + message_resource_file.Close() - def testHasMessageTableResourceWrc(self): - """Tests the HasMessageTableResource function.""" - test_file_path = self._GetTestFilePath(['wrc_test.dll']) - self._SkipIfPathNotExists(test_file_path) + def testHasMessageTableResourceWrc(self): + """Tests the HasMessageTableResource function.""" + test_file_path = self._GetTestFilePath(["wrc_test.dll"]) + self._SkipIfPathNotExists(test_file_path) - message_resource_file = resource_file.MessageResourceFile( - 'C:\\Windows\\System32\\wrc_test.dll') + message_resource_file = resource_file.MessageResourceFile( + "C:\\Windows\\System32\\wrc_test.dll" + ) - with open(test_file_path, 'rb') as file_object: - message_resource_file.OpenFileObject(file_object) + with open(test_file_path, "rb") as file_object: + message_resource_file.OpenFileObject(file_object) - try: - result = message_resource_file.HasMessageTableResource() - self.assertTrue(result) + try: + result = message_resource_file.HasMessageTableResource() + self.assertTrue(result) - finally: - message_resource_file.Close() + finally: + message_resource_file.Close() -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_lib.py b/tests/test_lib.py index c02a91a..377987c 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -5,63 +5,62 @@ import tempfile import unittest - # The path to top of the winevt-kb source tree. -PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # The paths below are all derived from the project path directory. # They are enumerated explicitly here so that they can be overwritten for # compatibility with different build systems. -TEST_DATA_PATH = os.path.join(PROJECT_PATH, 'test_data') +TEST_DATA_PATH = os.path.join(PROJECT_PATH, "test_data") class BaseTestCase(unittest.TestCase): - """The base test case.""" + """The base test case.""" - # Show full diff results, part of TestCase so does not follow our naming - # conventions. - maxDiff = None + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None - def _GetTestFilePath(self, path_segments): - """Retrieves the path of a test file in the test data directory. + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file in the test data directory. - Args: - path_segments (list[str]): path segments inside the test data directory. + Args: + path_segments (list[str]): path segments inside the test data directory. - Returns: - str: path of the test file. - """ - # Note that we need to pass the individual path segments to os.path.join - # and not a list. - return os.path.join(TEST_DATA_PATH, *path_segments) + Returns: + str: path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(TEST_DATA_PATH, *path_segments) - def _SkipIfPathNotExists(self, path): - """Skips the test if the path does not exist. + def _SkipIfPathNotExists(self, path): + """Skips the test if the path does not exist. - Args: - path (str): path of a test file. + Args: + path (str): path of a test file. - Raises: - SkipTest: if the path does not exist and the test should be skipped. - """ - if not os.path.exists(path): - filename = os.path.basename(path) - raise unittest.SkipTest(f'missing test file: {filename:s}') + Raises: + SkipTest: if the path does not exist and the test should be skipped. + """ + if not os.path.exists(path): + filename = os.path.basename(path) + raise unittest.SkipTest(f"missing test file: {filename:s}") class TempDirectory: - """Class that implements a temporary directory.""" + """Class that implements a temporary directory.""" - def __init__(self): - """Initializes a temporary directory.""" - super().__init__() - self.name = '' + def __init__(self): + """Initializes a temporary directory.""" + super().__init__() + self.name = "" - def __enter__(self): - """Make this work with the 'with' statement.""" - self.name = tempfile.mkdtemp() - return self.name + def __enter__(self): + """Make this work with the 'with' statement.""" + self.name = tempfile.mkdtemp() + return self.name - def __exit__(self, unused_type, unused_value, unused_traceback): - """Make this work with the 'with' statement.""" - shutil.rmtree(self.name, True) + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make this work with the 'with' statement.""" + shutil.rmtree(self.name, True) diff --git a/tools/check_artifacts.py b/tools/check_artifacts.py index 00fb9e2..d51287f 100755 --- a/tools/check_artifacts.py +++ b/tools/check_artifacts.py @@ -20,153 +20,208 @@ def Main(): - """Entry point of console script to check artifact definitions. - - Returns: - int: exit code that is provided to sys.exit(). - """ - argument_parser = argparse.ArgumentParser(description=( - 'Checks artifact definitions on a storage media image.')) - - argument_parser.add_argument( - '--artifact_definitions', '--artifact-definitions', - dest='artifact_definitions', type=str, metavar='PATH', action='store', - help=('Path to a directory or file containing the artifact definition ' - '.yaml files.')) - - argument_parser.add_argument( - '--back_end', '--back-end', dest='back_end', action='store', - metavar='NTFS', default=None, help='preferred dfVFS back-end.') - - argument_parser.add_argument( - '--partitions', '--partition', dest='partitions', action='store', - type=str, default=None, help=( - 'Define partitions to be processed. A range of partitions can be ' - 'defined as: "3..5". Multiple partitions can be defined as: "1,3,5" ' - '(a list of comma separated values). Ranges and lists can also be ' - 'combined as: "1,3..5". The first partition is 1. All partitions ' - 'can be specified with: "all".')) - - argument_parser.add_argument( - '--snapshots', '--snapshot', dest='snapshots', action='store', type=str, - default=None, help=( - 'Define snapshots to be processed. A range of snapshots can be ' - 'defined as: "3..5". Multiple snapshots can be defined as: "1,3,5" ' - '(a list of comma separated values). Ranges and lists can also be ' - 'combined as: "1,3..5". The first snapshot is 1. All snapshots can ' - 'be specified with: "all".')) - - argument_parser.add_argument( - '--volumes', '--volume', dest='volumes', action='store', type=str, - default=None, help=( - 'Define volumes to be processed. A range of volumes can be defined ' - 'as: "3..5". Multiple volumes can be defined as: "1,3,5" (a list ' - 'of comma separated values). Ranges and lists can also be combined ' - 'as: "1,3..5". The first volume is 1. All volumes can be specified ' - 'with: "all".')) - - argument_parser.add_argument( - '-w', '--windows_version', '--windows-version', - dest='windows_version', action='store', metavar='Windows XP', - default=None, help='string that identifies the Windows version.') - - argument_parser.add_argument( - 'source', nargs='?', action='store', metavar='image.raw', - default=None, help='path of the storage media image.') - - options = argument_parser.parse_args() - - if not options.source: - print('Path to source storage media image is missing.') - print('') - argument_parser.print_help() - print('') - return 1 - - if not options.artifact_definitions: - print('Path to artifact definitions is missing.') - print('') - argument_parser.print_help() - print('') - return 1 - - dfimagetools_helpers.SetDFVFSBackEnd(options.back_end) - - logging.basicConfig( - level=logging.INFO, format='[%(levelname)s] %(message)s') - - registry = artifacts_registry.ArtifactDefinitionsRegistry() - reader = artifacts_reader.YamlArtifactsReader() - - if os.path.isdir(options.artifact_definitions): - registry.ReadFromDirectory(reader, options.artifact_definitions) - elif os.path.isfile(options.artifact_definitions): - registry.ReadFromFile(reader, options.artifact_definitions) - - mediator = dfvfs_command_line.CLIVolumeScannerMediator() - scanner = volume_scanner.ArtifactDefinitionsVolumeScanner( - registry, mediator=mediator) - - volume_scanner_options = dfvfs_volume_scanner.VolumeScannerOptions() - volume_scanner_options.partitions = mediator.ParseVolumeIdentifiersString( - options.partitions) - - if options.snapshots == 'none': - volume_scanner_options.snapshots = ['none'] - else: - volume_scanner_options.snapshots = mediator.ParseVolumeIdentifiersString( - options.snapshots) - - volume_scanner_options.volumes = mediator.ParseVolumeIdentifiersString( - options.volumes) - - try: - if not scanner.ScanForOperatingSystemVolumes( - options.source, options=volume_scanner_options): - print((f'Unable to retrieve an operating system volume from: ' - f'{options.source:s}.')) - print('') - return 1 - - definitions_with_check_results = {} - for artifact_definition in registry.GetDefinitions(): - group_only = True - for source in artifact_definition.sources: - if source.type_indicator != ( - artifacts_definitions.TYPE_INDICATOR_ARTIFACT_GROUP): - group_only = False - break - - if group_only: - # Not interested in results of group-only artifact definitions. - continue - - check_result = scanner.CheckArtifactDefinition(artifact_definition) - if check_result.number_of_file_entries: - definitions_with_check_results[artifact_definition.name] = check_result - - except dfvfs_errors.ScannerError as exception: - print(f'[ERROR] {exception!s}', file=sys.stderr) - print('') - return 1 - - except KeyboardInterrupt: - print('Aborted by user.', file=sys.stderr) - print('') - return 1 - - print('Aritfact definitions found:') - for name, check_result in sorted(definitions_with_check_results.items()): - text = f'* {name:s} [results: {check_result.number_of_file_entries:d}]' - if check_result.data_formats: - formats_string = ', '.join(sorted(check_result.data_formats)) - text = f'{text:s} [formats: {formats_string:s}]' - - print(text) - print('') - - return 0 - - -if __name__ == '__main__': - sys.exit(Main()) + """Entry point of console script to check artifact definitions. + + Returns: + int: exit code that is provided to sys.exit(). + """ + argument_parser = argparse.ArgumentParser( + description=("Checks artifact definitions on a storage media image.") + ) + + argument_parser.add_argument( + "--artifact_definitions", + "--artifact-definitions", + dest="artifact_definitions", + type=str, + metavar="PATH", + action="store", + help=( + "Path to a directory or file containing the artifact definition " + ".yaml files." + ), + ) + + argument_parser.add_argument( + "--back_end", + "--back-end", + dest="back_end", + action="store", + metavar="NTFS", + default=None, + help="preferred dfVFS back-end.", + ) + + argument_parser.add_argument( + "--partitions", + "--partition", + dest="partitions", + action="store", + type=str, + default=None, + help=( + "Define partitions to be processed. A range of partitions can be " + 'defined as: "3..5". Multiple partitions can be defined as: "1,3,5" ' + "(a list of comma separated values). Ranges and lists can also be " + 'combined as: "1,3..5". The first partition is 1. All partitions ' + 'can be specified with: "all".' + ), + ) + + argument_parser.add_argument( + "--snapshots", + "--snapshot", + dest="snapshots", + action="store", + type=str, + default=None, + help=( + "Define snapshots to be processed. A range of snapshots can be " + 'defined as: "3..5". Multiple snapshots can be defined as: "1,3,5" ' + "(a list of comma separated values). Ranges and lists can also be " + 'combined as: "1,3..5". The first snapshot is 1. All snapshots can ' + 'be specified with: "all".' + ), + ) + + argument_parser.add_argument( + "--volumes", + "--volume", + dest="volumes", + action="store", + type=str, + default=None, + help=( + "Define volumes to be processed. A range of volumes can be defined " + 'as: "3..5". Multiple volumes can be defined as: "1,3,5" (a list ' + "of comma separated values). Ranges and lists can also be combined " + 'as: "1,3..5". The first volume is 1. All volumes can be specified ' + 'with: "all".' + ), + ) + + argument_parser.add_argument( + "-w", + "--windows_version", + "--windows-version", + dest="windows_version", + action="store", + metavar="Windows XP", + default=None, + help="string that identifies the Windows version.", + ) + + argument_parser.add_argument( + "source", + nargs="?", + action="store", + metavar="image.raw", + default=None, + help="path of the storage media image.", + ) + + options = argument_parser.parse_args() + + if not options.source: + print("Path to source storage media image is missing.") + print("") + argument_parser.print_help() + print("") + return 1 + + if not options.artifact_definitions: + print("Path to artifact definitions is missing.") + print("") + argument_parser.print_help() + print("") + return 1 + + dfimagetools_helpers.SetDFVFSBackEnd(options.back_end) + + logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") + + registry = artifacts_registry.ArtifactDefinitionsRegistry() + reader = artifacts_reader.YamlArtifactsReader() + + if os.path.isdir(options.artifact_definitions): + registry.ReadFromDirectory(reader, options.artifact_definitions) + elif os.path.isfile(options.artifact_definitions): + registry.ReadFromFile(reader, options.artifact_definitions) + + mediator = dfvfs_command_line.CLIVolumeScannerMediator() + scanner = volume_scanner.ArtifactDefinitionsVolumeScanner( + registry, mediator=mediator + ) + + volume_scanner_options = dfvfs_volume_scanner.VolumeScannerOptions() + volume_scanner_options.partitions = mediator.ParseVolumeIdentifiersString( + options.partitions + ) + + if options.snapshots == "none": + volume_scanner_options.snapshots = ["none"] + else: + volume_scanner_options.snapshots = mediator.ParseVolumeIdentifiersString( + options.snapshots + ) + + volume_scanner_options.volumes = mediator.ParseVolumeIdentifiersString( + options.volumes + ) + + try: + if not scanner.ScanForOperatingSystemVolumes( + options.source, options=volume_scanner_options + ): + print( + ( + f"Unable to retrieve an operating system volume from: " + f"{options.source:s}." + ) + ) + print("") + return 1 + + definitions_with_check_results = {} + for artifact_definition in registry.GetDefinitions(): + group_only = True + for source in artifact_definition.sources: + if source.type_indicator != ( + artifacts_definitions.TYPE_INDICATOR_ARTIFACT_GROUP + ): + group_only = False + break + + if group_only: + # Not interested in results of group-only artifact definitions. + continue + + check_result = scanner.CheckArtifactDefinition(artifact_definition) + if check_result.number_of_file_entries: + definitions_with_check_results[artifact_definition.name] = check_result + + except dfvfs_errors.ScannerError as exception: + print(f"[ERROR] {exception!s}", file=sys.stderr) + print("") + return 1 + + except KeyboardInterrupt: + print("Aborted by user.", file=sys.stderr) + print("") + return 1 + + print("Aritfact definitions found:") + for name, check_result in sorted(definitions_with_check_results.items()): + text = f"* {name:s} [results: {check_result.number_of_file_entries:d}]" + if check_result.data_formats: + formats_string = ", ".join(sorted(check_result.data_formats)) + text = f"{text:s} [formats: {formats_string:s}]" + + print(text) + print("") + + return 0 + + +if __name__ == "__main__": + sys.exit(Main()) diff --git a/tools/generate_docs.py b/tools/generate_docs.py index b30ebdd..c3da8a3 100755 --- a/tools/generate_docs.py +++ b/tools/generate_docs.py @@ -14,191 +14,195 @@ class IndexRstOutputWriter: - """Index.rst output writer.""" - - def __init__(self, path): - """Initializes an index.rst output writer.""" - super().__init__() - self._file_object = None - self._path = path - - def __enter__(self): - """Make this work with the 'with' statement.""" - self._file_object = open(self._path, 'w', encoding='utf-8') - - text = '\n'.join([ - '#####################################', - 'Digital Forensics Artifact definition', - '#####################################', - '', - '.. toctree::', - ' :maxdepth: 1', - '', - '']) - self._file_object.write(text) - - return self - - def __exit__(self, exception_type, value, traceback): - """Make this work with the 'with' statement.""" - self._file_object.close() - self._file_object = None - - def WriteArtifactDefinition(self, artifact_name): - """Writes an artifact definition to the index.rst file. - - Args: - artifact_name (str): artifact name. - """ - self._file_object.write( - f' {artifact_name:s} <{artifact_name:s}>\n') + """Index.rst output writer.""" + + def __init__(self, path): + """Initializes an index.rst output writer.""" + super().__init__() + self._file_object = None + self._path = path + + def __enter__(self): + """Make this work with the 'with' statement.""" + self._file_object = open(self._path, "w", encoding="utf-8") + + text = "\n".join( + [ + "#####################################", + "Digital Forensics Artifact definition", + "#####################################", + "", + ".. toctree::", + " :maxdepth: 1", + "", + "", + ] + ) + self._file_object.write(text) + + return self + + def __exit__(self, exception_type, value, traceback): + """Make this work with the 'with' statement.""" + self._file_object.close() + self._file_object = None + + def WriteArtifactDefinition(self, artifact_name): + """Writes an artifact definition to the index.rst file. + + Args: + artifact_name (str): artifact name. + """ + self._file_object.write(f" {artifact_name:s} <{artifact_name:s}>\n") class MarkdownOutputWriter: - """Markdown output writer.""" + """Markdown output writer.""" - _URL_PREFIX = 'https://artifacts-kb.readthedocs.io/en/latest/sources/' + _URL_PREFIX = "https://artifacts-kb.readthedocs.io/en/latest/sources/" - def __init__(self, path): - """Initializes a Markdown output writer.""" - super().__init__() - self._file_object = None - self._path = path + def __init__(self, path): + """Initializes a Markdown output writer.""" + super().__init__() + self._file_object = None + self._path = path - def __enter__(self): - """Make this work with the 'with' statement.""" - self._file_object = open(self._path, 'w', encoding='utf-8') - return self + def __enter__(self): + """Make this work with the 'with' statement.""" + self._file_object = open(self._path, "w", encoding="utf-8") + return self - def __exit__(self, exception_type, value, traceback): - """Make this work with the 'with' statement.""" - self._file_object.close() - self._file_object = None + def __exit__(self, exception_type, value, traceback): + """Make this work with the 'with' statement.""" + self._file_object.close() + self._file_object = None - def WriteArtifactDefinition(self, artifact_definition): - """Writes an artifact definition to a Markdown file. + def WriteArtifactDefinition(self, artifact_definition): + """Writes an artifact definition to a Markdown file. - Args: - artifact_definition (list[artifacs.ArtifactDefinition]): artifact - definition. - """ - lines = [ - f'## {artifact_definition.name:s}', - '', - artifact_definition.description, - ''] + Args: + artifact_definition (list[artifacs.ArtifactDefinition]): artifact + definition. + """ + lines = [ + f"## {artifact_definition.name:s}", + "", + artifact_definition.description, + "", + ] - for source in sorted( - artifact_definition.sources, key=lambda source: source.supported_os): - if source.type_indicator in ( - artifacts_definitions.TYPE_INDICATOR_FILE, - artifacts_definitions.TYPE_INDICATOR_PATH): + for source in sorted( + artifact_definition.sources, key=lambda source: source.supported_os + ): + if source.type_indicator in ( + artifacts_definitions.TYPE_INDICATOR_FILE, + artifacts_definitions.TYPE_INDICATOR_PATH, + ): - supported_os = ', '.join( - source.supported_os or artifact_definition.supported_os) + supported_os = ", ".join( + source.supported_os or artifact_definition.supported_os + ) - lines.extend([ - f'Paths on: {supported_os:s}', - '', - '```']) + lines.extend([f"Paths on: {supported_os:s}", "", "```"]) - for path in sorted(source.paths): - lines.append(path) + for path in sorted(source.paths): + lines.append(path) - lines.extend([ - '```', - '']) + lines.extend(["```", ""]) - if source.type_indicator == ( - artifacts_definitions.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY): - lines.append('```') - for key_path in sorted(source.keys): - lines.append(key_path) + if source.type_indicator == ( + artifacts_definitions.TYPE_INDICATOR_WINDOWS_REGISTRY_KEY + ): + lines.append("```") + for key_path in sorted(source.keys): + lines.append(key_path) - lines.extend([ - '```', - '']) + lines.extend(["```", ""]) - if artifact_definition.urls: - lines.extend([ - '### References', - '']) + if artifact_definition.urls: + lines.extend(["### References", ""]) - for url in artifact_definition.urls: - if url.startswith(self._URL_PREFIX) and url.endswith('.html'): - url = url[len(self._URL_PREFIX):-5] - url = f'../{url:s}.md' + for url in artifact_definition.urls: + if url.startswith(self._URL_PREFIX) and url.endswith(".html"): + url = url[len(self._URL_PREFIX) : -5] + url = f"../{url:s}.md" - lines.append(f'* {url:s}') + lines.append(f"* {url:s}") - lines.extend([ - '', - '']) + lines.extend(["", ""]) - text = '\n'.join(lines) - self._file_object.write(text) + text = "\n".join(lines) + self._file_object.write(text) def Main(): - """Entry point of console script to generate documentation. - - Returns: - int: exit code that is provided to sys.exit(). - """ - argument_parser = argparse.ArgumentParser(description=( - 'Generated artifact definition documentation.')) - - argument_parser.add_argument( - '--artifact_definitions', '--artifact-definitions', - dest='artifact_definitions', type=str, metavar='PATH', action='store', - help=('Path to a directory or file containing the artifact definition ' - '.yaml files.')) - - options = argument_parser.parse_args() - - artifact_definitions = options.artifact_definitions - if not artifact_definitions: - artifact_definitions = os.path.join( - os.path.dirname(artifacts.__file__), 'data') - if not os.path.exists(artifact_definitions): - artifact_definitions = os.path.join('/', 'usr', 'share', 'artifacts') - if not os.path.exists(artifact_definitions): - artifact_definitions = None - - if not artifact_definitions: - print('Missing artifact definitions.') - print('') - argument_parser.print_help() - print('') - return 1 - - logging.basicConfig( - level=logging.INFO, format='[%(levelname)s] %(message)s') - - registry = artifacts_registry.ArtifactDefinitionsRegistry() - reader = artifacts_reader.YamlArtifactsReader() - - if os.path.isdir(artifact_definitions): - registry.ReadFromDirectory(reader, artifact_definitions) - elif os.path.isfile(artifact_definitions): - registry.ReadFromFile(reader, artifact_definitions) - - output_directory = os.path.join('docs', 'sources', 'definitions') - os.makedirs(output_directory, exist_ok=True) - - index_rst_file_path = os.path.join(output_directory, 'index.rst') - with IndexRstOutputWriter(index_rst_file_path) as index_rst_writer: - for artifact_definition in sorted( - registry.GetDefinitions(), key=lambda definition: definition.name): - markdown_file_path = os.path.join( - output_directory, f'{artifact_definition.name:s}.md') - with MarkdownOutputWriter(markdown_file_path) as markdown_writer: - markdown_writer.WriteArtifactDefinition(artifact_definition) - - index_rst_writer.WriteArtifactDefinition(artifact_definition.name) - - return 0 - - -if __name__ == '__main__': - sys.exit(Main()) + """Entry point of console script to generate documentation. + + Returns: + int: exit code that is provided to sys.exit(). + """ + argument_parser = argparse.ArgumentParser( + description=("Generated artifact definition documentation.") + ) + + argument_parser.add_argument( + "--artifact_definitions", + "--artifact-definitions", + dest="artifact_definitions", + type=str, + metavar="PATH", + action="store", + help=( + "Path to a directory or file containing the artifact definition " + ".yaml files." + ), + ) + + options = argument_parser.parse_args() + + artifact_definitions = options.artifact_definitions + if not artifact_definitions: + artifact_definitions = os.path.join(os.path.dirname(artifacts.__file__), "data") + if not os.path.exists(artifact_definitions): + artifact_definitions = os.path.join("/", "usr", "share", "artifacts") + if not os.path.exists(artifact_definitions): + artifact_definitions = None + + if not artifact_definitions: + print("Missing artifact definitions.") + print("") + argument_parser.print_help() + print("") + return 1 + + logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") + + registry = artifacts_registry.ArtifactDefinitionsRegistry() + reader = artifacts_reader.YamlArtifactsReader() + + if os.path.isdir(artifact_definitions): + registry.ReadFromDirectory(reader, artifact_definitions) + elif os.path.isfile(artifact_definitions): + registry.ReadFromFile(reader, artifact_definitions) + + output_directory = os.path.join("docs", "sources", "definitions") + os.makedirs(output_directory, exist_ok=True) + + index_rst_file_path = os.path.join(output_directory, "index.rst") + with IndexRstOutputWriter(index_rst_file_path) as index_rst_writer: + for artifact_definition in sorted( + registry.GetDefinitions(), key=lambda definition: definition.name + ): + markdown_file_path = os.path.join( + output_directory, f"{artifact_definition.name:s}.md" + ) + with MarkdownOutputWriter(markdown_file_path) as markdown_writer: + markdown_writer.WriteArtifactDefinition(artifact_definition) + + index_rst_writer.WriteArtifactDefinition(artifact_definition.name) + + return 0 + + +if __name__ == "__main__": + sys.exit(Main()) From fc360d4404dfb1d448883b2b03faf2b8f6cb542f Mon Sep 17 00:00:00 2001 From: Joachim Metz Date: Sat, 16 May 2026 06:03:52 +0200 Subject: [PATCH 3/3] Changes to use black Python formatter --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 766854c..7f90a35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,7 @@ include = "\\.pyi?$" [tool.docformatter] black = true -non-cap = [] +non-cap = ["str:"] non-strict = true [tool.setuptools]