Repository: mkorman90/regipy Branch: master Commit: f9b19e688612 Files: 223 Total size: 544.5 KB Directory structure: gitextract_zwilyk2_/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── publish.yml │ └── scorecard.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── docs/ │ ├── PLUGINS.md │ └── README.rst ├── pyproject.toml ├── regipy/ │ ├── __init__.py │ ├── cli.py │ ├── cli_utils.py │ ├── constants.py │ ├── exceptions.py │ ├── hive_types.py │ ├── plugins/ │ │ ├── __init__.py │ │ ├── amcache/ │ │ │ ├── __init__.py │ │ │ └── amcache.py │ │ ├── bcd/ │ │ │ ├── __init__.py │ │ │ └── boot_entry_list.py │ │ ├── ntuser/ │ │ │ ├── __init__.py │ │ │ ├── appkeys.py │ │ │ ├── classes_installer.py │ │ │ ├── comdlg32.py │ │ │ ├── installed_programs_ntuser.py │ │ │ ├── muicache.py │ │ │ ├── network_drives.py │ │ │ ├── persistence.py │ │ │ ├── putty.py │ │ │ ├── recentdocs.py │ │ │ ├── runmru.py │ │ │ ├── shellbags_ntuser.py │ │ │ ├── sysinternals.py │ │ │ ├── tsclient.py │ │ │ ├── typed_paths.py │ │ │ ├── typed_urls.py │ │ │ ├── user_assist.py │ │ │ ├── winrar.py │ │ │ ├── winscp_saved_sessions.py │ │ │ ├── word_wheel_query.py │ │ │ └── wsl.py │ │ ├── plugin.py │ │ ├── plugin_template.py │ │ ├── sam/ │ │ │ ├── __init__.py │ │ │ ├── local_sid.py │ │ │ └── samparse.py │ │ ├── security/ │ │ │ ├── __init__.py │ │ │ └── domain_sid.py │ │ ├── software/ │ │ │ ├── __init__.py │ │ │ ├── appcompatflags.py │ │ │ ├── appinitdlls.py │ │ │ ├── apppaths.py │ │ │ ├── classes_installer.py │ │ │ ├── defender.py │ │ │ ├── disablesr.py │ │ │ ├── execpolicy.py │ │ │ ├── image_file_execution_options.py │ │ │ ├── installed_programs.py │ │ │ ├── last_logon.py │ │ │ ├── networklist.py │ │ │ ├── persistence.py │ │ │ ├── printdemon.py │ │ │ ├── profilelist.py │ │ │ ├── pslogging.py │ │ │ ├── spp_clients.py │ │ │ ├── susclient.py │ │ │ ├── tracing.py │ │ │ ├── uac.py │ │ │ └── winver.py │ │ ├── system/ │ │ │ ├── __init__.py │ │ │ ├── active_controlset.py │ │ │ ├── appcertdlls.py │ │ │ ├── backuprestore.py │ │ │ ├── bam.py │ │ │ ├── bootkey.py │ │ │ ├── codepage.py │ │ │ ├── computer_name.py │ │ │ ├── crash_dump.py │ │ │ ├── diag_sr.py │ │ │ ├── disablelastaccess.py │ │ │ ├── external/ │ │ │ │ ├── ShimCacheParser.py │ │ │ │ └── __init__.py │ │ │ ├── host_domain_name.py │ │ │ ├── lsa_packages.py │ │ │ ├── mountdev.py │ │ │ ├── network_data.py │ │ │ ├── pagefile.py │ │ │ ├── pending_file_rename.py │ │ │ ├── previous_winver.py │ │ │ ├── processor_architecture.py │ │ │ ├── routes.py │ │ │ ├── safeboot_configuration.py │ │ │ ├── services.py │ │ │ ├── shares.py │ │ │ ├── shimcache.py │ │ │ ├── shutdown.py │ │ │ ├── timezone_data.py │ │ │ ├── timezone_data2.py │ │ │ ├── usb_devices.py │ │ │ ├── usbstor.py │ │ │ └── wdigest.py │ │ ├── usrclass/ │ │ │ ├── __init__.py │ │ │ └── shellbags_usrclass.py │ │ ├── utils.py │ │ ├── validated_plugins.json │ │ └── validation_status.py │ ├── py.typed │ ├── recovery.py │ ├── regdiff.py │ ├── registry.py │ ├── security_utils.py │ ├── structs.py │ └── utils.py ├── regipy_mcp_server/ │ ├── README.md │ ├── claude_desktop_config.example.json │ ├── server.py │ └── test_local.py ├── regipy_tests/ │ ├── __init__.py │ ├── cli_tests.py │ ├── conftest.py │ ├── data/ │ │ ├── BCD.xz │ │ ├── NTUSER-WSL.DAT.xz │ │ ├── NTUSER.DAT.xz │ │ ├── NTUSER_BAGMRU.DAT.xz │ │ ├── NTUSER_modified.DAT.xz │ │ ├── NTUSER_with_winscp.DAT.xz │ │ ├── SAM.xz │ │ ├── SECURITY.xz │ │ ├── SOFTWARE.xz │ │ ├── SYSTEM.xz │ │ ├── SYSTEM_2.xz │ │ ├── SYSTEM_B.LOG1.xz │ │ ├── SYSTEM_B.LOG2.xz │ │ ├── SYSTEM_B.xz │ │ ├── SYSTEM_WIN_10_1709.xz │ │ ├── UsrClass.dat.LOG1.xz │ │ ├── UsrClass.dat.LOG2.xz │ │ ├── UsrClass.dat.xz │ │ ├── amcache.hve.xz │ │ ├── corrupted_system_hive.xz │ │ ├── ntuser_software_partial.xz │ │ ├── transactions_NTUSER.DAT.xz │ │ ├── transactions_ntuser.dat.log1.xz │ │ └── transactions_ntuser.dat.log2.xz │ ├── profiling.py │ ├── test_packaging.py │ ├── test_utils.py │ ├── tests.py │ └── validation/ │ ├── plugin_validation.md │ ├── plugin_validation.py │ ├── utils.py │ ├── validation.py │ └── validation_tests/ │ ├── __init_.py │ ├── active_control_set_validation.py │ ├── amcache_validation.py │ ├── app_paths_plugin_validation.py │ ├── backuprestore_plugin_validation.py │ ├── bam_validation.py │ ├── boot_entry_list_plugin_validation.py │ ├── boot_key_plugin_validation.py │ ├── codepage_validation.py │ ├── computer_name_plugin_validation.py │ ├── crash_dump_validation.py │ ├── diag_sr_validation.py │ ├── disable_last_access_validation.py │ ├── disablesr_plugin_validation.py │ ├── domain_sid_plugin_validation.py │ ├── execution_policy_plugin_validation.py │ ├── host_domain_name_plugin_validation.py │ ├── image_file_execution_options_validation.py │ ├── installed_programs_ntuser_validation.py │ ├── installed_programs_software_plugin_validation.py │ ├── last_logon_plugin_validation.py │ ├── local_sid_plugin_validation.py │ ├── lsa_packages_plugin_validation.py │ ├── mounted_devices_plugin_validation.py │ ├── network_data_plugin_validation.py │ ├── network_drives_plugin_validation.py │ ├── networklist_plugin_validation.py │ ├── ntuser_classes_installer_plugin_validation.py │ ├── ntuser_persistence_validation.py │ ├── ntuser_userassist_validation.py │ ├── pagefile_plugin_validation.py │ ├── previous_winver_plugin_validation.py │ ├── print_demon_plugin_validation.py │ ├── processor_architecture_validation.py │ ├── profile_list_plugin_validation.py │ ├── ras_tracing_plugin_validation.py │ ├── routes_validation.py │ ├── safeboot_configuration_validation.py │ ├── samparse_plugin_validation.py │ ├── services_plugin_validation.py │ ├── shell_bag_ntuser_plugin_validation.py │ ├── shell_bag_usrclass_plugin_validation.py │ ├── shimcache_validation.py │ ├── shutdown_validation.py │ ├── software_classes_installer_plugin_validation.py │ ├── software_persistence_validation.py │ ├── spp_clients_plugin_validation.py │ ├── susclient_plugin_validation.py │ ├── terminal_services_history_validation.py │ ├── timezone_data2_validation.py │ ├── timezone_data_validation.py │ ├── typed_paths_plugin_validation.py │ ├── typed_urls_plugin_validation.py │ ├── uac_status_plugin_validation.py │ ├── usb_devices_plugin_validation.py │ ├── usbstor_plugin_validation.py │ ├── wdigest_plugin_validation.py │ ├── windows_defender_plugin_validation.py │ ├── winrar_plugin_validation.py │ ├── winscp_saved_sessions_plugin_validation.py │ ├── winver_plugin_validation.py │ ├── word_wheel_query_ntuser_validation.py │ └── wsl_plugin_validation.py ├── renovate.json └── requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # Python dependencies - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 5 commit-message: prefix: "deps" # GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" commit-message: prefix: "ci" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master] pull_request: branches: [master] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install ruff run: pip install ruff - name: Run ruff linter run: ruff check . - name: Run ruff formatter check run: ruff format --check . test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Cache pip dependencies uses: actions/cache@v5 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.python-version }}- ${{ runner.os }}-pip- - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[full,dev]" - name: Run tests with pytest run: pytest -v regipy_tests/tests.py - name: Run CLI tests run: pytest -v regipy_tests/cli_tests.py - name: Run packaging tests run: pytest -v regipy_tests/test_packaging.py - name: Run plugin validation run: PYTHONPATH=. python regipy_tests/validation/plugin_validation.py validation-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[full,dev]" - name: Generate plugin validation documentation run: PYTHONPATH=. python regipy_tests/validation/plugin_validation.py - name: Upload validation documentation uses: actions/upload-artifact@v6 with: name: plugin-validation-docs path: regipy_tests/validation/plugin_validation.md type-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[full,dev]" - name: Run mypy run: mypy regipy/ --ignore-missing-imports continue-on-error: true # Allow failures while adding type hints incrementally security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[full,dev]" pip install pip-audit cyclonedx-bom - name: Run pip-audit (vulnerability scan) run: pip-audit --skip-editable - name: Generate SBOM (CycloneDX) run: | cyclonedx-py environment -o sbom.json --of JSON cyclonedx-py environment -o sbom.xml --of XML - name: Upload SBOM uses: actions/upload-artifact@v6 with: name: sbom path: | sbom.json sbom.xml ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to PyPI on: release: types: [published] permissions: contents: write # Needed to attach assets to release jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build cyclonedx-bom - name: Build package run: python -m build - name: Generate SBOM (CycloneDX) run: | pip install -e ".[full]" cyclonedx-py environment -o sbom.json --of JSON cyclonedx-py environment -o sbom.xml --of XML - name: Upload build artifacts uses: actions/upload-artifact@v6 with: name: dist path: dist/ - name: Upload SBOM artifacts uses: actions/upload-artifact@v6 with: name: sbom path: | sbom.json sbom.xml publish: needs: build runs-on: ubuntu-latest environment: pypi permissions: id-token: write # Required for trusted publishing contents: write # Required to attach assets to release steps: - name: Download build artifacts uses: actions/download-artifact@v7 with: name: dist path: dist/ - name: Download SBOM artifacts uses: actions/download-artifact@v7 with: name: sbom path: sbom/ # Uses trusted publishing if configured, otherwise falls back to token - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: # Fallback to API token if trusted publishing is not configured # To use trusted publishing, configure it at https://pypi.org/manage/account/publishing/ password: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true - name: Attach SBOM to GitHub Release uses: softprops/action-gh-release@v2 with: files: | sbom/sbom.json sbom/sbom.xml ================================================ FILE: .github/workflows/scorecard.yml ================================================ name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. branch_protection_rule: # To ensure Maintained check is refreshed periodically. schedule: - cron: '21 5 * * 2' push: branches: [ "master" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: security-events: write # Needed to upload results to code scanning id-token: write # Needed for publishing to OSSF API # contents: read # Uncomment for private repos # actions: read # Uncomment for private repos steps: - name: "Checkout code" uses: actions/checkout@v6 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@v2.4.3 with: results_file: results.sarif results_format: sarif publish_results: true # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Optional: for private repos or enabling Branch-Protection on public ones - name: "Upload artifact" uses: actions/upload-artifact@v6 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ *.ipynb # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so .idea .vscode .claude/* # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .DS_Store ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.4 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.13.0 hooks: - id: mypy additional_dependencies: [] args: [--ignore-missing-imports] pass_filenames: false entry: mypy regipy/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [6.1.0] - 2025-12-27 ### Added - **22 new plugins** covering additional forensic artifacts: - **NTUSER plugins**: `recentdocs`, `comdlg32`, `runmru`, `muicache`, `appkeys`, `sysinternals`, `putty` - **SOFTWARE plugins**: `app_paths`, `appinit_dlls`, `appcert_dlls`, `appcompat_flags`, `windows_defender`, `powershell_logging`, `execution_policy`, `networklist` - **SYSTEM plugins**: `usb_devices`, `mounted_devices`, `shares`, `pagefile`, `lsa_packages`, `pending_file_rename` - **SAM plugins**: `samparse` - Parses user accounts with login times, password info, account flags - **Plugin validation system** - Plugins are now tracked for validation status - Validation status stored in `validated_plugins.json`, generated by the test framework and shipped with the package - `is_plugin_validated()` function to check validation status - Unvalidated plugins log a warning when executed - **`--include-unvalidated` CLI flag** for `regipy-plugins-run` command - By default, only validated plugins are executed - Use this flag to include plugins that don't have validation test cases - **`include_unvalidated` parameter** for `run_relevant_plugins()` function - Default: `False` (only validated plugins run) - Set to `True` to include unvalidated plugins ### Changed - **Default plugin behavior**: Only validated plugins run by default. This is a safer default as unvalidated plugins may return incomplete or inaccurate data. - Updated README with comprehensive plugin list organized by hive type - Updated plugin validation documentation ## [6.0.0] - 2025-12-25 ### Breaking Changes - **Minimum Python version raised to 3.9** - Dropped support for Python 3.6, 3.7, and 3.8 - **Removed `attrs` dependency** - All data classes (`Cell`, `VKRecord`, `LIRecord`, `Value`, `Subkey`) now use Python's built-in `dataclasses` module instead of `attrs` - If you used `attr.asdict()` on these classes, switch to `dataclasses.asdict()` - If you used `attr.fields()` or other attrs introspection, switch to `dataclasses.fields()` - **Removed `setup.py`** - Package now uses `pyproject.toml` exclusively (PEP 517/518) ### Added - `pyproject.toml` with full PEP 621 metadata - `py.typed` marker for PEP 561 type checking support - Pre-commit configuration with ruff and mypy hooks - Consolidated CI workflow with test matrix for Python 3.9-3.13 - Development documentation in README ### Changed - Migrated from `flake8` to `ruff` for linting and formatting - Modernized Python syntax throughout codebase (f-strings, type hints, import sorting) - Consolidated GitHub Actions workflows into unified `ci.yml` and `publish.yml` - Updated all GitHub Actions to latest versions (v4/v5) ### Removed - `setup.py` (replaced by `pyproject.toml`) - `.flake8` configuration (replaced by ruff config in `pyproject.toml`) - Legacy GitHub workflow files (`python-package.yml`, `python-publish.yml`, `tests.yml`) ## [5.2.0] and earlier See [GitHub Releases](https://github.com/mkorman90/regipy/releases) for previous versions. ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md - regipy > OS-independent Python library for parsing offline Windows registry hives ## Project Overview regipy is a forensic-focused library for parsing Windows registry hive files (files with REGF header). It's designed for digital forensics and incident response (DFIR) workflows, providing both a Python API and CLI tools. ### Core Capabilities - Parse offline registry hives without Windows dependencies - Recursive traversal of keys and values from any path - Transaction log recovery (dirty hive reconstruction) - Hive comparison/diffing (like RegShot) - Extensible plugin system for artifact extraction - Timeline generation for forensic analysis ## Architecture ``` regipy/ ├── registry.py # Core RegistryHive class - entry point for all parsing ├── structs.py # Binary struct definitions (REGF header, NK/VK records, etc.) ├── hive_types.py # Hive type constants (NTUSER, SYSTEM, SOFTWARE, SAM, etc.) ├── exceptions.py # Custom exceptions (RegistryKeyNotFoundException, etc.) ├── utils.py # Helpers: convert_wintime, boomerang_stream, etc. ├── recovery.py # Transaction log application logic ├── plugins/ │ ├── plugin.py # Base Plugin class - all plugins inherit from this │ ├── utils.py # run_relevant_plugins() - auto-detects hive and runs matching plugins │ ├── ntuser/ # NTUSER.DAT plugins (persistence, typed_urls, user_assist, etc.) │ ├── system/ # SYSTEM hive plugins (computer_name, shimcache, bam, bootkey, etc.) │ ├── software/ # SOFTWARE hive plugins (installed_programs, profilelist, etc.) │ ├── sam/ # SAM hive plugins (local_sid, etc.) │ ├── security/ # SECURITY hive plugins │ ├── amcache/ # Amcache.hve plugins │ └── usrclass/ # UsrClass.dat plugins (shellbags) ├── cli.py # CLI entry points regipy_tests/ ├── data/ # Test hive files (often .xz compressed) ├── validation/ # ValidationCase framework for plugin testing docs/ └── PLUGINS.md # Plugin development guide ``` ## Key Classes and Patterns ### RegistryHive The main entry point. Handles hive parsing, key navigation, and value retrieval. ```python from regipy.registry import RegistryHive reg = RegistryHive('/path/to/NTUSER.DAT') # Navigate to a key key = reg.get_key(r'Software\Microsoft\Windows\CurrentVersion\Run') # Get values values = key.get_values(as_json=True) # Iterate subkeys for sk in key.iter_subkeys(): print(sk.name, sk.header.last_modified) # Recursive traversal for entry in reg.recurse_subkeys(as_json=True): print(entry) # Control sets (SYSTEM hive) for path in reg.get_control_sets(r'Control\ComputerName\ComputerName'): # Yields: ControlSet001\Control\..., ControlSet002\Control\..., etc. pass ``` ### Plugin System Plugins inherit from `Plugin` base class and define: - `NAME`: Snake_case identifier - `DESCRIPTION`: Human-readable description - `COMPATIBLE_HIVE`: Hive type constant from `hive_types.py` - `run()`: Extraction logic, appends results to `self.entries` ```python from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin class MyPlugin(Plugin): NAME = 'my_plugin' DESCRIPTION = 'Extract something useful' COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: key = self.registry_hive.get_key(r'Software\MyKey') for value in key.get_values(as_json=self.as_json): self.entries.append(value) except RegistryKeyNotFoundException: pass # Key doesn't exist in this hive ``` ### Timestamp Handling Always use `convert_wintime()` for Windows FILETIME conversion: ```python from regipy.utils import convert_wintime timestamp = convert_wintime(key.header.last_modified, as_json=True) # Returns ISO format string when as_json=True, datetime object otherwise ``` ### Transaction Log Recovery ```python from regipy.recovery import apply_transaction_logs apply_transaction_logs( hive_path='/path/to/NTUSER.DAT', transaction_log_path='/path/to/NTUSER.DAT.LOG1', restored_hive_path='/path/to/recovered.DAT' ) ``` ## Hive Types Defined in `hive_types.py`: | Constant | Typical Files | |----------|---------------| | `NTUSER_HIVE_TYPE` | NTUSER.DAT | | `SYSTEM_HIVE_TYPE` | SYSTEM | | `SOFTWARE_HIVE_TYPE` | SOFTWARE | | `SAM_HIVE_TYPE` | SAM | | `SECURITY_HIVE_TYPE` | SECURITY | | `USRCLASS_HIVE_TYPE` | UsrClass.dat | | `AMCACHE_HIVE_TYPE` | Amcache.hve | | `BCD_HIVE_TYPE` | BCD | ## CLI Tools - `regipy-parse-header` - Display hive header, validate checksums - `regipy-dump` - Export entire hive to JSON (or timeline with `-t`) - `regipy-plugins-run` - Auto-detect hive type and run relevant plugins - `regipy-diff` - Compare two hives, output differences to CSV - `regipy-process-transaction-logs` - Apply transaction logs to recover dirty hive ## Development Guidelines ### Adding a New Plugin 1. Create file in appropriate `plugins//` directory 2. Inherit from `Plugin`, set `NAME`, `DESCRIPTION`, `COMPATIBLE_HIVE` 3. Implement `run()` method - append results to `self.entries` 4. Use try/except for `RegistryKeyNotFoundException` (key may not exist) 5. Add validation case in `regipy_tests/validation/` ### Validation Cases Required for all new plugins: ```python from regipy_tests.validation.validation import ValidationCase from regipy.plugins.system.my_plugin import MyPlugin class MyPluginValidationCase(ValidationCase): plugin = MyPlugin test_hive_file_name = "SYSTEM_WIN_10.xz" # Must be in regipy_tests/data/ # Option 1: Check for presence of specific entries expected_entries = [ {"field": "value", ...} ] # Option 2: Exact match exact_expected_result = [...] expected_entries_count = 5 # Optional count validation ``` ### Code Style - Use `logging` module (not `logbook` in newer code) - Prefer `as_json=True` parameter for JSON-serializable output - Handle corrupted values gracefully (`is_corrupted` field) - Document Windows-specific quirks in comments ## Installation Variants ```bash pip install regipy[full] # All dependencies including compiled ones pip install regipy # Minimal dependencies pip install -e .[full] # Development install ``` ## Testing ```bash pytest regipy_tests/ ``` Test hives are stored as `.xz` compressed files in `regipy_tests/data/`. ## Common Forensic Artifacts by Hive **NTUSER.DAT**: User activity - Run/RunOnce keys (persistence) - TypedURLs (browser history) - UserAssist (program execution) - RecentDocs, MRU lists **SYSTEM**: System configuration - ComputerName - Shimcache/AppCompatCache (execution history) - BAM/DAM (background activity) - Services, network interfaces **SOFTWARE**: Installed software - Uninstall keys - ProfileList (user profiles) - Installed programs **Amcache.hve**: Application compatibility - File execution with SHA1 hashes - Driver information **UsrClass.dat**: Shell data - Shellbags (folder access history) ## MCP Server Integration regipy includes an MCP (Model Context Protocol) server that enables natural language forensic analysis through Claude Desktop or other MCP-compatible AI assistants. ### What it Does The MCP server bridges Claude and regipy's plugin ecosystem, allowing investigators to: - Ask forensic questions in plain English instead of remembering CLI syntax - Auto-detect hive types from a directory of collected registry files - Leverage all 75+ plugins without knowing which plugin extracts which artifact - Get correlated results across multiple hives in a single conversational query ### Example Workflow Instead of: ```bash regipy-plugins-run SYSTEM -o system_output.json regipy-plugins-run NTUSER.DAT -o ntuser_output.json # manually correlate results... ``` You can ask: > "What persistence mechanisms exist on this machine?" Claude will automatically run both `software_persistence` and `ntuser_persistence` plugins and synthesize the results. ### Setup 1. Install regipy with MCP support 2. Configure Claude Desktop to use the regipy MCP server 3. Point at your evidence directory 4. Start investigating conversationally ### Design Philosophy The MCP server exposes plugin metadata (names, descriptions, compatible hives) to Claude, letting it reason about which plugins to run based on the investigator's natural language questions. This means: - New plugins automatically become available to Claude without prompt updates - Claude can chain multiple plugins when a question spans artifacts - Investigators can follow their instincts with follow-up questions For more details, see the blog post: [Regipy MCP: Natural Language Registry Forensics with Claude](https://medium.com/dfir-dudes/regipy-mcp-natural-language-registry-forensics-with-claude-984d378784d6) ## External References ### Microsoft Documentation - [Registry Hives (Win32)](https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-hives) - Official overview of hive concepts, file formats, and locations - [Windows Registry for Advanced Users](https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users) - Detailed reference on registry structure, data types, and hive files - [Inside the Registry](https://learn.microsoft.com/en-us/previous-versions/cc750583(v=technet.10)) - Mark Russinovich's deep dive into registry internals (cells, bins, hive file format) ### Community Resources - [Windows Registry File Format Specification](https://github.com/msuhanov/regf) - Maxim Suhanov's comprehensive REGF format documentation (the de facto standard for forensic tool authors) - [Google Project Zero: Registry Hives](https://projectzero.google/2024/10/the-windows-registry-adventure-4-hives.html) - Deep technical analysis of hive internals and security boundaries ### Registry File Locations | Hive | File Path | |------|-----------| | SYSTEM | `%SystemRoot%\System32\config\SYSTEM` | | SOFTWARE | `%SystemRoot%\System32\config\SOFTWARE` | | SAM | `%SystemRoot%\System32\config\SAM` | | SECURITY | `%SystemRoot%\System32\config\SECURITY` | | DEFAULT | `%SystemRoot%\System32\config\DEFAULT` | | NTUSER.DAT | `%UserProfile%\NTUSER.DAT` | | UsrClass.dat | `%LocalAppData%\Microsoft\Windows\UsrClass.dat` | | Amcache.hve | `%SystemRoot%\AppCompat\Programs\Amcache.hve` | ### Transaction Logs Registry hives use transaction logs (`.LOG1`, `.LOG2`) for crash recovery. When a hive's sequence numbers don't match (primary ≠ secondary), the hive is "dirty" and transaction logs should be applied before analysis. regipy handles this via: ```bash regipy-process-transaction-logs NTUSER.DAT -p ntuser.dat.log1 -s ntuser.dat.log2 -o recovered.DAT ``` Or programmatically via `regipy.recovery.apply_transaction_logs()`. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ # Include the README include *.md # Include the license file include LICENSE.txt recursive-include docs * ================================================ FILE: README.md ================================================ # regipy [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/mkorman90/regipy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/mkorman90/regipy) > **⚠️ Breaking Changes in v6.0.0** > > Version 6.0.0 includes significant modernization changes: > - **Python 3.9+ required** - Dropped support for Python 3.6, 3.7, and 3.8 > - **`attrs` library removed** - Data classes now use Python's built-in `dataclasses` module > - If your code imports internal classes (`Cell`, `VKRecord`, `Value`, `Subkey`) and uses `attrs` functions like `attr.asdict()`, switch to `dataclasses.asdict()` > > See the [CHANGELOG](CHANGELOG.md) for full details. Regipy is a python library for parsing offline registry hives (Hive files with REGF header). regipy has a lot of capabilities: * Use as a library: * Recurse over the registry hive, from root or a given path and get all subkeys and values * Read specific subkeys and values * Apply transaction logs on a registry hive * Command Line Tools * Dump an entire registry hive to json * Apply transaction logs on a registry hive * Compare registry hives * Execute plugins from a robust plugin system (i.e: amcache, shimcache, extract computer name...) **Requires Python 3.9 or higher.** ## Installation Regipy latest version can be installed from pypi: ```bash pip install regipy[full] ``` NOTE: ``regipy[full]`` installs dependencies that require compilation tools and might take some time. It is possible to install a version with relaxed dependencies, by omitting the ``[full]``. Also, it is possible to install from source by cloning the repository and executing: ```bash pip install --editable .[full] ``` ## CLI #### Parse the header: ```bash regipy-parse-header ~/Documents/TestEvidence/Registry/SYSTEM ``` Example output: ``` ╒════════════════════════╤══════════╕ │ signature │ b'regf' │ ├────────────────────────┼──────────┤ │ primary_sequence_num │ 11639 │ ├────────────────────────┼──────────┤ │ secondary_sequence_num │ 11638 │ ├────────────────────────┼──────────┤ │ last_modification_time │ 0 │ ├────────────────────────┼──────────┤ │ major_version │ 1 │ ├────────────────────────┼──────────┤ │ minor_version │ 5 │ ├────────────────────────┼──────────┤ │ file_type │ 0 │ ├────────────────────────┼──────────┤ │ file_format │ 1 │ ├────────────────────────┼──────────┤ │ root_key_offset │ 32 │ ├────────────────────────┼──────────┤ │ hive_bins_data_size │ 10534912 │ ├────────────────────────┼──────────┤ │ clustering_factor │ 1 │ ├────────────────────────┼──────────┤ │ file_name │ SYSTEM │ ├────────────────────────┼──────────┤ │ checksum │ 0 │ ╘════════════════════════╧══════════╛ [2019-02-09 13:46:12.111654] WARNING: regipy.cli: Hive is not clean! You should apply transaction logs ``` * When parsing the header of a hive, also checksum validation and transaction validations are done #### Dump entire hive to disk (this might take some time) ```bash regipy-dump ~/Documents/TestEvidence/Registry/NTUSER-CCLEANER.DAT -o /tmp/output.json ``` regipy-dump util can also output a timeline instead of a JSON, by adding the `-t` flag #### Run relevant plugins on Hive ```bash regipy-plugins-run ~/Documents/TestEvidence/Registry/SYSTEM -o /tmp/plugins_output.json ``` The hive type will be detected automatically and the relevant plugins will be executed. [**See the plugins section for more information**](docs/PLUGINS.md) #### Compare registry hives Compare registry hives of the same type and output to CSV (if `-o` is not specified output will be printed to screen) ```bash regipy-diff NTUSER.dat NTUSER_modified.dat -o /tmp/diff.csv ``` Example output: ``` [2019-02-11 19:49:18.824245] INFO: regipy.cli: Comparing NTUSER.DAT vs NTUSER_modified.DAT ╒══════════════╤══════════════╤════════════════════════════════════════════════════════════════════════════════╤════════════════════════════════════════════════╕ │ difference │ first_hive │ second_hive │ description │ ╞══════════════╪══════════════╪════════════════════════════════════════════════════════════════════════════════╪════════════════════════════════════════════════╡ │ new_subkey │ │ 2019-02-11T19:46:31.832134+00:00 │ \Software\Microsoft\legitimate_subkey │ ├──────────────┼──────────────┼────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤ │ new_value │ │ not_a_malware: c:\temp\legitimate_binary.exe @ 2019-02-11 19:45:25.516346+00:00 │ \Software\Microsoft\Windows\CurrentVersion\Run │ ╘══════════════╧══════════════╧════════════════════════════════════════════════════════════════════════════════╧════════════════════════════════════════════════╛ [2019-02-11 19:49:18.825328] INFO: regipy.cli: Detected 2 differences ``` ## Recover a registry hive, using transaction logs: ```bash regipy-process-transaction-logs NTUSER.DAT -p ntuser.dat.log1 -s ntuser.dat.log2 -o recovered_NTUSER.dat ``` After recovering, compare the hives with registry-diff to see what changed ## Using as a library #### Initiate the registry hive object ```python from regipy.registry import RegistryHive reg = RegistryHive('/Users/martinkorman/Documents/TestEvidence/Registry/Vibranium-NTUSER.DAT') ``` #### Iterate recursively over the entire hive, from root key ```python for entry in reg.recurse_subkeys(as_json=True): print(entry) ``` #### Iterate over a key and get all subkeys and their modification time: ```python for sk in reg.get_key('Software').iter_subkeys(): print(sk.name, convert_wintime(sk.header.last_modified).isoformat()) Adobe 2019-02-03T22:05:32.525965 AppDataLow 2019-02-03T22:05:32.526047 McAfee 2019-02-03T22:05:32.526140 Microsoft 2019-02-03T22:05:32.526282 Netscape 2019-02-03T22:05:32.526352 ODBC 2019-02-03T22:05:32.526521 Policies 2019-02-03T22:05:32.526592 ``` #### Get the values of a key: ```python reg.get_key('Software\Microsoft\Internet Explorer\BrowserEmulation').get_values(as_json=True) [{'name': 'CVListTTL', 'value': 0, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'UnattendLoaded', 'value': 0, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'TLDUpdates', 'value': 0, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'CVListXMLVersionLow', 'value': 2097211, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'CVListXMLVersionHigh', 'value': None, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'CVListLastUpdateTime', 'value': None, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'IECompatVersionHigh', 'value': None, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'IECompatVersionLow', 'value': 2097211, 'value_type': 'REG_DWORD', 'is_corrupted': False}, {'name': 'StaleCompatCache', 'value': 0, 'value_type': 'REG_DWORD', 'is_corrupted': False}] ``` #### Use as a plugin: ```python from regipy.plugins.ntuser.ntuser_persistence import NTUserPersistencePlugin NTUserPersistencePlugin(reg, as_json=True).run() { 'Software\\Microsoft\\Windows\\CurrentVersion\\Run': { 'timestamp': '2019-02-03T22:10:52.655462', 'values': [{ 'name': 'Sidebar', 'value': '%ProgramFiles%\\Windows Sidebar\\Sidebar.exe /autoRun', 'value_type': 'REG_EXPAND_SZ', 'is_corrupted': False }] } } ``` #### Run all relevant plugins for a specific hive ```python from regipy.plugins.utils import run_relevant_plugins reg = RegistryHive('/Users/martinkorman/Documents/TestEvidence/Registry/SYSTEM') run_relevant_plugins(reg, as_json=True) { 'routes': {}, 'computer_name': [{ 'control_set': 'ControlSet001\\Control\\ComputerName\\ComputerName', 'computer_name': 'DESKTOP-5EG84UG', 'timestamp': '2019-02-03T22:19:28.853219' }] } ``` ## Validation cases [Validation cases report](regipy_tests/validation/plugin_validation.md) All new plugins should have one or more basic validation cases (which can be expanded in the future), for example: ```python from regipy.plugins.system.bam import BAMPlugin from regipy_tests.validation.validation import ValidationCase class NTUserUserAssistValidationCase(ValidationCase): # define your plugin class plugin = BAMPlugin # define the test file name, which should be present in `regipy_tests/data` test_hive_file_name = "SYSTEM_WIN_10_1709.xz" # Use `expected_entries` to test for presence of a few samples from the plugin results expected_entries = [ { "sequence_number": 9, "version": 1, "sid": "S-1-5-90-0-1", "executable": "\\Device\\HarddiskVolume2\\Windows\\System32\\dwm.exe", "timestamp": "2020-04-19T09:09:35.731816+00:00", "key_path": "\\ControlSet001\\Services\\bam\\state\\UserSettings\\S-1-5-90-0-1", } ] # OR use `exact_expected_result` to test for an exact result: exact_expected_result = [ { "sequence_number": 9, "version": 1, "sid": "S-1-5-90-0-1", "executable": "\\Device\\HarddiskVolume2\\Windows\\System32\\dwm.exe", "timestamp": "2020-04-19T09:09:35.731816+00:00", "key_path": "\\ControlSet001\\Services\\bam\\state\\UserSettings\\S-1-5-90-0-1", }, { "sequence_number": 8, "version": 1, "sid": "S-1-5-90-0-1", "executable": "\\Device\\HarddiskVolume2\\Windows\\System32\\cmd.exe", "timestamp": "2020-04-19T09:09:34.544224+00:00", "key_path": "\\ControlSet001\\Services\\bam\\state\\UserSettings\\S-1-5-90-0-1", } ] expected_entries_count = 2 ``` ## Development ### Setting up for development ```bash # Clone the repository git clone https://github.com/mkorman90/regipy.git cd regipy # Install in development mode with all dependencies pip install -e ".[full,dev]" # Install pre-commit hooks pre-commit install ``` ### Running tests ```bash # Run all tests pytest # Run specific test files pytest regipy_tests/tests.py pytest regipy_tests/cli_tests.py # Run plugin validation PYTHONPATH=. python regipy_tests/validation/plugin_validation.py ``` ### Code quality ```bash # Run linter ruff check . # Run formatter ruff format . # Run type checker mypy regipy/ ``` ### Testing GitHub Actions Locally To test CI workflow changes locally before pushing, use [act](https://github.com/nektos/act): ```bash # Install act (Fedora) sudo dnf install act-cli # Install act (macOS) brew install act # Install act (other) # See https://nektosact.com/installation/index.html ``` Make sure Docker is running, then: ```bash # List available jobs act -l # Run the lint job act -j lint # Run all jobs for a push event act push # Run the test job with a specific Python version act -j test # Test the build job from publish workflow (simulates a release) act release -j build --eventpath /dev/stdin <<< '{"action": "published"}' ``` Note: Some jobs may require secrets. You can provide them with: ```bash act -j publish --secret PYPI_API_TOKEN=your_token ``` ## License MIT ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | --------- | | 6.x.x | ✅ | | < 6.0 | ❌ | ## Reporting a Vulnerability Please report vulnerabilities via [GitHub Security Advisories](https://github.com/mkorman90/regipy/security/advisories/new). ## Supply Chain Security This project implements several supply chain security measures: - **SBOM**: Each release includes a [CycloneDX](https://cyclonedx.org/) Software Bill of Materials (`sbom.json`, `sbom.xml`) attached to the GitHub release - **Dependency Scanning**: [pip-audit](https://github.com/pypa/pip-audit) runs on every CI build to detect known vulnerabilities - **Dependabot**: Automated dependency updates are enabled for both Python packages and GitHub Actions - **Trusted Publishing**: PyPI releases use [trusted publishing](https://docs.pypi.org/trusted-publishers/) via GitHub Actions OIDC ================================================ FILE: docs/PLUGINS.md ================================================ ## regipy Plugins * The plugin system is a robust and extensive feature that auto-detects the hive type and execute the relevant plugins. To see a comprehensive list, please refer to the [Validation cases report](../validation/plugin_validation.md) ## Contributing new plugins Adding a new plugin is very straight forward: 1. Copy the `regipy/plugins/plugin_template.py` file to the relevant folder (according to hive type) 2. Update the code: * Update the `NAME` parameter and the Class name accordingly (NAME in snake case, Class name in camel case) * Feel free to use/add any utility function to `regipy/utils.py` * Import your class in `regipy/plugins/__init__.py` 3. Add a [validation case](../README.md#validation-cases). This is mandatory and replaces the old regipy tests. ================================================ FILE: docs/README.rst ================================================ regipy ========== Regipy is a python library for parsing offline registry hives! **Requires Python 3.9 or higher.** Features: * Use as a library * Recurse over the registry hive, from root or a given path and get all subkeys and values * Read specific subkeys and values * Apply transaction logs on a registry hive * Command Line Tools: * Dump an entire registry hive to json * Apply transaction logs on a registry hive * Compare registry hives * Execute plugins from a robust plugin system (i.e: amcache, shimcache, extract computer name...) :Project page: https://github.com/mkorman90/regipy Using as a library: -------------------- .. code:: python from regipy.registry import RegistryHive reg = RegistryHive('/Users/martinkorman/Documents/TestEvidence/Registry/Vibranium-NTUSER.DAT') # Iterate over a registry hive recursively: for entry in reg.recurse_subkeys(as_json=True): print(entry) # Iterate over a key and get all subkeys and their modification time: for sk in reg.get_key('Software').get_subkeys(): print(sk.name, convert_wintime(sk.header.last_modified).isoformat()) # Get values from a specific registry key: reg.get_key('Software\Microsoft\Internet Explorer\BrowserEmulation').get_values(as_json=True) # Use plugins: from regipy.plugins.ntuser.ntuser_persistence import NTUserPersistencePlugin NTUserPersistencePlugin(reg, as_json=True).run() # Run all validated plugins on a registry hive: run_relevant_plugins(reg, as_json=True) # Include unvalidated plugins (may return incomplete/inaccurate data): run_relevant_plugins(reg, as_json=True, include_unvalidated=True) Install ^^^^^^^ Install regipy and the command line tools dependencies: ``pip install regipy[cli]`` NOTE: using pip with ``regipy[cli]`` instead of the plain ``regipy`` is a significant change from version 1.9.x For using regipy as a library, install only ``regipy`` which comes with fewer dependencies: ``pip install regipy`` Plugin Validation ^^^^^^^^^^^^^^^^^ Regipy plugins are validated using test cases to ensure they return accurate data. By default, only validated plugins are executed when using ``run_relevant_plugins()`` or the CLI tools. To include unvalidated plugins (which may return incomplete or inaccurate data): .. code:: python # As a library: run_relevant_plugins(reg, as_json=True, include_unvalidated=True) .. code:: bash # CLI: regipy-plugins-run /path/to/hive -o output.json --include-unvalidated Unvalidated plugins will log a warning when executed. Use them at your own discretion. Available Plugins ^^^^^^^^^^^^^^^^^ **NTUSER Plugins:** * ``user_assist`` - Parses User Assist execution history * ``typed_urls`` - Retrieves typed URLs from IE history * ``typed_paths`` - Retrieves typed file paths * ``ntuser_persistence`` - Retrieves persistence entries (Run, RunOnce, etc.) * ``shellbag_plugin`` - Parses Shellbag items * ``network_drives_plugin`` - Retrieves mapped network drives * ``terminal_services_history`` - Parses RDP/Terminal Server client data * ``winrar_plugin`` - Parses WinRAR archive history * ``winscp_saved_sessions`` - Retrieves WinSCP saved sessions * ``word_wheel_query`` - Parses Windows Search word wheel query * ``wsl`` - Gets WSL distribution information * ``installed_programs_ntuser`` - Retrieves installed programs * ``ntuser_classes_installer`` - Parses class installer information * ``recentdocs`` - Parses recently opened documents * ``comdlg32`` - Parses Open/Save dialog MRU lists * ``runmru`` - Parses Run dialog MRU list * ``muicache`` - Parses MUI Cache (application display names) * ``appkeys`` - Parses application keyboard shortcuts * ``sysinternals`` - Parses Sysinternals tools EULA acceptance * ``putty`` - Parses PuTTY sessions and SSH host keys **SOFTWARE Plugins:** * ``installed_programs_software`` - Retrieves installed programs * ``profilelist_plugin`` - Parses user profile information * ``uac_plugin`` - Gets User Access Control settings * ``winver_plugin`` - Gets OS version information * ``last_logon_plugin`` - Gets last logon information * ``image_file_execution_options`` - Retrieves IFEO entries * ``print_demon_plugin`` - Gets installed printer ports * ``ras_tracing`` - Gets tracing/debugging configuration * ``disablesr_plugin`` - Gets system restore disable status * ``spp_clients_plugin`` - Determines volumes monitored by VSS * ``susclient_plugin`` - Extracts Windows Update client info * ``software_classes_installer`` - Parses class installer information * ``software_plugin`` - Retrieves persistence entries * ``app_paths`` - Parses application paths registry entries * ``appinit_dlls`` - Parses AppInit_DLLs persistence entries * ``appcert_dlls`` - Parses AppCertDLLs persistence entries * ``appcompat_flags`` - Parses application compatibility flags * ``windows_defender`` - Parses Windows Defender configuration * ``powershell_logging`` - Parses PowerShell logging configuration * ``execution_policy`` - Parses PowerShell execution policies * ``networklist`` - Parses network connection history **SYSTEM Plugins:** * ``shimcache`` - Parses Shimcache/AppCompatCache * ``services`` - Enumerates services and parameters * ``usbstor_plugin`` - Parses connected USB storage devices * ``background_activity_moderator`` - Gets BAM execution data * ``network_data`` - Gets network interface configuration * ``routes`` - Gets network routes * ``computer_name`` - Gets the computer name * ``timezone_data`` / ``timezone_data2`` - Gets timezone configuration * ``safeboot_configuration`` - Gets safeboot configuration * ``active_control_set`` - Gets the active control set * ``backuprestore_plugin`` - Gets backup/restore exclusion entries * ``processor_architecture`` - Gets processor architecture info * ``previous_winver_plugin`` - Gets previous Windows version info * ``codepage`` - Gets system codepage information * ``host_domain_name`` - Gets host and domain name * ``crash_dump`` - Gets crash control information * ``diag_sr`` - Gets diagnostic system restore information * ``disable_last_access`` - Gets LastAccessTime disable status * ``wdigest`` - Gets WDIGEST authentication configuration * ``bootkey`` - Extracts the Windows boot key * ``shutdown`` - Gets shutdown time data * ``usb_devices`` - Parses USB device connection history * ``mounted_devices`` - Parses mounted device information * ``shares`` - Parses network share configuration * ``pagefile`` - Parses pagefile configuration * ``lsa_packages`` - Parses LSA security packages * ``pending_file_rename`` - Parses pending file rename operations **SAM Plugins:** * ``local_sid`` - Extracts the machine local SID * ``samparse`` - Parses user accounts from SAM hive **SECURITY Plugins:** * ``domain_sid`` - Extracts domain name and SID **AMCACHE Plugins:** * ``amcache`` - Parses Amcache application execution history **BCD Plugins:** * ``boot_entry_list`` - Lists Windows BCD boot entries **USRCLASS Plugins:** * ``usrclass_shellbag_plugin`` - Parses USRCLASS Shellbag items ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "regipy" version = "6.2.1" description = "Python Registry Parser" readme = "docs/README.rst" license = {text = "MIT"} authors = [ {name = "Martin G. Korman", email = "martin@centauri.co.il"} ] keywords = ["Python", "Python3", "registry", "windows registry", "registry parser"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Utilities", ] requires-python = ">=3.9" dependencies = [ "construct>=2.10", "inflection>=0.5.1", "pytz", ] [project.optional-dependencies] cli = [ "click>=7.0.0", "tabulate", ] full = [ "click>=7.0.0", "tabulate", "libfwsi-python>=20240315", "libfwps-python>=20240310", ] dev = [ "pytest>=8.0", "pytest-cov", "ruff", "mypy", "pre-commit", "tabulate", # Required for plugin validation "tomli; python_version < '3.11'", # For test_packaging.py on Python 3.9/3.10 ] [project.scripts] regipy-parse-header = "regipy.cli:parse_header" regipy-dump = "regipy.cli:registry_dump" regipy-plugins-run = "regipy.cli:run_plugins" regipy-plugins-list = "regipy.cli:list_plugins" regipy-diff = "regipy.cli:reg_diff" regipy-process-transaction-logs = "regipy.cli:parse_transaction_log" [project.urls] Homepage = "https://github.com/mkorman90/regipy/" Repository = "https://github.com/mkorman90/regipy/" Issues = "https://github.com/mkorman90/regipy/issues" [tool.setuptools] packages = ["regipy", "regipy.plugins", "regipy.plugins.ntuser", "regipy.plugins.system", "regipy.plugins.system.external", "regipy.plugins.software", "regipy.plugins.sam", "regipy.plugins.security", "regipy.plugins.amcache", "regipy.plugins.bcd", "regipy.plugins.usrclass"] include-package-data = true [tool.setuptools.package-data] "regipy.plugins" = ["validated_plugins.json"] [tool.ruff] target-version = "py39" line-length = 128 [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # Pyflakes "I", # isort "UP", # pyupgrade (modernize syntax) "B", # flake8-bugbear "C4", # flake8-comprehensions "SIM", # flake8-simplify ] ignore = [ "E501", # line too long (handled by formatter) "B008", # do not perform function calls in argument defaults "B904", # raise from err (too noisy for existing code) "SIM108", # use ternary operator instead of if-else "SIM115", # use context manager for opening files ] [tool.ruff.lint.per-file-ignores] "regipy_tests/*" = ["B007"] # Unused loop variables in tests are fine "regipy/cli.py" = ["B007"] # subkey_count is used after the loop "regipy/cli_utils.py" = ["B007"] # Loop counter used for progress "regipy/plugins/utils.py" = ["B007"] # Loop counter pattern [tool.ruff.lint.isort] known-first-party = ["regipy"] [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" [tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_ignores = true warn_redundant_casts = true disallow_untyped_defs = false # Start lenient, can increase strictness later check_untyped_defs = true ignore_missing_imports = true [tool.pytest.ini_options] testpaths = ["regipy_tests"] python_files = ["tests.py", "test_*.py", "*_tests.py"] addopts = "-v" [tool.coverage.run] source = ["regipy"] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.:", ] ================================================ FILE: regipy/__init__.py ================================================ from .registry import * # noqa: F401, F403 __title__ = "regipy" __version__ = "6.2.1" ================================================ FILE: regipy/cli.py ================================================ import csv import json import logging import os import time from dataclasses import asdict import click from tabulate import tabulate from regipy.cli_utils import _normalize_subkey_fields, get_filtered_subkeys from regipy.exceptions import RegistryKeyNotFoundException from regipy.plugins.plugin import PLUGINS from regipy.plugins.utils import run_relevant_plugins from regipy.recovery import apply_transaction_logs from regipy.regdiff import compare_hives from regipy.registry import RegistryHive from regipy.utils import _setup_logging, calculate_xor32_checksum logger = logging.getLogger(__name__) @click.command() @click.argument( "hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option("-v", "--verbose", is_flag=True, default=True, help="Verbosity") def parse_header(hive_path, verbose): _setup_logging(verbose=verbose) registry_hive = RegistryHive(hive_path) click.secho(tabulate(registry_hive.header.items(), tablefmt="fancy_grid")) if registry_hive.header.primary_sequence_num != registry_hive.header.secondary_sequence_num: click.secho("Hive is not clean! You should apply transaction logs", fg="red") calculated_checksum = calculate_xor32_checksum(registry_hive._stream.read(508)) if registry_hive.header.checksum != calculated_checksum: click.secho("Hive is not clean! Header checksum does not match", fg="red") @click.command() @click.argument( "hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option( "-o", "output_path", type=click.Path(exists=False, dir_okay=False, resolve_path=True), required=False, ) @click.option("-p", "--registry-path", help="A registry path to start iterating from") @click.option( "-t", "--timeline", is_flag=True, default=False, help="Create a CSV timeline instead", ) @click.option( "-l", "--hive-type", type=click.STRING, required=False, help="Specify a hive type, if it could not be identified for some reason", ) @click.option( "-r", "--partial_hive_path", type=click.STRING, required=False, help='The path from which the partial hive actually starts, for example: -t ntuser -r "/Software" ' "would mean this is actually a HKCU hive, starting from HKCU/Software", ) @click.option("-v", "--verbose", is_flag=True, default=False, help="Verbosity") @click.option( "-d", "--do-not-fetch-values", is_flag=True, default=False, help="Not fetching the values for each subkey makes the iteration way faster. Values count will still be returned", ) @click.option( "-s", "--start-date", type=click.STRING, required=False, help='If "-s" was specified, fetch only values for subkeys starting this timestamp in isoformat', ) @click.option( "-e", "--end-date", type=click.STRING, required=False, help='If "-e" was specified, fetch only values for subkeys until this timestamp in isoformat', ) def registry_dump( hive_path, output_path, registry_path, timeline, hive_type, partial_hive_path, verbose, do_not_fetch_values, start_date, end_date, ): _setup_logging(verbose=verbose) registry_hive = RegistryHive(hive_path, hive_type=hive_type, partial_hive_path=partial_hive_path) start_time = time.monotonic() if registry_path: try: name_key_entry = registry_hive.get_key(registry_path) except RegistryKeyNotFoundException as ex: logger.debug(f"Did not find the key: {ex}") return else: name_key_entry = registry_hive.root if timeline and not output_path: click.secho("You must provide an output path if choosing timeline output!", fg="red") return if output_path: with open(output_path, "w") as output_file: if timeline: csvwriter = csv.DictWriter( output_file, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL, fieldnames=["timestamp", "subkey_name", "values_count", "values"], ) csvwriter.writeheader() for subkey_count, entry in enumerate( get_filtered_subkeys( registry_hive, name_key_entry, fetch_values=not do_not_fetch_values, start_date=start_date, end_date=end_date, ) ): if timeline: csvwriter.writerow( { "subkey_name": entry.path, "timestamp": entry.timestamp, "values_count": entry.values_count, "values": entry.values, } ) else: output_file.write( json.dumps( asdict( entry, ), separators=( ",", ":", ), default=_normalize_subkey_fields, ) ) output_file.write("\n") else: for subkey_count, entry in enumerate( registry_hive.recurse_subkeys(name_key_entry, as_json=True, fetch_values=not do_not_fetch_values) ): click.secho(json.dumps(asdict(entry), indent=4)) click.secho(f"Completed in {time.monotonic() - start_time}s ({subkey_count} subkeys enumerated)") @click.command() @click.argument( "hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option( "-o", "output_path", type=click.Path(exists=False, dir_okay=False, resolve_path=True), required=True, help="Output path for plugins result", ) @click.option( "-p", "--plugins", type=click.STRING, required=False, help="A plugin or list of plugins to execute command separated", ) @click.option( "-t", "--hive-type", type=click.STRING, required=False, help="Specify a hive type, if it could not be identified for some reason", ) @click.option( "-r", "--partial_hive_path", type=click.STRING, required=False, help='The path from which the partial hive actually starts, for example: -t ntuser -r "/Software" ' "would mean this is actually a HKCU hive, starting from HKCU/Software", ) @click.option("-v", "--verbose", is_flag=True, default=False, help="Verbosity") @click.option( "--include-unvalidated", is_flag=True, default=False, help="Include plugins that don't have validation test cases. " "These plugins may return incomplete or inaccurate data. Use at your own risk.", ) def run_plugins(hive_path, output_path, plugins, hive_type, partial_hive_path, verbose, include_unvalidated): _setup_logging(verbose=verbose) registry_hive = RegistryHive(hive_path, hive_type=hive_type, partial_hive_path=partial_hive_path) click.secho(f"Loaded {len(PLUGINS)} plugins", fg="white") if plugins: plugin_names = {x.NAME for x in PLUGINS} plugins = plugins.split(",") plugins = set(plugins) if not plugins.issubset(plugin_names): click.secho( "Invalid plugin names given: {}".format(",".join(set(plugins) - plugin_names)), fg="red", ) click.secho( "Use --help or -h to get list of plugins and their descriptions", fg="red", ) return if include_unvalidated: click.secho( "Warning: Including unvalidated plugins. These may return incomplete or inaccurate data.", fg="yellow", ) # Run relevant plugins plugin_results = run_relevant_plugins( registry_hive, as_json=True, plugins=plugins, include_unvalidated=include_unvalidated, ) # If output path was set, dump results to disk if output_path: with open(output_path, "w") as f: f.write(json.dumps(plugin_results, indent=4)) else: print(json.dumps(plugin_results, indent=4)) click.secho( f"Finished: {len(plugin_results)}/{len(PLUGINS)} plugins matched the hive type", fg="green", ) @click.command() def list_plugins(): click.secho( tabulate( [(x.NAME, x.COMPATIBLE_HIVE, x.DESCRIPTION) for x in PLUGINS], headers=["Plugin Name", "Compatible hive", "Description"], ) ) @click.command() @click.argument( "first_hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.argument( "second_hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option( "-o", "output_path", type=click.Path(exists=False, dir_okay=False, resolve_path=True), required=False, ) @click.option("-v", "--verbose", is_flag=True, default=False, help="Verbosity") def reg_diff(first_hive_path, second_hive_path, output_path, verbose): _setup_logging(verbose=verbose) REGDIFF_HEADERS = ["difference", "first_hive", "second_hive", "description"] found_differences = compare_hives(first_hive_path, second_hive_path, verbose=verbose) click.secho(f"Comparing {os.path.basename(first_hive_path)} vs {os.path.basename(second_hive_path)}") if output_path: with open(output_path, "w") as csvfile: csvwriter = csv.writer(csvfile, delimiter="|", quoting=csv.QUOTE_MINIMAL) csvwriter.writerow(REGDIFF_HEADERS) for difference in found_differences: csvwriter.writerow(difference) else: click.secho(tabulate(found_differences, headers=REGDIFF_HEADERS, tablefmt="fancy_grid")) click.secho(f"Detected {len(found_differences)} differences", fg="green") @click.command() @click.argument( "hive_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option( "-p", "primary_log_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=True, ) @click.option( "-s", "secondary_log_path", type=click.Path(exists=True, dir_okay=False, resolve_path=True), required=False, ) @click.option( "-o", "output_path", type=click.Path(exists=False, dir_okay=False, resolve_path=True), required=False, ) @click.option("-v", "--verbose", is_flag=True, default=True, help="Verbosity") def parse_transaction_log(hive_path, primary_log_path, secondary_log_path, output_path, verbose): _setup_logging(verbose=verbose) logger.info(f"Processing hive {hive_path} with transaction log {primary_log_path}") if secondary_log_path: logger.info(f"Processing hive {hive_path} with secondary transaction log {secondary_log_path}") restored_hive_path, recovered_dirty_pages_count = apply_transaction_logs( hive_path, primary_log_path, secondary_log_path=secondary_log_path, restored_hive_path=output_path, verbose=verbose, ) if recovered_dirty_pages_count: click.secho( f"Recovered {recovered_dirty_pages_count} dirty pages. Restored hive is at {restored_hive_path}", fg="green", ) ================================================ FILE: regipy/cli_utils.py ================================================ import binascii import datetime as dt import logging from collections.abc import Iterator import pytz from click import progressbar from regipy import NKRecord, RegistryHive, Subkey from regipy.utils import MAX_LEN logger = logging.getLogger(__name__) def get_filtered_subkeys( registry_hive: RegistryHive, name_key_entry: NKRecord, start_date: str = None, end_date: str = None, verbose=False, fetch_values=True, ) -> Iterator[NKRecord]: """ Get records filtered by the specified timestamps :param registry_hive: A RegistryHive object :param name_key_entry: A list of paths as strings :param start_date: Include only subkeys modified after the specified date in isoformat UTC, for example: 2020-02-18T14:15:00.000000 :param end_date: Include only subkeys modified before the specified date in isoformat UTC, for example: 2020-02-20T14:15:00.000000 """ skipped_entries_count = 0 if start_date: start_date = pytz.utc.localize(dt.datetime.fromisoformat(start_date)) if end_date: end_date = pytz.utc.localize(dt.datetime.fromisoformat(end_date)) with progressbar(registry_hive.recurse_subkeys(name_key_entry, fetch_values=False)) as reg_subkeys: for subkey_count, subkey in enumerate(reg_subkeys): if start_date and subkey.timestamp < start_date: skipped_entries_count += 1 logger.debug(f"Skipping entry {subkey} which has a timestamp prior to start_date") continue if end_date and subkey.timestamp > end_date: skipped_entries_count += 1 logger.debug(f"Skipping entry {subkey} which has a timestamp after the end_date") continue nk = registry_hive.get_key(subkey.path) yield Subkey( subkey_name=subkey.subkey_name, path=subkey.path, timestamp=subkey.timestamp, values=list(nk.iter_values(as_json=True)) if fetch_values else [], values_count=subkey.values_count, ) logger.info(f"{skipped_entries_count} out of {subkey_count} subkeys were filtered out due to timestamp constrains") def _normalize_subkey_fields(field) -> str: if isinstance(field, bytes): return binascii.b2a_hex(field[:MAX_LEN]) elif isinstance(field, dt.datetime): return field.isoformat() return field ================================================ FILE: regipy/constants.py ================================================ # ShellBags Known GUIDs KNOWN_GUIDS = { "008ca0b1-55b4-4c56-b8a8-4de4b299d3be": "Account Pictures", "00bcfc5a-ed94-4e48-96a1-3f6217f21990": "RoamingTiles", "00c6d95f-329c-409a-81d7-c46c66ea7f33": "Default Location", "00f2886f-cd64-4fc9-8ec5-30ef6cdbe8c3": "Scanners and Cameras", "0139d44e-6afe-49f2-8690-3dafcae6ffb8": "Programs", "0142e4d0-fb7a-11dc-ba4a-000ffe7ab428": "Biometric Devices (Biometrics)", "018d5c66-4533-4307-9b53-224de2ed1fe6": "OneDrive", "025a5937-a6be-4686-a844-36fe4bec8b6d": "Power Options", "031e4825-7b94-4dc3-b131-e946b44c8dd5": "Users Libraries", "04731b67-d933-450a-90e6-4acd2e9408fe": "Search Folder", "0482af6c-08f1-4c34-8c90-e17ec98b1e17": "Public Account Pictures", "054fae61-4dd8-4787-80b6-090220c4b700": "GameExplorer", "05d7b0f4-2121-4eff-bf6b-ed3f69b894d9": "Taskbar (Notification Area Icons)", "0762d272-c50a-4bb0-a382-697dcd729b80": "Users", "087da31b-0dd3-4537-8e23-64a18591f88b": "Windows Security Center", "0907616e-f5e6-48d8-9d61-a91c3d28106d": "Hyper-V Remote File Browsing", "0ac0837c-bbf8-452a-850d-79d08e667ca7": "Computer", "0afaced1-e828-11d1-9187-b532f1e9575d": "Folder Shortcut", "0b2baaeb-0042-4dca-aa4d-3ee8648d03e5": "Pictures Library", "0c15d503-d017-47ce-9016-7b3f978721cc": "Portable Device Values", "0c39a5cf-1a7a-40c8-ba74-8900e6df5fcd": "Recent Items", "0cd7a5c0-9f37-11ce-ae65-08002b2e1262": "Cabinet File", "0d4c3db6-03a3-462f-a0e6-08924c41b5d4": "History", "0df44eaa-ff21-4412-828e-260a8728e7f1": "Taskbar and Start Menu", "0f214138-b1d3-4a90-bba9-27cbc0c5389a": "Sync Setup", "11016101-e366-4d22-bc06-4ada335c892b": "Internet Explorer History and Feeds Shell Data Source for Windows Search", "1206f5f1-0569-412c-8fec-3204630dfb70": "Credential Manager", "13e7f612-f261-4391-bea2-39df4f3fa311": "Windows Desktop Search", "15ca69b3-30ee-49c1-ace1-6b5ec372afb5": "Sample Playlists", "15eae92e-f17a-4431-9f28-805e482dafd4": "Install New Programs (Get Programs)", "1723d66a-7a12-443e-88c7-05e1bfe79983": "Previous Versions Delegate Folder", "1777f761-68ad-4d8a-87bd-30b759fa33dd": "Favorites", "17cd9488-1228-4b2f-88ce-4298e93e0966": "Default Programs (Set User Defaults)", "18989b1d-99b5-455b-841c-ab7c74e4ddfc": "Videos", "190337d1-b8ca-4121-a639-6d472d16972a": "Search Results", "1a6fdba2-f42d-4358-a798-b74d745926c5": "Recorded TV", "1a9ba3a0-143a-11cf-8350-444553540000": "Shell Favorite Folder", "1ac14e77-02e7-4e5d-b744-2eb1ae5198b7": "System32", "1b3ea5dc-b587-4786-b4ef-bd1dc332aeae": "Libraries", "1cf1260c-4dd0-4ebb-811f-33c572699fde": "Music", "1d2680c9-0e2a-469d-b787-065558bc7d43": "Fusion Cache", "1e87508d-89c2-42f0-8a7e-645a0f50ca58": "Applications", "1f3427c8-5c10-4210-aa03-2ee45287d668": "User Pinned", "1f43a58c-ea28-43e6-9ec4-34574a16ebb7": "Windows Desktop Search MAPI Namespace Extension Class", "1f4de370-d627-11d1-ba4f-00a0c91eedba": "Search Results - Computers (Computer Search Results Folder, Network Computers)", "1fa9085f-25a2-489b-85d4-86326eedcd87": "Manage Wireless Networks", "208d2c60-3aea-1069-a2d7-08002b30309d": "My Network Places", "20d04fe0-3aea-1069-a2d8-08002b30309d": "My Computer", "2112ab0a-c86a-4ffe-a368-0de96e47012e": "Music", "21ec2020-3aea-1069-a2dd-08002b30309d": "Control Panel", "2227a280-3aea-1069-a2de-08002b30309d": "Printers and Faxes", "22877a6d-37a1-461a-91b0-dbda5aaebc99": "Recent Places", "2400183a-6185-49fb-a2d8-4a392a602ba3": "Public Videos", "241d7c96-f8bf-4f85-b01f-e2b043341a4b": "Workspaces Center (Remote Application and Desktop Connections)", "24d89e24-2f19-4534-9dde-6a6671fbb8fe": "Documents", "2559a1f0-21d7-11d4-bdaf-00c04f60b9f0": "Search", "2559a1f1-21d7-11d4-bdaf-00c04f60b9f0": "Help and Support", "2559a1f2-21d7-11d4-bdaf-00c04f60b9f0": "Windows Security", "2559a1f3-21d7-11d4-bdaf-00c04f60b9f0": "Run...", "2559a1f4-21d7-11d4-bdaf-00c04f60b9f0": "Internet", "2559a1f5-21d7-11d4-bdaf-00c04f60b9f0": "E-mail", "2559a1f6-21d7-11d4-bdaf-00c04f60b9f0": "OEM link", "2559a1f7-21d7-11d4-bdaf-00c04f60b9f0": "Set Program Access and Defaults", "259ef4b1-e6c9-4176-b574-481532c9bce8": "Game Controllers", "267cf8a9-f4e3-41e6-95b1-af881be130ff": "Location Folder", "26ee0668-a00a-44d7-9371-beb064c98683": "Control Panel", "2728520d-1ec8-4c68-a551-316b684c4ea7": "Network Setup Wizard", "27e2e392-a111-48e0-ab0c-e17705a05f85": "WPD Content Type Folder", "28803f59-3a75-4058-995f-4ee5503b023c": "Bluetooth Devices", "289978ac-a101-4341-a817-21eba7fd046d": "Sync Center Conflict Folder", "289a9a43-be44-4057-a41b-587a76d7e7f9": "Sync Results", "289af617-1cc3-42a6-926c-e6a863f0e3ba": "DLNA Media Servers Data Source", "292108be-88ab-4f33-9a26-7748e62e37ad": "Videos library", "2965e715-eb66-4719-b53f-1672673bbefa": "Results Folder", "2a00375e-224c-49de-b8d1-440df7ef3ddc": "LocalizedResourcesDir", "2b0f765d-c0e9-4171-908e-08a611b84ff6": "Cookies", "2c36c0aa-5812-4b87-bfd0-4cd0dfb19b39": "Original Images", "2e9e59c0-b437-4981-a647-9c34b9b90891": "Sync Setup Folder", "2f6ce85c-f9ee-43ca-90c7-8a9bd53a2467": "File History Data Source", "3080f90d-d7ad-11d9-bd98-0000947b0257": "Show Desktop", "3080f90e-d7ad-11d9-bd98-0000947b0257": "Window Switcher", "3214fab5-9757-4298-bb61-92a9deaa44ff": "Public Music", "323ca680-c24d-4099-b94d-446dd2d7249e": "Common Places", "328b0346-7eaf-4bbe-a479-7cb88a095f5b": "Layout Folder", "335a31dd-f04b-4d76-a925-d6b47cf360df": "Backup and Restore Center", "339719b5-8c47-4894-94c2-d8f77add44a6": "Pictures", "33e28130-4e1e-4676-835a-98395c3bc3bb": "Pictures", "352481e8-33be-4251-ba85-6007caedcf9d": "Temporary Internet Files", "35786d3c-b075-49b9-88dd-029876e11c01": "Portable Devices", "36011842-dccc-40fe-aa3d-6177ea401788": "Documents Search Results", "36eef7db-88ad-4e81-ad49-0e313f0c35f8": "Windows Update", "374de290-123f-4565-9164-39c4925e467b": "Downloads", "088e3905-0323-4b02-9826-5d99428e115f": "Downloads", "37efd44d-ef8d-41b1-940d-96973a50e9e0": "Desktop Gadgets", "38a98528-6cbf-4ca9-8dc0-b1e1d10f7b1b": "Connect To", "3add1653-eb32-4cb0-bbd7-dfa0abb5acca": "Pictures", "3c5c43a3-9ce9-4a9b-9699-2ac0cf6cc4bf": "Configure Wireless Network", "3d644c9b-1fb8-4f30-9b45-f670235f79c0": "Public Downloads", "3e7efb4c-faf1-453d-89eb-56026875ef90": "Windows Marketplace", "3eb685db-65f9-4cf6-a03a-e3ef65729f3d": "RoamingAppData", "3f2a72a7-99fa-4ddb-a5a8-c604edf61d6b": "Music Library", "3f6bc534-dfa1-4ab4-ae54-ef25a74e0107": "System Restore", "3f98a740-839c-4af7-8c36-5badfb33d5fd": "Documents library", "4026492f-2f69-46b8-b9bf-5654fc07e423": "Windows Firewall", "40419485-c444-4567-851a-2dd7bfa1684d": "Phone and Modem", "418c8b64-5463-461d-88e0-75e2afa3c6fa": "Explorer Browser Results Folder", "4234d49b-0245-4df3-b780-3893943456e1": "Applications", "4336a54d-038b-4685-ab02-99bb52d3fb8b": "Samples", "43668bf8-c14e-49b2-97c9-747784d784b7": "Sync Center", "437ff9c0-a07f-4fa0-af80-84b6c6440a16": "Command Folder", "450d8fba-ad25-11d0-98a8-0800361b1103": "My Documents", "4564b25e-30cd-4787-82ba-39e73a750b14": "Recent Items Instance Folder", "45c6afa5-2c13-402f-bc5d-45cc8172ef6b": "Toshiba Bluetooth Stack", "46137b78-0ec3-426d-8b89-ff7c3a458b5e": "Network Neighborhood", "46e06680-4bf0-11d1-83ee-00a0c90dc849": "NETWORK_DOMAIN", "48daf80b-e6cf-4f4e-b800-0e69d84ee384": "Libraries", "48e7caab-b918-4e58-a94d-505519c795dc": "Start Menu Folder", "491e922f-5643-4af4-a7eb-4e7a138d8174": "Videos", "4bd8d571-6d19-48d3-be97-422220080e43": "Music", "4bfefb45-347d-4006-a5be-ac0cb0567192": "Conflicts", "4c5c32ff-bb9d-43b0-b5b4-2d72e54eaaa4": "Saved Games", "4d9f7874-4e0c-4904-967b-40b0d20c3e4b": "Internet", "4dcafe13-e6a7-4c28-be02-ca8c2126280d": "Pictures Search Results", "5224f545-a443-4859-ba23-7b5a95bdc8ef": "People Near Me", "52528a6b-b9e3-4add-b60d-588c2dba842d": "Homegroup", "52a4f021-7b75-48a9-9f6b-4b87a210bc8f": "Quick Launch", "5399e694-6ce5-4d6c-8fce-1d8870fdcba0": "Control Panel command object for Start menu and desktop", "54a754c0-4bf1-11d1-83ee-00a0c90dc849": "NETWORK_SHARE", "56784854-c6cb-462b-8169-88e350acb882": "Contacts", "58e3c745-d971-4081-9034-86e34b30836a": "Speech Recognition Options", "59031a47-3f72-44a7-89c5-5595fe6b30ee": "Shared Documents Folder (Users Files)", "5b3749ad-b49f-49c1-83eb-15370fbd4882": "TreeProperties", "5b934b42-522b-4c34-bbfe-37a3ef7b9c90": "This Device Folder", "5c4f28b5-f869-4e84-8e60-f11db97c5cc7": "Generic (All folder items)", "5cd7aee2-2219-4a67-b85d-6c9ce15660cb": "Programs", "5ce4a5e9-e4eb-479d-b89f-130c02886155": "DeviceMetadataStore", "5e6c858f-0e22-4760-9afe-ea3317b67173": "Profile", "5e8fc967-829a-475c-93ea-51fce6d9ffce": "RealPlayer Cloud", "5ea4f148-308c-46d7-98a9-49041b1dd468": "Mobility Center Control Panel", "5f4eab9a-6833-4f61-899d-31cf46979d49": "Generic library", "5fa947b5-650a-4374-8a9a-5efa4f126834": "OpenDrive", "5fa96407-7e77-483c-ac93-691d05850de8": "Videos", "5fcd4425-ca3a-48f4-a57c-b8a75c32acb1": "Hewlett-Packard Recovery (Protect.dll)", "60632754-c523-4b62-b45c-4172da012619": "User Accounts", "625b53c3-ab48-4ec1-ba1f-a1ef4146fc19": "Start Menu", "62ab5d82-fdc1-4dc3-a9dd-070d1d495d97": "ProgramData", "62d8ed13-c9d0-4ce8-a914-47dd628fb1b0": "Regional and Language Options", "631958a6-ad0f-4035-a745-28ac066dc6ed": "Videos Library", "6365d5a7-0f0d-45e5-87f6-0da56b6a4f7d": "Common Files", "63da6ec0-2e98-11cf-8d82-444553540000": "Microsoft FTP Folder", "640167b4-59b0-47a6-b335-a6b3c0695aea": "Portable Media Devices", "645ff040-5081-101b-9f08-00aa002f954e": "Recycle Bin", "64693913-1c21-4f30-a98f-4e52906d3b56": "CLSID_AppInstanceFolder", "67718415-c450-4f3c-bf8a-b487642dc39b": "Windows Features", "6785bfac-9d2d-4be5-b7e2-59937e8fb80a": "Other Users Folder", "679f85cb-0220-4080-b29b-5540cc05aab6": "Home Folder", "67ca7650-96e6-4fdd-bb43-a8e774f73a57": "Home Group Control Panel (Home Group)", "692f0339-cbaa-47e6-b5b5-3b84db604e87": "Extensions Manager Folder", "69d2cf90-fc33-4fb7-9a0c-ebb0f0fcb43c": "Slide Shows", "6c8eec18-8d75-41b2-a177-8831d59d2d50": "Mouse", "6dfd7c5c-2451-11d3-a299-00c04f8ef6af": "Folder Options", "6f0cd92b-2e97-45d1-88ff-b0d186b8dedd": "Network Connections", "7007acc7-3202-11d1-aad2-00805fc1270e": "Network Connections", "708e1662-b832-42a8-bbe1-0a77121e3908": "Tree property value folder", "71689ac1-cc88-45d0-8a22-2943c3e7dfb3": "Music Search Results", "71d99464-3b6b-475c-b241-e15883207529": "Sync Results Folder", "724ef170-a42d-4fef-9f26-b60e846fba4f": "Administrative tools", "725be8f7-668e-4c7b-8f90-46bdb0936430": "Keyboard", "72b36e70-8700-42d6-a7f7-c9ab3323ee51": "Search Connector Folder", "74246bfc-4c96-11d0-abef-0020af6b0b7a": "Device Manager", "767e6811-49cb-4273-87c2-20f355e1085b": "Camera Roll", "76fc4e2d-d6ad-4519-a663-37bd56068185": "Printers", "78cb147a-98ea-4aa6-b0df-c8681f69341c": "Windows CardSpace", "78f3955e-3b90-4184-bd14-5397c15f1efc": "Performance Information and Tools", "7a979262-40ce-46ff-aeee-7884ac3b6136": "Add Hardware", "7a9d77bd-5403-11d2-8785-2e0420524153": "User Accounts (Users and Passwords)", "7b0db17d-9cd2-4a93-9733-46cc89022e7c": "Documents", "7b396e54-9ec5-4300-be0a-2482ebae1a26": "Gadgets", "7b81be6a-ce2b-4676-a29e-eb907a5126c5": "Programs and Features", "7bd29e00-76c1-11cf-9dd0-00a0c9034933": "Temporary Internet Files", "7bd29e01-76c1-11cf-9dd0-00a0c9034933": "Temporary Internet Files", "7be9d83c-a729-4d97-b5a7-1b7313c39e0a": "Programs Folder", "7c5a40ef-a0fb-4bfc-874a-c0f2e0b9fa8e": "Program Files", "7d1d3a04-debb-4115-95cf-2f29da2920da": "Searches", "7d49d726-3c21-4f05-99aa-fdc2c9474656": "Documents", "7e636bfe-dfa9-4d5e-b456-d7b39851d8a9": "Templates", "7fde1a1e-8b31-49a5-93b8-6be14cfa4943": "Generic Search Results", "80213e82-bcfd-4c4f-8817-bb27601267a9": "Compressed Folder (zip folder)", "8060b2e3-c9d7-4a5d-8c6b-ce8eba111328": "Proximity CPL", "80f3f1d5-feca-45f3-bc32-752c152e456e": "Tablet PC Settings", "82a5ea35-d9cd-47c5-9629-e15d2f714e6e": "CommonStartup", "82a74aeb-aeb4-465c-a014-d097ee346d63": "Control Panel", "82ba0782-5b7a-4569-b5d7-ec83085f08cc": "TopViews", "8343457c-8703-410f-ba8b-8b026e431743": "Feedback Tool", "859ead94-2e85-48ad-a71a-0969cb56a6cd": "Sample Videos", "85bbd920-42a0-1069-a2e4-08002b30309d": "Briefcase", "863aa9fd-42df-457b-8e4d-0de1b8015c60": "Remote Printers", "865e5e76-ad83-4dca-a109-50dc2113ce9a": "Programs Folder and Fast Items", "871c5380-42a0-1069-a2ea-08002b30309d": "Internet Explorer (Homepage)", "87630419-6216-4ff8-a1f0-143562d16d5c": "Mobile Broadband Profile Settings Editor", "877ca5ac-cb41-4842-9c69-9136e42d47e2": "File Backup Index", "87d66a43-7b11-4a28-9811-c86ee395acf7": "Indexing Options", "88c6c381-2e85-11d0-94de-444553540000": "ActiveX Cache Folder", "896664f7-12e1-490f-8782-c0835afd98fc": "Libraries delegate folder that appears in Users Files Folder", "8983036c-27c0-404b-8f08-102d10dcfd74": "SendTo", "89d83576-6bd1-4c86-9454-beb04e94c819": "MAPI Folder", "8ad10c31-2adb-4296-a8f7-e4701232c972": "Resources", "8e74d236-7f35-4720-b138-1fed0b85ea75": "OneDrive", "8e908fc9-becc-40f6-915b-f4ca0e70d03d": "Network and Sharing Center", "8fd8b88d-30e1-4f25-ac2b-553d3d65f0ea": "DXP", "905e63b6-c1bf-494e-b29c-65b732d3d21a": "Program Files", "9113a02d-00a3-46b9-bc5f-9c04daddd5d7": "Enhanced Storage Data Source", "9274bd8d-cfd1-41c3-b35e-b13f55a758f4": "Printer Shortcuts", "93412589-74d4-4e4e-ad0e-e0cb621440fd": "Font Settings", "9343812e-1c37-4a49-a12e-4b2d810d956b": "Search Home", "94d6ddcc-4a68-4175-a374-bd584a510b78": "Music", "96437431-5a90-4658-a77c-25478734f03e": "Server Manager", "96ae8d84-a250-4520-95a5-a47a7e3c548b": "Parental Controls", "978e0ed7-92d6-4cec-9b59-3135b9c49ccf": "Music library", "98d99750-0b8a-4c59-9151-589053683d73": "Windows Search Service Media Center Namespace Extension Handler", "98ec0e18-2098-4d44-8644-66979315a281": "Microsoft Office Outlook", "98f275b4-4fff-11e0-89e2-7b86dfd72085": "CLSID_StartMenuLauncherProviderFolder", "992cffa0-f557-101a-88ec-00dd010ccc48": "Network Connections (Network and Dial-up Connections)", "9a096bb5-9dc3-4d1c-8526-c3cbf991ea4e": "Internet Explorer RSS Feeds Folder", "9b74b6a3-0dfd-4f11-9e78-5f7800f2e772": "The user's username (%USERNAME%)", "9c60de1e-e5fc-40f4-a487-460851a8d915": "AutoPlay", "9c73f5e5-7ae7-4e32-a8e8-8d23b85255bf": "Sync Center Folder", "9db7a13c-f208-4981-8353-73cc61ae2783": "Previous Versions", "9e3995ab-1f9c-4f13-b827-48b24b6c7174": "User Pinned", "9e52ab10-f80d-49df-acb8-4330f5687855": "CDBurning", "9f433b7c-5f96-4ce1-ac28-aeaa1cc04d7c": "Security Center", "9fe63afd-59cf-4419-9775-abcc3849f861": "System Recovery (Recovery)", "a00ee528-ebd9-48b8-944a-8942113d46ac": "CLSID_StartMenuCommandingProviderFolder", "a0275511-0e86-4eca-97c2-ecd8f1221d08": "Infrared", "a0953c92-50dc-43bf-be83-3742fed03c9c": "Videos", "a302545d-deff-464b-abe8-61c8648d939b": "Libraries", "a304259d-52b8-4526-8b1a-a1d6cecc8243": "iSCSI Initiator", "a305ce99-f527-492b-8b1a-7e76fa98d6e4": "Installed Updates", "a3918781-e5f2-4890-b3d9-a7e54332328c": "Application Shortcuts", "a3c3d402-e56c-4033-95f7-4885e80b0111": "Previous Versions Results Delegate Folder", "a3dd4f92-658a-410f-84fd-6fbbbef2fffe": "Internet Options", "a4115719-d62e-491d-aa7c-e74b8be3b067": "Start Menu", "a5110426-177d-4e08-ab3f-785f10b4439c": "Sony Ericsson File Manager", "a520a1a4-1780-4ff6-bd18-167343c5af16": "AppDataLow", "a52bba46-e9e1-435f-b3d9-28daa648c0f6": "OneDrive", "a5a3563a-5755-4a6f-854e-afa3230b199f": "Library Folder", "a5e46e3a-8849-11d1-9d8c-00c04fc99d61": "Microsoft Browser Architecture", "a63293e8-664e-48db-a079-df759e0509f7": "Templates", "a6482830-08eb-41e2-84c1-73920c2badb9": "Removable Storage Devices", "a75d362e-50fc-4fb7-ac2c-a8beaa314493": "SidebarParts", "a77f5d77-2e2b-44c3-a6a2-aba601054a51": "Programs", "a8a91a66-3a7d-4424-8d24-04e180695c7a": "Device Center (Devices and Printers)", "a8cdff1c-4878-43be-b5fd-f8091c1c60d0": "Documents", "a990ae9f-a03b-4e80-94bc-9912d7504104": "Pictures", "aaa8d5a5-f1d6-4259-baa8-78e7ef60835e": "RoamedTileImages", "ab4f43ca-adcd-4384-b9af-3cecea7d6544": "Sitios Web", "ab5fb87b-7ce2-4f83-915d-550846c9537b": "Camera Roll", "ae50c081-ebd2-438a-8655-8a092e34987a": "Recent Items", "aee2420f-d50e-405c-8784-363c582bf45a": "Device Pairing Folder", "afdb1f70-2a4c-11d2-9039-00c04f8eeb3e": "Offline Files Folder", "b155bdf8-02f0-451e-9a26-ae317cfd7779": "Delegate folder that appears in Computer", "b250c668-f57d-4ee1-a63c-290ee7d1aa1f": "Sample Music", "b28aa736-876b-46da-b3a8-84c5e30ba492": "Web sites", "b2952b16-0e07-4e5a-b993-58c52cb94cae": "DB Folder", "b2c761c6-29bc-4f19-9251-e6195265baf1": "Color Management", "b3690e58-e961-423b-b687-386ebfd83239": "Pictures folder", "b4bfcc3a-db2c-424c-b029-7fe99a87c641": "Desktop", "b4fb3f98-c1ea-428d-a78a-d1f5659cba93": "Other Users Folder", "b5947d7f-b489-4fde-9e77-23780cc610d1": "Virtual Machines", "b689b0d0-76d3-4cbb-87f7-585d0e0ce070": "Games folder", "b6ebfb86-6907-413c-9af7-4fc2abf07cc5": "Public Pictures", "b7534046-3ecb-4c18-be4e-64cd4cb7d6ac": "Recycle Bin", "b7bede81-df94-4682-a7d8-57a52620b86f": "Screenshots", "b94237e7-57ac-4347-9151-b08c6c32d1f7": "CommonTemplates", "b97d20bb-f46a-4c97-ba10-5e3608430854": "Startup", "b98a2bea-7d42-4558-8bd1-832f41bac6fd": "Backup And Restore (Windows 7)", "bb06c0e4-d293-4f75-8a90-cb05b6477eee": "System", "bb64f8a7-bee7-4e1a-ab8d-7d8273f7fdb6": "Action Center Control Panel", "bc476f4c-d9d7-4100-8d4e-e043f6dec409": "Microsoft Browser Architecture", "bc48b32f-5910-47f5-8570-5074a8a5636a": "Sync Results Delegate Folder", "bcb5256f-79f6-4cee-b725-dc34e402fd46": "ImplicitAppShortcuts", "bcbd3057-ca5c-4622-b42d-bc56db0ae516": "Programs", "bd7a2e7b-21cb-41b2-a086-b309680c6b7e": "Client Side Cache Folder", "bd84b380-8ca2-1069-ab1d-08000948f534": "Microsoft Windows Font Folder", "bd85e001-112e-431e-983b-7b15ac09fff1": "RecordedTV", "bdbe736f-34f5-4829-abe8-b550e65146c4": "TopViews", "bdeadf00-c265-11d0-bced-00a0c90ab50f": "Web Folders", "be122a0e-4503-11da-8bde-f66bad1e3f3a": "Windows Anytime Upgrade", "bf782cc9-5a52-4a17-806c-2a894ffeeac5": "Language Settings", "bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968": "Links", "c0542a90-4bf0-11d1-83ee-00a0c90dc849": "NETWORK_SERVER", "c1bae2d0-10df-4334-bedd-7aa20b227a9d": "Common OEM Links", "c1f8339f-f312-4c97-b1c6-ecdf5910c5c0": "Pictures library", "c291a080-b400-4e34-ae3f-3d2b9637d56c": "UNCFATShellFolder Class", "c2b136e2-d50e-405c-8784-363c582bf43e": "Device Center Initialization", "c4900540-2379-4c75-844b-64e6faf8716b": "Sample Pictures", "c4aa340d-f20f-4863-afef-f87ef2e6ba25": "Public Desktop", "c4d98f09-6124-4fe0-9942-826416082da9": "Users libraries", "c555438b-3c23-4769-a71f-b6d3d9b6053a": "Display", "c57a6066-66a3-4d91-9eb9-41532179f0a5": "Application Suggested Locations", "c58c4893-3be0-4b45-abb5-a63e4b8c8651": "Troubleshooting", "c5abbf53-e17f-4121-8900-86626fc2c973": "Network Shortcuts", "c870044b-f49e-4126-a9c3-b52a1ff411e8": "Ringtones", "cac52c1a-b53d-4edc-92d7-6b2e8ac19434": "Games", "cb1b7f8c-c50a-4176-b604-9e24dee8d4d1": "Welcome Center (Getting Started)", "cce6191f-13b2-44fa-8d14-324728beef2c": "{Unknown CSIDL}", "d0384e7d-bac3-4797-8f14-cba229b392b5": "Administrative Tools", "d17d1d6d-cc3f-4815-8fe3-607e7d5d10b3": "Text to Speech", "d2035edf-75cb-4ef1-95a7-410d9ee17170": "DLNA Content Directory Data Source", "d20beec4-5ca8-4905-ae3b-bf251ea09b53": "Network", "d20ea4e1-3957-11d2-a40b-0c5020524152": "Fonts", "d20ea4e1-3957-11d2-a40b-0c5020524153": "Administrative Tools", "d24f75aa-4f2b-4d07-a3c4-469b3d9030c4": "Offline Files", "d34a6ca6-62c2-4c34-8a7c-14709c1ad938": "Common Places FS Folder", "d426cfd0-87fc-4906-98d9-a23f5d515d61": "Windows Search Service Outlook Express Protocol Handler", "d4480a50-ba28-11d1-8e75-00c04fa31a86": "Add Network Place", "d450a8a1-9568-45c7-9c0e-b4f9fb4537bd": "Installed Updates", "d555645e-d4f8-4c29-a827-d93c859c4f2a": "Ease of Access (Ease of Access Center)", "d5b1944e-db4e-482e-b3f1-db05827f0978": "Softex OmniPass Encrypted Folder", "d6277990-4c6a-11cf-8d87-00aa0060f5bf": "Scheduled Tasks", "d65231b0-b2f1-4857-a4ce-a8e7c6ea7d27": "System32", "d8559eb9-20c0-410e-beda-7ed416aecc2a": "Windows Defender", "d9dc8a3b-b784-432e-a781-5a1130a75963": "History", "d9ef8727-cac2-4e60-809e-86f80a666c91": "Secure Startup (BitLocker Drive Encryption)", "da3f6866-35fe-4229-821a-26553a67fc18": "General (Generic) library", "daf95313-e44d-46af-be1b-cbacea2c3065": "CLSID_StartMenuProviderFolder", "de2b70ec-9bf7-4a93-bd3d-243f7881d492": "Contacts", "de61d971-5ebc-4f02-a3a9-6c82895e5c04": "AddNewPrograms", "de92c1c7-837f-4f69-a3bb-86e631204a23": "Playlists", "de974d24-d9c6-4d3e-bf91-f4455120b917": "Common Files", "debf2536-e1a8-4c59-b6a2-414586476aea": "GameExplorer", "df7266ac-9274-4867-8d55-3bd661de872d": "Programs and Features", "dfdf76a2-c82a-4d63-906a-5644ac457385": "Public", "dffacdc5-679f-4156-8947-c5c76bc0b67f": "Delegate folder that appears in Users Files Folder", "e17d4fc0-5564-11d1-83f2-00a0c90dc849": "Search Results Folder", "e211b736-43fd-11d1-9efb-0000f8757fcd": "Scanners and Cameras", "e2e7934b-dce5-43c4-9576-7fe4f75e7480": "Date and Time", "e345f35f-9397-435c-8f95-4e922c26259e": "CLSID_StartMenuPathCompleteProviderFolder", "e413d040-6788-4c22-957e-175d1c513a34": "Sync Center Conflict Delegate Folder", "e555ab60-153b-4d17-9f04-a5fe99fc15ec": "Ringtones", "e773f1af-3a65-4866-857d-846fc9c4598a": "Shell Storage Folder Viewer", "e7de9b1a-7533-4556-9484-b26fb486475e": "Network Map", "e7e4bc40-e76a-11ce-a9bb-00aa004ae837": "Shell DocObject Viewer", "e88dcce0-b7b3-11d1-a9f0-00aa0060fa31": "Compressed Folder", "e95a4861-d57a-4be1-ad0f-35267e261739": "Windows SideShow", "e9950154-c418-419e-a90a-20c5287ae24b": "Sensors (Location and Other Sensors)", "ea25fbd7-3bf7-409e-b97f-3352240903f4": "Videos Search Results", "ecdb0924-4208-451e-8ee0-373c0956de16": "Work Folders", "ed228fdf-9ea8-4870-83b1-96b02cfe0d52": "My Games", "ed4824af-dce4-45a8-81e2-fc7965083634": "Public Documents", "ed50fc29-b964-48a9-afb3-15ebb9b97f36": "PrintHood delegate folder", "ed7ba470-8e54-465e-825c-99712043e01c": "All Tasks", "ed834ed6-4b5a-4bfe-8f11-a626dcb6a921": "Personalization Control Panel", "edc978d6-4d53-4b2f-a265-5805674be568": "Stream Backed Folder", "ee32e446-31ca-4aba-814f-a5ebd2fd6d5e": "Offline Files", "f02c1a0d-be21-4350-88b0-7367fc96ef3c": "Computers and Devices", "f0d63f85-37ec-4097-b30d-61b4a8917118": "Photo Stream", "f1390a9a-a3f4-4e5d-9c5f-98f3bd8d935c": "Sync Setup Delegate Folder", "f1b32785-6fba-4fcf-9d55-7b8e7f157091": "LocalAppData", "f2ddfc82-8f12-4cdd-b7dc-d4fe1425aa4d": "Sound", "f38bf404-1d43-42f2-9305-67de0b28fc23": "Windows", "f3ce0f7c-4901-4acc-8648-d5d44b04ef8f": "Users Files", "f3f5824c-ad58-4728-af59-a1ebe3392799": "Sticky Notes Namespace Extension for Windows Desktop Search", "f5175861-2688-11d0-9c5e-00aa00a45957": "Subscription Folder", "f6b6e965-e9b2-444b-9286-10c9152edbc5": "History Vault", "f7f1ed05-9f6d-47a2-aaae-29d317c6f066": "Common Files", "f82df8f7-8b9f-442e-a48c-818ea735ff9b": "Pen and Input Devices", "f8c2ab3b-17bc-41da-9758-339d7dbf2d88": "Previous Versions Results Folder", "f90c627b-7280-45db-bc26-cce7bdd620a4": "All Tasks", "f942c606-0914-47ab-be56-1321b8035096": "Storage Spaces", "fb0c9c8a-6c50-11d1-9f1d-0000f8757fcd": "Scanners & Cameras", "fbb3477e-c9e4-4b3b-a2ba-d3f5d3cd46f9": "Documents Library", "fc9fb64a-1eb2-4ccf-af5e-1a497a9b5c2d": "My sharing folders", "fcfeecae-ee1b-4849-ae50-685dcf7717ec": "Problem Reports and Solutions", "fd228cb7-ae11-4ae3-864c-16f3910ab8fe": "Fonts", "fdd39ad0-238f-46af-adb4-6c85480369c7": "Documents", "d3162b92-9365-467a-956b-92703aca08af": "Documents", "fe1290f0-cfbd-11cf-a330-00aa00c16e65": "Directory", "ff393560-c2a7-11cf-bff4-444553540000": "History", "0db7e03f-fc29-4dc6-9020-ff41b59e513a": "3D Objects", "24ad3ad4-a569-4530-98e1-ab02f9417aa8": "Pictures", "f86fa3ab-70d2-4fc7-9c99-fcbf05467f3a": "Videos", "3dfdf296-dbec-4fb4-81d1-6a3438bcf4de": "Music", "e31ea727-12ed-4702-820c-4b6445f28e1a": "Dropbox", "4a8fcd9f-623c-4283-96f0-10f41846a98a": "Box Sync", "fbf23b42-e3f0-101b-8488-00aa003e56f8": "Internet Explorer", "00020d75-0000-0000-c000-000000000046": "Inbox", "00020d76-0000-0000-c000-000000000046": "Inbox", "0": "All Control Panel Items", "1": "Appearance and Personalization", "2": "Hardware and Sound", "3": "Network and Internet", "4": "Sounds, Speech, and Audio Devices", "5": "System and Security", "6": "Clock, Language, and Region", "7": "Ease of Access", "8": "Programs", "9": "User Accounts", "10": "Security Center", "11": "Mobile PC", "0ddd015d-b06c-45d5-8c4c-f59713854639": "Local Pictures", "a0c69a99-21c8-4671-8703-7934162fcf1d": "Local Music", "7d83ee9b-2244-4e70-b1f5-5393042af1e4": "Local Downloads", "35286a68-3c57-41a1-bbb1-0eae73d76c95": "Local Videos", "f42ee2d3-909f-4907-8871-4c22fc0bf756": "Local Documents", "5ed4f38c-d3ff-4d61-b506-6820320aebfe": "All Settings", "1bef2128-2f96-4500-ba7c-098dc0049cb2": "CLSID_DBFolderBoth", "00021400-0000-0000-c000-000000000046": "Desktop", "3936e9e4-d92c-4eee-a85a-bc16d5ea0819": "Frequent folders", "45e8e0e8-7ae9-41ad-a9e8-594972716684": "Pictures", "f5fb2c77-0e2f-4a16-a381-3e560c68bc83": "Removable Drives", "0e5aae11-a475-4c5b-ab00-c66de400274e": "Shell File System Folder", "f3364ba0-65b9-11ce-a9ba-00aa004ae837": "Shell File System Folder", "5e5f29ce-e0a8-49d3-af32-7a7bdc173478": "This PC", "e44e5d18-0652-4508-a4e2-8a090067bcb0": "Default Programs", "e88865ea-0e1c-4e20-9aa6-edcd0212c87c": "Gallery", "b2b4a4d1-2754-4140-a2eb-9a76d9d7cdc6": "Linux", "2559a1f8-21d7-11d4-bdaf-00c04f60b9f0": "Windows Search", "e342f0fe-ff1c-4c41-be37-a0271fc90396": "Intel Rapid Storage Technology", "0bbca823-e77d-419e-9a44-5adec2c8eeb0": "NVIDIA Control Panel", "8e0c279d-0bd1-43c3-9ebd-31c3dc5b8a77": "Windows To Go", "00028b00-0000-0000-c000-000000000046": "Microsoft Network", } ================================================ FILE: regipy/exceptions.py ================================================ class RegipyException(Exception): """ This is the parent exception for all regipy exceptions """ pass class RegipyGeneralException(RegipyException): """ General exception """ pass class RegistryValueNotFoundException(RegipyException): pass class NoRegistrySubkeysException(RegipyException): pass class NoRegistryValuesException(RegipyException): pass class RegistryKeyNotFoundException(RegipyException): pass class UnidentifiedHiveException(RegipyException): pass class RegistryRecoveryException(RegipyException): pass class RegistryParsingException(RegipyException): """ Raised when there is a parsing error, most probably a corrupted hive """ pass ================================================ FILE: regipy/hive_types.py ================================================ NTUSER_HIVE_TYPE = "ntuser" SYSTEM_HIVE_TYPE = "system" AMCACHE_HIVE_TYPE = "amcache" SOFTWARE_HIVE_TYPE = "software" SAM_HIVE_TYPE = "sam" SECURITY_HIVE_TYPE = "security" CLASSES_ROOT_HIVE_TYPE = "classes_root" BCD_HIVE_TYPE = "bcd" USRCLASS_HIVE_TYPE = "usrclass" SUPPORTED_HIVE_TYPES = [ NTUSER_HIVE_TYPE, SYSTEM_HIVE_TYPE, AMCACHE_HIVE_TYPE, SOFTWARE_HIVE_TYPE, SAM_HIVE_TYPE, SECURITY_HIVE_TYPE, BCD_HIVE_TYPE, USRCLASS_HIVE_TYPE, ] HVLE_TRANSACTION_LOG_MAGIC = b"HvLE" DIRT_TRANSACTION_LOG_MAGIC = b"DIRT" ================================================ FILE: regipy/plugins/__init__.py ================================================ # flake8: noqa from .amcache.amcache import AmCachePlugin from .bcd.boot_entry_list import BootEntryListPlugin from .ntuser.installed_programs_ntuser import InstalledProgramsNTUserPlugin from .ntuser.network_drives import NetworkDrivesPlugin from .ntuser.persistence import NTUserPersistencePlugin from .ntuser.shellbags_ntuser import ShellBagNtuserPlugin from .ntuser.tsclient import TSClientPlugin from .ntuser.typed_urls import TypedUrlsPlugin from .ntuser.typed_paths import TypedPathsPlugin from .ntuser.user_assist import UserAssistPlugin from .ntuser.winrar import WinRARPlugin from .ntuser.winscp_saved_sessions import WinSCPSavedSessionsPlugin from .ntuser.word_wheel_query import WordWheelQueryPlugin from .sam.local_sid import LocalSidPlugin from .security.domain_sid import DomainSidPlugin from .software.classes_installer import SoftwareClassesInstallerPlugin from .software.image_file_execution_options import ImageFileExecutionOptions from .software.installed_programs import InstalledProgramsSoftwarePlugin from .software.last_logon import LastLogonPlugin from .software.persistence import SoftwarePersistencePlugin from .software.printdemon import PrintDemonPlugin from .software.profilelist import ProfileListPlugin from .software.tracing import RASTracingPlugin from .software.uac import UACStatusPlugin from .system.active_controlset import ActiveControlSetPlugin from .system.bam import BAMPlugin from .system.bootkey import BootKeyPlugin from .system.computer_name import ComputerNamePlugin from .system.host_domain_name import HostDomainNamePlugin from .system.routes import RoutesPlugin from .system.safeboot_configuration import SafeBootConfigurationPlugin from .system.services import ServicesPlugin from .system.shimcache import ShimCachePlugin from .system.timezone_data import TimezoneDataPlugin from .system.usbstor import USBSTORPlugin from .system.wdigest import WDIGESTPlugin from .usrclass.shellbags_usrclass import ShellBagUsrclassPlugin from .ntuser.classes_installer import NtuserClassesInstallerPlugin from .system.network_data import NetworkDataPlugin from .software.winver import WinVersionPlugin from .system.previous_winver import PreviousWinVersionPlugin from .system.shutdown import ShutdownPlugin from .system.processor_architecture import ProcessorArchitecturePlugin from .system.crash_dump import CrashDumpPlugin from .software.susclient import SusclientPlugin from .system.disablelastaccess import DisableLastAccessPlugin from .system.codepage import CodepagePlugin from .software.disablesr import DisableSRPlugin from .system.diag_sr import DiagSRPlugin from .software.spp_clients import SppClientsPlugin from .system.backuprestore import BackupRestorePlugin from .system.timezone_data2 import TimezoneDataPlugin2 from .ntuser.wsl import WSLPlugin from .ntuser.recentdocs import RecentDocsPlugin from .ntuser.comdlg32 import ComDlg32Plugin from .ntuser.runmru import RunMRUPlugin from .ntuser.muicache import MUICachePlugin from .ntuser.appkeys import AppKeysPlugin from .ntuser.sysinternals import SysinternalsPlugin from .ntuser.putty import PuTTYPlugin from .software.appinitdlls import AppInitDLLsPlugin from .system.appcertdlls import AppCertDLLsPlugin from .software.appcompatflags import AppCompatFlagsPlugin from .software.apppaths import AppPathsPlugin from .software.defender import WindowsDefenderPlugin from .software.pslogging import PowerShellLoggingPlugin from .software.execpolicy import ExecutionPolicyPlugin from .software.networklist import NetworkListPlugin from .system.usb_devices import USBDevicesPlugin from .system.mountdev import MountedDevicesPlugin from .system.shares import SharesPlugin from .system.pagefile import PagefilePlugin from .system.lsa_packages import LSAPackagesPlugin from .system.pending_file_rename import PendingFileRenamePlugin from .sam.samparse import SAMParsePlugin ================================================ FILE: regipy/plugins/amcache/__init__.py ================================================ ================================================ FILE: regipy/plugins/amcache/amcache.py ================================================ import logging from inflection import underscore from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import AMCACHE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) AMCACHE_FIELD_NUMERIC_MAPPINGS = { "0": "product_name", "1": "company_name", "2": "file_version_number", "3": "language_code", "4": "switchback_context", "5": "file_version", "6": "file_size", "7": "pe_header_hash", "8": "unknown1", "9": "pe_header_checksum", "a": "unknown2", "b": "unknown3", "c": "file_description", "d": "unknown4", "f": "linker_compile_time", "10": "unknown5", "11": "last_modified_timestamp", "12": "created_timestamp", "15": "full_path", "16": "unknown6", "17": "last_modified_timestamp_2", "100": "program_id", "101": "sha1", } WIN8_TS_FIELDS = [ "last_modified_timestamp", "created_timestamp", "last_modified_timestamp_2", ] class AmCachePlugin(Plugin): NAME = "amcache" DESCRIPTION = "Parse Amcache" COMPATIBLE_HIVE = AMCACHE_HIVE_TYPE def parse_amcache_file_entry(self, subkey): entry = {underscore(x.name): x.value for x in subkey.iter_values(as_json=self.as_json)} # Sometimes the value names might be numeric instead. Translate them: for k, v in AMCACHE_FIELD_NUMERIC_MAPPINGS.items(): content = entry.pop(k, None) if content: entry[v] = content if "sha1" in entry: entry["sha1"] = entry["sha1"][4:] if "file_id" in entry and entry["file_id"] != 0: entry["file_id"] = entry["file_id"][4:] if "sha1" not in entry: entry["sha1"] = entry["file_id"] if "program_id" in entry: entry["program_id"] = entry["program_id"][4:] entry["timestamp"] = convert_wintime(subkey.header.last_modified, as_json=self.as_json) if "size" in entry: entry["size"] = int(entry["size"], 16) if isinstance(entry["size"], str) else entry["size"] is_pefile = entry.get("is_pe_file") if is_pefile is not None: entry["is_pe_file"] = bool(is_pefile) is_os_component = entry.get("is_os_component") if is_os_component is not None: entry["is_os_component"] = bool(is_os_component) if entry.get("link_date") == 0: entry.pop("link_date") for ts_field_name in WIN8_TS_FIELDS: ts = entry.pop(ts_field_name, None) if ts: entry[ts_field_name] = convert_wintime(ts, as_json=self.as_json) self.entries.append(entry) def run(self): logger.debug("Started AmCache Plugin...") try: amcache_file_subkey = self.registry_hive.get_key(r"\Root\File") except RegistryKeyNotFoundException: logger.error(r"Could not find \Root\File subkey") amcache_file_subkey = None try: amcache_inventory_file_subkey = self.registry_hive.get_key(r"\Root\InventoryApplicationFile") except RegistryKeyNotFoundException: logger.info(r"Could not find \Root\InventoryApplicationFile subkey") amcache_inventory_file_subkey = None if amcache_file_subkey: for subkey in amcache_file_subkey.iter_subkeys(): if subkey.header.subkey_count > 0: for file_subkey in subkey.iter_subkeys(): self.parse_amcache_file_entry(file_subkey) if subkey.header.values_count > 0: self.entries.append(subkey) if amcache_inventory_file_subkey: for file_subkey in amcache_inventory_file_subkey.iter_subkeys(): self.parse_amcache_file_entry(file_subkey) ================================================ FILE: regipy/plugins/bcd/__init__.py ================================================ ================================================ FILE: regipy/plugins/bcd/boot_entry_list.py ================================================ """ Windows Boot Configuration Data (BCD) boot entry list plugin """ import logging import uuid from typing import Union from regipy.hive_types import BCD_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.registry import NKRecord from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # BCD object store key path # See https://www.geoffchappell.com/notes/windows/boot/bcd/objects.htm BCD_OBJECTS_PATH = r"\Objects" # Relevant BCD object element types: # BcdLibraryDevice_ApplicationDevice ELEM_TYPE_APPLICATION_DEVICE = 0x11000001 # BcdLibraryString_ApplicationPath ELEM_TYPE_APPLICATION_PATH = 0x12000002 # BcdLibraryString_Description ELEM_TYPE_DESCRIPTION = 0x12000004 def _get_element_by_type(obj_key: NKRecord, datatype: int) -> Union[str, bytes, None]: """ Retrieves stored BCD object elements by their datatype. See https://www.geoffchappell.com/notes/windows/boot/bcd/elements.htm """ # The BCD object attributes are stored as "elements" instead of normal values elements_key = obj_key.get_subkey("Elements", raise_on_missing=False) if elements_key.subkey_count == 0: return None elem_key = elements_key.get_subkey(f"{datatype:08X}", raise_on_missing=False) if elem_key is None: return None return elem_key.get_value("Element") class BootEntryListPlugin(Plugin): """ Windows Boot Configuration Data (BCD) boot entry list extractor """ NAME = "boot_entry_list" DESCRIPTION = "List the Windows BCD boot entries" COMPATIBLE_HIVE = BCD_HIVE_TYPE def run(self) -> None: logger.debug("Started Boot Entry List Plugin...") objects_key = self.registry_hive.get_key(BCD_OBJECTS_PATH) for obj_key in objects_key.iter_subkeys(): desc_key = obj_key.get_subkey("Description") # Object type defines the boot entry features desc_type = desc_key.get_value("Type") # The remaining boot entry attributes are stored as object elements desc_name = _get_element_by_type(obj_key, ELEM_TYPE_DESCRIPTION) path_name = _get_element_by_type(obj_key, ELEM_TYPE_APPLICATION_PATH) device_data = _get_element_by_type(obj_key, ELEM_TYPE_APPLICATION_DEVICE) # Filter out objects that do not look like boot entries if desc_name is None or path_name is None or device_data is None: continue # TODO: Figure out the device data blob format if not isinstance(device_data, bytes) or len(device_data) < 72: continue # TODO: Figure out how non-GPT partitions are encoded gpt_part_guid = str(uuid.UUID(bytes_le=device_data[32:48])) gpt_disk_guid = str(uuid.UUID(bytes_le=device_data[56:72])) entry_type = f"0x{desc_type:08X}" if self.as_json else desc_type self.entries.append( { "guid": obj_key.name, "type": entry_type, "name": desc_name, "gpt_disk": gpt_disk_guid, "gpt_partition": gpt_part_guid, "image_path": path_name, "timestamp": convert_wintime(obj_key.header.last_modified, as_json=self.as_json), } ) ================================================ FILE: regipy/plugins/ntuser/__init__.py ================================================ ================================================ FILE: regipy/plugins/ntuser/appkeys.py ================================================ """ AppKeys plugin - Parses application-specific keyboard shortcuts """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) APPKEYS_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\AppKey" class AppKeysPlugin(Plugin): """ Parses Application Keys from NTUSER.DAT Registry Key: Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\AppKey These are keyboard shortcuts that launch specific applications. """ NAME = "appkeys" DESCRIPTION = "Parses application keyboard shortcuts" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started AppKeys Plugin...") try: appkeys_key = self.registry_hive.get_key(APPKEYS_PATH) except RegistryKeyNotFoundException as ex: logger.debug(f"Could not find {self.NAME} plugin data at: {APPKEYS_PATH}: {ex}") return for subkey in appkeys_key.iter_subkeys(): key_id = subkey.name subkey_path = f"{APPKEYS_PATH}\\{key_id}" entry = { "key_path": subkey_path, "key_id": key_id, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } for value in subkey.iter_values(): if value.name == "ShellExecute": entry["shell_execute"] = value.value elif value.name == "Association": entry["association"] = value.value elif value.name == "RegisteredApp": entry["registered_app"] = value.value # [comment] why filter? if "shell_execute" in entry or "association" in entry or "registered_app" in entry: self.entries.append(entry) ================================================ FILE: regipy/plugins/ntuser/classes_installer.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) CLASSES_INSTALLER_PATH = r"\Software\Microsoft\Installer\Products" class NtuserClassesInstallerPlugin(Plugin): NAME = "ntuser_classes_installer" DESCRIPTION = "List of installed software from NTUSER hive" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: classes_installer_subkey = self.registry_hive.get_key(CLASSES_INSTALLER_PATH) except RegistryKeyNotFoundException as ex: logger.error(ex) return for entry in classes_installer_subkey.iter_subkeys(): identifier = entry.name timestamp = convert_wintime(entry.header.last_modified, as_json=self.as_json) product_name = entry.get_value("ProductName") self.entries.append( { "identifier": identifier, "timestamp": timestamp, "product_name": product_name, "is_hidden": product_name is None, } ) ================================================ FILE: regipy/plugins/ntuser/comdlg32.py ================================================ """ ComDlg32 plugin - Parses Open/Save dialog history (OpenSavePidlMRU, OpenSaveMRU) """ import logging from typing import Optional from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # Windows Vista+ path OPEN_SAVE_PIDL_MRU_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSavePidlMRU" # Windows XP path OPEN_SAVE_MRU_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\OpenSaveMRU" # Last visited path LAST_VISITED_PIDL_MRU_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\ComDlg32\LastVisitedPidlMRU" def parse_pidl_mru_value(data: bytes) -> Optional[str]: """ Parse the PIDL MRU value to extract path/filename. """ if not data or len(data) < 4: return None try: # Try to find readable unicode strings result_parts = [] i = 0 while i < len(data) - 1: # Look for printable unicode sequences start = i while i < len(data) - 1: char_code = int.from_bytes(data[i : i + 2], "little") # Check if it's a printable character or common path character if 0x20 <= char_code <= 0x7E or char_code in [0x5C, 0x2F, 0x3A]: # \, /, : i += 2 else: break if i - start >= 4: # At least 2 unicode characters try: part = data[start:i].decode("utf-16-le", errors="ignore").strip("\x00") if part and len(part) >= 2: result_parts.append(part) except Exception: pass i += 2 # Return the longest meaningful path-like string for part in sorted(result_parts, key=len, reverse=True): if "\\" in part or "/" in part or "." in part: return part if result_parts: return result_parts[0] except Exception: pass return None class ComDlg32Plugin(Plugin): """ Parses Open/Save dialog history from NTUSER.DAT Provides information about files opened or saved through common dialogs. """ NAME = "comdlg32" DESCRIPTION = "Parses Open/Save dialog MRU lists" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started ComDlg32 Plugin...") # Try OpenSavePidlMRU (Vista+) self._parse_open_save_mru(OPEN_SAVE_PIDL_MRU_PATH, "OpenSavePidlMRU") # Try OpenSaveMRU (XP) self._parse_open_save_mru(OPEN_SAVE_MRU_PATH, "OpenSaveMRU") # Try LastVisitedPidlMRU self._parse_last_visited_mru(LAST_VISITED_PIDL_MRU_PATH) def _parse_open_save_mru(self, base_path: str, mru_type: str): """Parse OpenSaveMRU or OpenSavePidlMRU entries""" try: base_key = self.registry_hive.get_key(base_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find {mru_type} at: {base_path}") return # Process the main key and all extension subkeys for subkey in base_key.iter_subkeys(): extension = subkey.name subkey_path = f"{base_path}\\{extension}" entry = { "key_path": subkey_path, "mru_type": mru_type, "extension": extension, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "items": [], } mru_list = None mru_values = {} for value in subkey.iter_values(): if value.name == "MRUListEx": mru_list = value.value elif value.name.isdigit(): parsed = parse_pidl_mru_value(value.value) if parsed: mru_values[int(value.name)] = parsed # Build ordered list if mru_list and mru_values: for i in range(0, len(mru_list) - 4, 4): index = int.from_bytes(mru_list[i : i + 4], "little", signed=True) if index == -1: break if index in mru_values: entry["items"].append({"index": index, "path": mru_values[index]}) if entry["items"]: self.entries.append(entry) def _parse_last_visited_mru(self, path: str): """Parse LastVisitedPidlMRU entries""" try: key = self.registry_hive.get_key(path) except RegistryKeyNotFoundException: logger.debug(f"Could not find LastVisitedPidlMRU at: {path}") return entry = { "key_path": path, "mru_type": "LastVisitedPidlMRU", "last_write": convert_wintime(key.header.last_modified, as_json=self.as_json), "items": [], } mru_list = None mru_values = {} for value in key.iter_values(): if value.name == "MRUListEx": mru_list = value.value elif value.name.isdigit(): parsed = parse_pidl_mru_value(value.value) if parsed: mru_values[int(value.name)] = parsed if mru_list and mru_values: for i in range(0, len(mru_list) - 4, 4): index = int.from_bytes(mru_list[i : i + 4], "little", signed=True) if index == -1: break if index in mru_values: entry["items"].append({"index": index, "path": mru_values[index]}) if entry["items"]: self.entries.append(entry) ================================================ FILE: regipy/plugins/ntuser/installed_programs_ntuser.py ================================================ import logging from regipy import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) INSTALLED_SOFTWARE_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Uninstall" class InstalledProgramsNTUserPlugin(Plugin): NAME = "installed_programs_ntuser" DESCRIPTION = "Retrieve list of installed programs and their install date from the NTUSER Hive" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def _get_installed_software(self, subkey_path): try: uninstall_sk = self.registry_hive.get_key(subkey_path) except RegistryKeyNotFoundException as ex: logger.error(ex) return for installed_program in uninstall_sk.iter_subkeys(): values = ( {x.name: x.value for x in installed_program.iter_values(as_json=self.as_json)} if installed_program.values_count else {} ) self.entries.append( { "service_name": installed_program.name, "timestamp": convert_wintime(installed_program.header.last_modified, as_json=self.as_json), "registry_path": subkey_path, **values, } ) def run(self): self._get_installed_software(INSTALLED_SOFTWARE_PATH) ================================================ FILE: regipy/plugins/ntuser/muicache.py ================================================ """ MUICache plugin - Parses MUI Cache entries (application display names) """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # Windows Vista+ path MUICACHE_PATH_VISTA = r"\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" # Windows XP path MUICACHE_PATH_XP = r"\Software\Microsoft\Windows\ShellNoRoam\MUICache" class MUICachePlugin(Plugin): """ Parses MUICache entries from NTUSER.DAT or UsrClass.dat MUICache stores the display names of applications that have been run. """ NAME = "muicache" DESCRIPTION = "Parses MUI Cache (application display names)" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started MUICache Plugin...") # Try Vista+ path first muicache_found = self._parse_muicache(MUICACHE_PATH_VISTA) # Try XP path if Vista+ not found if not muicache_found: self._parse_muicache(MUICACHE_PATH_XP) def _parse_muicache(self, path: str) -> bool: """Parse MUICache at the given path""" try: muicache_key = self.registry_hive.get_key(path) except RegistryKeyNotFoundException: logger.debug(f"Could not find MUICache at: {path}") return False entry = { "key_path": path, "last_write": convert_wintime(muicache_key.header.last_modified, as_json=self.as_json), "applications": [], } for value in muicache_key.iter_values(): # Skip system values if value.name.startswith("@") or value.name == "LangID": continue app_entry = { "path": value.name, "display_name": value.value if isinstance(value.value, str) else None, } # Extract just the filename from the path for easier reading if "\\" in value.name: app_entry["filename"] = value.name.split("\\")[-1] else: app_entry["filename"] = value.name entry["applications"].append(app_entry) if entry["applications"]: self.entries.append(entry) return True return False ================================================ FILE: regipy/plugins/ntuser/network_drives.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) NETWORK_DRIVES = r"\Network" class NetworkDrivesPlugin(Plugin): NAME = "network_drives_plugin" DESCRIPTION = "Parse the user's mapped network drives" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: network_drives = self.registry_hive.get_key(NETWORK_DRIVES) for mapped_drive in network_drives.iter_subkeys(): timestamp = convert_wintime(mapped_drive.header.last_modified, as_json=self.as_json) self.entries.append( { "last_write": timestamp, "drive_letter": mapped_drive.name, "network_path": mapped_drive.get_value("RemotePath"), } ) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {NETWORK_DRIVES}: {ex}") ================================================ FILE: regipy/plugins/ntuser/persistence.py ================================================ import logging from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import get_subkey_values_from_list logger = logging.getLogger(__name__) PERSISTENCE_ENTRIES = [ r"\Software\Microsoft\Windows NT\CurrentVersion\Run", r"\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\Run", r"\Software\Microsoft\Windows NT\CurrentVersion\Terminal Server\Install\Software\Microsoft\Windows\CurrentVersion\RunOnce", r"\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", r"\Software\Microsoft\Windows\CurrentVersion\Run", r"\Software\Microsoft\Windows\CurrentVersion\RunOnce", r"\Software\Microsoft\Windows\CurrentVersion\RunOnceEx", r"\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup", r"\Software\Microsoft\Windows\CurrentVersion\RunServices", r"\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce", r"\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", r"\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run", r"\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", r"\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnceEx", r"\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce\Setup", r"\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify", ] class NTUserPersistencePlugin(Plugin): NAME = "ntuser_persistence" DESCRIPTION = "Retrieve values from known persistence subkeys in NTUSER hive" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): self.entries = get_subkey_values_from_list( self.registry_hive, PERSISTENCE_ENTRIES, as_json=self.as_json, trim_values=False, ) ================================================ FILE: regipy/plugins/ntuser/putty.py ================================================ """ PuTTY plugin - Parses PuTTY SSH client configuration and session history """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) PUTTY_SESSIONS_PATH = r"\Software\SimonTatham\PuTTY\Sessions" PUTTY_SSH_HOST_KEYS_PATH = r"\Software\SimonTatham\PuTTY\SshHostKeys" PUTTY_JUMPLIST_PATH = r"\Software\SimonTatham\PuTTY\Jumplist" class PuTTYPlugin(Plugin): """ Parses PuTTY configuration and session history from NTUSER.DAT Extracts: - Saved sessions with connection details - SSH host keys (evidence of connections) - Jump list entries """ NAME = "putty" DESCRIPTION = "Parses PuTTY sessions and SSH host keys" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started PuTTY Plugin...") self._parse_sessions() self._parse_ssh_host_keys() self._parse_jumplist() def _parse_sessions(self): """Parse saved PuTTY sessions""" try: sessions_key = self.registry_hive.get_key(PUTTY_SESSIONS_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PuTTY sessions at: {PUTTY_SESSIONS_PATH}") return for subkey in sessions_key.iter_subkeys(): session_name = subkey.name # Session names are URL-encoded try: from urllib.parse import unquote decoded_name = unquote(session_name) except Exception: decoded_name = session_name entry = { "type": "session", "key_path": f"{PUTTY_SESSIONS_PATH}\\{session_name}", "session_name": decoded_name, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } # Extract required fields extract_values( subkey, { "HostName": "hostname", "PortNumber": "port", "UserName": "username", "Protocol": ("protocol", self._get_protocol_name), }, entry, ) # Extract optional fields (only if non-empty) for value in subkey.iter_values(): name = value.name val = value.value if name == "ProxyHost" and val: entry["proxy_host"] = val elif name == "ProxyPort" and val and val != 0: entry["proxy_port"] = val elif name == "ProxyUsername" and val: entry["proxy_username"] = val elif name == "PublicKeyFile" and val: entry["public_key_file"] = val elif name == "RemoteCommand" and val: entry["remote_command"] = val elif name == "PortForwardings" and val: entry["port_forwardings"] = val elif name == "LogFileName" and val: entry["log_filename"] = val elif name == "WinTitle" and val: entry["window_title"] = val self.entries.append(entry) def _parse_ssh_host_keys(self): """Parse SSH host keys - evidence of connections made""" try: hostkeys_key = self.registry_hive.get_key(PUTTY_SSH_HOST_KEYS_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PuTTY SSH host keys at: {PUTTY_SSH_HOST_KEYS_PATH}") return entry = { "type": "ssh_host_keys", "key_path": PUTTY_SSH_HOST_KEYS_PATH, "last_write": convert_wintime(hostkeys_key.header.last_modified, as_json=self.as_json), "hosts": [], } for value in hostkeys_key.iter_values(): # Format: algorithm@port:hostname # e.g., rsa2@22:192.168.1.1 host_entry = {"raw_key": value.name} if "@" in value.name and ":" in value.name: try: algo_port, hostname = value.name.rsplit(":", 1) algo, port = algo_port.split("@", 1) host_entry["algorithm"] = algo host_entry["port"] = int(port) host_entry["hostname"] = hostname except Exception: pass entry["hosts"].append(host_entry) if entry["hosts"]: self.entries.append(entry) def _parse_jumplist(self): """Parse PuTTY jump list entries""" try: jumplist_key = self.registry_hive.get_key(PUTTY_JUMPLIST_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PuTTY jumplist at: {PUTTY_JUMPLIST_PATH}") return entry = { "type": "jumplist", "key_path": PUTTY_JUMPLIST_PATH, "last_write": convert_wintime(jumplist_key.header.last_modified, as_json=self.as_json), "recent_sessions": [], } for value in jumplist_key.iter_values(): if value.name == "Recent sessions" and value.value: # Value is a comma-separated list of session names sessions = [s.strip() for s in value.value.split(",") if s.strip()] entry["recent_sessions"] = sessions if entry["recent_sessions"]: self.entries.append(entry) @staticmethod def _get_protocol_name(protocol_id): """Convert protocol ID to name""" protocols = { 0: "Raw", 1: "Telnet", 2: "Rlogin", 3: "SSH", 4: "Serial", } return protocols.get(protocol_id, f"Unknown ({protocol_id})") ================================================ FILE: regipy/plugins/ntuser/recentdocs.py ================================================ """ RecentDocs plugin - Parses recently opened documents from the registry """ import logging from typing import Optional from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) RECENT_DOCS_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs" def parse_mru_value(data: bytes) -> Optional[str]: """ Parse the binary MRU value to extract the filename. The format is: filename (null-terminated unicode) + shell item data """ if not data: return None try: # Find the null terminator for the unicode string null_pos = 0 for i in range(0, len(data) - 1, 2): if data[i] == 0 and data[i + 1] == 0: null_pos = i break if null_pos > 0: return data[:null_pos].decode("utf-16-le", errors="replace") except Exception: pass return None class RecentDocsPlugin(Plugin): """ Parses Recently opened documents from NTUSER.DAT Registry Key: Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs """ NAME = "recentdocs" DESCRIPTION = "Parses recently opened documents" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started RecentDocs Plugin...") try: recent_docs_key = self.registry_hive.get_key(RECENT_DOCS_PATH) except RegistryKeyNotFoundException as ex: logger.debug(f"Could not find {self.NAME} plugin data at: {RECENT_DOCS_PATH}: {ex}") return # Process the main RecentDocs key self._process_recent_docs_key(recent_docs_key, RECENT_DOCS_PATH) # Process extension subkeys (e.g., .txt, .docx, .pdf, etc.) for subkey in recent_docs_key.iter_subkeys(): subkey_path = f"{RECENT_DOCS_PATH}\\{subkey.name}" self._process_recent_docs_key(subkey, subkey_path, extension=subkey.name) def _process_recent_docs_key(self, key, key_path: str, extension: str = None): """Process a RecentDocs key and extract document entries""" entry = { "key_path": key_path, "last_write": convert_wintime(key.header.last_modified, as_json=self.as_json), "extension": extension, "documents": [], } mru_list = None mru_values = {} for value in key.iter_values(): if value.name == "MRUListEx": # MRUListEx contains the order of recently accessed items # It's an array of DWORDs representing indices mru_list = value.value elif value.name.isdigit(): # Numeric values contain the actual document data parsed_name = parse_mru_value(value.value) if parsed_name: mru_values[int(value.name)] = parsed_name # Build ordered list of documents based on MRUListEx if mru_list and mru_values: # MRUListEx is a binary blob of 4-byte integers for i in range(0, len(mru_list) - 4, 4): index = int.from_bytes(mru_list[i : i + 4], "little", signed=True) if index == -1: # End marker break if index in mru_values: entry["documents"].append({"index": index, "name": mru_values[index]}) if entry["documents"]: self.entries.append(entry) ================================================ FILE: regipy/plugins/ntuser/runmru.py ================================================ """ RunMRU plugin - Parses Run dialog history """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) RUN_MRU_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU" class RunMRUPlugin(Plugin): """ Parses Run dialog MRU (Most Recently Used) list from NTUSER.DAT Registry Key: Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU """ NAME = "runmru" DESCRIPTION = "Parses Run dialog MRU list" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started RunMRU Plugin...") try: runmru_key = self.registry_hive.get_key(RUN_MRU_PATH) except RegistryKeyNotFoundException as ex: logger.debug(f"Could not find {self.NAME} plugin data at: {RUN_MRU_PATH}: {ex}") return entry = { "key_path": RUN_MRU_PATH, "last_write": convert_wintime(runmru_key.header.last_modified, as_json=self.as_json), "mru_order": None, "commands": [], } mru_list = None mru_values = {} for value in runmru_key.iter_values(): if value.name == "MRUList": # MRUList contains the order as a string of letters (e.g., "dcba") mru_list = value.value elif len(value.name) == 1 and value.name.isalpha(): # Single letter values (a, b, c, etc.) contain the commands # Commands end with \1 which indicates the command was typed command = value.value if command and isinstance(command, str): # Remove the trailing \1 marker if present command = command.rstrip("\x01") mru_values[value.name] = command if mru_list: entry["mru_order"] = mru_list # Build ordered list based on MRUList if mru_list and mru_values: for letter in mru_list: if letter in mru_values: entry["commands"].append({"letter": letter, "command": mru_values[letter]}) if entry["commands"] or entry["mru_order"]: self.entries.append(entry) ================================================ FILE: regipy/plugins/ntuser/shellbags_ntuser.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime from regipy.constants import KNOWN_GUIDS logger = logging.getLogger(__name__) NTUSER_SHELLBAG = "\\Software\\Microsoft\\Windows\\Shell\\BagMRU" DEFAULT_CODEPAGE = "cp1252" class ShellBagNtuserPlugin(Plugin): NAME = "ntuser_shellbag_plugin" DESCRIPTION = "Parse NTUSER Shellbag items" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE @staticmethod def _parse_mru(mru_val): mru_order_string = "" if isinstance(mru_val, bytes): mru_val = mru_val[:-4] for i in range(0, len(mru_val), 4): current_val = int.from_bytes(mru_val[i : i + 4], byteorder="little") mru_order_string += f"{current_val}-" return mru_order_string[:-1] else: return mru_order_string @staticmethod def _get_shell_item_type(shell_item): try: import pyfwsi except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex if isinstance(shell_item, pyfwsi.volume): item_type = "Volume" elif isinstance(shell_item, pyfwsi.file_entry): item_type = "Directory" elif isinstance(shell_item, pyfwsi.network_location): item_type = "Network Location" elif isinstance(shell_item, pyfwsi.root_folder): item_type = "Root Folder" elif isinstance(shell_item, pyfwsi.control_panel_category): item_type = "Control Panel Category" elif isinstance(shell_item, pyfwsi.control_panel_item): item_type = "Control Panel Item" elif isinstance(shell_item, pyfwsi.users_property_view): item_type = "Users Property View" else: item_type = "unknown" return item_type @staticmethod def _check_known_guids(guid): if guid in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[guid] else: path_segment = "{{{0:s}}}".format(guid) return path_segment @staticmethod def _get_entry_string(fwps_record): if fwps_record.entry_name: entry_string = fwps_record.entry_name else: entry_string = f"{fwps_record.entry_type:d}" return entry_string @staticmethod def _create_entry( value, slot, reg_path, value_name, node_slot, shell_type, path, full_path=None, location_description=None, creation_time=None, access_time=None, modification_time=None, last_write=None, mru_order=None, mru_order_location=None, first_interacted=None, ): return { "value": value, "slot": slot, "reg_path": reg_path, "value_name": value_name, "node_slot": node_slot, "shell_type": shell_type, "path": path, "full path": full_path, "location description": location_description, "creation_time": creation_time, "access_time": access_time, "modification_time": modification_time, "last_write": last_write, "mru_order": mru_order, "mru_order_location": mru_order_location, "first_interacted": first_interacted, } @staticmethod def _parse_shell_item_path_segment(self, shell_item): """Parses a shell item path segment. Args: shell_item (pyfwsi.item): shell item. Returns: str: shell item path segment. """ try: import pyfwsi import pyfwps except ModuleNotFoundError as ex: logger.exception( f"Plugin `shellbag_plugin` has missing modules, install regipy using" f" `pip install regipy[full]` in order to install plugin dependencies. " f"This might take some time... " ) raise ex path_segment = None full_path = None location_description = None if isinstance(shell_item, pyfwsi.volume): if shell_item.name: path_segment = shell_item.name elif shell_item.identifier: path_segment = self._check_known_guids(shell_item.identifier) elif isinstance(shell_item, pyfwsi.file_entry): long_name = "" for extension_block in shell_item.extension_blocks: if isinstance(extension_block, pyfwsi.file_entry_extension): long_name = extension_block.long_name if long_name: path_segment = long_name elif shell_item.name: path_segment = shell_item.name elif isinstance(shell_item, pyfwsi.network_location): if shell_item.location: path_segment = shell_item.location if shell_item.description: location_description = shell_item.description if shell_item.comments: location_description += f", {shell_item.comments}" elif isinstance(shell_item, pyfwsi.root_folder): if shell_item.shell_folder_identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.shell_folder_identifier] elif hasattr(shell_item, "identifier") and shell_item.identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.identifier] else: path_segment = "{{{0:s}}}".format(shell_item.shell_folder_identifier) elif isinstance(shell_item, pyfwsi.users_property_view): # Users property view if shell_item.delegate_folder_identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.delegate_folder_identifier] elif hasattr(shell_item, "identifier") and shell_item.identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.identifier] # Variable: Users property view elif shell_item.property_store_data: fwps_store = pyfwps.store() fwps_store.copy_from_byte_stream(shell_item.property_store_data) for fwps_set in iter(fwps_store.sets): if fwps_set.identifier == "b725f130-47ef-101a-a5f1-02608c9eebac": for fwps_record in iter(fwps_set.records): entry_string = self._get_entry_string(fwps_record) # PKEY_DisplayName: {b725f130-47ef-101a-a5f1-02608c9eebac}/10 if entry_string == "10": if fwps_record.value_type == 0x0001: value_string = "" elif fwps_record.value_type in ( 0x0003, 0x0013, 0x0014, 0x0015, ): value_string = str(fwps_record.get_data_as_integer()) elif fwps_record.value_type in (0x0008, 0x001E, 0x001F): value_string = fwps_record.get_data_as_string() elif fwps_record.value_type == 0x000B: value_string = str(fwps_record.get_data_as_boolean()) elif fwps_record.value_type == 0x0040: filetime = fwps_record.get_data_as_integer() value_string = self._FormatFiletimeValue(filetime) elif fwps_record.value_type == 0x0042: # TODO: add support value_string = "" elif fwps_record.value_type == 0x0048: value_string = fwps_record.get_data_as_guid() elif fwps_record.value_type & 0xF000 == 0x1000: # TODO: add support value_string = "" else: value_string = None path_segment = value_string elif fwps_set.identifier == "28636aa6-953d-11d2-b5d6-00c04fd918d0": for fwps_record in iter(fwps_set.records): entry_string = self._get_entry_string(fwps_record) # PKEY_ParsingPath: {28636aa6-953d-11d2-b5d6-00c04fd918d0}/30 if entry_string == "30": full_path = fwps_record.get_data_as_string() elif isinstance(shell_item, pyfwsi.control_panel_category): path_segment = self._check_known_guids(str(shell_item.identifier)) elif isinstance(shell_item, pyfwsi.control_panel_item): path_segment = self._check_known_guids(shell_item.identifier) if path_segment is None: path_segment = "".format(shell_item.class_type) return path_segment, full_path, location_description def iter_sk(self, key, reg_path, codepage=DEFAULT_CODEPAGE, base_path="", path=""): try: import pyfwsi except ModuleNotFoundError as ex: logger.exception( f"Plugin `shellbag_plugin` has missing modules, install regipy using" f" `pip install regipy[full]` in order to install plugin dependencies. " f"This might take some time... " ) raise ex last_write = convert_wintime(key.header.last_modified, as_json=True) mru_val = key.get_value("MRUListEx") mru_order = self._parse_mru(mru_val) base_path = path if key.get_value("NodeSlot"): node_slot = str(key.get_value("NodeSlot")) else: node_slot = "" processed_values = set() for v in key.iter_values(trim_values=False): if v.name.isdigit(): processed_values.add(v.name) slot = v.name byte_stream = v.value shell_items = pyfwsi.item_list() shell_items.copy_from_byte_stream(byte_stream, ascii_codepage=codepage) for item in shell_items.items: shell_type = self._get_shell_item_type(item) value, full_path, location_description = self._parse_shell_item_path_segment(self, item) if not path: path = value base_path = "" else: path += f"\\{value}" creation_time = None access_time = None modification_time = None if len(item.extension_blocks) > 0: for extension_block in item.extension_blocks: if isinstance(extension_block, pyfwsi.file_entry_extension): try: creation_time = extension_block.get_creation_time() if self.as_json: creation_time = creation_time.isoformat() except OSError: logger.exception(f"Malformed creation time for {path}") try: access_time = extension_block.get_access_time() if self.as_json: access_time = access_time.isoformat() except OSError: logger.exception(f"Malformed access time for {path}") try: if hasattr(item, "modification_time"): modification_time = item.get_modification_time() if self.as_json: modification_time = modification_time.isoformat() except OSError: logger.exception(f"Malformed modification time for {path}") value_name = v.name mru_order_location = mru_order.split("-").index(value_name) self.entries.append( self._create_entry( value=value, slot=slot, reg_path=reg_path, value_name=value_name, node_slot=node_slot, shell_type=shell_type, path=path, full_path=full_path, location_description=location_description, creation_time=creation_time, access_time=access_time, modification_time=modification_time, last_write=last_write, mru_order=mru_order, mru_order_location=mru_order_location, ) ) sk_reg_path = f"{reg_path}\\{value_name}" try: sk = self.registry_hive.get_key(sk_reg_path) self.iter_sk(sk, sk_reg_path, codepage, base_path, path) except RegistryKeyNotFoundException: pass # Subkey doesn't exist, continue path = base_path # Issue #268: Handle childless subkeys - subkeys without corresponding numbered # values contain "first interacted" timestamps for when a folder was initially accessed for subkey in key.iter_subkeys(): if subkey.name not in processed_values: childless_last_write = convert_wintime(subkey.header.last_modified, as_json=True) self.entries.append( self._create_entry( value=None, slot=subkey.name, reg_path=f"{reg_path}\\{subkey.name}", value_name=subkey.name, node_slot=str(subkey.get_value("NodeSlot")) if subkey.get_value("NodeSlot") else "", shell_type="Childless", path=path or None, last_write=childless_last_write, first_interacted=childless_last_write, ) ) self.iter_sk(subkey, f"{reg_path}\\{subkey.name}", codepage, base_path, path) def run(self, codepage=DEFAULT_CODEPAGE): try: # flake8: noqa import pyfwsi except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex try: shellbag_ntuser_subkey = self.registry_hive.get_key(NTUSER_SHELLBAG) self.iter_sk(shellbag_ntuser_subkey, NTUSER_SHELLBAG, codepage=codepage) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {NTUSER_SHELLBAG}: {ex}") ================================================ FILE: regipy/plugins/ntuser/sysinternals.py ================================================ """ Sysinternals plugin - Parses Sysinternals tools EULA acceptance records """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SYSINTERNALS_PATH = r"\Software\Sysinternals" class SysinternalsPlugin(Plugin): """ Parses Sysinternals EULA acceptance records from NTUSER.DAT Registry Key: Software\\Sysinternals When a Sysinternals tool is run and the EULA is accepted, it creates a subkey with the tool name containing an EulaAccepted value. This provides evidence of which Sysinternals tools have been executed. """ NAME = "sysinternals" DESCRIPTION = "Parses Sysinternals tools EULA acceptance" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): logger.debug("Started Sysinternals Plugin...") try: sysinternals_key = self.registry_hive.get_key(SYSINTERNALS_PATH) except RegistryKeyNotFoundException as ex: logger.debug(f"Could not find {self.NAME} plugin data at: {SYSINTERNALS_PATH}: {ex}") return for subkey in sysinternals_key.iter_subkeys(): tool_name = subkey.name subkey_path = f"{SYSINTERNALS_PATH}\\{tool_name}" entry = { "key_path": subkey_path, "tool_name": tool_name, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "eula_accepted": False, } for value in subkey.iter_values(): if value.name == "EulaAccepted": entry["eula_accepted"] = value.value == 1 self.entries.append(entry) ================================================ FILE: regipy/plugins/ntuser/tsclient.py ================================================ import logging from regipy import ( NoRegistrySubkeysException, RegistryKeyNotFoundException, convert_wintime, ) from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) TSCLIENT_HISTORY_PATH = r"\Software\Microsoft\Terminal Server Client\Servers" class TSClientPlugin(Plugin): NAME = "terminal_services_history" DESCRIPTION = "Retrieve history of RDP connections" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: tsclient_subkey = self.registry_hive.get_key(TSCLIENT_HISTORY_PATH) except (RegistryKeyNotFoundException, NoRegistrySubkeysException) as ex: logger.error(ex) return for server in tsclient_subkey.iter_subkeys(): self.entries.append( { "server": server.name, "last_connection": convert_wintime(server.header.last_modified, as_json=self.as_json), "username_hint": server.get_value("UsernameHint"), } ) ================================================ FILE: regipy/plugins/ntuser/typed_paths.py ================================================ import logging from inflection import underscore from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) TYPED_PATHS_KEY_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths" class TypedPathsPlugin(Plugin): NAME = "typed_paths" DESCRIPTION = "Retrieve the typed Paths from the history" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: subkey = self.registry_hive.get_key(TYPED_PATHS_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {TYPED_PATHS_KEY_PATH}: {ex}") return None self.entries = { "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "entries": [{underscore(x.name): x.value} for x in subkey.iter_values(as_json=self.as_json)], } ================================================ FILE: regipy/plugins/ntuser/typed_urls.py ================================================ import logging from inflection import underscore from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) TYPED_URLS_KEY_PATH = r"\Software\Microsoft\Internet Explorer\TypedURLs" class TypedUrlsPlugin(Plugin): NAME = "typed_urls" DESCRIPTION = "Retrieve the typed URLs from IE history" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: subkey = self.registry_hive.get_key(TYPED_URLS_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {TYPED_URLS_KEY_PATH}: {ex}") return None self.entries = { "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "entries": [{underscore(x.name): x.value} for x in subkey.iter_values(as_json=self.as_json)], } ================================================ FILE: regipy/plugins/ntuser/user_assist.py ================================================ import codecs import logging from construct import Bytes, Const, ConstError, Int32ul, Int64ul, Struct from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) USER_ASSIST_KEY_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist" # guids for the various Operating Systems GUIDS = [ "{75048700-EF1F-11D0-9888-006097DEACF9}", # Windows XP GUIDs "{5E6AB780-7743-11CF-A12B-00AA004AE837}", "{75048700-EF1F-11D0-9888-006097DEACF9}", # Windows vista "{5E6AB780-7743-11CF-A12B-00AA004AE837}", "{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}", # Windows 7 "{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}", "{FA99DFC7-6AC2-453A-A5E2-5E2AFF4507BD}", # Windows 8 "{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}", "{F2A1CB5A-E3CC-4A2E-AF9D-505A7009D442}", "{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}", "{CAA59E3C-4792-41A5-9909-6A6A8D32490E}", "{B267E3AD-A825-4A09-82B9-EEC22AA3B847}", "{A3D53349-6E61-4557-8FC7-0028EDCEEBF6}", "{9E04CAB2-CC14-11DF-BB8C-A2F1DED72085}", ] GUID_TO_PATH_MAPPINGS = { "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}": r"%SYSTEM32%", "{6D809377-6AF0-444B-8957-A3773F02200E}": r"%PROGRAMFILES%", "{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}": r"%PROGRAMFILES(X86)%", "{F38BF404-1D43-42F2-9305-67DE0B28FC23}": r"%WINDIR%", "{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}": r"%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs", "{9E3995AB-1F9C-4F13-B827-48B24B6C7174}": r"%AppData%\Roaming\Microsoft\Internet Explorer\Quick Launch\User Pinned", "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}": r"%AppData%\Roaming\Microsoft\Windows\Start Menu\Programs", "{D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27}": r"%WINDIR%\SysWOW64", } WHITELISTED_NAMES = ["UEME_CTLSESSION"] WIN_XP_USER_ASSIST = Struct( "session_id" / Int32ul, "run_counter" / Int32ul, "last_execution_timestamp" / Int64ul, ) WIN7_USER_ASSIST = Struct( "session_id" / Int32ul, "run_counter" / Int32ul, "focus_count" / Int32ul, "total_focus_time_ms" / Int32ul, "unknown" * Bytes(44), "last_execution_timestamp" / Int64ul, "Const" * Const(b"\x00\x00\x00\x00"), ) class UserAssistPlugin(Plugin): NAME = "user_assist" DESCRIPTION = "Parse User Assist artifact" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): for guid in GUIDS: try: subkey = self.registry_hive.get_key(rf"{USER_ASSIST_KEY_PATH}\{guid}") count_subkey = subkey.get_subkey("Count") if not count_subkey.values_count: logger.debug(f"Skipping {guid}") continue for value in count_subkey.iter_values(trim_values=False): name = codecs.decode(value.name, encoding="rot-13") if name in WHITELISTED_NAMES: continue for k, v in GUID_TO_PATH_MAPPINGS.items(): if k in name: name = name.replace(k, v) break entry = None data = value.value if len(data) == 72: try: parsed_entry = WIN7_USER_ASSIST.parse(data) except ConstError as ex: logger.error(f"Could not parse user assist entry named {name}: {ex}") continue entry = { "name": name, "timestamp": convert_wintime( parsed_entry.last_execution_timestamp, as_json=self.as_json, ), "run_counter": parsed_entry.run_counter, "focus_count": parsed_entry.focus_count, "total_focus_time_ms": parsed_entry.total_focus_time_ms, "session_id": parsed_entry.session_id, } elif len(data) == 16: try: parsed_entry = WIN_XP_USER_ASSIST.parse(data) except ConstError as ex: logger.error(f"Could not parse user assist entry named {name}: {ex}") continue entry = { "name": name, "timestamp": convert_wintime( parsed_entry.last_execution_timestamp, as_json=self.as_json, ), "session_id": parsed_entry.session_id, "run_counter": parsed_entry.run_counter - 5, } if entry: self.entries.append(entry) except RegistryKeyNotFoundException: continue ================================================ FILE: regipy/plugins/ntuser/winrar.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WINRAR_ARCHIVE_CREATION_HIST = r"\SOFTWARE\WinRAR\DialogEditHistory\ArcName" WINRAR_ARCHIVE_EXTRACT_HIST = r"\SOFTWARE\WinRAR\DialogEditHistory\ExtrPath" WINRAR_ARCHIVE_OPEN_HIST = r"\SOFTWARE\WinRAR\ArcHistory" class WinRARPlugin(Plugin): NAME = "winrar_plugin" DESCRIPTION = "Parse the WinRAR archive history" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: open_subkey = self.registry_hive.get_key(WINRAR_ARCHIVE_OPEN_HIST) timestamp = convert_wintime(open_subkey.header.last_modified, as_json=self.as_json) for value in open_subkey.iter_values(as_json=self.as_json): self.entries.append( { "last_write": timestamp, "file_path": value.value, "value_name": value.name, "operation": "archive_opened", } ) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {WINRAR_ARCHIVE_OPEN_HIST}: {ex}") try: create_subkey = self.registry_hive.get_key(WINRAR_ARCHIVE_CREATION_HIST) timestamp = convert_wintime(create_subkey.header.last_modified, as_json=self.as_json) for value in create_subkey.iter_values(as_json=self.as_json): self.entries.append( { "last_write": timestamp, "file_name": value.value, "value_name": value.name, "operation": "archive_created", } ) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {WINRAR_ARCHIVE_CREATION_HIST}: {ex}") try: extract_subkey = self.registry_hive.get_key(WINRAR_ARCHIVE_EXTRACT_HIST) timestamp = convert_wintime(extract_subkey.header.last_modified, as_json=self.as_json) for value in extract_subkey.iter_values(as_json=self.as_json): self.entries.append( { "last_write": timestamp, "file_path": value.value, "value_name": value.name, "operation": "archive_extracted", } ) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {WINRAR_ARCHIVE_EXTRACT_HIST}: {ex}") ================================================ FILE: regipy/plugins/ntuser/winscp_saved_sessions.py ================================================ import logging from regipy import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WINSCP_SAVED_SESSIONS_PATH = r"\Software\Martin Prikryl\WinSCP 2\Sessions" class WinSCPSavedSessionsPlugin(Plugin): NAME = "winscp_saved_sessions" DESCRIPTION = "Retrieve list of WinSCP saved sessions" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def _get_winscp_saved_sessions(self, subkey_path): try: sessions_sk = self.registry_hive.get_key(subkey_path) except RegistryKeyNotFoundException as ex: logger.error(ex) return for winscp_saved_session in sessions_sk.iter_subkeys(): values = ( {x.name: x.value for x in winscp_saved_session.iter_values(as_json=self.as_json)} if winscp_saved_session.values_count else {} ) self.entries.append( { "timestamp": convert_wintime(winscp_saved_session.header.last_modified, as_json=self.as_json), "hive_name": "HKEY_CURRENT_USER", "key_path": rf"HKEY_CURRENT_USER{subkey_path}\{winscp_saved_session.name}", **values, } ) def run(self): self._get_winscp_saved_sessions(WINSCP_SAVED_SESSIONS_PATH) ================================================ FILE: regipy/plugins/ntuser/word_wheel_query.py ================================================ import logging from construct import CString, GreedyRange, Int32ul from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WORD_WHEEL_QUERY_KEY_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery" class WordWheelQueryPlugin(Plugin): NAME = "word_wheel_query" DESCRIPTION = "Parse the word wheel query artifact" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def run(self): try: subkey = self.registry_hive.get_key(WORD_WHEEL_QUERY_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {WORD_WHEEL_QUERY_KEY_PATH}: {ex}") return None timestamp = convert_wintime(subkey.header.last_modified, as_json=self.as_json) mru_list_order = subkey.get_value("MRUListEx") # If this is the value, the list is empty if mru_list_order == 0xFFFFFFFF: return None for i, entry_name in enumerate(GreedyRange(Int32ul).parse(mru_list_order)): entry_value = subkey.get_value(str(entry_name)) if not entry_value: continue self.entries.append( { "last_write": timestamp, "mru_id": entry_name, "order": i, "name": CString("utf-16").parse(entry_value), } ) ================================================ FILE: regipy/plugins/ntuser/wsl.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # Ressources : https://patrickwu.space/2020/07/19/wsl-related-registry/ WSL_PATH = r"\Software\Microsoft\Windows\CurrentVersion\Lxss" class WSLPlugin(Plugin): NAME = "wsl" DESCRIPTION = "Get WSL information" COMPATIBLE_HIVE = NTUSER_HIVE_TYPE def get_wsl_info(self, subkey, distribs=None): if distribs is None: distribs = [] try: flags = subkey.get_value("Flags") state = subkey.get_value("State") version = subkey.get_value("Version") # Initialize the entry for a distribution with its GUID as the key distribution_entry = { "GUID": subkey.name, "last_modified": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "wsl_distribution_source_location": subkey.get_value("BasePath"), "default_uid": subkey.get_value("DefaultUid"), "distribution_name": subkey.get_value("DistributionName"), "default_environment": subkey.get_value("DefaultEnvironment"), # REG_MULTI_SZ "flags": flags, "kernel_command_line": subkey.get_value("KernelCommandLine"), "package_family_name": subkey.get_value("PackageFamilyName"), "state": state, "filesystem": ("lxfs" if version == 1 else "wslfs" if version == 2 else "Unknown"), } # Decode flags for additional information if flags is not None: distribution_entry["enable_interop"] = bool(flags & 0x1) distribution_entry["append_nt_path"] = bool(flags & 0x2) distribution_entry["enable_drive_mounting"] = bool(flags & 0x4) # Decode the state of the distribution if state is not None: if state == 0x1: distribution_entry["state"] = "Normal" elif state == 0x3: distribution_entry["state"] = "Installing" elif state == 0x4: distribution_entry["state"] = "Uninstalling" else: distribution_entry["state"] = "Unknown" # Add the distribution entry with its GUID to the list of distributions distribs.append(distribution_entry) except Exception as e: logger.error(f"Error processing subkey {subkey.name}: {e}") raise return distribs def run(self): try: # Attempt to get the WSL registry key wsl_key = self.registry_hive.get_key(WSL_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Registry key not found at path {WSL_PATH}: {ex}") return self.entries = { WSL_PATH: { "last_modified": convert_wintime(wsl_key.header.last_modified, as_json=self.as_json), "number_of_distrib": wsl_key.header.subkey_count, "default_distrib_GUID": wsl_key.get_value("DefaultDistribution"), "wsl_version": ( "WSL1" if wsl_key.get_value("DefaultVersion") == 1 else ("WSL2" if wsl_key.get_value("DefaultVersion") == 2 else "Unknown") ), "nat_ip_address": wsl_key.get_value("NatIpAddress"), "distributions": [], } } try: for distrib in wsl_key.iter_subkeys(): self.get_wsl_info(distrib, self.entries[WSL_PATH]["distributions"]) except Exception as e: logger.error(f"Error iterating over subkeys in {distrib.path}: {e}") ================================================ FILE: regipy/plugins/plugin.py ================================================ import logging from typing import Any from regipy.registry import RegistryHive PLUGINS = set() logger = logging.getLogger(__name__) class Plugin: NAME: str = None DESCRIPTION: str = None COMPATIBLE_HIVE: str = None def __init_subclass__(cls): PLUGINS.add(cls) def __init__(self, registry_hive: RegistryHive, as_json=False, trim_values=False): self.registry_hive = registry_hive self.as_json = as_json self.trim_values = trim_values self.partial_hive_path = registry_hive.partial_hive_path # This variable should always hold the final result - in order to use it in anomaly detection and timeline gen. self.entries: list[dict[str, Any]] = [] def can_run(self): """ Wether the plugin can run or not, according to specific checks :return: """ return self.registry_hive.hive_type == self.COMPATIBLE_HIVE def run(self): """ Execute the plugin :return: """ def generate_timeline_artifacts(self): """ Run on the output of a plugin and generate timeline entries :return: """ pass def detect_anomalies(self): """ Run on the output of a plugin and detect possible anomalies :return: """ pass ================================================ FILE: regipy/plugins/plugin_template.py ================================================ import logging from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) class TemplatePlugin(Plugin): NAME = "template_plugin" DESCRIPTION = "template_description" def can_run(self): # TODO: Choose the relevant condition - to determine if the plugin is relevant for the given hive return self.registry_hive.hive_type == NTUSER_HIVE_TYPE def run(self): # TODO: Return the relevant values raise NotImplementedError ================================================ FILE: regipy/plugins/sam/__init__.py ================================================ ================================================ FILE: regipy/plugins/sam/local_sid.py ================================================ """ Windows machine local SID extractor plugin """ import logging from regipy.hive_types import SAM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.security_utils import convert_sid from regipy.structs import SID from regipy.utils import convert_wintime logger = logging.getLogger(__name__) ACCOUNT_PATH = r"\SAM\Domains\Account" class LocalSidPlugin(Plugin): """ Windows machine local SID extractor """ NAME = "local_sid" DESCRIPTION = "Get the machine local SID" COMPATIBLE_HIVE = SAM_HIVE_TYPE def run(self) -> None: logger.debug("Started Machine Local SID Plugin...") account_key = self.registry_hive.get_key(ACCOUNT_PATH) # A computer's SID is stored in the SECURITY hive # under 'SECURITY\SAM\Domains\Account'. # This key has a value named 'F' and a value named 'V'. v_value = account_key.get_value("V") # The 'V' value is a binary value that has the computer SID embedded # within it at the end of its data. sid_value = v_value[-24:] parsed_sid = SID.parse(sid_value) self.entries.append( { "machine_sid": convert_sid(parsed_sid), "timestamp": convert_wintime(account_key.header.last_modified, as_json=self.as_json), } ) ================================================ FILE: regipy/plugins/sam/samparse.py ================================================ """ SAM Parse plugin - Parses user account information from SAM hive """ import contextlib import logging import struct from datetime import datetime from typing import Optional from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SAM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SAM_USERS_PATH = r"\SAM\Domains\Account\Users" SAM_NAMES_PATH = r"\SAM\Domains\Account\Users\Names" # Account type flags ACCOUNT_FLAGS = { 0x0001: "Account Disabled", 0x0002: "Home Directory Required", 0x0004: "Password Not Required", 0x0008: "Temp Duplicate Account", 0x0010: "Normal User Account", 0x0020: "MNS Logon Account", 0x0040: "Interdomain Trust Account", 0x0080: "Workstation Trust Account", 0x0100: "Server Trust Account", 0x0200: "Password Does Not Expire", 0x0400: "Account Auto Locked", 0x0800: "Encrypted Text Password Allowed", 0x1000: "Smartcard Required", 0x2000: "Trusted For Delegation", 0x4000: "Not Delegated", 0x8000: "Use DES Key Only", 0x10000: "Preauth Not Required", 0x20000: "Password Expired", 0x40000: "Trusted To Auth For Delegation", 0x80000: "No Auth Data Required", 0x100000: "Partial Secrets Account", } def filetime_to_datetime(filetime: int) -> Optional[str]: """Convert Windows FILETIME to ISO datetime string""" if filetime == 0 or filetime == 0x7FFFFFFFFFFFFFFF: return None try: # FILETIME is 100-nanosecond intervals since January 1, 1601 epoch_diff = 116444736000000000 # Difference between 1601 and 1970 in 100-ns timestamp = (filetime - epoch_diff) / 10000000 dt = datetime.utcfromtimestamp(timestamp) return dt.isoformat() + "+00:00" except (ValueError, OSError, OverflowError): return None def parse_account_flags(flags: int) -> list: """Parse account flags bitmask to list of descriptions""" result = [] for bit, description in ACCOUNT_FLAGS.items(): if flags & bit: result.append(description) return result class SAMParsePlugin(Plugin): """ Parses user account information from SAM hive Extracts: - User names and RIDs - Account creation time - Last login time - Password last set time - Login count - Account flags (disabled, locked, etc.) - Group memberships Registry Key: SAM\\Domains\\Account\\Users """ NAME = "samparse" DESCRIPTION = "Parses user accounts from SAM hive" COMPATIBLE_HIVE = SAM_HIVE_TYPE def run(self): logger.debug("Started SAM Parse Plugin...") # First, build a mapping of RID to username from the Names subkey rid_to_name = self._get_rid_to_name_mapping() # Then parse user data from the RID subkeys try: users_key = self.registry_hive.get_key(SAM_USERS_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find SAM Users at: {SAM_USERS_PATH}: {ex}") return for subkey in users_key.iter_subkeys(): # Skip the Names subkey if subkey.name == "Names": continue # RID is the subkey name in hex (e.g., "000001F4" = 500 = Administrator) try: rid = int(subkey.name, 16) except ValueError: continue entry = { "key_path": f"{SAM_USERS_PATH}\\{subkey.name}", "rid": rid, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } # Get username from mapping if rid in rid_to_name: entry["username"] = rid_to_name[rid]["username"] entry["name_key_last_write"] = rid_to_name[rid]["last_write"] # Parse the V value (contains most user data) v_value = None f_value = None for value in subkey.iter_values(): if value.name == "V": v_value = value.value elif value.name == "F": f_value = value.value if f_value: self._parse_f_value(f_value, entry) if v_value: self._parse_v_value(v_value, entry) self.entries.append(entry) def _get_rid_to_name_mapping(self) -> dict: """Build mapping of RID to username from Names subkey""" mapping = {} try: names_key = self.registry_hive.get_key(SAM_NAMES_PATH) except RegistryKeyNotFoundException: return mapping for subkey in names_key.iter_subkeys(): username = subkey.name last_write = convert_wintime(subkey.header.last_modified, as_json=self.as_json) # The default value's type contains the RID for value in subkey.iter_values(): if value.name == "" or value.name == "(Default)": # The value type is the RID rid = value.value_type_raw if hasattr(value, "value_type_raw") else None if rid is None: # Try to get from the raw type with contextlib.suppress(Exception): rid = value._vk_record.data_type if rid: mapping[rid] = {"username": username, "last_write": last_write} break return mapping def _parse_f_value(self, data, entry: dict): """Parse the F value containing user account metadata""" if not data: return # Ensure data is bytes if isinstance(data, str): try: data = bytes.fromhex(data) except ValueError: data = data.encode("latin-1") if len(data) < 72: return try: # F value structure (offsets are for Vista+) # 0x08: Last Login Time (FILETIME) # 0x18: Password Last Set (FILETIME) # 0x20: Account Expires (FILETIME) # 0x28: Last Failed Login (FILETIME) # 0x30: RID # 0x38: Account Control Flags # 0x40: Failed Login Count # 0x42: Login Count last_login = struct.unpack(" 0: username = data[name_offset : name_offset + name_length].decode("utf-16-le", errors="replace") if "username" not in entry: entry["username"] = username # Full Name at offset 0x18 fullname_offset = struct.unpack(" 0: entry["full_name"] = data[fullname_offset : fullname_offset + fullname_length].decode( "utf-16-le", errors="replace" ) # Comment at offset 0x24 comment_offset = struct.unpack(" 0: entry["comment"] = data[comment_offset : comment_offset + comment_length].decode("utf-16-le", errors="replace") # User Comment at offset 0x30 user_comment_offset = struct.unpack(" 0: entry["user_comment"] = data[user_comment_offset : user_comment_offset + user_comment_length].decode( "utf-16-le", errors="replace" ) # Home Directory at offset 0x48 homedir_offset = struct.unpack(" 0: entry["home_directory"] = data[homedir_offset : homedir_offset + homedir_length].decode( "utf-16-le", errors="replace" ) # Home Directory Connect at offset 0x54 homedir_connect_offset = struct.unpack(" 0: entry["home_directory_connect"] = data[ homedir_connect_offset : homedir_connect_offset + homedir_connect_length ].decode("utf-16-le", errors="replace") # Script Path at offset 0x60 script_offset = struct.unpack(" 0: entry["script_path"] = data[script_offset : script_offset + script_length].decode("utf-16-le", errors="replace") # Profile Path at offset 0x6C profile_offset = struct.unpack(" 0: entry["profile_path"] = data[profile_offset : profile_offset + profile_length].decode( "utf-16-le", errors="replace" ) # Workstations at offset 0x78 workstations_offset = struct.unpack(" 0: entry["workstations"] = data[workstations_offset : workstations_offset + workstations_length].decode( "utf-16-le", errors="replace" ) except (struct.error, IndexError) as e: logger.debug(f"Error parsing V value: {e}") ================================================ FILE: regipy/plugins/security/__init__.py ================================================ ================================================ FILE: regipy/plugins/security/domain_sid.py ================================================ """ Windows machine domain name and SID extractor plugin """ import logging from typing import Optional from regipy.hive_types import SECURITY_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.security_utils import convert_sid from regipy.structs import SID from regipy.utils import convert_wintime logger = logging.getLogger(__name__) DOMAIN_NAME_PATH = r"\Policy\PolPrDmN" DOMAIN_SID_PATH = r"\Policy\PolMachineAccountS" class DomainSidPlugin(Plugin): """ Windows machine domain name and SID extractor """ NAME = "domain_sid" DESCRIPTION = "Get the machine domain name and SID" COMPATIBLE_HIVE = SECURITY_HIVE_TYPE def run(self) -> None: logger.debug("Started Machine Domain SID Plugin...") name_key = self.registry_hive.get_key(DOMAIN_NAME_PATH) # Primary Domain Name or Workgroup Name (binary-encoded and length-prefixed) name_value = name_key.get_value() # Skip UNICODE_STRING struct header and strip trailing \x0000 domain_name = name_value[8:].decode("utf-16-le", errors="replace").rstrip("\x00") sid_key = self.registry_hive.get_key(DOMAIN_SID_PATH) # Domain SID value (binary-encoded) sid_value = sid_key.get_value() domain_sid: Optional[str] = None machine_sid: Optional[str] = None # The default key value is 0x00000000 (REG_DWORD) when # the Windows machine is not in an AD domain. # Otherwise, it contains the domain machine SID data # in the standard binary format (REG_BINARY). if isinstance(sid_value, bytes): parsed_sid = SID.parse(sid_value) domain_sid = convert_sid(parsed_sid, strip_rid=True) machine_sid = convert_sid(parsed_sid) self.entries.append( { "domain_name": domain_name, "domain_sid": domain_sid, "machine_sid": machine_sid, "timestamp": convert_wintime(sid_key.header.last_modified, as_json=self.as_json), } ) ================================================ FILE: regipy/plugins/software/__init__.py ================================================ ================================================ FILE: regipy/plugins/software/appcompatflags.py ================================================ """ AppCompatFlags plugin - Parses application compatibility flags and layers """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) APPCOMPAT_FLAGS_PATH = r"\Microsoft\Windows NT\CurrentVersion\AppCompatFlags" APPCOMPAT_LAYERS_PATH = r"\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" APPCOMPAT_CUSTOM_PATH = r"\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Custom" class AppCompatFlagsPlugin(Plugin): """ Parses Application Compatibility Flags from SOFTWARE hive Provides information about: - Compatibility layers (e.g., RunAsAdmin, WinXP compatibility mode) - Custom compatibility shims applied to applications - Evidence of application execution Registry Keys: - Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags - Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers - Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Custom """ NAME = "appcompat_flags" DESCRIPTION = "Parses application compatibility flags and layers" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started AppCompatFlags Plugin...") self._parse_layers() self._parse_custom() def _parse_layers(self): """Parse compatibility layers applied to applications""" try: layers_key = self.registry_hive.get_key(APPCOMPAT_LAYERS_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find AppCompatFlags Layers at: {APPCOMPAT_LAYERS_PATH}") return entry = { "type": "layers", "key_path": APPCOMPAT_LAYERS_PATH, "last_write": convert_wintime(layers_key.header.last_modified, as_json=self.as_json), "applications": [], } for value in layers_key.iter_values(): # Value name is the application path # Value data is the compatibility settings (e.g., "~ RUNASADMIN") app_entry = { "path": value.name, "layers": value.value, } # Parse the layers string if isinstance(value.value, str): layers = [layer.strip() for layer in value.value.split() if layer.strip() and layer.strip() != "~"] app_entry["parsed_layers"] = layers entry["applications"].append(app_entry) if entry["applications"]: self.entries.append(entry) def _parse_custom(self): """Parse custom compatibility shims""" try: custom_key = self.registry_hive.get_key(APPCOMPAT_CUSTOM_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find AppCompatFlags Custom at: {APPCOMPAT_CUSTOM_PATH}") return for subkey in custom_key.iter_subkeys(): # Each subkey is an application name (e.g., "program.exe") app_name = subkey.name subkey_path = f"{APPCOMPAT_CUSTOM_PATH}\\{app_name}" entry = { "type": "custom", "key_path": subkey_path, "application": app_name, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "shims": [], } for value in subkey.iter_values(): # Value names are shim database identifiers entry["shims"].append( { "name": value.name, "value": value.value, } ) if entry["shims"]: self.entries.append(entry) ================================================ FILE: regipy/plugins/software/appinitdlls.py ================================================ """ AppInit_DLLs plugin - Parses persistence via AppInit_DLLs """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # 32-bit path APPINIT_DLLS_PATH = r"\Microsoft\Windows NT\CurrentVersion\Windows" # 64-bit path (WoW6432Node) APPINIT_DLLS_WOW64_PATH = r"\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows" class AppInitDLLsPlugin(Plugin): """ Parses AppInit_DLLs persistence mechanism from SOFTWARE hive AppInit_DLLs is a registry value that causes Windows to load specified DLLs into every user-mode process that links to User32.dll. This is a known persistence mechanism used by malware. Registry Keys: - Microsoft\\Windows NT\\CurrentVersion\\Windows - Wow6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Windows (64-bit systems) """ NAME = "appinit_dlls" DESCRIPTION = "Parses AppInit_DLLs persistence entries" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started AppInit_DLLs Plugin...") self._parse_appinit_dlls(APPINIT_DLLS_PATH, "x64") self._parse_appinit_dlls(APPINIT_DLLS_WOW64_PATH, "x86") def _parse_appinit_dlls(self, path: str, architecture: str): """Parse AppInit_DLLs at the given path""" try: windows_key = self.registry_hive.get_key(path) except RegistryKeyNotFoundException: logger.debug(f"Could not find AppInit_DLLs at: {path}") return entry = { "key_path": path, "architecture": architecture, "last_write": convert_wintime(windows_key.header.last_modified, as_json=self.as_json), "appinit_dlls": None, "load_appinit_dlls": None, "require_signed_appinit_dlls": None, } for value in windows_key.iter_values(): name = value.name val = value.value if name == "AppInit_DLLs": entry["appinit_dlls"] = val elif name == "LoadAppInit_DLLs": entry["load_appinit_dlls"] = val == 1 elif name == "RequireSignedAppInit_DLLs": entry["require_signed_appinit_dlls"] = val == 1 # Only add entry if AppInit_DLLs has content or loading is enabled if entry["appinit_dlls"] or entry["load_appinit_dlls"]: self.entries.append(entry) ================================================ FILE: regipy/plugins/software/apppaths.py ================================================ """ App Paths plugin - Parses application paths registry entries """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) APP_PATHS_PATH = r"\Microsoft\Windows\CurrentVersion\App Paths" APP_PATHS_WOW64_PATH = r"\Wow6432Node\Microsoft\Windows\CurrentVersion\App Paths" class AppPathsPlugin(Plugin): """ Parses App Paths from SOFTWARE hive App Paths allows applications to be launched by name without specifying the full path. Each subkey under App Paths is an executable name, and contains the path to the actual executable. This is useful for: - Finding installed applications - Detecting persistence (malware can hijack app paths) - Understanding application configurations Registry Keys: - Microsoft\\Windows\\CurrentVersion\\App Paths - Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths """ NAME = "app_paths" DESCRIPTION = "Parses application paths registry entries" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started App Paths Plugin...") self._parse_app_paths(APP_PATHS_PATH, "x64") self._parse_app_paths(APP_PATHS_WOW64_PATH, "x86") def _parse_app_paths(self, path: str, architecture: str): """Parse App Paths at the given path""" try: app_paths_key = self.registry_hive.get_key(path) except RegistryKeyNotFoundException: logger.debug(f"Could not find App Paths at: {path}") return for subkey in app_paths_key.iter_subkeys(): app_name = subkey.name subkey_path = f"{path}\\{app_name}" entry = { "key_path": subkey_path, "application": app_name, "architecture": architecture, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } for value in subkey.iter_values(): name = value.name.lower() if value.name else "(default)" val = value.value if name == "(default)" or value.name == "": entry["path"] = val elif name == "path": entry["app_path"] = val elif name == "useurl": entry["use_url"] = val elif name == "dropmenu": entry["drop_menu"] = val elif name == "dontusedestoolbar": entry["dont_use_des_toolbar"] = val self.entries.append(entry) ================================================ FILE: regipy/plugins/software/classes_installer.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) CLASSES_INSTALLER_PATH = r"\Classes\Installer\Products" class SoftwareClassesInstallerPlugin(Plugin): NAME = "software_classes_installer" DESCRIPTION = "List of installed software from SOFTWARE hive" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): try: installer_subkey = self.registry_hive.get_key(CLASSES_INSTALLER_PATH) except RegistryKeyNotFoundException as ex: logger.error(ex) return for entry in installer_subkey.iter_subkeys(): identifier = entry.name timestamp = convert_wintime(entry.header.last_modified, as_json=self.as_json) product_name = entry.get_value("ProductName") self.entries.append( { "identifier": identifier, "timestamp": timestamp, "product_name": product_name, "is_hidden": product_name is None, } ) ================================================ FILE: regipy/plugins/software/defender.py ================================================ """ Windows Defender plugin - Parses Windows Defender configuration """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) DEFENDER_PATH = r"\Microsoft\Windows Defender" DEFENDER_POLICY_PATH = r"\Policies\Microsoft\Windows Defender" DEFENDER_EXCLUSIONS_PATH = r"\Microsoft\Windows Defender\Exclusions" class WindowsDefenderPlugin(Plugin): """ Parses Windows Defender configuration from SOFTWARE hive Extracts: - Defender enabled/disabled status - Real-time protection settings - Exclusions (paths, processes, extensions) - Policy overrides Registry Keys: - Microsoft\\Windows Defender - Policies\\Microsoft\\Windows Defender """ NAME = "windows_defender" DESCRIPTION = "Parses Windows Defender configuration and exclusions" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started Windows Defender Plugin...") self._parse_defender_config() self._parse_defender_policy() self._parse_exclusions() def _parse_defender_config(self): """Parse main Defender configuration""" try: defender_key = self.registry_hive.get_key(DEFENDER_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find Windows Defender at: {DEFENDER_PATH}") return entry = { "type": "configuration", "key_path": DEFENDER_PATH, "last_write": convert_wintime(defender_key.header.last_modified, as_json=self.as_json), } extract_values( defender_key, { "DisableAntiSpyware": ("antispyware_disabled", lambda v: v == 1), "DisableAntiVirus": ("antivirus_disabled", lambda v: v == 1), "ProductStatus": "product_status", "InstallLocation": "install_location", }, entry, ) # Parse Real-Time Protection subkey try: rtp_key = self.registry_hive.get_key(f"{DEFENDER_PATH}\\Real-Time Protection") extract_values( rtp_key, { "DisableRealtimeMonitoring": ("realtime_monitoring_disabled", lambda v: v == 1), "DisableBehaviorMonitoring": ("behavior_monitoring_disabled", lambda v: v == 1), "DisableOnAccessProtection": ("on_access_protection_disabled", lambda v: v == 1), "DisableScanOnRealtimeEnable": ("scan_on_realtime_enable_disabled", lambda v: v == 1), }, entry, ) except RegistryKeyNotFoundException: pass self.entries.append(entry) def _parse_defender_policy(self): """Parse Defender Group Policy settings""" try: policy_key = self.registry_hive.get_key(DEFENDER_POLICY_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find Windows Defender policy at: {DEFENDER_POLICY_PATH}") return entry = { "type": "policy", "key_path": DEFENDER_POLICY_PATH, "last_write": convert_wintime(policy_key.header.last_modified, as_json=self.as_json), } extract_values( policy_key, { "DisableAntiSpyware": ("policy_antispyware_disabled", lambda v: v == 1), "DisableAntiVirus": ("policy_antivirus_disabled", lambda v: v == 1), "DisableRoutinelyTakingAction": ("routine_action_disabled", lambda v: v == 1), }, entry, ) # Check for Real-Time Protection policy try: rtp_policy_key = self.registry_hive.get_key(f"{DEFENDER_POLICY_PATH}\\Real-Time Protection") extract_values( rtp_policy_key, { "DisableRealtimeMonitoring": ("policy_realtime_monitoring_disabled", lambda v: v == 1), }, entry, ) except RegistryKeyNotFoundException: pass if len(entry) > 3: # More than just type, key_path, last_write self.entries.append(entry) def _parse_exclusions(self): """Parse Defender exclusions""" exclusion_types = { "Paths": "path_exclusions", "Processes": "process_exclusions", "Extensions": "extension_exclusions", "IpAddresses": "ip_exclusions", } for exclusion_type in exclusion_types: path = f"{DEFENDER_EXCLUSIONS_PATH}\\{exclusion_type}" try: exclusions_key = self.registry_hive.get_key(path) except RegistryKeyNotFoundException: continue exclusions = [] for value in exclusions_key.iter_values(): # Value name is the excluded item exclusions.append(value.name) if exclusions: entry = { "type": "exclusion", "exclusion_type": exclusion_type.lower(), "key_path": path, "last_write": convert_wintime(exclusions_key.header.last_modified, as_json=self.as_json), "exclusions": exclusions, } self.entries.append(entry) ================================================ FILE: regipy/plugins/software/disablesr.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SYS_RESTORE_PATH = r"\Microsoft\Windows NT\CurrentVersion\SystemRestore" value_list = ["DisableSR"] class DisableSRPlugin(Plugin): NAME = "disablesr_plugin" DESCRIPTION = "Gets the value that turns System Restore either on or off" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SOFTWARE_HIVE_TYPE def run(self): logger.info("Started disablesr Plugin...") try: key = self.registry_hive.get_key(SYS_RESTORE_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {SYS_RESTORE_PATH}: {ex}") return None self.entries = {SYS_RESTORE_PATH: {"last_write": convert_wintime(key.header.last_modified).isoformat()}} for val in key.iter_values(): if val.name in value_list: self.entries[SYS_RESTORE_PATH][val.name] = val.value ================================================ FILE: regipy/plugins/software/execpolicy.py ================================================ """ Execution Policy plugin - Parses PowerShell and script execution policies """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # PowerShell execution policy paths PS_SHELL_IDS_PATH = r"\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" PS_POLICY_PATH = r"\Policies\Microsoft\Windows\PowerShell" # Script execution paths WSH_SETTINGS_PATH = r"\Microsoft\Windows Script Host\Settings" class ExecutionPolicyPlugin(Plugin): """ Parses execution policies from SOFTWARE hive Extracts: - PowerShell execution policy - Windows Script Host (WSH) settings Registry Keys: - Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.PowerShell - Policies\\Microsoft\\Windows\\PowerShell - Microsoft\\Windows Script Host\\Settings """ NAME = "execution_policy" DESCRIPTION = "Parses PowerShell and script execution policies" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started Execution Policy Plugin...") self._parse_powershell_policy() self._parse_powershell_group_policy() self._parse_wsh_settings() def _parse_powershell_policy(self): """Parse PowerShell execution policy""" try: ps_key = self.registry_hive.get_key(PS_SHELL_IDS_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PowerShell ShellIds at: {PS_SHELL_IDS_PATH}") return entry = { "type": "powershell", "key_path": PS_SHELL_IDS_PATH, "last_write": convert_wintime(ps_key.header.last_modified, as_json=self.as_json), } extract_values( ps_key, { "ExecutionPolicy": "execution_policy", "Path": "path", }, entry, ) self.entries.append(entry) def _parse_powershell_group_policy(self): """Parse PowerShell Group Policy settings""" try: gp_key = self.registry_hive.get_key(PS_POLICY_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PowerShell policy at: {PS_POLICY_PATH}") return entry = { "type": "powershell_policy", "key_path": PS_POLICY_PATH, "last_write": convert_wintime(gp_key.header.last_modified, as_json=self.as_json), } extract_values( gp_key, { "ExecutionPolicy": "execution_policy", "EnableScripts": ("scripts_enabled", lambda v: v == 1), }, entry, ) if "execution_policy" in entry or "scripts_enabled" in entry: self.entries.append(entry) def _parse_wsh_settings(self): """Parse Windows Script Host settings""" try: wsh_key = self.registry_hive.get_key(WSH_SETTINGS_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find WSH Settings at: {WSH_SETTINGS_PATH}") return entry = { "type": "wsh", "key_path": WSH_SETTINGS_PATH, "last_write": convert_wintime(wsh_key.header.last_modified, as_json=self.as_json), } extract_values( wsh_key, { "Enabled": ("enabled", lambda v: v != 0), "Remote": ("remote_enabled", lambda v: v == 1), "TrustPolicy": "trust_policy", "IgnoreUserSettings": ("ignore_user_settings", lambda v: v == 1), "LogSecuritySuccesses": ("log_security_successes", lambda v: v == 1), "DisplayLogo": ("display_logo", lambda v: v == 1), }, entry, ) if len(entry) > 3: self.entries.append(entry) ================================================ FILE: regipy/plugins/software/image_file_execution_options.py ================================================ import logging from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) IMAGE_FILE_EXECUTION_OPTIONS = r"\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" class ImageFileExecutionOptions(Plugin): NAME = "image_file_execution_options" DESCRIPTION = "Retrieve image file execution options - a persistence method" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): image_file_execution_options = self.registry_hive.get_key(IMAGE_FILE_EXECUTION_OPTIONS) if image_file_execution_options.subkey_count: for subkey in image_file_execution_options.iter_subkeys(): values = {x.name: x.value for x in subkey.iter_values(as_json=self.as_json)} if subkey.values_count else {} self.entries.append( { "name": subkey.name, "timestamp": convert_wintime(subkey.header.last_modified, as_json=self.as_json), **values, } ) ================================================ FILE: regipy/plugins/software/installed_programs.py ================================================ import logging from regipy import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) X64_INSTALLED_SOFTWARE_PATH = r"\Microsoft\Windows\CurrentVersion\Uninstall" X86_INSTALLED_SOFTWARE_PATH = r"\Wow6432Node" + X64_INSTALLED_SOFTWARE_PATH class InstalledProgramsSoftwarePlugin(Plugin): NAME = "installed_programs_software" DESCRIPTION = "Retrieve list of installed programs and their install date from the SOFTWARE Hive" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def _get_installed_software(self, subkey_path): try: uninstall_sk = self.registry_hive.get_key(subkey_path) except RegistryKeyNotFoundException as ex: logger.error(ex) return for installed_program in uninstall_sk.iter_subkeys(): values = ( {x.name: x.value for x in installed_program.iter_values(as_json=self.as_json)} if installed_program.values_count else {} ) self.entries.append( { "service_name": installed_program.name, "timestamp": convert_wintime(installed_program.header.last_modified, as_json=self.as_json), "registry_path": subkey_path, **values, } ) def run(self): self._get_installed_software(X64_INSTALLED_SOFTWARE_PATH) self._get_installed_software(X86_INSTALLED_SOFTWARE_PATH) ================================================ FILE: regipy/plugins/software/last_logon.py ================================================ import logging from inflection import underscore from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) LAST_LOGON_KEY_PATH = r"\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" class LastLogonPlugin(Plugin): NAME = "last_logon_plugin" DESCRIPTION = "Get the last logged on username" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): try: subkey = self.registry_hive.get_key(LAST_LOGON_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {LAST_LOGON_KEY_PATH}: {ex}") return None self.entries = { "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), **{underscore(x.name): x.value for x in subkey.iter_values(as_json=self.as_json)}, } ================================================ FILE: regipy/plugins/software/networklist.py ================================================ """ NetworkList plugin - Parses network connection history """ import logging import struct from datetime import datetime from typing import Optional from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) NETWORK_LIST_PATH = r"\Microsoft\Windows NT\CurrentVersion\NetworkList" PROFILES_PATH = r"\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles" SIGNATURES_PATH = r"\Microsoft\Windows NT\CurrentVersion\NetworkList\Signatures" NLAS_PATH = r"\Microsoft\Windows NT\CurrentVersion\NetworkList\Nla\Wireless" # Lookup tables for network types CATEGORY_TYPES = {0: "Public", 1: "Private", 2: "Domain"} NAME_TYPES = {0x06: "Wired", 0x17: "Broadband", 0x47: "Wireless"} def format_mac_address(val) -> Optional[str]: """Format bytes as MAC address (XX:XX:XX:XX:XX:XX)""" if isinstance(val, bytes) and len(val) == 6: return ":".join(f"{b:02X}" for b in val) return val def parse_network_date(data: bytes) -> Optional[str]: """Parse the binary date format used in NetworkList""" if not data or len(data) < 16: return None try: # Format: Year(2) Month(2) DayOfWeek(2) Day(2) Hour(2) Minute(2) Second(2) Milliseconds(2) year = struct.unpack(" 0 and month > 0 and day > 0: dt = datetime(year, month, day, hour, minute, second) return dt.isoformat() except Exception: pass return None class NetworkListPlugin(Plugin): """ Parses Network List (NetworkList) from SOFTWARE hive Provides information about: - Network profiles (wired and wireless networks connected to) - First and last connection times - Network signatures (MAC addresses, SSIDs) Registry Key: Microsoft\\Windows NT\\CurrentVersion\\NetworkList """ NAME = "networklist" DESCRIPTION = "Parses network connection history" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started NetworkList Plugin...") self._parse_profiles() self._parse_signatures() def _parse_profiles(self): """Parse network profiles""" try: profiles_key = self.registry_hive.get_key(PROFILES_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find NetworkList Profiles at: {PROFILES_PATH}") return for subkey in profiles_key.iter_subkeys(): profile_guid = subkey.name subkey_path = f"{PROFILES_PATH}\\{profile_guid}" entry = { "type": "profile", "key_path": subkey_path, "profile_guid": profile_guid, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } extract_values( subkey, { "ProfileName": "profile_name", "Description": "description", "Managed": ("managed", lambda v: v == 1), "Category": ("category", lambda v: CATEGORY_TYPES.get(v, f"Unknown ({v})")), "CategoryType": "category_type", "NameType": ("name_type", lambda v: NAME_TYPES.get(v, f"Unknown ({v})")), "DateCreated": ("date_created", parse_network_date), "DateLastConnected": ("date_last_connected", parse_network_date), }, entry, ) self.entries.append(entry) def _parse_signatures(self): """Parse network signatures (contains MAC addresses)""" signature_types = ["Managed", "Unmanaged"] for sig_type in signature_types: sig_path = f"{SIGNATURES_PATH}\\{sig_type}" try: sig_key = self.registry_hive.get_key(sig_path) except RegistryKeyNotFoundException: continue for subkey in sig_key.iter_subkeys(): signature_id = subkey.name subkey_path = f"{sig_path}\\{signature_id}" entry = { "type": "signature", "signature_type": sig_type.lower(), "key_path": subkey_path, "signature_id": signature_id, "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } extract_values( subkey, { "ProfileGuid": "profile_guid", "Description": "description", "Source": "source", "DefaultGatewayMac": ("default_gateway_mac", format_mac_address), "DnsSuffix": "dns_suffix", "FirstNetwork": "first_network", }, entry, ) self.entries.append(entry) ================================================ FILE: regipy/plugins/software/persistence.py ================================================ import logging from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import get_subkey_values_from_list logger = logging.getLogger(__name__) PERSISTENCE_ENTRIES = [ r"\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", r"\Microsoft\Windows\CurrentVersion\Run", r"\Microsoft\Windows\CurrentVersion\RunOnce", r"\Microsoft\Windows\CurrentVersion\RunOnce\Setup", r"\Microsoft\Windows\CurrentVersion\RunOnceEx", r"\Wow6432Node\Microsoft\Windows\CurrentVersion\Run", r"\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce", r"\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnce\Setup", r"\Wow6432Node\Microsoft\Windows\CurrentVersion\RunOnceEx", r"\Wow6432Node\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run", r"\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify", ] class SoftwarePersistencePlugin(Plugin): NAME = "software_plugin" DESCRIPTION = "Retrieve values from known persistence subkeys in Software hive" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): self.entries = get_subkey_values_from_list(self.registry_hive, PERSISTENCE_ENTRIES, as_json=self.as_json) ================================================ FILE: regipy/plugins/software/printdemon.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) PORTS_KEY_PATH = r"\Microsoft\Windows NT\CurrentVersion\Ports" class PrintDemonPlugin(Plugin): NAME = "print_demon_plugin" DESCRIPTION = "Get list of installed printer ports, as could be taken advantage by cve-2020-1048" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): try: subkey = self.registry_hive.get_key(PORTS_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {PORTS_KEY_PATH}: {ex}") return None last_write = convert_wintime(subkey.header.last_modified).isoformat() for port in subkey.iter_values(): self.entries.append( { "timestamp": last_write, "port_name": port.name, "parameters": (port.value if isinstance(port.value, int) else port.value.split(",")), } ) ================================================ FILE: regipy/plugins/software/profilelist.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_filetime, convert_wintime logger = logging.getLogger(__name__) PROFILE_LIST_KEY_PATH = r"\Microsoft\Windows NT\CurrentVersion\ProfileList" class ProfileListPlugin(Plugin): NAME = "profilelist_plugin" DESCRIPTION = "Parses information about user profiles found in the ProfileList key" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started profile list plugin...") try: subkey = self.registry_hive.get_key(PROFILE_LIST_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(ex) for profile in subkey.iter_subkeys(): self.entries.append( { "last_write": convert_wintime(profile.header.last_modified, as_json=self.as_json), "path": profile.get_value("ProfileImagePath"), "flags": profile.get_value("Flags"), "full_profile": profile.get_value("FullProfile"), "state": profile.get_value("State"), "sid": profile.name, "load_time": convert_filetime( profile.get_value("ProfileLoadTimeLow"), profile.get_value("ProfileLoadTimeHigh"), ), "local_load_time": convert_filetime( profile.get_value("LocalProfileLoadTimeLow"), profile.get_value("LocalProfileLoadTimeHigh"), ), } ) ================================================ FILE: regipy/plugins/software/pslogging.py ================================================ """ PowerShell Logging plugin - Parses PowerShell logging configuration """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) # PowerShell policy paths PS_POLICY_PATH = r"\Policies\Microsoft\Windows\PowerShell" PS_SCRIPTBLOCK_PATH = r"\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" PS_MODULE_PATH = r"\Policies\Microsoft\Windows\PowerShell\ModuleLogging" PS_TRANSCRIPTION_PATH = r"\Policies\Microsoft\Windows\PowerShell\Transcription" class PowerShellLoggingPlugin(Plugin): """ Parses PowerShell logging configuration from SOFTWARE hive Extracts: - Script Block Logging settings - Module Logging settings - Transcription settings - Execution Policy These settings are important for security monitoring and incident response. Registry Key: Policies\\Microsoft\\Windows\\PowerShell """ NAME = "powershell_logging" DESCRIPTION = "Parses PowerShell logging and execution policy" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): logger.debug("Started PowerShell Logging Plugin...") self._parse_main_policy() self._parse_scriptblock_logging() self._parse_module_logging() self._parse_transcription() def _parse_main_policy(self): """Parse main PowerShell policy settings""" try: ps_key = self.registry_hive.get_key(PS_POLICY_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find PowerShell policy at: {PS_POLICY_PATH}") return entry = { "type": "policy", "key_path": PS_POLICY_PATH, "last_write": convert_wintime(ps_key.header.last_modified, as_json=self.as_json), } extract_values( ps_key, { "EnableScripts": ("scripts_enabled", lambda v: v == 1), "ExecutionPolicy": "execution_policy", }, entry, ) if len(entry) > 3: self.entries.append(entry) def _parse_scriptblock_logging(self): """Parse Script Block Logging settings""" try: sb_key = self.registry_hive.get_key(PS_SCRIPTBLOCK_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find ScriptBlockLogging at: {PS_SCRIPTBLOCK_PATH}") return entry = { "type": "scriptblock_logging", "key_path": PS_SCRIPTBLOCK_PATH, "last_write": convert_wintime(sb_key.header.last_modified, as_json=self.as_json), } extract_values( sb_key, { "EnableScriptBlockLogging": ("enabled", lambda v: v == 1), "EnableScriptBlockInvocationLogging": ("invocation_logging_enabled", lambda v: v == 1), }, entry, ) self.entries.append(entry) def _parse_module_logging(self): """Parse Module Logging settings""" try: ml_key = self.registry_hive.get_key(PS_MODULE_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find ModuleLogging at: {PS_MODULE_PATH}") return entry = { "type": "module_logging", "key_path": PS_MODULE_PATH, "last_write": convert_wintime(ml_key.header.last_modified, as_json=self.as_json), } extract_values( ml_key, { "EnableModuleLogging": ("enabled", lambda v: v == 1), }, entry, ) # Check for module names subkey try: modules_key = self.registry_hive.get_key(f"{PS_MODULE_PATH}\\ModuleNames") module_names = [] for value in modules_key.iter_values(): module_names.append(value.name) if module_names: entry["logged_modules"] = module_names except RegistryKeyNotFoundException: pass self.entries.append(entry) def _parse_transcription(self): """Parse Transcription settings""" try: tr_key = self.registry_hive.get_key(PS_TRANSCRIPTION_PATH) except RegistryKeyNotFoundException: logger.debug(f"Could not find Transcription at: {PS_TRANSCRIPTION_PATH}") return entry = { "type": "transcription", "key_path": PS_TRANSCRIPTION_PATH, "last_write": convert_wintime(tr_key.header.last_modified, as_json=self.as_json), } extract_values( tr_key, { "EnableTranscripting": ("enabled", lambda v: v == 1), "OutputDirectory": "output_directory", "EnableInvocationHeader": ("invocation_header_enabled", lambda v: v == 1), }, entry, ) self.entries.append(entry) ================================================ FILE: regipy/plugins/software/spp_clients.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SPP_CLIENT_PATH = r"\Microsoft\Windows NT\CurrentVersion\SPP\Clients" value_list = "{09F7EDC5-294E-4180-AF6A-FB0E6A0E9513}" class SppClientsPlugin(Plugin): NAME = "spp_clients_plugin" DESCRIPTION = "Determines volumes monitored by VSS" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SOFTWARE_HIVE_TYPE def run(self): logger.info("Started spp_clients Plugin...") try: key = self.registry_hive.get_key(SPP_CLIENT_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {SPP_CLIENT_PATH}: {ex}") return None self.entries = {SPP_CLIENT_PATH: {"last_write": convert_wintime(key.header.last_modified).isoformat()}} for val in key.iter_values(): if val.name in value_list: aux_list = [] for value in val.value: aux_list.append(value.replace("%3A", ":")) self.entries[SPP_CLIENT_PATH][val.name] = aux_list ================================================ FILE: regipy/plugins/software/susclient.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WIN_VER_PATH = r"\Microsoft\Windows\CurrentVersion\WindowsUpdate" value_list = ("LastRestorePointSetTime", "SusClientId") class SusclientPlugin(Plugin): NAME = "susclient_plugin" DESCRIPTION = "Extracts SusClient* info, including HDD SN" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SOFTWARE_HIVE_TYPE def run(self): logger.info("Started susclient Plugin...") try: key = self.registry_hive.get_key(WIN_VER_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {WIN_VER_PATH}: {ex}") return None self.entries = {WIN_VER_PATH: {"last_write": convert_wintime(key.header.last_modified).isoformat()}} for val in key.iter_values(): if val.name == "SusClientIdValidation": self.entries[WIN_VER_PATH][val.name] = get_SN(val.value) elif val.name in value_list: self.entries[WIN_VER_PATH][val.name] = val.value def get_SN(data): offset = int(data[:2], 16) length = int(data[4:6], 16) return bytes.fromhex(data[2 * offset : 2 * (length + offset)]).decode("utf-16le") ================================================ FILE: regipy/plugins/software/tracing.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) TRACING_PATH = r"\Microsoft\Tracing" X86_TRACING_PATH = r"\Wow6432Node" + TRACING_PATH class RASTracingPlugin(Plugin): NAME = "ras_tracing" DESCRIPTION = "Retrieve list of executables using ras" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def _get_installed_software(self, subkey_path): try: ras_subkey = self.registry_hive.get_key(subkey_path) except RegistryKeyNotFoundException as ex: logger.error(ex) return for entry in ras_subkey.iter_subkeys(): timestamp = convert_wintime(entry.header.last_modified, as_json=self.as_json) self.entries.append({"key": subkey_path, "name": entry.name, "timestamp": timestamp}) def run(self): self._get_installed_software(TRACING_PATH) self._get_installed_software(X86_TRACING_PATH) ================================================ FILE: regipy/plugins/software/uac.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) UAC_KEY_PATH = r"\Microsoft\Windows\CurrentVersion\Policies\System" class UACStatusPlugin(Plugin): NAME = "uac_plugin" DESCRIPTION = "Get the status of User Access Control" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def run(self): """ This plugin checks the following parameters: EnableLUA - Windows notifies the user when programs try to make changes to the computer EnableVirtualization - enables the redirection of legacy application File and Registry writes that would normally fail as standard user to a user-writable data location. FilterAdministratorToken - If enabled approval is required when performing administrative tasks. ConsentPromptBehaviorAdmin - This option allows the Consent Admin to perform an operation that requires elevation without consent or credentials. ConsentPromptBehaviorUser - If enabled, a standard user that needs to perform an operation that requires elevation of privilege will be prompted for an administrative user name and password """ try: subkey = self.registry_hive.get_key(UAC_KEY_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {UAC_KEY_PATH}: {ex}") return None self.entries = { "last_write": convert_wintime(subkey.header.last_modified, as_json=self.as_json), "enable_limited_user_accounts": subkey.get_value("EnableLUA", as_json=self.as_json), "enable_virtualization": subkey.get_value("EnableVirtualization", as_json=self.as_json), "filter_admin_token": subkey.get_value("FilterAdministratorToken", as_json=self.as_json), "consent_prompt_admin": subkey.get_value("ConsentPromptBehaviorAdmin", as_json=self.as_json), "consent_prompt_user": subkey.get_value("ConsentPromptBehaviorUser", as_json=self.as_json), } ================================================ FILE: regipy/plugins/software/winver.py ================================================ import logging from datetime import datetime, timezone from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SOFTWARE_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WIN_VER_PATH = r"\Microsoft\Windows NT\CurrentVersion" os_list = ( "ProductName", "ReleaseID", "CSDVersion", "CurrentVersion", "CurrentBuild", "CurrentBuildNumber", "InstallationType", "EditionID", "ProductName", "ProductId", "BuildLab", "BuildLabEx", "CompositionEditionID", "RegisteredOrganization", "RegisteredOwner", "InstallDate", ) class WinVersionPlugin(Plugin): NAME = "winver_plugin" DESCRIPTION = "Get relevant OS information" COMPATIBLE_HIVE = SOFTWARE_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SOFTWARE_HIVE_TYPE def run(self): logger.info("Started winver Plugin...") try: key = self.registry_hive.get_key(WIN_VER_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {WIN_VER_PATH}: {ex}") return None self.entries = {WIN_VER_PATH: {"last_write": convert_wintime(key.header.last_modified).isoformat()}} for val in key.iter_values(): if val.name in os_list: if val.name == "InstallDate": self.entries[WIN_VER_PATH][val.name] = datetime.fromtimestamp(val.value, timezone.utc).strftime( "%Y-%m-%d %H:%M:%S" ) else: self.entries[WIN_VER_PATH][val.name] = val.value ================================================ FILE: regipy/plugins/system/__init__.py ================================================ ================================================ FILE: regipy/plugins/system/active_controlset.py ================================================ import logging from dataclasses import asdict from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) SELECT = r"\Select" class ActiveControlSetPlugin(Plugin): NAME = "active_control_set" DESCRIPTION = "Get information on SYSTEM hive control sets" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): subkey = self.registry_hive.get_key(SELECT) self.entries = list(subkey.iter_values(as_json=self.as_json)) if self.as_json: self.entries = [asdict(x) for x in self.entries] ================================================ FILE: regipy/plugins/system/appcertdlls.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SESSION_MANAGER_PATH = r"Control\Session Manager" class AppCertDLLsPlugin(Plugin): """ Parses AppCertDLLs persistence mechanism from SYSTEM hive AppCertDLLs contains DLLs that are loaded into every process that calls CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, CreateProcessWithTokenW, or WinExec. This is a known persistence and code injection technique. Registry Key: ControlSet*\\Control\\Session Manager Value: AppCertDLLs (REG_MULTI_SZ) """ NAME = "appcert_dlls" DESCRIPTION = "Parses AppCertDLLs persistence entries" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started AppCertDLLs Plugin...") for controlset_path in self.registry_hive.get_control_sets(SESSION_MANAGER_PATH): try: session_manager_key = self.registry_hive.get_key(controlset_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find Session Manager at: {controlset_path}") continue for value in session_manager_key.iter_values(): if value.name == "AppCertDLLs": entry = { "key_path": controlset_path, "last_write": convert_wintime(session_manager_key.header.last_modified, as_json=self.as_json), "appcert_dlls": value.value, } self.entries.append(entry) break ================================================ FILE: regipy/plugins/system/backuprestore.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) BACKUPRESTORE_PATH = [ r"Control\BackupRestore\FilesNotToSnapshot", r"Control\BackupRestore\FilesNotToBackup", r"Control\BackupRestore\KeysNotToRestore", ] class BackupRestorePlugin(Plugin): NAME = "backuprestore_plugin" DESCRIPTION = "Gets the contents of the FilesNotToSnapshot, KeysNotToRestore, and FilesNotToBackup keys" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} for br_path in BACKUPRESTORE_PATH: br_subkeys = self.registry_hive.get_control_sets(br_path) for br_subkey in br_subkeys: try: backuprestore = self.registry_hive.get_key(br_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {br_subkey}: {ex}") continue self.entries[br_subkey] = {"last_write": convert_wintime(backuprestore.header.last_modified).isoformat()} for val in backuprestore.iter_values(): self.entries[br_subkey][val.name] = val.value ================================================ FILE: regipy/plugins/system/bam.py ================================================ import logging from construct import Int64ul from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) BAM_PATH = [r"Services\bam\UserSettings", r"Services\bam\state\UserSettings"] class BAMPlugin(Plugin): NAME = "background_activity_moderator" DESCRIPTION = "Get the computer name" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Computer Name Plugin...") for path in BAM_PATH: try: for subkey_path in self.registry_hive.get_control_sets(path): subkey = self.registry_hive.get_key(subkey_path) for sid_subkey in subkey.iter_subkeys(): sid = sid_subkey.name logger.debug(f"Parsing BAM for {sid}") sequence_number = None version = None entries = [] for value in sid_subkey.get_values(trim_values=self.trim_values): if value.name == "SequenceNumber": sequence_number = value.value elif value.name == "Version": version = value.value else: entries.append( { "executable": value.name, "timestamp": convert_wintime( Int64ul.parse(value.value), as_json=self.as_json, ), } ) self.entries.extend( [ { "sequence_number": sequence_number, "version": version, "sid": sid, "key_path": f"{subkey_path}\\{sid}", **x, } for x in entries ] ) except RegistryKeyNotFoundException as ex: logger.error(ex) ================================================ FILE: regipy/plugins/system/bootkey.py ================================================ """ The boot key is an encryption key that is stored in the Windows SYSTEM registry hive. This key is used by several Windows components to encrypt sensitive information like the AD database, machine account password or system certificates etc. """ import logging from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.registry import NKRecord from regipy.utils import convert_wintime logger = logging.getLogger(__name__) LSA_KEY_PATH = r"Control\Lsa" def _collect_bootkey(lsa_key: NKRecord) -> str: """ Extracts the 128-bit scrambled boot key from four "secret" LSA subkeys as a 32-character hex string. """ # The boot key is taken from four separate keys: # SYSTEM\CurrentControlSet\Control\Lsa\{JD,Skew1,GBG,Data}. # However, the actual data needed is stored in a hidden field of the key # that cannot be seen using tools like regedit. # Specifically, each part of the key is stored in the key's Class attribute, # and is stored as a Unicode string giving the hex value of that piece of the key. bootkey_subkeys = { "JD": "", "Skew1": "", "GBG": "", "Data": "", } for subkey in lsa_key.iter_subkeys(): if subkey.name in bootkey_subkeys: bootkey_subkeys[subkey.name] = subkey.get_class_name() return "".join(bootkey_subkeys.values()) # Permutation matrix for the boot key # fmt: off _BOOTKEY_PBOX = [ 0x8, 0x5, 0x4, 0x2, 0xB, 0x9, 0xD, 0x3, 0x0, 0x6, 0x1, 0xC, 0xE, 0xA, 0xF, 0x7 ] # fmt: on def _descramble_bootkey(key: str) -> bytes: """ Parses the 128-bit binary boot key from a hex string and reverses the bytewise scrambling transform. """ binkey = bytes.fromhex(key) return bytes([binkey[i] for i in _BOOTKEY_PBOX]) class BootKeyPlugin(Plugin): """ The boot key is an encryption key that is stored in the Windows SYSTEM registry hive. """ NAME = "bootkey" DESCRIPTION = "Get the Windows boot key" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started BootKey Plugin...") for subkey_path in self.registry_hive.get_control_sets(LSA_KEY_PATH): lsa_key = self.registry_hive.get_key(subkey_path) bootkey = _descramble_bootkey(_collect_bootkey(lsa_key)) self.entries.append( { "key": bootkey.hex() if self.as_json else bootkey, "timestamp": convert_wintime(lsa_key.header.last_modified, as_json=self.as_json), } ) ================================================ FILE: regipy/plugins/system/codepage.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) PROCESSOR_PATH = r"Control\Nls\CodePage" crash_items = "ACP" class CodepagePlugin(Plugin): NAME = "codepage" DESCRIPTION = "Get codepage value" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} codepage_subkeys = self.registry_hive.get_control_sets(PROCESSOR_PATH) for codepage_subkey in codepage_subkeys: try: codepage = self.registry_hive.get_key(codepage_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {codepage_subkey}: {ex}") continue self.entries[codepage_subkey] = {"last_write": convert_wintime(codepage.header.last_modified).isoformat()} for val in codepage.iter_values(): if val.name in crash_items: self.entries[codepage_subkey][val.name] = val.value ================================================ FILE: regipy/plugins/system/computer_name.py ================================================ import logging from regipy.exceptions import RegistryValueNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) COMPUTER_NAME_PATH = r"Control\ComputerName\ComputerName" class ComputerNamePlugin(Plugin): NAME: str = "computer_name" DESCRIPTION = "Get the computer name" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Computer Name Plugin...") for subkey_path in self.registry_hive.get_control_sets(COMPUTER_NAME_PATH): subkey = self.registry_hive.get_key(subkey_path) try: self.entries.append( { "name": subkey.get_value("ComputerName", as_json=self.as_json), "timestamp": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } ) except RegistryValueNotFoundException: logger.exception("Could not get computer name") continue ================================================ FILE: regipy/plugins/system/crash_dump.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) PROCESSOR_PATH = r"Control\CrashControl" crash_items = ("CrashDumpEnabled", "DumpFile", "MinidumpDir", "LogEvent") dump_enabled = { "0": "None", "1": "Complete memory dump", "2": "Kernel memory dump", "3": "Small memory dump (64 KB)", "7": "Automatic memory dump", } class CrashDumpPlugin(Plugin): NAME = "crash_dump" DESCRIPTION = "Get crash control information" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} architecture_subkeys = self.registry_hive.get_control_sets(PROCESSOR_PATH) for architecture_subkey in architecture_subkeys: try: architecture = self.registry_hive.get_key(architecture_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {architecture_subkey}: {ex}") continue self.entries[architecture_subkey] = {"last_write": convert_wintime(architecture.header.last_modified).isoformat()} for val in architecture.iter_values(): if val.name in crash_items: self.entries[architecture_subkey][val.name] = val.value if val.name == "CrashDumpEnabled": self.entries[architecture_subkey]["CrashDumpEnabledStr"] = dump_enabled.get(str(val.value), "") ================================================ FILE: regipy/plugins/system/diag_sr.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_filetime2, convert_wintime logger = logging.getLogger(__name__) DIAGSR_PATH = r"Services\VSS\Diag\SystemRestore" crash_items = ("CrashDumpEnabled", "DumpFile", "MinidumpDir", "LogEvent") class DiagSRPlugin(Plugin): NAME = "diag_sr" DESCRIPTION = "Get Diag\\SystemRestore values and data" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} diagsr_subkeys = self.registry_hive.get_control_sets(DIAGSR_PATH) for diagsr_subkey in diagsr_subkeys: try: diagsr = self.registry_hive.get_key(diagsr_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {diagsr_subkey}: {ex}") continue self.entries[diagsr_subkey] = {"last_write": convert_wintime(diagsr.header.last_modified).isoformat()} for val in diagsr.iter_values(): self.entries[diagsr_subkey][val.name] = convert_filetime2(val.value[16:32]) ================================================ FILE: regipy/plugins/system/disablelastaccess.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) LAST_ACCESS_PATH = r"Control\FileSystem" crash_items = ("NtfsDisableLastAccessUpdate", "NtfsDisableLastAccessUpdate") last_acc = { "80000000": "(User Managed, Updates Enabled)", "80000001": "(User Managed, Updates Disabled)", "80000002": "(System Managed, Updates Enabled)", "80000003": "(System Managed, Updates Disabled)", } class DisableLastAccessPlugin(Plugin): NAME = "disable_last_access" DESCRIPTION = "Get NTFSDisableLastAccessUpdate value" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} access_subkeys = self.registry_hive.get_control_sets(LAST_ACCESS_PATH) for access_subkey in access_subkeys: try: access = self.registry_hive.get_key(access_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {access_subkey}: {ex}") continue self.entries[access_subkey] = {"last_write": convert_wintime(access.header.last_modified).isoformat()} for val in access.iter_values(): if val.name in crash_items: self.entries[access_subkey][val.name] = f"{val.value:0x}" self.entries[access_subkey][f"{val.name}Str"] = last_acc.get(f"{val.value:0x}", "") ================================================ FILE: regipy/plugins/system/external/ShimCacheParser.py ================================================ # Andrew Davis, andrew.davis@mandiant.com # Copyright 2012 Mandiant # # Mandiant licenses this file to you under the Apache License, Version # 2.0 (the "License"); you may not use this file except in compliance with the # License. You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing # permissions and limitations under the License. # # Identifies and parses Application Compatibility Shim Cache entries for forensic data. # Original Mandiant shim cache parser, ported to Python 3 # Identifies and parses Application Compatibility Shim Cache entries for forensic data. import datetime import io as sio import logging import struct import pytz CACHE_MAGIC_NT5_2 = 0xBADC0FFE CACHE_HEADER_SIZE_NT5_2 = 0x8 NT5_2_ENTRY_SIZE32 = 0x18 NT5_2_ENTRY_SIZE64 = 0x20 # Values used by Windows 6.1 (Win7 and Server 2008 R2) CACHE_MAGIC_NT6_1 = 0xBADC0FEE CACHE_HEADER_SIZE_NT6_1 = 0x80 NT6_1_ENTRY_SIZE32 = 0x20 NT6_1_ENTRY_SIZE64 = 0x30 CSRSS_FLAG = 0x2 # Values used by Windows 5.1 (WinXP 32-bit) WINXP_MAGIC32 = 0xDEADBEEF WINXP_HEADER_SIZE32 = 0x190 WINXP_ENTRY_SIZE32 = 0x228 MAX_PATH = 520 # Values used by Windows 8 WIN8_STATS_SIZE = 0x80 WIN8_MAGIC = b"00ts" # Magic value used by Windows 8.1 WIN81_MAGIC = b"10ts" # Values used by Windows 10 WIN10_STATS_SIZE = 0x30 WIN10_MAGIC = b"10ts" CACHE_HEADER_SIZE_NT6_4 = 0x30 CACHE_MAGIC_NT6_4 = 0x30 BAD_ENTRY_DATA = "N/A" G_VERBOSE = False G_USE_BOM = False OUTPUT_HEADER = ["Last Modified", "Last Update", "Path", "File Size", "Exec Flag"] logger = logging.getLogger(__name__) # Shim Cache format used by Windows 5.2 and 6.0 (Server 2003 through Vista/Server 2008) class CacheEntryNt5: def __init__(self, is_32_bit, data=None): self.w_length = None self.w_maximum_length = None self.offset = None self.dw_low_date_time = None self.dw_high_date_time = None self.dw_file_size_low = None self.dw_file_size_high = None self.is_32_bit = is_32_bit if data is not None: self.update(data) def update(self, data): if self.is_32_bit: entry = struct.unpack("<2H 3L 2L", data) else: entry = struct.unpack("<2H 4x Q 2L 2L", data) self.w_length = entry[0] self.w_maximum_length = entry[1] self.offset = entry[2] self.dw_low_date_time = entry[3] self.dw_high_date_time = entry[4] self.dw_file_size_low = entry[5] self.dw_file_size_high = entry[6] def size(self): if self.is_32_bit: return NT5_2_ENTRY_SIZE32 else: return NT5_2_ENTRY_SIZE64 # Shim Cache format used by Windows 6.1 (Win7 through Server 2008 R2). class CacheEntryNt6: def __init__(self, is_32_bit, data=None): self.w_length = None self.w_maximum_length = None self.offset = None self.dw_low_date_time = None self.dw_high_date_time = None self.file_flags = None self.flags = None self.blob_size = None self.blob_offset = None self.is_32_bit = is_32_bit if data is not None: self.update(data) def update(self, data): if self.is_32_bit: entry = struct.unpack("<2H 7L", data) else: entry = struct.unpack("<2H 4x Q 4L 2Q", data) self.w_length = entry[0] self.w_maximum_length = entry[1] self.offset = entry[2] self.dw_low_date_time = entry[3] self.dw_high_date_time = entry[4] self.file_flags = entry[5] self.flags = entry[6] self.blob_size = entry[7] self.blob_offset = entry[8] def size(self): if self.is_32_bit: return NT6_1_ENTRY_SIZE32 else: return NT6_1_ENTRY_SIZE64 # Convert FILETIME to datetime. # Based on http://code.activestate.com/recipes/511425-filetime-to-datetime/ def convert_filetime(dw_low_date_time, dw_high_date_time): try: date = datetime.datetime(1601, 1, 1, 0, 0, 0) temp_time = dw_high_date_time temp_time <<= 32 temp_time |= dw_low_date_time return pytz.utc.localize(date + datetime.timedelta(microseconds=temp_time / 10)) except OverflowError: return None # Return a unique list while preserving ordering. def unique_list(li): ret_list = [] for entry in li: if entry not in ret_list: ret_list.append(entry) return ret_list # Read the Shim Cache format, return a list of last modified dates/paths. def get_shimcache_entries(cachebin, as_json=False): if len(cachebin) < 16: # Data size less than minimum header size. return None # Get the format type magic = struct.unpack(" WIN8_STATS_SIZE and cachebin[WIN8_STATS_SIZE : WIN8_STATS_SIZE + 4] == WIN8_MAGIC: logger.debug("[+] Found Windows 8/2k12 Apphelp Cache data...") yield from read_win8_entries(cachebin, WIN8_MAGIC, as_json=as_json) # Windows 8.1 will use a different magic dword, check for it elif len(cachebin) > WIN8_STATS_SIZE and cachebin[WIN8_STATS_SIZE : WIN8_STATS_SIZE + 4] == WIN81_MAGIC: logger.debug("[+] Found Windows 8.1 Apphelp Cache data...") yield from read_win8_entries(cachebin, WIN81_MAGIC, as_json=as_json) # Windows 10 will use a different magic dword, check for it elif len(cachebin) > WIN10_STATS_SIZE and cachebin[WIN10_STATS_SIZE : WIN10_STATS_SIZE + 4] == WIN10_MAGIC: logger.debug("[+] Found Windows 10 Apphelp Cache data...") yield from read_win10_entries(cachebin, WIN10_MAGIC, as_json=as_json) # Windows 10 creators update moved the damn magic 4 bytes forward... elif len(cachebin) > WIN10_STATS_SIZE and cachebin[WIN10_STATS_SIZE + 4 : WIN10_STATS_SIZE + 8] == WIN10_MAGIC: logger.debug("[+] Found Windows 10 Apphelp Cache data... (creators update)") yield from read_win10_entries(cachebin, WIN10_MAGIC, creators_update=True, as_json=as_json) else: raise Exception(f"Got an unrecognized magic value of 0x{magic:x}... bailing") # Read Windows 8/2k12/8.1 Apphelp Cache entry formats. def read_win8_entries(bin_data, ver_magic, as_json=False): entry_meta_len = 12 # Skip past the stats in the header cache_data = bin_data[WIN8_STATS_SIZE:] data = sio.BytesIO(cache_data) while data.tell() < len(cache_data): header = data.read(entry_meta_len) # Read in the entry metadata # Note: the crc32 hash is of the cache entry data magic, crc32_hash, entry_len = struct.unpack("<4sLL", header) # Check the magic tag if magic != ver_magic: raise Exception("Invalid version magic tag found: {}".format(struct.unpack("I", magic)[0])) entry_data = sio.BytesIO(data.read(entry_len)) # Read the path length path_len = struct.unpack(" 0: # Just skip past the package data if present (for now) entry_data.seek(package_len, 1) # Read the remaining entry data flags, unk_1, low_datetime, high_datetime, unk_2 = struct.unpack(" 3: contains_file_size = True break # Now grab all the data in the value. for offset in range( CACHE_HEADER_SIZE_NT5_2, (num_entries * entry_size) + CACHE_HEADER_SIZE_NT5_2, entry_size, ): entry.update(bin_data[offset : offset + entry_size]) last_mod_date = convert_filetime(entry.dw_low_date_time, entry.dw_high_date_time) path = bin_data[entry.offsets : entry.offset + entry.w_length].decode("utf-16le", "replace") # It contains file size data. exec_flag = None if contains_file_size: yield { "last_mod_date": (last_mod_date.isoformat() if as_json else last_mod_date), "path": path, "file_size": entry.dw_file_size_low, } # It contains flags. else: # Check the flag set in CSRSS if entry.dw_file_size_low & CSRSS_FLAG: exec_flag = "True" else: exec_flag = "False" yield { "last_mod_date": (last_mod_date.isoformat() if as_json else last_mod_date), "path": path, "exec_flag": exec_flag, } # Read the Shim Cache Windows 7/2k8-R2 entry format, # return a list of last modifed dates/paths. def read_nt6_entries(bin_data, entry, as_json=False): entry_size = entry.size() num_entries = struct.unpack(" str: """Get description for LM compatibility level""" descriptions = { 0: "Send LM & NTLM responses", 1: "Send LM & NTLM - use NTLMv2 if negotiated", 2: "Send NTLM response only", 3: "Send NTLMv2 response only", 4: "Send NTLMv2 response only, refuse LM", 5: "Send NTLMv2 response only, refuse LM & NTLM", } return descriptions.get(level, f"Unknown level ({level})") ================================================ FILE: regipy/plugins/system/mountdev.py ================================================ """ MountedDevices plugin - Parses mounted device information """ import logging import re from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) MOUNTED_DEVICES_PATH = r"\MountedDevices" def parse_device_data(data: bytes) -> dict: """Parse the binary device data to extract device information""" result = {} if not data: return result try: # Try to decode as unicode path decoded = data.decode("utf-16-le", errors="ignore").rstrip("\x00") if decoded: result["path"] = decoded # Extract volume GUID if present guid_match = re.search(r"\{[0-9A-Fa-f-]+\}", decoded) if guid_match: result["volume_guid"] = guid_match.group(0) # Check for device type indicators if "\\DosDevices\\" in decoded: result["type"] = "dos_device" elif "\\??\\Volume" in decoded: result["type"] = "volume" elif "_??_USBSTOR" in decoded or "USBSTOR" in decoded: result["type"] = "usb_storage" elif "IDE" in decoded or "SCSI" in decoded: result["type"] = "disk" except Exception: pass # If no path found, check for disk signature format (12 bytes) if "path" not in result and len(data) == 12: # First 4 bytes are disk signature, next 8 bytes are partition offset try: disk_sig = data[0:4].hex() partition_offset = int.from_bytes(data[4:12], "little") result["disk_signature"] = disk_sig result["partition_offset"] = partition_offset result["type"] = "mbr_partition" except Exception: pass # Check for GPT disk format (24 bytes) if "path" not in result and len(data) == 24: try: # GPT disks have a different format with GUID guid_bytes = data[0:16] partition_offset = int.from_bytes(data[16:24], "little") # Format GUID guid = ( f"{guid_bytes[3]:02x}{guid_bytes[2]:02x}{guid_bytes[1]:02x}{guid_bytes[0]:02x}-" f"{guid_bytes[5]:02x}{guid_bytes[4]:02x}-" f"{guid_bytes[7]:02x}{guid_bytes[6]:02x}-" f"{guid_bytes[8]:02x}{guid_bytes[9]:02x}-" f"{guid_bytes[10]:02x}{guid_bytes[11]:02x}{guid_bytes[12]:02x}" f"{guid_bytes[13]:02x}{guid_bytes[14]:02x}{guid_bytes[15]:02x}" ) result["disk_guid"] = guid result["partition_offset"] = partition_offset result["type"] = "gpt_partition" except Exception: pass return result class MountedDevicesPlugin(Plugin): """ Parses MountedDevices from SYSTEM hive Provides information about: - Drive letter assignments - Volume mappings - USB device volume assignments - Disk and partition identifiers Registry Key: MountedDevices """ NAME = "mounted_devices" DESCRIPTION = "Parses mounted device information" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started MountedDevices Plugin...") try: mounted_key = self.registry_hive.get_key(MOUNTED_DEVICES_PATH) except RegistryKeyNotFoundException as ex: logger.debug(f"Could not find MountedDevices at: {MOUNTED_DEVICES_PATH}: {ex}") return base_entry = { "key_path": MOUNTED_DEVICES_PATH, "last_write": convert_wintime(mounted_key.header.last_modified, as_json=self.as_json), } for value in mounted_key.iter_values(): name = value.name data = value.value entry = base_entry.copy() entry["value_name"] = name # Determine mount point type if name.startswith("\\DosDevices\\"): entry["mount_point"] = name.replace("\\DosDevices\\", "") entry["mount_type"] = "drive_letter" elif name.startswith("\\??\\Volume"): entry["mount_point"] = name entry["mount_type"] = "volume" elif name == "#{": entry["mount_type"] = "database" else: entry["mount_type"] = "other" # Parse the device data if isinstance(data, bytes): device_info = parse_device_data(data) entry.update(device_info) entry["data_size"] = len(data) self.entries.append(entry) ================================================ FILE: regipy/plugins/system/network_data.py ================================================ import logging from datetime import datetime, timezone from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) INTERFACES_PATH = r"Services\Tcpip\Parameters\Interfaces" class NetworkDataPlugin(Plugin): NAME = "network_data" DESCRIPTION = "Get network data from many interfaces" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def get_network_info(self, subkey, interfaces=None): if interfaces is None: interfaces = [] try: for interface in subkey.iter_subkeys(): entries = { "interface_name": interface.name, "last_modified": convert_wintime(interface.header.last_modified, as_json=self.as_json), "incomplete_data": False, # New key to indicate incomplete data "dhcp_enabled": interface.get_value("EnableDHCP") == 1, # Boolean value } if entries["dhcp_enabled"]: entries.update( { "dhcp_server": interface.get_value("DhcpServer"), "dhcp_ip_address": interface.get_value("DhcpIPAddress"), "dhcp_subnet_mask": interface.get_value("DhcpSubnetMask"), "dhcp_default_gateway": interface.get_value("DhcpDefaultGateway"), "dhcp_name_server": interface.get_value("DhcpNameServer"), "dhcp_domain": interface.get_value("DhcpDomain"), } ) # Lease Obtained Time lease_obtained_time = interface.get_value("LeaseObtainedTime") if lease_obtained_time is not None: try: lease_obtained_time_str = datetime.fromtimestamp(lease_obtained_time, timezone.utc).strftime( "%Y-%m-%d %H:%M:%S" ) entries["dhcp_lease_obtained_time"] = lease_obtained_time_str except (OSError, ValueError) as e: logger.error(f"Error converting DHCP lease obtained time for interface {interface.name}: {e}") entries["incomplete_data"] = True # Lease Terminates Time lease_terminates_time = interface.get_value("LeaseTerminatesTime") if lease_terminates_time is not None: try: lease_terminates_time_str = datetime.fromtimestamp(lease_terminates_time, timezone.utc).strftime( "%Y-%m-%d %H:%M:%S" ) entries["dhcp_lease_terminates_time"] = lease_terminates_time_str except (OSError, ValueError) as e: logger.error(f"Error converting DHCP lease terminates time for interface {interface.name}: {e}") entries["incomplete_data"] = True else: entries.update( { "ip_address": interface.get_value("IPAddress"), "subnet_mask": interface.get_value("SubnetMask"), "default_gateway": interface.get_value("DefaultGateway"), "name_server": interface.get_value("NameServer"), "domain": interface.get_value("Domain"), } ) try: if interface.subkey_count: sub_interfaces = [] sub_interfaces = self.get_network_info(interface, sub_interfaces) entries["sub_interface"] = sub_interfaces except Exception as e: logger.error(f"Error processing sub-interfaces for interface {interface.name}: {e}") entries["incomplete_data"] = True interfaces.append(entries) except Exception as e: logger.error(f"Error iterating over subkeys in {subkey.path}: {e}") return interfaces def run(self): self.entries = {} for control_set_interfaces_path in self.registry_hive.get_control_sets(INTERFACES_PATH): try: subkey = self.registry_hive.get_key(control_set_interfaces_path) except RegistryKeyNotFoundException as ex: logger.error(f"Registry key not found at path {control_set_interfaces_path}: {ex}") continue # Skip to the next path if the key is not found try: self.entries[control_set_interfaces_path] = { "timestamp": convert_wintime(subkey.header.last_modified, as_json=self.as_json) } interfaces = [] interfaces = self.get_network_info(subkey, interfaces) self.entries[control_set_interfaces_path]["interfaces"] = interfaces except Exception as ex: logger.error(f"Error processing registry key {control_set_interfaces_path}: {ex}") self.entries[control_set_interfaces_path]["incomplete_data"] = True ================================================ FILE: regipy/plugins/system/pagefile.py ================================================ """ Pagefile plugin - Parses pagefile configuration """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) MEMORY_MANAGEMENT_PATH = r"Control\Session Manager\Memory Management" class PagefilePlugin(Plugin): """ Parses pagefile configuration from SYSTEM hive Provides information about: - Pagefile locations - Pagefile sizes - ClearPageFileAtShutdown setting Registry Key: Control\\Session Manager\\Memory Management """ NAME = "pagefile" DESCRIPTION = "Parses pagefile configuration" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Pagefile Plugin...") for controlset_path in self.registry_hive.get_control_sets(MEMORY_MANAGEMENT_PATH): try: mm_key = self.registry_hive.get_key(controlset_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find Memory Management at: {controlset_path}") continue entry = { "key_path": controlset_path, "last_write": convert_wintime(mm_key.header.last_modified, as_json=self.as_json), } for value in mm_key.iter_values(): name = value.name val = value.value if name == "PagingFiles": # REG_MULTI_SZ containing pagefile paths and sizes # Format: "C:\pagefile.sys min_size max_size" if isinstance(val, list): entry["paging_files"] = val entry["parsed_paging_files"] = [] for pf in val: if pf: parts = pf.split() pf_entry = {"path": parts[0]} if len(parts) >= 2: pf_entry["min_size_mb"] = int(parts[1]) if parts[1].isdigit() else parts[1] if len(parts) >= 3: pf_entry["max_size_mb"] = int(parts[2]) if parts[2].isdigit() else parts[2] entry["parsed_paging_files"].append(pf_entry) else: entry["paging_files"] = val elif name == "ExistingPageFiles": entry["existing_page_files"] = val elif name == "ClearPageFileAtShutdown": entry["clear_pagefile_at_shutdown"] = val == 1 elif name == "PagefileOnOsVolume": entry["pagefile_on_os_volume"] = val elif name == "TempPageFile": entry["temp_page_file"] = val if "paging_files" in entry or "existing_page_files" in entry: self.entries.append(entry) ================================================ FILE: regipy/plugins/system/pending_file_rename.py ================================================ """ Pending File Rename plugin - Parses pending file rename operations """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SESSION_MANAGER_PATH = r"Control\Session Manager" class PendingFileRenamePlugin(Plugin): """ Parses Pending File Rename Operations from SYSTEM hive This registry value contains files scheduled for rename/delete on reboot. This is commonly used by installers and can also be abused by malware. Registry Key: Control\\Session Manager Values: - PendingFileRenameOperations - PendingFileRenameOperations2 Format: Source path, destination path (or empty for delete), repeating """ NAME = "pending_file_rename" DESCRIPTION = "Parses pending file rename operations" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Pending File Rename Plugin...") for controlset_path in self.registry_hive.get_control_sets(SESSION_MANAGER_PATH): try: sm_key = self.registry_hive.get_key(controlset_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find Session Manager at: {controlset_path}") continue entry = { "key_path": controlset_path, "last_write": convert_wintime(sm_key.header.last_modified, as_json=self.as_json), "operations": [], } for value in sm_key.iter_values(): if value.name in ["PendingFileRenameOperations", "PendingFileRenameOperations2"]: operations = self._parse_operations(value.value, value.name) entry["operations"].extend(operations) if entry["operations"]: self.entries.append(entry) def _parse_operations(self, data, value_name: str) -> list: """Parse pending file rename operations""" operations = [] if not data: return operations # Data is REG_MULTI_SZ - list of strings if isinstance(data, list): items = data elif isinstance(data, str): items = [data] else: return operations # Operations come in pairs: source, destination (or empty for delete) i = 0 while i < len(items): source = items[i] if i < len(items) else None destination = items[i + 1] if i + 1 < len(items) else None if source: # Remove NT path prefix if present if source.startswith("\\??\\"): source = source[4:] op = { "source": source, "value_name": value_name, } if destination: if destination.startswith("\\??\\"): destination = destination[4:] op["destination"] = destination op["operation"] = "rename" else: op["operation"] = "delete" operations.append(op) i += 2 return operations ================================================ FILE: regipy/plugins/system/previous_winver.py ================================================ import logging import re from datetime import datetime, timezone from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) WIN_VER_PATH = r"\Setup" os_list = [ "ProductName", "ReleaseID", "CSDVersion", "CurrentVersion", "CurrentBuild", "CurrentBuildNumber", "InstallationType", "EditionID", "ProductName", "ProductId", "BuildLab", "BuildLabEx", "CompositionEditionID", "RegisteredOrganization", "RegisteredOwner", "InstallDate", ] class PreviousWinVersionPlugin(Plugin): NAME = "previous_winver_plugin" DESCRIPTION = "Get previous relevant OS information" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): logger.info("Started winver Plugin...") try: key = self.registry_hive.get_key(WIN_VER_PATH) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {WIN_VER_PATH}: {ex}") return None for sk in key.iter_subkeys(): if sk.name.startswith("Source OS"): old_date = re.search( r"Updated on (\d{1,2})/(\d{1,2})/(\d{4}) (\d{2}):(\d{2}):(\d{2})", sk.name, ) dt = datetime( int(old_date.group(3)), int(old_date.group(1)), int(old_date.group(2)), int(old_date.group(4)), int(old_date.group(5)), int(old_date.group(6)), ).strftime("%Y-%m-%d %H:%M:%S") temp_dict = {"key": f"\\{key.name}\\{sk.name}"} temp_dict["update_date"] = dt for val in sk.iter_values(): if val.name in os_list: if val.name == "InstallDate": temp_dict[val.name] = datetime.fromtimestamp(val.value, timezone.utc).strftime("%Y-%m-%d %H:%M:%S") else: temp_dict[val.name] = val.value self.entries.append(temp_dict) ================================================ FILE: regipy/plugins/system/processor_architecture.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) PROCESSOR_PATH = r"Control\Session Manager\Environment" processor_list = ( "PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS", "PROCESSOR_IDENTIFIER", "PROCESSOR_REVISION", ) class ProcessorArchitecturePlugin(Plugin): NAME = "processor_architecture" DESCRIPTION = "Get processor architecture info from the System's environment key" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def can_run(self): return self.registry_hive.hive_type == SYSTEM_HIVE_TYPE def run(self): self.entries = {} processor_subkeys = self.registry_hive.get_control_sets(PROCESSOR_PATH) for processor_subkey in processor_subkeys: try: processor = self.registry_hive.get_key(processor_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {processor_subkey}: {ex}") continue self.entries[processor_subkey] = {} for val in processor.iter_values(): if val.name in processor_list: self.entries[processor_subkey][val.name] = val.value ================================================ FILE: regipy/plugins/system/routes.py ================================================ import logging from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import get_subkey_values_from_list logger = logging.getLogger(__name__) ROUTES_PATH = r"Services\Tcpip\Parameters\PersistentRoutes" class RoutesPlugin(Plugin): NAME = "routes" DESCRIPTION = "Get list of routes" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Routes Plugin...") routes_path_list = self.registry_hive.get_control_sets(ROUTES_PATH) self.entries = get_subkey_values_from_list(self.registry_hive, routes_path_list, as_json=self.as_json) ================================================ FILE: regipy/plugins/system/safeboot_configuration.py ================================================ import logging from regipy import RegistryKeyNotFoundException, convert_wintime from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) SAFEBOOT_NETWORK_PATH = r"Control\SafeBoot\Network" SAFEBOOT_MINIMAL_PATH = r"Control\SafeBoot\Minimal" class SafeBootConfigurationPlugin(Plugin): NAME = "safeboot_configuration" DESCRIPTION = "Get safeboot configuration" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def _get_safeboot_entries(self, subkey_path): entries = [] for safeboot_network_path in self.registry_hive.get_control_sets(subkey_path): control_set_name = safeboot_network_path.split("\\")[1] try: subkey = self.registry_hive.get_key(safeboot_network_path) except RegistryKeyNotFoundException as ex: logger.error(ex) continue for safeboot_network_subkey in subkey.iter_subkeys(): timestamp = convert_wintime(safeboot_network_subkey.header.last_modified, as_json=self.as_json) entries.append( { "timestamp": timestamp, "name": safeboot_network_subkey.name, "description": safeboot_network_subkey.get_value(), "control_set_name": control_set_name, } ) return entries def run(self): logger.debug("Started Safeboot Configuration Plugin...") self.entries = { "network": self._get_safeboot_entries(SAFEBOOT_NETWORK_PATH), "minimal": self._get_safeboot_entries(SAFEBOOT_MINIMAL_PATH), } ================================================ FILE: regipy/plugins/system/services.py ================================================ import logging from dataclasses import asdict from regipy.exceptions import ( RegistryKeyNotFoundException, RegistryParsingException, ) from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) SERVICES_PATH = r"Services" class ServicesPlugin(Plugin): NAME = "services" DESCRIPTION = "Enumerate the services in the SYSTEM hive" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): self.entries = {} logger.debug("Started Services enumeration Plugin...") for control_set_services_path in self.registry_hive.get_control_sets(SERVICES_PATH): try: subkey = self.registry_hive.get_key(control_set_services_path) except RegistryKeyNotFoundException as ex: logger.error(ex) continue self.entries[control_set_services_path] = { "timestamp": convert_wintime(subkey.header.last_modified, as_json=self.as_json) } services = [] for service in subkey.iter_subkeys(): values = None parameters = [] if service.values_count > 0: try: values = [asdict(x) for x in service.iter_values(as_json=True)] except RegistryParsingException as ex: logger.error( f"Exception while parsing data for service {service.name[:10] if service.name else None}: {ex}" ) if service.subkey_count: try: service_parameters_path = rf"{control_set_services_path}\{service.name}" for parameter in self.registry_hive.recurse_subkeys( nk_record=service, path_root=service_parameters_path, as_json=True, ): parameters.append(asdict(parameter)) except RegistryParsingException as ex: logger.info(f"Exception while parsing parameters for service {service.name}: {ex}") entry = { "name": service.name, "last_modified": convert_wintime(service.header.last_modified, as_json=self.as_json), "values": values, "parameters": parameters, } services.append(entry) self.entries[control_set_services_path]["services"] = services ================================================ FILE: regipy/plugins/system/shares.py ================================================ """ Shares plugin - Parses network share configuration """ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) LANMAN_SHARES_PATH = r"Services\LanmanServer\Shares" class SharesPlugin(Plugin): """ Parses network shares from SYSTEM hive Provides information about: - Configured network shares - Share paths and descriptions - Share permissions Registry Key: Services\\LanmanServer\\Shares under each ControlSet """ NAME = "shares" DESCRIPTION = "Parses network share configuration" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Shares Plugin...") for controlset_path in self.registry_hive.get_control_sets(LANMAN_SHARES_PATH): try: shares_key = self.registry_hive.get_key(controlset_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find shares at: {controlset_path}") continue self._parse_shares(shares_key, controlset_path) def _parse_shares(self, shares_key, key_path: str): """Parse share entries""" for value in shares_key.iter_values(): share_name = value.name share_data = value.value entry = { "key_path": key_path, "share_name": share_name, "last_write": convert_wintime(shares_key.header.last_modified, as_json=self.as_json), } # Share data is a REG_MULTI_SZ with key=value pairs if isinstance(share_data, list): for item in share_data: if "=" in item: key, val = item.split("=", 1) key_lower = key.lower() if key_lower == "path": entry["path"] = val elif key_lower == "remark": entry["remark"] = val elif key_lower == "permissions": entry["permissions"] = val elif key_lower == "maxuses": entry["max_uses"] = int(val) if val.isdigit() else val elif key_lower == "type": entry["type"] = self._get_share_type(int(val) if val.isdigit() else 0) elif key_lower == "cscflags": entry["csc_flags"] = val self.entries.append(entry) @staticmethod def _get_share_type(type_value: int) -> str: """Convert share type value to description""" types = { 0: "Disk Drive", 1: "Print Queue", 2: "Device", 3: "IPC", 0x80000000: "Temporary", 0x40000000: "Special", } base_type = type_value & 0x0FFFFFFF special = type_value & 0xF0000000 type_str = types.get(base_type, f"Unknown ({base_type})") if special & 0x80000000: type_str += " (Temporary)" if special & 0x40000000: type_str += " (Special)" return type_str ================================================ FILE: regipy/plugins/system/shimcache.py ================================================ import logging from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.system.external.ShimCacheParser import get_shimcache_entries logger = logging.getLogger(__name__) COMPUTER_NAME_PATH = r"Control\Session Manager" class ShimCachePlugin(Plugin): NAME = "shimcache" DESCRIPTION = "Parse Shimcache artifact" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started Shim Cache Plugin...") for subkey_path in self.registry_hive.get_control_sets(COMPUTER_NAME_PATH): appcompat_cache = self.registry_hive.get_key(subkey_path).get_subkey("AppCompatCache") if not appcompat_cache: logger.info("No shimcache data was found") return shimcache = appcompat_cache.get_value("AppCompatCache") if shimcache: for entry in get_shimcache_entries(shimcache, as_json=self.as_json): self.entries.append(entry) ================================================ FILE: regipy/plugins/system/shutdown.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_filetime2, convert_wintime logger = logging.getLogger(__name__) SHUTDOWN_DATA_PATH = r"Control\Windows" class ShutdownPlugin(Plugin): NAME = "shutdown" DESCRIPTION = "Get shutdown data" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): self.entries = {} shutdown_subkeys = self.registry_hive.get_control_sets(SHUTDOWN_DATA_PATH) for shutdown_subkey in shutdown_subkeys: try: shutdown = self.registry_hive.get_key(shutdown_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {shutdown_subkey}: {ex}") continue self.entries[shutdown_subkey] = {"last_write": convert_wintime(shutdown.header.last_modified).isoformat()} for val in shutdown.iter_values(): if val.name == "ShutdownTime": self.entries[shutdown_subkey]["date"] = convert_filetime2(val.value) ================================================ FILE: regipy/plugins/system/timezone_data.py ================================================ import logging from dataclasses import asdict from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin logger = logging.getLogger(__name__) TZ_DATA_PATH = r"Control\TimeZoneInformation" class TimezoneDataPlugin(Plugin): NAME = "timezone_data" DESCRIPTION = "Get timezone data" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): self.entries = {} tzdata_subkeys = self.registry_hive.get_control_sets(TZ_DATA_PATH) for tzdata_subkey in tzdata_subkeys: tzdata = self.registry_hive.get_key(tzdata_subkey) self.entries[tzdata_subkey] = list(tzdata.iter_values(as_json=self.as_json)) if self.as_json: for k, v in self.entries.items(): self.entries[k] = [asdict(x) for x in v] ================================================ FILE: regipy/plugins/system/timezone_data2.py ================================================ import logging import struct from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) TZ_DATA_PATH = r"Control\TimeZoneInformation" class TimezoneDataPlugin2(Plugin): NAME = "timezone_data2" DESCRIPTION = "Get timezone data" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): self.entries = {} tzdata_subkeys = self.registry_hive.get_control_sets(TZ_DATA_PATH) for tzdata_subkey in tzdata_subkeys: try: tzdata = self.registry_hive.get_key(tzdata_subkey) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} subkey at {tzdata_subkey}: {ex}") continue self.entries[tzdata_subkey] = list(tzdata.iter_values(as_json=self.as_json)) self.entries[tzdata_subkey] = {"last_write": convert_wintime(tzdata.header.last_modified).isoformat()} for val in tzdata.iter_values(): if val.name in ("ActiveTimeBias", "Bias", "DaylightBias"): self.entries[tzdata_subkey][val.name] = struct.unpack(">l", struct.pack(">L", val.value & 0xFFFFFFFF))[0] elif isinstance(val.value, bytes): # Decode bytes (often UTF-16 encoded strings like TimeZoneKeyName) try: self.entries[tzdata_subkey][val.name] = val.value.decode("utf-16-le").rstrip("\x00") except UnicodeDecodeError: self.entries[tzdata_subkey][val.name] = val.value.hex() else: self.entries[tzdata_subkey][val.name] = val.value ================================================ FILE: regipy/plugins/system/usb_devices.py ================================================ """ USB Devices plugin - Parses USB device history (Enum\\USB) """ import logging from typing import Optional from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.plugins.utils import extract_values from regipy.utils import convert_wintime logger = logging.getLogger(__name__) ENUM_USB_PATH = r"Enum\USB" def strip_resource_ref(val) -> Optional[str]: """Remove resource reference prefix if present (e.g., '@oem123.inf,...;Device Name')""" if isinstance(val, str) and ";" in val: return val.split(";")[-1] return val class USBDevicesPlugin(Plugin): """ Parses USB device history from SYSTEM hive (Enum\\USB) This complements USBSTOR by providing information about non-storage USB devices such as keyboards, mice, webcams, and other USB peripherals. Registry Key: Enum\\USB under each ControlSet """ NAME = "usb_devices" DESCRIPTION = "Parses USB device connection history" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started USB Devices Plugin...") for controlset_path in self.registry_hive.get_control_sets(ENUM_USB_PATH): try: usb_key = self.registry_hive.get_key(controlset_path) except RegistryKeyNotFoundException: logger.debug(f"Could not find USB devices at: {controlset_path}") continue self._parse_usb_key(usb_key, controlset_path) def _parse_usb_key(self, usb_key, base_path: str): """Parse USB device entries""" # Each subkey is a VID_xxxx&PID_xxxx entry for vid_pid_key in usb_key.iter_subkeys(): vid_pid = vid_pid_key.name vid_pid_path = f"{base_path}\\{vid_pid}" # Parse VID and PID from the key name vid = None pid = None if "VID_" in vid_pid and "PID_" in vid_pid: try: vid_start = vid_pid.index("VID_") + 4 vid_end = vid_pid.index("&", vid_start) if "&" in vid_pid[vid_start:] else vid_start + 4 vid = vid_pid[vid_start:vid_end] pid_start = vid_pid.index("PID_") + 4 pid_end_chars = ["&", "\\"] pid_end = len(vid_pid) for char in pid_end_chars: if char in vid_pid[pid_start:]: pid_end = min(pid_end, vid_pid.index(char, pid_start)) pid = vid_pid[pid_start:pid_end] except (ValueError, IndexError): pass # Each sub-subkey is a serial number or instance ID for instance_key in vid_pid_key.iter_subkeys(): instance_id = instance_key.name instance_path = f"{vid_pid_path}\\{instance_id}" entry = { "key_path": instance_path, "vid_pid": vid_pid, "vid": vid, "pid": pid, "instance_id": instance_id, "last_write": convert_wintime(instance_key.header.last_modified, as_json=self.as_json), } extract_values( instance_key, { "DeviceDesc": ("device_desc", strip_resource_ref), "FriendlyName": "friendly_name", "Mfg": ("manufacturer", strip_resource_ref), "Service": "service", "Class": "class", "ClassGUID": "class_guid", "Driver": "driver", "ContainerID": "container_id", "HardwareID": "hardware_id", }, entry, ) self.entries.append(entry) ================================================ FILE: regipy/plugins/system/usbstor.py ================================================ import logging from regipy.exceptions import NoRegistrySubkeysException, RegistryKeyNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) USBSTOR_KEY_PATH = r"Enum\USBSTOR" DISK_GUID_PATH = r"Device Parameters\Partmgr" PROPERTIES_NAME_GUID = r"{540b947e-8b40-45bc-a8a2-6a0b894cbda2}" PROPERTIES_DATES_GUID = r"{83da6326-97a6-4088-9453-a1923f573b29}" DEVICE_NAME_KEY = "0004" FIRST_INSTALLED_TIME_KEY = "0065" LAST_CONNECTED_TIME_KEY = "0066" LAST_REMOVED_TIME_KEY = "0067" LAST_INSTALLED_TIME_KEY = "0064" class USBSTORPlugin(Plugin): NAME = "usbstor_plugin" DESCRIPTION = "Parse the connected USB devices history" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): try: for subkey_path in self.registry_hive.get_control_sets(USBSTOR_KEY_PATH): usbstor_key = self.registry_hive.get_key(subkey_path) for usbstor_drive in usbstor_key.iter_subkeys(): try: disk, manufacturer, title, version = usbstor_drive.name.split("&") except ValueError: manufacturer, title, version = None, None, None for serial_subkey in usbstor_drive.iter_subkeys(): timestamp = convert_wintime(serial_subkey.header.last_modified, as_json=self.as_json) serial_number = serial_subkey.name key_path = rf"{subkey_path}\{usbstor_drive.name}\{serial_number}" try: device_guid_key = self.registry_hive.get_key( rf"{subkey_path}\{usbstor_drive.name}\{serial_number}\{DISK_GUID_PATH}" ) disk_guid = device_guid_key.get_value("DiskId") except RegistryKeyNotFoundException: disk_guid = None properties_subkey = serial_subkey.get_subkey("Properties") if not properties_subkey: ( device_name, first_installed_time, last_connected_time, last_removed_time, last_installed_time, ) = (None, None, None, None, None) else: try: device_name_guid_key = properties_subkey.get_subkey(PROPERTIES_NAME_GUID) device_name_key = device_name_guid_key.get_subkey(DEVICE_NAME_KEY) device_name = device_name_key.get_value() except ( RegistryKeyNotFoundException, NoRegistrySubkeysException, ): device_name = None ( first_installed_time, last_connected_time, last_removed_time, last_installed_time, ) = (None, None, None, None) dates_subkey = properties_subkey.get_subkey(PROPERTIES_DATES_GUID, raise_on_missing=False) if dates_subkey: first_installed_key = dates_subkey.get_subkey(FIRST_INSTALLED_TIME_KEY, raise_on_missing=False) if first_installed_key: first_installed_time = first_installed_key.get_value(as_json=self.as_json) last_connected_key = dates_subkey.get_subkey(LAST_CONNECTED_TIME_KEY, raise_on_missing=False) if last_connected_key: last_connected_time = last_connected_key.get_value(as_json=self.as_json) last_removed_key = dates_subkey.get_subkey(LAST_REMOVED_TIME_KEY, raise_on_missing=False) if last_removed_key: last_removed_time = last_removed_key.get_value(as_json=self.as_json) last_installed_key = dates_subkey.get_subkey(LAST_INSTALLED_TIME_KEY, raise_on_missing=False) if last_installed_key: last_installed_time = last_installed_key.get_value(as_json=self.as_json) self.entries.append( { "last_write": timestamp, "key_path": key_path, "last_connected": last_connected_time, "last_removed": last_removed_time, "first_installed": first_installed_time, "last_installed": last_installed_time, "serial_number": serial_number, "device_name": device_name, "disk_guid": disk_guid, "manufacturer": manufacturer, "version": version, "title": title, } ) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {USBSTOR_KEY_PATH}: {ex}") ================================================ FILE: regipy/plugins/system/wdigest.py ================================================ import logging from regipy.exceptions import RegistryValueNotFoundException from regipy.hive_types import SYSTEM_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime logger = logging.getLogger(__name__) WDIGEST_PATH = r"Control\SecurityProviders\WDigest" class WDIGESTPlugin(Plugin): NAME = "wdigest" DESCRIPTION = "Get WDIGEST configuration" COMPATIBLE_HIVE = SYSTEM_HIVE_TYPE def run(self): logger.debug("Started WDIGEST Plugin...") for subkey_path in self.registry_hive.get_control_sets(WDIGEST_PATH): subkey = self.registry_hive.get_key(subkey_path) try: self.entries.append( { "subkey": subkey_path, "use_logon_credential": subkey.get_value("UseLogonCredential", as_json=self.as_json), "timestamp": convert_wintime(subkey.header.last_modified, as_json=self.as_json), } ) except RegistryValueNotFoundException as ex: logger.exception(f"Could not find Wdigest for registry hive: {ex}") continue ================================================ FILE: regipy/plugins/usrclass/__init__.py ================================================ ================================================ FILE: regipy/plugins/usrclass/shellbags_usrclass.py ================================================ import logging from regipy.exceptions import RegistryKeyNotFoundException from regipy.hive_types import USRCLASS_HIVE_TYPE from regipy.plugins.plugin import Plugin from regipy.utils import convert_wintime from regipy.constants import KNOWN_GUIDS import re logger = logging.getLogger(__name__) USRCLASS_SHELLBAG = "\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU" DEFAULT_CODEPAGE = "cp1252" class ShellBagUsrclassPlugin(Plugin): NAME = "usrclass_shellbag_plugin" DESCRIPTION = "Parse USRCLASS Shellbag items" COMPATIBLE_HIVE = USRCLASS_HIVE_TYPE @staticmethod def _parse_mru(mru_val): mru_order_string = "" if isinstance(mru_val, bytes): mru_val = mru_val[:-4] for i in range(0, len(mru_val), 4): current_val = int.from_bytes(mru_val[i : i + 4], byteorder="little") mru_order_string += f"{current_val}-" return mru_order_string[:-1] else: return mru_order_string @staticmethod def _get_shell_item_type(shell_item): try: import pyfwsi except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex if isinstance(shell_item, pyfwsi.volume): item_type = "Volume" elif isinstance(shell_item, pyfwsi.file_entry): item_type = "Directory" elif isinstance(shell_item, pyfwsi.network_location): item_type = "Network Location" elif isinstance(shell_item, pyfwsi.root_folder): item_type = "Root Folder" elif isinstance(shell_item, pyfwsi.control_panel_category): item_type = "Control Panel Category" elif isinstance(shell_item, pyfwsi.control_panel_item): item_type = "Control Panel Item" elif isinstance(shell_item, pyfwsi.users_property_view): item_type = "Users Property View" else: item_type = "unknown" return item_type @staticmethod def _check_known_guids(guid): if guid in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[guid] else: path_segment = "{{{0:s}}}".format(guid) return path_segment @staticmethod def _get_entry_string(fwps_record): if fwps_record.entry_name: entry_string = fwps_record.entry_name else: entry_string = f"{fwps_record.entry_type:d}" return entry_string @staticmethod def _parse_shell_item_path_segment(self, shell_item): """Parses a shell item path segment. Args: shell_item (pyfwsi.item): shell item. Returns: str: shell item path segment. """ try: import pyfwsi import pyfwps except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex path_segment = None full_path = None location_description = None if isinstance(shell_item, pyfwsi.volume): if shell_item.name: path_segment = shell_item.name elif shell_item.identifier: path_segment = self._check_known_guids(shell_item.identifier) elif isinstance(shell_item, pyfwsi.file_entry): long_name = "" for extension_block in shell_item.extension_blocks: if isinstance(extension_block, pyfwsi.file_entry_extension): long_name = extension_block.long_name if long_name: path_segment = long_name elif shell_item.name: path_segment = shell_item.name elif isinstance(shell_item, pyfwsi.network_location): if shell_item.location: path_segment = shell_item.location if shell_item.description: location_description = shell_item.description if shell_item.comments: location_description += f", {shell_item.comments}" elif isinstance(shell_item, pyfwsi.root_folder): if shell_item.shell_folder_identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.shell_folder_identifier] elif hasattr(shell_item, "identifier") and shell_item.identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.identifier] else: path_segment = "{{{0:s}}}".format(shell_item.shell_folder_identifier) elif isinstance(shell_item, pyfwsi.users_property_view): # Users property view if shell_item.delegate_folder_identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.delegate_folder_identifier] elif hasattr(shell_item, "identifier") and shell_item.identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.identifier] # Variable elif hasattr(shell_item, "known_folder_identifier") and shell_item.known_folder_identifier in KNOWN_GUIDS: path_segment = KNOWN_GUIDS[shell_item.known_folder_identifier] # Variable: Users property view elif shell_item.property_store_data: fwps_store = pyfwps.store() fwps_store.copy_from_byte_stream(shell_item.property_store_data) for fwps_set in iter(fwps_store.sets): if fwps_set.identifier == "b725f130-47ef-101a-a5f1-02608c9eebac": for fwps_record in iter(fwps_set.records): entry_string = self._get_entry_string(fwps_record) # PKEY_DisplayName: {b725f130-47ef-101a-a5f1-02608c9eebac}/10 if entry_string == "10": if fwps_record.value_type == 0x0001: value_string = "" elif fwps_record.value_type in ( 0x0003, 0x0013, 0x0014, 0x0015, ): value_string = str(fwps_record.get_data_as_integer()) elif fwps_record.value_type in (0x0008, 0x001E, 0x001F): value_string = fwps_record.get_data_as_string() elif fwps_record.value_type == 0x000B: value_string = str(fwps_record.get_data_as_boolean()) elif fwps_record.value_type == 0x0040: filetime = fwps_record.get_data_as_integer() value_string = self._FormatFiletimeValue(filetime) elif fwps_record.value_type == 0x0042: # TODO: add support value_string = "" elif fwps_record.value_type == 0x0048: value_string = fwps_record.get_data_as_guid() elif fwps_record.value_type & 0xF000 == 0x1000: # TODO: add support value_string = "" else: value_string = None path_segment = value_string elif fwps_set.identifier == "28636aa6-953d-11d2-b5d6-00c04fd918d0": for fwps_record in iter(fwps_set.records): entry_string = self._get_entry_string(fwps_record) # PKEY_ParsingPath: {28636aa6-953d-11d2-b5d6-00c04fd918d0}/30 if entry_string == "30": full_path = fwps_record.get_data_as_string() elif isinstance(shell_item, pyfwsi.control_panel_category): path_segment = self._check_known_guids(str(shell_item.identifier)) elif isinstance(shell_item, pyfwsi.control_panel_item): path_segment = self._check_known_guids(shell_item.identifier) if path_segment is None: path_segment = "".format(shell_item.class_type) return path_segment, full_path, location_description def iter_sk(self, key, reg_path, codepage=DEFAULT_CODEPAGE, base_path="", path=""): try: import pyfwsi except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex last_write = convert_wintime(key.header.last_modified, as_json=True) mru_val = key.get_value("MRUListEx") mru_order = self._parse_mru(mru_val) base_path = path if key.get_value("NodeSlot"): node_slot = str(key.get_value("NodeSlot")) else: node_slot = "" for v in key.iter_values(trim_values=False): if re.match(r"\d+", v.name): slot = v.name byte_stream = v.value shell_items = pyfwsi.item_list() shell_items.copy_from_byte_stream(byte_stream, ascii_codepage=codepage) for item in shell_items.items: shell_type = self._get_shell_item_type(item) value, full_path, location_description = self._parse_shell_item_path_segment(self, item) if not path: path = value base_path = "" else: path += f"\\{value}" creation_time = None access_time = None modification_time = None if len(item.extension_blocks) > 0: for extension_block in item.extension_blocks: if isinstance(extension_block, pyfwsi.file_entry_extension): try: creation_time = extension_block.get_creation_time() if self.as_json: creation_time = creation_time.isoformat() except OSError: logger.exception(f"Malformed creation time for {path}") try: access_time = extension_block.get_access_time() if self.as_json: access_time = access_time.isoformat() except OSError: logger.exception(f"Malformed access time for {path}") try: if hasattr(item, "modification_time"): modification_time = item.get_modification_time() if self.as_json: modification_time = modification_time.isoformat() except OSError: logger.exception(f"Malformed modification time for {path}") value_name = v.name mru_order_location = mru_order.split("-").index(value_name) entry = { "value": value, "slot": slot, "reg_path": reg_path, "value_name": value_name, "node_slot": node_slot, "shell_type": shell_type, "path": path, "full path": full_path if full_path else None, "location description": (location_description if location_description else None), "creation_time": creation_time, "access_time": access_time, "modification_time": modification_time, "last_write": last_write, "mru_order": mru_order, "mru_order_location": mru_order_location, } self.entries.append(entry) sk_reg_path = f"{reg_path}\\{value_name}" sk = self.registry_hive.get_key(sk_reg_path) self.iter_sk(sk, sk_reg_path, codepage, base_path, path) path = base_path def run(self, codepage=DEFAULT_CODEPAGE): try: # TODO: handle this import requirement in a better way # flake8: noqa import pyfwsi except ModuleNotFoundError as ex: logger.exception( "Plugin `shellbag_plugin` has missing modules, install regipy using" " `pip install regipy[full]` in order to install plugin dependencies. " "This might take some time... " ) raise ex try: shellbag_usrclass_subkey = self.registry_hive.get_key(USRCLASS_SHELLBAG) self.iter_sk(shellbag_usrclass_subkey, USRCLASS_SHELLBAG, codepage=codepage) except RegistryKeyNotFoundException as ex: logger.error(f"Could not find {self.NAME} plugin data at: {USRCLASS_SHELLBAG}: {ex}") ================================================ FILE: regipy/plugins/utils.py ================================================ import json import logging from dataclasses import asdict from typing import Any, Callable, Union from regipy import NKRecord from regipy.plugins.plugin import PLUGINS from regipy.plugins.validation_status import ( is_plugin_validated, warn_unvalidated_plugin, ) logger = logging.getLogger(__name__) # Type alias for value mapping specification # Can be: # - str: simple rename (registry "Foo" -> entry "foo") # - tuple[str, Callable]: rename + transform function ValueSpec = Union[str, tuple[str, Callable[[Any], Any]]] def extract_values( registry_key, value_map: dict[str, ValueSpec], entry: dict[str, Any], ) -> None: """ Extract registry values into an entry dict using a declarative mapping. Args: registry_key: Registry key to iterate values from value_map: Mapping of registry value names to output specifications entry: Dict to populate with extracted values Example value_map: { "ProfileName": "profile_name", # Simple rename "Enabled": ("enabled", lambda v: v == 1), # Convert with function "DateCreated": ("date_created", parse_date_func), # Custom transform } """ for value in registry_key.iter_values(): name = value.name if name not in value_map: continue spec = value_map[name] if isinstance(spec, str): entry[spec] = value.value else: output_name, converter = spec entry[output_name] = converter(value.value) def dump_hive_to_json( registry_hive, output_path, name_key_entry: NKRecord, verbose=False, fetch_values=True, ): """ Write the hive subkeys to a JSON-lines file, one line per entry. :param registry_hive: a RegistryHive object :param output_path: Output path to save the JSON :param name_key_entry: The NKRecord to start iterating from :param verbose: verbosity :return: The result, as dict """ with open(output_path, mode="w") as writer: for subkey_count, entry in enumerate( registry_hive.recurse_subkeys(name_key_entry, as_json=True, fetch_values=fetch_values) ): writer.write( json.dumps( asdict(entry), separators=( ",", ":", ), ) ) writer.write("\n") return subkey_count def run_relevant_plugins( registry_hive, as_json=False, plugins=None, include_unvalidated=False, ): """ Execute the relevant plugins on the hive :param registry_hive: a RegistryHive object :param as_json: Whether to return result as json :param plugins: List of plugin to execute (names according to the NAME field in each plugin) :param include_unvalidated: Whether to include plugins that don't have validation test cases. If False (default), only validated plugins will be executed. Unvalidated plugins may return incomplete or inaccurate data. :return: The result, as dict """ plugin_results = {} for plugin_class in PLUGINS: plugin = plugin_class(registry_hive, as_json=as_json) # If the list of plugins is defined, but the plugin is not in the list skip it. if plugins and plugin.NAME not in plugins: continue # Check validation status if not is_plugin_validated(plugin.NAME): if not include_unvalidated: logger.debug(f"Skipping unvalidated plugin: {plugin.NAME}") continue # Always warn when running unvalidated plugins warn_unvalidated_plugin(plugin.NAME) if plugin.can_run(): try: plugin.run() plugin_results[plugin.NAME] = plugin.entries except ModuleNotFoundError: logger.error(f"Plugin {plugin.NAME} has missing dependencies") return plugin_results ================================================ FILE: regipy/plugins/validated_plugins.json ================================================ [ "active_control_set", "amcache", "app_paths", "background_activity_moderator", "backuprestore_plugin", "boot_entry_list", "bootkey", "codepage", "computer_name", "crash_dump", "diag_sr", "disable_last_access", "disablesr_plugin", "domain_sid", "execution_policy", "host_domain_name", "image_file_execution_options", "installed_programs_ntuser", "installed_programs_software", "last_logon_plugin", "local_sid", "lsa_packages", "mounted_devices", "network_data", "network_drives_plugin", "networklist", "ntuser_classes_installer", "ntuser_persistence", "ntuser_shellbag_plugin", "pagefile", "previous_winver_plugin", "print_demon_plugin", "processor_architecture", "profilelist_plugin", "ras_tracing", "routes", "safeboot_configuration", "samparse", "services", "shimcache", "shutdown", "software_classes_installer", "software_plugin", "spp_clients_plugin", "susclient_plugin", "terminal_services_history", "timezone_data", "timezone_data2", "typed_paths", "typed_urls", "uac_plugin", "usb_devices", "usbstor_plugin", "user_assist", "usrclass_shellbag_plugin", "wdigest", "windows_defender", "winrar_plugin", "winscp_saved_sessions", "winver_plugin", "word_wheel_query", "wsl" ] ================================================ FILE: regipy/plugins/validation_status.py ================================================ """ Plugin validation status tracking. This module tracks which plugins have been validated with test cases. Plugins without validation may return invalid or incomplete data. Validation status is stored in validated_plugins.json, which is generated by the validation test framework and shipped with the package. """ import json import logging from pathlib import Path logger = logging.getLogger(__name__) # Load validated plugins from JSON file shipped with the package _VALIDATED_PLUGINS_FILE = Path(__file__).parent / "validated_plugins.json" try: with open(_VALIDATED_PLUGINS_FILE) as f: VALIDATED_PLUGINS: set[str] = set(json.load(f)) except FileNotFoundError: logger.warning(f"Validated plugins file not found: {_VALIDATED_PLUGINS_FILE}") VALIDATED_PLUGINS = set() def is_plugin_validated(plugin_name: str) -> bool: """Check if a plugin has validation test cases.""" return plugin_name in VALIDATED_PLUGINS def get_validated_plugins() -> set[str]: """Get set of all validated plugin names.""" return VALIDATED_PLUGINS.copy() def get_unvalidated_plugins(plugin_names: list[str]) -> list[str]: """Get list of plugin names that don't have validation.""" return [name for name in plugin_names if name not in VALIDATED_PLUGINS] def warn_unvalidated_plugin(plugin_name: str) -> None: """Log a warning for an unvalidated plugin.""" logger.warning( f"Plugin '{plugin_name}' does not have a validation test case. " "Results may be incomplete or inaccurate. Use at your own discretion." ) ================================================ FILE: regipy/py.typed ================================================ ================================================ FILE: regipy/recovery.py ================================================ import logging import os from io import BytesIO from construct import Int32ul from regipy import boomerang_stream from regipy.exceptions import RegistryRecoveryException from regipy.hive_types import DIRT_TRANSACTION_LOG_MAGIC, HVLE_TRANSACTION_LOG_MAGIC from regipy.registry import RegistryHive from regipy.structs import REGF_HEADER_SIZE, TRANSACTION_LOG logger = logging.getLogger(__name__) def _parse_hvle_block(hive_path, transaction_log_stream, log_size, expected_sequence_number): """ :param hive_path: :param transaction_log_stream: :param log_size: :param expected_sequence_number: :return: """ recovered_dirty_pages_count = 0 restored_hive_buffer = BytesIO(open(hive_path, "rb").read()) hvle_block_start_offset = transaction_log_stream.tell() while hvle_block_start_offset < log_size: logger.info(f"Parsing hvle block at {hex(hvle_block_start_offset)}") with boomerang_stream(transaction_log_stream) as x: if x.read(4) != b"HvLE": logger.info("Reached a non HvLE object. stopping") break logger.info(f"Parsing HvLE block at {hex(hvle_block_start_offset)}") parsed_hvle_block = TRANSACTION_LOG.parse_stream(transaction_log_stream) logger.info(f"Currently at start of dirty pages: {transaction_log_stream.tell()}") logger.info(f"seq number: {parsed_hvle_block.sequence_number}") logger.info(f"dirty pages: {parsed_hvle_block.dirty_pages_count}") if parsed_hvle_block.sequence_number == expected_sequence_number: logger.info("This hvle block holds valid dirty blocks") expected_sequence_number += 1 for dirty_page_entry in parsed_hvle_block.dirty_pages_references: # Write the actual dirty page to the original hive target_offset = REGF_HEADER_SIZE + dirty_page_entry.offset restored_hive_buffer.seek(target_offset) transaction_log_stream_offset = transaction_log_stream.tell() dirty_page_buffer = transaction_log_stream.read(dirty_page_entry.size) restored_hive_buffer.write(dirty_page_buffer) logger.info( f"Restored {dirty_page_entry.size} bytes to offset {hex(target_offset)} " f"from offset {hex(transaction_log_stream_offset)}" ) recovered_dirty_pages_count += 1 # TODO: update hive flags from hvle to original header # Update sequence numbers are at offsets 4 & 8: restored_hive_buffer.seek(4) restored_hive_buffer.write(Int32ul.build(expected_sequence_number)) restored_hive_buffer.write(Int32ul.build(expected_sequence_number)) # Update hbins size from hvle to original header at offset 40 restored_hive_buffer.seek(40) restored_hive_buffer.write(Int32ul.build(parsed_hvle_block.hive_bin_size)) transaction_log_stream.seek(hvle_block_start_offset + parsed_hvle_block.log_size) hvle_block_start_offset = hvle_block_start_offset + parsed_hvle_block.log_size return restored_hive_buffer, recovered_dirty_pages_count def _parse_dirt_block(hive_path, transaction_log, hbins_data_size): restored_hive_buffer = BytesIO(open(hive_path, "rb").read()) recovered_dirty_pages_count = 0 dirty_vector_length = hbins_data_size // 4096 if transaction_log.read(4) != b"DIRT": raise RegistryRecoveryException("Expected DIRT signature!") log_file_base = 1024 # 512 + len(b'DIRT') + dirty_vector_length primary_file_base = 4096 bitmap = transaction_log.read(dirty_vector_length) bit_counter = 0 bitmap_offset = 0 # Tuples of offset in primary and offset in transaction log offsets = [] while bit_counter < dirty_vector_length * 8: is_bit_set = ((bitmap[bit_counter // 8] >> (bit_counter % 8)) & 1) != 0 if is_bit_set: # We skip the basic block for the offsets registry_offset = primary_file_base + (bit_counter * 512) # And also the DIRT signature in the transaction log transaction_log_offset = log_file_base + (bitmap_offset * 512) offsets.append((registry_offset, transaction_log_offset)) bitmap_offset += 1 bit_counter += 1 for registry_offset, transaction_log_offset in offsets: logger.debug(f"Reading 512 bytes from {transaction_log_offset} writing to {registry_offset}") restored_hive_buffer.seek(registry_offset) transaction_log.seek(transaction_log_offset) restored_hive_buffer.write(transaction_log.read(512)) recovered_dirty_pages_count += 1 return restored_hive_buffer, recovered_dirty_pages_count def _parse_transaction_log(registry_hive, hive_path, transaction_log_path): log_size = os.path.getsize(transaction_log_path) logger.info(f"Log Size: {log_size}") expected_sequence_number = registry_hive.header.secondary_sequence_num with open(transaction_log_path, "rb") as transaction_log: # Skip the REGF header transaction_log.seek(512, 0) # Read the header of the transaction log vector and determine its type with boomerang_stream(transaction_log) as s: magic = s.read(4) if magic == HVLE_TRANSACTION_LOG_MAGIC: # This is an HvLE block restored_hive_buffer, recovered_dirty_pages_count = _parse_hvle_block( hive_path, transaction_log, log_size, expected_sequence_number ) elif magic == DIRT_TRANSACTION_LOG_MAGIC: # This is an old transaction log - DIRT hbins_data_size = registry_hive.header.hive_bins_data_size restored_hive_buffer, recovered_dirty_pages_count = _parse_dirt_block(hive_path, transaction_log, hbins_data_size) else: raise RegistryRecoveryException(f"The transaction log vector magic was not expected: {magic}") return restored_hive_buffer, recovered_dirty_pages_count def apply_transaction_logs( hive_path, primary_log_path, secondary_log_path=None, restored_hive_path=None, verbose=False, ): """ Apply transactions logs :param hive_path: The path to the original hive :param primary_log_path: The path to the primary log path :param secondary_log_path: The path to the secondary log path (optional). :param restored_hive_path: Path to save the restored hive :param verbose: verbosity :return: """ recovered_dirty_pages_total_count = 0 if not restored_hive_path: restored_hive_path = f"{hive_path}.restored" registry_hive = RegistryHive(hive_path) if secondary_log_path: registry_hive = RegistryHive(hive_path) restored_hive_buffer, recovered_dirty_pages_count = _parse_transaction_log(registry_hive, hive_path, secondary_log_path) # Write to disk the modified registry hive with open(restored_hive_path, "wb") as f: restored_hive_buffer.seek(0) f.write(restored_hive_buffer.read()) recovered_dirty_pages_total_count += recovered_dirty_pages_count logger.info(f"Recovered {recovered_dirty_pages_count} pages from secondary transaction log") # Parse the primary transaction log log_size = os.path.getsize(primary_log_path) logger.info(f"Log Size: {log_size}") # If no secondary transaction log was give, apply the first transaction log on the original hive target_hive = restored_hive_path if secondary_log_path else hive_path restored_hive_buffer, recovered_dirty_pages_count = _parse_transaction_log(registry_hive, target_hive, primary_log_path) logger.info(f"Recovered {recovered_dirty_pages_count} pages from primary transaction log") recovered_dirty_pages_total_count += recovered_dirty_pages_count # Write to disk the modified registry hive with open(restored_hive_path, "wb") as f: restored_hive_buffer.seek(0) f.write(restored_hive_buffer.read()) return restored_hive_path, recovered_dirty_pages_total_count ================================================ FILE: regipy/regdiff.py ================================================ import logging import os from typing import Any from regipy.exceptions import RegistryKeyNotFoundException from regipy.registry import NKRecord, RegistryHive from regipy.utils import calculate_sha1, convert_wintime logger = logging.getLogger(__name__) def get_subkeys_and_timestamps(registry_hive): subkeys_and_timestamps = set() for subkey in registry_hive.recurse_subkeys(): subkey_path = subkey.path ts = subkey.timestamp subkeys_and_timestamps.add((subkey_path, ts)) return subkeys_and_timestamps def get_values_from_tuples(value_tuples, value_name_list): for value_name, value_data in value_tuples: if value_name in value_name_list: yield value_name, value_data def get_timestamp_for_subkeys(registry_hive, subkey_list): for subkey_path in subkey_list: try: subkey = registry_hive.get_key(subkey_path) except RegistryKeyNotFoundException: logger.exception(f"Could not obtain timestamp for subkey {subkey_path}") continue yield subkey_path, convert_wintime(subkey.header.last_modified, as_json=True) def _get_name_value_tuples(subkey: NKRecord) -> set[tuple[str, Any]]: """ Iterate over value in a subkey and return a set of tuples containing value names and values :param subkey: NKRecord to iterate over :return: A set of tuples containing value names and values """ values_tuple: set[tuple[str, Any]] = set() for value in subkey.iter_values(as_json=True): if not value.value: continue if isinstance(value.value, list): values_tuple.update({(value.name, x) for x in value.value}) else: values_tuple.add((value.name, value.value)) return values_tuple def compare_hives(first_hive_path, second_hive_path, verbose=False): # The list will contain tuples, in the following format: (Difference type, first value, second value, description) found_differences = [] # Compare hash, verify they are indeed different first_hive_sha1 = calculate_sha1(first_hive_path) second_hive_sha1 = calculate_sha1(second_hive_path) if first_hive_sha1 == second_hive_sha1: logger.info("Hives have the same hash!") return found_differences # Compare header parameters first_registry_hive = RegistryHive(first_hive_path) second_registry_hive = RegistryHive(second_hive_path) if first_registry_hive.header.hive_bins_data_size != second_registry_hive.header.hive_bins_data_size: found_differences.append( ( "different_hive_bin_data_size", first_registry_hive.header.hive_bins_data_size, second_registry_hive.header.hive_bins_data_size, "", ) ) # Enumerate subkeys for each hive and start comparing logger.info(f"Enumerating subkeys in {os.path.basename(first_hive_path)}") first_hive_subkeys = get_subkeys_and_timestamps(first_registry_hive) logger.info(f"Enumerating subkeys in {os.path.basename(second_hive_path)}") second_hive_subkeys = get_subkeys_and_timestamps(second_registry_hive) # Get a set of keys present in one hive and not the other and vice versa first_hive_subkey_names = {x[0] for x in first_hive_subkeys if x[0] is not None} second_hive_subkey_names = {x[0] for x in second_hive_subkeys if x[0] is not None} found_differences.extend( ("new_subkey", ts, None, subkey_path) for subkey_path, ts in get_timestamp_for_subkeys( first_registry_hive, first_hive_subkey_names - second_hive_subkey_names ) ) found_differences.extend( ("new_subkey", None, ts, subkey_path) for subkey_path, ts in get_timestamp_for_subkeys( second_registry_hive, second_hive_subkey_names - first_hive_subkey_names ) ) # Remove duplicate keys from each of the sets first_hive_diff_subkeys = first_hive_subkeys - second_hive_subkeys second_hive_diff_subkeys = second_hive_subkeys - first_hive_subkeys # Find subkeys that exist in both hives, but were modified. Look for new values and subkeys for path_1, ts_1 in first_hive_diff_subkeys: for path_2, ts_2 in second_hive_diff_subkeys: if path_1 and path_1 == path_2 and ts_1 != ts_2: first_subkey_nk_record = first_registry_hive.get_key(path_1) second_subkey_nk_record = second_registry_hive.get_key(path_2) # Compare values between the subkeys first_subkey_values = set() second_subkey_values = set() if first_subkey_nk_record.values_count: first_subkey_values = _get_name_value_tuples(first_subkey_nk_record) if second_subkey_nk_record.values_count: second_subkey_values = _get_name_value_tuples(second_subkey_nk_record) # If one hive or the other contain values, and they are different, compare values if (first_subkey_values or second_subkey_values) and (first_subkey_values != second_subkey_values): first_hive_value_names = {x[0] for x in first_subkey_values} second_hive_value_names = {x[0] for x in second_subkey_values} values_in_first_but_not_in_second = first_hive_value_names - second_hive_value_names values_in_second_but_not_in_first = second_hive_value_names - first_hive_value_names # If there are value names that are present in the first subkey but not the second # Iterate over all values in the first subkey # If the value name is one of those that is not on the second subkey, add it to the set if values_in_first_but_not_in_second: found_differences.extend( ("new_value", f"{n}: {d} @ {ts_1}", None, path_1) for n, d in get_values_from_tuples(first_subkey_values, values_in_first_but_not_in_second) ) if values_in_second_but_not_in_first: found_differences.extend( ("new_value", None, f"{n}: {d} @ {ts_2}", path_1) for n, d in get_values_from_tuples(second_subkey_values, values_in_second_but_not_in_first) ) # We do not compare subkeys for each subkey, because we would have detected those. return found_differences ================================================ FILE: regipy/registry.py ================================================ import binascii import datetime as dt import logging from dataclasses import asdict, dataclass, field from io import BytesIO from typing import Optional, Union from construct import ( Bytes, ConstError, CString, EnumIntegerString, GreedyRange, Int32sl, Int32ul, Int64ul, StreamError, ) from regipy.exceptions import ( NoRegistrySubkeysException, RegipyGeneralException, RegistryKeyNotFoundException, RegistryParsingException, RegistryValueNotFoundException, UnidentifiedHiveException, ) from regipy.hive_types import SUPPORTED_HIVE_TYPES from regipy.security_utils import convert_sid, get_acls from regipy.structs import ( BIG_DATA_BLOCK, CM_KEY_NODE, DEFAULT_VALUE, FAST_LEAF_SIGNATURE, HASH_LEAF_SIGNATURE, HBIN_HEADER, INDEX_LEAF, INDEX_ROOT, INDEX_ROOT_SIGNATURE, LEAF_INDEX_SIGNATURE, LF_LH_SK_ELEMENT, REGF_HEADER, REGF_HEADER_SIZE, SECURITY_DESCRIPTOR, SID, VALUE_KEY, VALUE_TYPE_ENUM, SECURITY_KEY_v1_1, ) from regipy.utils import ( MAX_LEN, boomerang_stream, convert_wintime, identify_hive_type, trim_registry_data_for_error_msg, try_decode_binary, ) logger = logging.getLogger(__name__) @dataclass class Cell: """ Represents a Registry cell header """ offset: int cell_type: str size: int @dataclass class VKRecord: """ The VK Record contains a value """ value_type: EnumIntegerString value_type_str: str value: bytes size: int = 0 is_corrupted: bool = False @dataclass class LIRecord: data: bytes @dataclass class Value: name: str value: Union[str, int, bytes] value_type: str is_corrupted: bool = False @dataclass class Subkey: subkey_name: str path: str timestamp: dt.datetime values_count: int values: list[Value] = field(default_factory=list) # This field will be used if a partial hive was given, if not it would be None. actual_path: Optional[str] = None class RIRecord: data = None header = None def __init__(self, stream): self.header = INDEX_ROOT.parse_stream(stream) class RegistryHive: CONTROL_SETS = [r"\ControlSet001", r"\ControlSet002"] def __init__(self, hive_path, hive_type=None, partial_hive_path=None): """ Represents a registry hive :param hive_path: Path to the registry hive :param hive_type: The hive type can be specified if this is a partial hive, or for some other reason regipy cannot identify the hive type :param partial_hive_path: The path from which the partial hive actually starts, for example: hive_type=ntuser partial_hive_path="/Software" would mean this is actually a HKCU hive, starting from HKCU/Software """ self.partial_hive_path = None self.hive_type = None with open(hive_path, "rb") as f: self._stream = BytesIO(f.read()) with boomerang_stream(self._stream) as s: self.header = REGF_HEADER.parse_stream(s) # Get the first cell in root HBin, which is the root NKRecord: root_hbin = self.get_hbin_at_offset() root_hbin_cell = next(root_hbin.iter_cells(s)) self.root = NKRecord(root_hbin_cell, s) self.name = self.header.file_name if hive_type: if hive_type.lower() in SUPPORTED_HIVE_TYPES: self.hive_type = hive_type else: raise UnidentifiedHiveException( f"{hive_type} is not a supported hive type: only the following are supported: {SUPPORTED_HIVE_TYPES}" ) else: try: self.hive_type = identify_hive_type(self.name) except UnidentifiedHiveException: logger.info(f"Hive type for {hive_path} was not identified: {self.name}") if partial_hive_path: self.partial_hive_path = partial_hive_path def recurse_subkeys( self, nk_record=None, path_root=None, as_json=False, is_init=True, fetch_values=True, ): """ Recurse over a subkey, and yield all of its subkeys and values :param nk_record: an instance of NKRecord from which to start iterating, if None, will start from Root :param path_root: If we are iterating an incomplete hive, for example a hive tree starting from ControlSet001 and not SYSTEM, there is no way to know that. This string will be added as prefix to all paths. :param as_json: Whether to normalize the data as JSON or not :param fetch_values: If False, subkey values will not be returned, but the iteration will be faster """ # If None, will start iterating from Root NK entry if not nk_record: nk_record = self.root # Iterate over subkeys if nk_record.header.subkey_count: for subkey in nk_record.iter_subkeys(): if path_root: subkey_path = rf"{path_root}\{subkey.name}" if path_root else rf"\{subkey.name}" else: subkey_path = f"\\{subkey.name}" # Leaf Index records do not contain subkeys if isinstance(subkey, LIRecord): continue if subkey.subkey_count: yield from self.recurse_subkeys( nk_record=subkey, path_root=subkey_path, as_json=as_json, is_init=False, fetch_values=fetch_values, ) values = [] if fetch_values and subkey.values_count: try: if as_json: values = [asdict(x) for x in subkey.iter_values(as_json=as_json)] else: values = list(subkey.iter_values(as_json=as_json)) except RegistryParsingException: logger.exception(f"Failed to parse hive value at path: {trim_registry_data_for_error_msg(path_root)}") ts = convert_wintime(subkey.header.last_modified) yield Subkey( subkey_name=subkey.name, path=subkey_path, timestamp=ts.isoformat() if as_json else ts, values=values, values_count=subkey.values_count, actual_path=(f"{self.partial_hive_path}{subkey_path}" if self.partial_hive_path else None), ) if is_init: # Get the values of the subkey values = [] if nk_record.values_count: try: if as_json: values = [asdict(x) for x in nk_record.iter_values(as_json=as_json)] else: values = list(nk_record.iter_values(as_json=as_json)) except RegistryParsingException as ex: logger.exception(f"Failed to parse hive value at path: {trim_registry_data_for_error_msg(path_root)}: {ex}") values = [] ts = convert_wintime(nk_record.header.last_modified) subkey_path = path_root or "\\" yield Subkey( subkey_name=nk_record.name, path=subkey_path, timestamp=ts.isoformat() if as_json else ts, values=values, values_count=len(values), actual_path=(f"{self.partial_hive_path}\\{subkey_path}" if self.partial_hive_path else None), ) def get_hbin_at_offset(self, offset=0): """ This offset is from start of data (meaning that it starts from 4096) If not offset is given, will return the root Hbin :return: """ self._stream.seek(REGF_HEADER_SIZE + offset) return HBin(self._stream) def get_key(self, key_path): if self.partial_hive_path: if key_path.startswith(self.partial_hive_path): key_path = key_path.partition(self.partial_hive_path)[-1] else: raise RegistryKeyNotFoundException(f"Did not find subkey at {key_path}, because this is a partial hive") logger.debug(f"Getting key: {key_path}") # If the key path is \ we are just refering to root if key_path == "\\": return self.root # If the path contain slashes, this is a full path. Split it if "\\" in key_path: key_path_parts = key_path.split("\\")[1:] else: key_path_parts = [key_path] previous_key_name = [] subkey = self.root.get_subkey(key_path_parts.pop(0), raise_on_missing=False) if not subkey: raise RegistryKeyNotFoundException(f"Did not find subkey at {key_path}") if not key_path_parts: return subkey for path_part in key_path_parts: new_path = "\\".join(previous_key_name) previous_key_name.append(subkey.name) subkey = subkey.get_subkey(path_part, raise_on_missing=False) if not subkey: raise RegistryKeyNotFoundException(f"Did not find {path_part} at {new_path}") return subkey def get_control_sets(self, registry_path): """ Get the optional control sets for a registry hive :param registry_path: :return: A list of paths, including the control sets """ found_control_sets = [] for cs in self.CONTROL_SETS: try: found_control_sets.append(self.get_key(cs)) except RegistryKeyNotFoundException: continue result = [rf"\{subkey.name}\{registry_path}" for subkey in found_control_sets] logger.debug(f"Found control sets: {result}") return result class HBin: def __init__(self, stream): """ :param stream: a stream at the start of the hbin block """ self.header = HBIN_HEADER.parse_stream(stream) self.hbin_data_offset = stream.tell() def iter_cells(self, stream): stream.seek(self.hbin_data_offset) offset = stream.tell() while offset < self.hbin_data_offset + self.header.size - HBIN_HEADER.sizeof(): hbin_cell_size = Int32sl.parse_stream(stream) # If the cell size is positive, it means it is unallocated. We are not interested in those on a regular run if hbin_cell_size >= 0: continue bytes_to_read = (hbin_cell_size * -1) - 4 cell_type = Bytes(2).parse_stream(stream) # Yield the cell yield Cell(cell_type=cell_type.decode(), offset=stream.tell(), size=bytes_to_read) # Go to the next cell offset += stream.tell() + bytes_to_read class NKRecord: """ The NKRecord represents a Name Key entry """ def __init__(self, cell, stream): stream.seek(cell.offset) self.header = CM_KEY_NODE.parse_stream(stream) self._stream = stream # Sometimes the key names are ASCII and sometimes UTF-16 little endian if self.header.flags.KEY_COMP_NAME: # Compressed (ASCII) key name self.name = self.header.key_name_string.decode("ascii", errors="replace") else: # Unicode (UTF-16) key name self.name = self.header.key_name_string.decode("utf-16-le", errors="replace") logger.debug(f'Unicode key name identified: "{self.name}"') self.subkey_count = self.header.subkey_count self.values_count = self.header.values_count self.volatile_subkeys_count = self.header.volatile_subkey_count def get_subkey(self, key_name, raise_on_missing=True): if not self.subkey_count and raise_on_missing: raise NoRegistrySubkeysException(f"No subkeys for {self.header.key_name_string}") for subkey in self.iter_subkeys(): # This should not happen if not isinstance(subkey, NKRecord): raise RegipyGeneralException(f"Unknown record type: {subkey}") if subkey.name.upper() == key_name.upper(): return subkey if raise_on_missing: raise NoRegistrySubkeysException(f"No subkey {key_name} for {self.header.key_name_string}") def iter_subkeys(self): if not self.header.subkey_count: return None # Go to the offset where the subkey list starts (+4 is because of the cell header) target_offset = REGF_HEADER_SIZE + 4 + self.header.subkeys_list_offset self._stream.seek(target_offset) # Read the signature try: signature = Bytes(2).parse_stream(self._stream) except StreamError as ex: raise RegistryParsingException(f"Bad subkey at offset {target_offset}: {ex}") # LF, LH and RI contain subkeys if signature in [ HASH_LEAF_SIGNATURE, FAST_LEAF_SIGNATURE, LEAF_INDEX_SIGNATURE, ]: yield from self._parse_subkeys(self._stream, signature=signature) # RI contains pointers to arrays of subkeys elif signature == INDEX_ROOT_SIGNATURE: ri_record = RIRecord(self._stream) if ri_record.header.element_count > 0: for element in ri_record.header.elements: # We skip 6 because of the signature as well as the cell header element_target_offset = REGF_HEADER_SIZE + 4 + element.subkey_list_offset self._stream.seek(element_target_offset) yield from self._parse_subkeys(self._stream) @staticmethod def _parse_subkeys(stream, signature=None): """ Parse an LI , LF or LH Record :param stream: A stream at the header of the LH or LF entry, skipping the signature :return: """ if not signature: signature = stream.read(2) if signature in [HASH_LEAF_SIGNATURE, FAST_LEAF_SIGNATURE]: subkeys = LF_LH_SK_ELEMENT.parse_stream(stream) elif signature == LEAF_INDEX_SIGNATURE: subkeys = INDEX_LEAF.parse_stream(stream) else: raise RegistryParsingException(f"Expected a known signature, got: {signature} at offset {stream.tell()}") for subkey in subkeys.elements: stream.seek(REGF_HEADER_SIZE + subkey.key_node_offset) # This cell should always be allocated, therefor we expect a negative size cell_size = Int32sl.parse_stream(stream) * -1 # We read to this offset and skip 2 bytes, because that is the cell size we just read nk_cell = Cell(cell_type="nk", offset=stream.tell() + 2, size=cell_size) nk_record = NKRecord(cell=nk_cell, stream=stream) yield nk_record @staticmethod def read_value(vk, stream): """ Read a registry value :param vk: A parse VK record :param stream: The registry stream :return: A VKRecord """ stream.seek(REGF_HEADER_SIZE + 4 + vk.data_offset) data_type = vk.data_type data = stream.read(vk.data_size) return VKRecord( value_type=data_type, value_type_str=str(data_type), value=data, size=vk.data_size, ) @staticmethod def _parse_indirect_block(stream, value): # This is an indirect datablock (Bigger than 16344, therefor we handle it differently) # The value inside the vk entry actually contains a pointer to the buffers containing the data big_data_block_header = BIG_DATA_BLOCK.parse(value.value) # Go to the start of the segment offset list stream.seek(REGF_HEADER_SIZE + big_data_block_header.offset_to_list_of_segments) buffer = BytesIO() # Read them sequentially until we got all the size of the VK value_size = value.size while value_size > 0: data_segment_offset = Int32ul.parse_stream(stream) with boomerang_stream(stream) as tmpstream: tmpstream.seek(REGF_HEADER_SIZE + 4 + data_segment_offset) tmpbuffer = tmpstream.read(min(0x3FD8, value_size)) value_size -= len(tmpbuffer) buffer.write(tmpbuffer) buffer.seek(0) return buffer.read() def iter_values(self, as_json=False, max_len=MAX_LEN, trim_values=True): """ Get the values of a subkey. Will raise if no values exist :param as_json: Whether to normalize the data as JSON or not :param max_len: Max length of value to return :param trim_values: whether to trim values to MAX_LEN :return: List of values for the subkey """ if not self.values_count: return # Get the offset of the values key. We skip 4 because of Cell Header target_offset = REGF_HEADER_SIZE + 4 + self.header.values_list_offset self._stream.seek(target_offset) for _ in range(self.values_count): is_corrupted = False try: vk_offset = Int32ul.parse_stream(self._stream) except StreamError: logger.info(f"Skipping bad registry VK at {self._stream.tell()}") raise RegistryParsingException(f"Bad registry VK at {self._stream.tell()}") with boomerang_stream(self._stream) as substream: actual_vk_offset = REGF_HEADER_SIZE + 4 + vk_offset substream.seek(actual_vk_offset) try: vk = VALUE_KEY.parse_stream(substream) except (ConstError, StreamError): logger.error(f"Could not parse VK at {substream.tell()}, registry hive is probably corrupted.") return value = self.read_value(vk, substream) if vk.name_size == 0: value_name = "(default)" elif vk.flags.VALUE_COMP_NAME: # Compressed (ASCII) value name value_name = vk.name.decode("ascii", errors="replace") else: # Unicode (UTF-16) value name value_name = vk.name.decode("utf-16-le", errors="replace") logger.debug(f'Unicode value name identified: "{value_name}"') # If the value is bigger than this value, it means this is a DEVPROP structure # https://doxygen.reactos.org/d0/dba/devpropdef_8h_source.html # https://sourceforge.net/p/mingw-w64/mingw-w64/ci/668a1d3e85042c409e0c292e621b3dc0aa26177c/tree/ # mingw-w64-headers/include/devpropdef.h?diff=dd86a3b7594dadeef9d6a37c4b6be3ca42ef7e94 # We currently do not support these, We are going to make the best effort to dump as string. # This int casting will always work because the data_type is construct's EnumIntegerString if int(vk.data_type) > 0xFFFF0000: data_type = VALUE_TYPE_ENUM.parse(Int32ul.build(int(vk.data_type) & 0xFFFF)) logger.info(f"Value at {hex(actual_vk_offset)} contains DEVPROP structure of type {data_type}") # Skip this unknown data type, research pending :) # TODO: Add actual parsing elif int(vk.data_type) == 0x200000: logger.info(f"Skipped unknown data type value at {actual_vk_offset}") continue else: data_type = str(vk.data_type) if data_type in ["REG_SZ", "REG_EXPAND", "REG_EXPAND_SZ"]: if vk.data_size >= 0x80000000: # data is contained in the data_offset field value.size -= 0x80000000 actual_value = vk.data_offset elif vk.data_size > 0x3FD8 and value.value[:2] == b"db": data = self._parse_indirect_block(substream, value) actual_value = try_decode_binary(data, as_json=as_json, trim_values=trim_values) else: actual_value = try_decode_binary(value.value, as_json=as_json, trim_values=trim_values) elif data_type in ["REG_BINARY", "REG_NONE"]: if vk.data_size >= 0x80000000: # data is contained in the data_offset field actual_value = vk.data_offset elif vk.data_size > 0x3FD8 and value.value[:2] == b"db": try: actual_value = self._parse_indirect_block(substream, value) actual_value = ( try_decode_binary(actual_value, as_json=True, trim_values=trim_values) if as_json else actual_value ) except ConstError: logger.error(f"Bad value at {actual_vk_offset}") continue else: # Return the actual data actual_value = binascii.b2a_hex(value.value).decode()[:max_len] if trim_values else value.value elif data_type == "REG_SZ": actual_value = try_decode_binary(value.value, as_json=as_json, trim_values=trim_values) elif data_type == "REG_DWORD": # If the data size is bigger than 0x80000000, data is actually stored in the VK data offset. actual_value = vk.data_offset if vk.data_size >= 0x80000000 else Int32ul.parse(value.value) elif data_type == "REG_QWORD": actual_value = vk.data_offset if vk.data_size >= 0x80000000 else Int64ul.parse(value.value) elif data_type == "REG_MULTI_SZ": parsed_value = GreedyRange(CString("utf-16-le")).parse(value.value) # Because the ListContainer object returned by Construct cannot be turned into a list, # we do this trick actual_value = [x for x in parsed_value if x] # We currently dumps this as hex string or raw # TODO: Add actual parsing elif data_type in [ "REG_RESOURCE_REQUIREMENTS_LIST", "REG_RESOURCE_LIST", ]: actual_value = binascii.b2a_hex(value.value).decode()[:max_len] if trim_values else value.value elif data_type == "REG_FILETIME": actual_value = convert_wintime(Int64ul.parse(value.value), as_json=as_json) else: actual_value = try_decode_binary(value.value, as_json=as_json, trim_values=trim_values) yield Value( name=value_name, value_type=data_type, value=actual_value, is_corrupted=is_corrupted, ) def get_value( self, value_name=DEFAULT_VALUE, as_json=False, raise_on_missing=False, case_sensitive=True, ): """ Get a value by name. Will raise if raise_on_missing is set, if no value name is given, will return the content of the default value :param value_name: The value name to look for :param as_json: Whether to normalize the data as JSON or not :param raise_on_missing: Will raise exception if value is missing, else will return None :return: """ value_name = value_name if case_sensitive else value_name.lower() for value in self.iter_values(as_json=as_json, trim_values=False): v = value.name if case_sensitive else value.name.lower() if v == value_name: return value.value if raise_on_missing: raise RegistryValueNotFoundException(f"Did not find the value {value_name} on subkey {self.name}") return None def get_values(self, as_json=False, trim_values=False): return list(self.iter_values(as_json=as_json, trim_values=trim_values)) def get_security_key_info(self): self._stream.seek(REGF_HEADER_SIZE + self.header.security_key_offset) # TODO: If parsing fails, parse with SECURITY_KEY_v1_2 security_key = SECURITY_KEY_v1_1.parse_stream(self._stream) security_descriptor = SECURITY_DESCRIPTOR.parse(security_key.security_descriptor) with boomerang_stream(self._stream) as s: security_base_offset = REGF_HEADER_SIZE + self.header.security_key_offset + 24 s.seek(security_base_offset + security_descriptor.owner) owner_sid = convert_sid(SID.parse_stream(s)) s.seek(security_base_offset + security_descriptor.group) group_sid = convert_sid(SID.parse_stream(s)) sacl_aces = None if security_descriptor.offset_sacl > 0: s.seek(security_base_offset + security_descriptor.offset_sacl) sacl_aces = get_acls(s) dacl_aces = None if security_descriptor.offset_dacl > 0: s.seek(security_base_offset + security_descriptor.offset_dacl) dacl_aces = get_acls(s) return { "owner": owner_sid, "group": group_sid, "dacl": dacl_aces, "sacl": sacl_aces, } def get_class_name(self) -> str: """ Gets the key class name as would be returned via the `lpClass` argument of the `RegQueryInfoKey()` function. """ # Get the offset of the class name string. We skip 4 because of Cell Header read_offset = REGF_HEADER_SIZE + 4 + self.header.class_name_offset self._stream.seek(read_offset) class_name = self._stream.read(self.header.class_name_size) return class_name.decode("utf-16-le", errors="replace") def __dict__(self): return { "name": self.name, "subkey_count": self.subkey_count, "value_count": self.values_count, "values": ({x["name"]: x["value"] for x in self.iter_values()} if self.values_count else None), "subkeys": ({x["name"] for x in self.iter_subkeys()} if self.subkey_count else None), "timestamp": convert_wintime(self.header.last_modified, as_json=True), "volatile_subkeys": self.volatile_subkeys_count, } ================================================ FILE: regipy/security_utils.py ================================================ from typing import Any from construct import Int64ub from regipy.structs import ACE, ACL, SID def convert_sid(sid: Any, strip_rid: bool = False) -> str: identifier_authority = Int64ub.parse(b"\x00\x00" + sid.identifier_authority) sub_authorities = sid.subauthority[:-1] if strip_rid else sid.subauthority sub_identifier_authorities = "-".join(str(x) for x in sub_authorities) return f"S-{sid.revision}-{identifier_authority}-{sub_identifier_authorities}" def get_acls(s): aces = [] dacl = ACL.parse_stream(s) for _ in range(dacl.ace_count): parsed_ace = ACE.parse_stream(s) ace_sid = SID.parse(parsed_ace.sid) aces.append( { "ace_type": str(parsed_ace.type), "flags": dict(parsed_ace.flags), "access_mask": dict(parsed_ace.access_mask), "sid": convert_sid(ace_sid), } ) return aces ================================================ FILE: regipy/structs.py ================================================ from construct import ( Array, Bytes, Const, Enum, FlagsEnum, Int8ul, Int16ul, Int32ul, Int64ul, PaddedString, Struct, this, ) REGF_HEADER = Struct( "signature" / Const(b"regf"), "primary_sequence_num" / Int32ul, "secondary_sequence_num" / Int32ul, "last_modification_time" / Int64ul, "major_version" / Int32ul, "minor_version" / Int32ul, "file_type" / Int32ul, "file_format" / Int32ul, "root_key_offset" / Int32ul, "hive_bins_data_size" / Int32ul, "clustering_factor" / Int32ul, "file_name" / PaddedString(64, "utf-16-le"), "padding" * Bytes(396), "checksum" / Int32ul, ).compile() REGF_HEADER_SIZE = 4096 HBIN_HEADER = Struct( "signature" / Const(b"hbin"), "offset" / Int32ul, "size" / Int32ul, "unknown" * Int32ul, "unknown" * Int32ul, "timestamp" / Int64ul, "unknown" * Int32ul, ).compile() CM_KEY_NODE_SIZE = 76 CM_KEY_NODE = Struct( "flags" / FlagsEnum( Int16ul, KEY_VOLATILE=0x0001, KEY_HIVE_EXIT=0x0002, KEY_HIVE_ENTRY=0x0004, KEY_NO_DELETE=0x0008, KEY_SYM_LINK=0x0010, KEY_COMP_NAME=0x0020, KEY_PREDEF_HANDLE=0x0040, ), "last_modified" / Int64ul, "access_bits" / Bytes(4), "parent_key_offset" / Int32ul, "subkey_count" / Int32ul, "volatile_subkey_count" / Int32ul, "subkeys_list_offset" / Int32ul, "volatile_subkeys_list_offset" / Int32ul, "values_count" / Int32ul, "values_list_offset" / Int32ul, "security_key_offset" / Int32ul, "class_name_offset" / Int32ul, "largest_sk_name" / Int32ul, "largest_sk_class_name" / Int32ul, "largest_value_name" / Int32ul, "largest_value_data" / Int32ul, "unknown" * Bytes(4), "key_name_size" / Int16ul, "class_name_size" / Int16ul, "key_name_string" / Bytes(this.key_name_size), ).compile() SUBKEY_LIST_HEADER = Struct( "signature" / Bytes(2), "element_count" / Int16ul, ).compile() HASH_LEAF = Struct( "element_count" / Int16ul, "elements" / Array( this.element_count, Struct( "key_node_offset" / Int32ul, "name_hash" / Int32ul, ), ), ).compile() FAST_LEAF = Struct( "element_count" / Int16ul, "elements" / Array( this.element_count, Struct( "key_node_offset" / Int32ul, "name_hint" / Int32ul, ), ), ).compile() LEAF_INDEX_SIGNATURE = b"li" INDEX_LEAF = Struct( "element_count" / Int16ul, "elements" / Array( this.element_count, Struct( "key_node_offset" / Int32ul, ), ), ).compile() INDEX_ROOT_SIGNATURE = b"ri" INDEX_ROOT = Struct( "element_count" / Int16ul, "elements" / Array( this.element_count, Struct( "subkey_list_offset" / Int32ul, ), ), ).compile() KEY_SECURITY = Struct( "reserved" * Bytes(2), "forward_link" / Int32ul, "static_link" / Int32ul, "reference_count" / Int32ul, "security_descriptor_size" / Int32ul, "security_descriptor" / Bytes(this.security_descriptor_size), ).compile() VALUE_TYPE_ENUM = Enum( Int32ul, REG_NONE=0, REG_SZ=1, REG_EXPAND_SZ=2, REG_BINARY=3, REG_DWORD=4, REG_DWORD_BIG_ENDIAN=5, REG_LINK=6, REG_MULTI_SZ=7, REG_RESOURCE_LIST=8, REG_FULL_RESOURCE_DESCRIPTOR=9, REG_RESOURCE_REQUIREMENTS_LIST=10, REG_QWORD=11, REG_FILETIME=16, ) VALUE_KEY = Struct( "signature" / Const(b"vk"), "name_size" / Int16ul, "data_size" / Int32ul, "data_offset" / Int32ul, "data_type" / VALUE_TYPE_ENUM, "flags" / FlagsEnum(Int16ul, VALUE_COMP_NAME=0x0001), "padding" * Int16ul, "name" / Bytes(this.name_size), ).compile() HASH_LEAF_SIGNATURE = b"lh" FAST_LEAF_SIGNATURE = b"lf" LF_LH_SK_ELEMENT = Struct( "element_count" / Int16ul, "elements" / Array(this.element_count, Struct("key_node_offset" / Int32ul, "hash_value" / Int32ul)), ).compile() DIRTY_PAGES_REFERENCES = Struct("offset" / Int32ul, "size" / Int32ul) TRANSACTION_LOG = Struct( "signature" / Const(b"HvLE"), "log_size" / Int32ul, "flags" / Int32ul, "sequence_number" / Int32ul, "hive_bin_size" / Int32ul, "dirty_pages_count" / Int32ul, "hash_1" / Int64ul, "hash_2" / Int64ul, "dirty_pages_references" / Array(this.dirty_pages_count, DIRTY_PAGES_REFERENCES), ) BIG_DATA_SIGNATURE = b"db" BIG_DATA_BLOCK = Struct( "signature" / Const(BIG_DATA_SIGNATURE), "number_of_segments" / Int16ul, "offset_to_list_of_segments" / Int32ul, ) # This is the default name of a registry subkey DEFAULT_VALUE = "(default)" SECURITY_KEY_v1_1 = Struct( "unknown" * Bytes(4), "signature" / Const(b"sk"), "unknown" * Bytes(2), "prev_sk_offset" / Int32ul, "next_sk_offset" / Int32ul, "reference_count" / Int32ul, "security_descriptor_size" / Int32ul, "security_descriptor" / Bytes(this.security_descriptor_size), ) SECURITY_KEY_v1_2 = Struct( "signature" / Const(b"sk"), "unknown" * Bytes(2), "prev_sk_offset" / Int32ul, "next_sk_offset" / Int32ul, "reference_count" / Int32ul, "security_descriptor_size" / Int32ul, "security_descriptor" / Bytes(this.security_descriptor_size), "ref_count" / Int32ul, "sdlen" / Int32ul, ) # References: # https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor # https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-control SECURITY_DESCRIPTOR = Struct( "revision" / Bytes(1), "sbz1" / Bytes(1), "control" / FlagsEnum( Int16ul, SE_DACL_AUTO_INHERIT_REQ=0x0100, SE_DACL_AUTO_INHERITED=0x0400, SE_DACL_DEFAULTED=0x0008, SE_DACL_PRESENT=0x0004, SE_DACL_PROTECTED=0x1000, SE_GROUP_DEFAULTED=0x0002, SE_OWNER_DEFAULTED=0x0001, SE_RM_CONTROL_VALID=0x4000, SE_SACL_AUTO_INHERIT_REQ=0x0200, SE_SACL_AUTO_INHERITED=0x0800, SE_SACL_DEFAULTED=0x0008, SE_SACL_PRESENT=0x0010, SE_SACL_PROTECTED=0x2000, SE_SELF_RELATIVE=0x8000, ), "owner" / Int32ul, "group" / Int32ul, "offset_sacl" / Int32ul, "offset_dacl" / Int32ul, ) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c6ce4275-3d90-4890-ab3a-514745e4637e # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f992ad60-0fe4-4b87-9fed-beb478836861 # https://flatcap.org/linux-ntfs/ntfs/attributes/security_descriptor.html SID = Struct( "revision" / Int8ul, "sub_authority_count" / Int8ul, "identifier_authority" / Bytes(6), "subauthority" / Int32ul[this.sub_authority_count], ) ACL = Struct( "revision" / Int8ul, "sbz1" * Int8ul, "acl_size" / Int16ul, "ace_count" / Int16ul, "sbz2" * Int16ul, ).compile() ACE = Struct( "type" / Enum( Int8ul, ACCESS_ALLOWED=0, ACCESS_DENIED=1, SYSTEM_AUDIT=2, SYSTEM_ALARM=3, ACCESS_ALLOWED_COMPOUND=4, ACCESS_ALLOWED_OBJECT=5, ACCESS_DENIED_OBJECT=6, SYSTEM_AUDIT_OBJECT=7, SYSTEM_ALARM_OBJECT=8, ACCESS_ALLOWED_CALLBACK=9, ACCESS_DENIED_CALLBACK=10, ACCESS_ALLOWED_CALLBACK_OBJECT=11, ACCESS_DENIED_CALLBACK_OBJECT=12, SYSTEM_ALARM_CALLBACK=14, SYSTEM_AUDIT_CALLBACK_OBJECT=15, SYSTEM_ALARM_CALLBACK_OBJECT=16, ), "flags" / FlagsEnum( Int8ul, OBJECT_INHERIT_ACE=0x1, CONTAINER_INHERIT_ACE=0x2, NO_PROPAGATE_INHERIT_ACE=0x4, INHERIT_ONLY_ACE=0x8, ), "size" / Int16ul, "access_mask" / FlagsEnum( Int32ul, DELETE=0x00010000, READ_CONTROL=0x00020000, WRITE_DAC=0x00040000, WRITE_OWNER=0x00080000, SYNCHRONIZE=0x00100000, ACCESS_SYSTEM_SECURITY=0x01000000, MAXIMUM_ALLOWED=0x02000000, GENERIC_ALL=0x10000000, GENERIC_EXECUTE=0x20000000, GENERIC_WRITE=0x40000000, GENERIC_READ=0x80000000, ), "sid" / Bytes(this.size - 8), ).compile() ================================================ FILE: regipy/utils.py ================================================ import binascii import datetime as dt import hashlib import logging import struct import sys from collections.abc import Generator from contextlib import contextmanager from dataclasses import asdict from io import TextIOWrapper from typing import Union import pytz from regipy.exceptions import ( NoRegistrySubkeysException, RegipyGeneralException, RegistryKeyNotFoundException, UnidentifiedHiveException, ) from regipy.hive_types import ( AMCACHE_HIVE_TYPE, BCD_HIVE_TYPE, NTUSER_HIVE_TYPE, SAM_HIVE_TYPE, SECURITY_HIVE_TYPE, SOFTWARE_HIVE_TYPE, SYSTEM_HIVE_TYPE, USRCLASS_HIVE_TYPE, ) logger = logging.getLogger(__name__) # Max size of string to return when as_json=True MAX_LEN: int = 256 # The max string lenth when raising registry exceptions that include registry data # As the length can be arbitrarly large MAX_LEN_ERR_MSG_REGVALUE: int = 20 def calculate_sha1(file_path): sha1 = hashlib.sha1() with open(file_path, "rb") as f: while True: data = f.read(1024**2) if not data: break sha1.update(data) return sha1.hexdigest() def calculate_xor32_checksum(b: bytes) -> int: """ Calculate xor32 checksum from buffer :param b: buffer :return: The calculated checksum """ checksum = 0 if len(b) % 4 != 0: raise RegipyGeneralException(f"Buffer must be multiples of four, {len(b)} length buffer given") for i in range(0, len(b), 4): checksum = (b[i] + (b[i + 1] << 0x08) + (b[i + 2] << 0x10) + (b[i + 3] << 0x18)) ^ checksum return checksum @contextmanager def boomerang_stream(stream: TextIOWrapper) -> Generator[TextIOWrapper, None, None]: """ Yield a stream that goes back to the original offset after exiting the "with" context :param stream: The stream """ current_offset = stream.tell() yield stream stream.seek(current_offset) def convert_filetime(dw_low_date_time, dw_high_date_time): """ """ if dw_high_date_time is None or dw_low_date_time is None: return None try: date = dt.datetime(1601, 1, 1, 0, 0, 0) temp_time = dw_high_date_time temp_time <<= 32 temp_time |= dw_low_date_time date = pytz.utc.localize(date + dt.timedelta(microseconds=temp_time / 10)) return date.isoformat() except OverflowError: return None # Convert FILETIME to datetime. # Based on http://code.activestate.com/recipes/511425-filetime-to-datetime/ def convert_filetime2(dte): try: epoch = dt.datetime(1601, 1, 1, 0, 0, 0) nt_timestamp = struct.unpack(" Union[dt.datetime, str]: """ Get an integer containing a FILETIME date :param wintime: integer representing a FILETIME timestamp :param as_json: whether to return the date as string or not :return: datetime """ # http://stackoverflow.com/questions/4869769/convert-64-bit-windows-date-time-in-python us = wintime / 10 try: date = dt.datetime(1601, 1, 1, tzinfo=pytz.utc) + dt.timedelta(microseconds=us) except OverflowError: # If date is too big, it is probably corrupted' let's return the smallest possible windows timestamp. date = dt.datetime(1601, 1, 1, tzinfo=pytz.utc) return date.isoformat() if as_json else date def get_subkey_values_from_list(registry_hive, entries_list, as_json=False, trim_values=True): """ Return a list of registry subkeys given a list of paths :param registry_hive: A RegistryHive object :param entries_list: A list of paths as strings :param as_json: whether to return the subkey as json :param trim_values: Wether to trim values to MAX_LEN :return: A dict with each subkey and its values """ result = {} for path in entries_list: try: subkey = registry_hive.get_key(path) except (RegistryKeyNotFoundException, NoRegistrySubkeysException) as ex: logger.debug(f"Could not find subkey: {path} ({ex})") continue ts = convert_wintime(subkey.header.last_modified, as_json=as_json) values = [] if subkey.values_count: if as_json: values = [asdict(x) for x in subkey.iter_values(as_json=as_json, trim_values=trim_values)] else: values = list(subkey.iter_values(as_json=as_json, trim_values=trim_values)) if subkey.values_count: result[path] = {"timestamp": ts, "values": values} return result def identify_hive_type(name: str) -> str: hive_name = name.lower() if hive_name.endswith("ntuser.dat"): return NTUSER_HIVE_TYPE elif hive_name == SYSTEM_HIVE_TYPE: return SYSTEM_HIVE_TYPE elif hive_name.endswith("system32\\config\\software"): return SOFTWARE_HIVE_TYPE elif hive_name == r"\systemroot\system32\config\sam": return SAM_HIVE_TYPE elif hive_name.endswith(r"\system32\config\security"): return SECURITY_HIVE_TYPE elif hive_name.endswith(r"\boot\bcd"): return BCD_HIVE_TYPE elif hive_name == r"\microsoft\windows\usrclass.dat": return USRCLASS_HIVE_TYPE elif "amcache" in hive_name.lower(): return AMCACHE_HIVE_TYPE else: raise UnidentifiedHiveException(f"Could not identify hive: {name}") def try_decode_binary(data, as_json=False, max_len=MAX_LEN, trim_values=True): try: value = data.decode("utf-16-le").rstrip("\x00") except UnicodeDecodeError: try: value = data.decode().rstrip("\x00") except Exception as ex: logger.warning(f"Could not parse data as string, formating to hex: {ex}") value = binascii.b2a_hex(data).decode() if as_json else data if trim_values: value = value[:max_len] return value def _setup_logging(verbose): logging.basicConfig( stream=sys.stderr, level=logging.DEBUG if verbose else logging.INFO, ) def trim_registry_data_for_error_msg(s: str, max_len: int = MAX_LEN_ERR_MSG_REGVALUE) -> str: # Registry values included in Registry expections might be arbitrarly large, return s[:max_len] + f"... (trimmed original value from {len(s)} length)" ================================================ FILE: regipy_mcp_server/README.md ================================================ # Regipy MCP Server **Windows Registry Analysis for Claude Desktop** Enable Claude to analyze Windows registry hives and answer forensic questions in natural language. ## Quick Start (Windows) ### 1. Configure Claude Desktop Edit `%APPDATA%\Claude\claude_desktop_config.json`: ```json { "mcpServers": { "regipy": { "command": "C:\\Users\\YourUsername\\anaconda3\\envs\\regipy\\python.exe", "args": [ "path\\to\\regipy_mcp_server\\server.py", "--hives-dir", "C:\\path\\to\\your\\hives" ] } } } ``` ### 2. Restart Claude Desktop Completely quit and restart Claude Desktop (right-click tray icon → Quit) ### 3. Test It Ask Claude: **"What registry hives are available?"** ## Demo: Claude Desktop in Action ### Example 1: Listing Available Hives **Question:** "What registry hives are currently available in the system?" **Claude's Response:** ![Available Hives](screenshots/hives_list.png) Claude automatically: - Called the `list_available_hives` MCP tool - Categorized 8 hives by type (SYSTEM, SOFTWARE, NTUSER, SAM, etc.) - Explained what each hive contains ### Example 2: Finding Persistence Mechanisms **Question:** "What are the persistence mechanisms?" **Claude's Response:** ![Persistence Analysis](screenshots/persistence.png) Claude automatically: - Called the `answer_forensic_question` tool - Ran both SOFTWARE and NTUSER persistence plugins - Found VMware Tools, Adobe ARM, McAfee components - Flagged suspicious entry: `svchost` in an unusual location - Organized results by system-wide vs user-specific ### Example 3: Recent Software and Boot Time **Question:** "What is the most recent software installed on the machine and when was it last booted?" **Claude's Response:** ![Recent Software](screenshots/installed_software.png) Claude automatically: - Ran `installed_programs_software` plugin - Ran `shutdown` plugin - Found Adobe Reader X (10.1.0) was most recent (Aug 28, 2011) - Showed full software timeline - Reported last shutdown: April 4, 2012 ## What You Can Ask **System Information:** - "What is the hostname?" - "What is the timezone?" - "What is the Windows version?" **Security & Forensics:** - "What are the persistence mechanisms?" - "What user accounts exist?" - "What services are configured?" **User Activity:** - "What USB devices were connected?" - "What programs are installed?" - "What files were recently accessed?" **Investigation:** - "What programs have been executed?" - "Show me the network configuration" - "When was the system last booted?" ## How It Works 1. **Auto-discovers** registry hives from a directory 2. **Auto-loads** 75+ regipy plugins 3. **Maps** natural language questions to the right plugins 4. **Returns** structured forensic data to Claude ## Files - `server.py` - Main MCP server (530+ lines) - `README.md` - This file - `claude_desktop_config.example.json` - Configuration template - `test_local.py` - Test the server without Claude Desktop - `screenshots/` - Demo screenshots from Claude Desktop ## Requirements - Python 3.8+ (conda environment recommended) - regipy >= 6.0.0 - mcp >= 1.0.0 - Claude Desktop app - Windows registry hive files ## Configuration The server accepts the hives directory via: 1. **Command-line argument** (recommended): `--hives-dir C:\path\to\hives` 2. **Environment variable**: `REGIPY_HIVE_DIRECTORY=C:\path\to\hives` 3. **MCP tool**: Call `set_hive_directory` from Claude at runtime ## Supported Hive Types - **SYSTEM** - Computer name, timezone, services, USB devices, network config (28 plugins) - **SOFTWARE** - Windows version, installed programs, persistence (21 plugins) - **NTUSER** - User activity, recent docs, typed URLs, UserAssist (21 plugins) - **SAM** - User accounts, password hashes (2 plugins) - **SECURITY** - Domain SID (1 plugin) - **AMCACHE** - Application execution history (1 plugin) - **USRCLASS** - Shell bags (1 plugin) - **BCD** - Boot configuration (1 plugin) Total: **75+ plugins** for comprehensive forensic analysis ## Architecture ``` Claude Desktop ↓ (MCP Protocol) Regipy MCP Server ↓ (Auto-discovers) Registry Hives → 75 Plugins → Structured Results ↓ Claude presents findings in natural language ``` ## Troubleshooting ### Server not connecting? 1. Run `test_local.py` to verify the server works 2. Check config file location: `%APPDATA%\Claude\claude_desktop_config.json` 3. Ensure you completely quit Claude Desktop (not just closed window) 4. See `docs/WINDOWS_SETUP.md` for detailed troubleshooting ### No hives loaded? 1. Verify hive directory exists and contains hive files 2. Check paths use double backslashes in JSON: `C:\\path\\to\\hives` 3. Make sure hive files aren't corrupted ## License Uses the regipy library for registry parsing. Designed for authorized forensic analysis only. ## Credits - Built on [regipy](https://github.com/mkorman90/regipy) by Martin Korman - Uses [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic - Designed for [Claude Desktop](https://claude.ai/download) ================================================ FILE: regipy_mcp_server/claude_desktop_config.example.json ================================================ { "mcpServers": { "regipy": { "command": "C:\\Users\\marti\\anaconda3\\envs\\regipy\\python.exe", "args": [ "c:\\Users\\marti\\Documents\\GitHub\\regipy\\regipy_mcp_server\\server.py", "--hives-dir", "C:\\Users\\marti\\Downloads\\hives" ] } } } ================================================ FILE: regipy_mcp_server/server.py ================================================ #!/usr/bin/env python3 """ Regipy MCP Server - Windows Registry Analysis for Claude Desktop This server enables Claude to analyze Windows registry hives and answer forensic questions about system configuration, user activity, and persistence. """ import logging import os from datetime import datetime from pathlib import Path from typing import Optional from mcp.server.fastmcp import FastMCP # Import all plugins so they auto-register via __init_subclass__ import regipy.plugins # noqa from regipy.plugins.plugin import PLUGINS from regipy.plugins.utils import run_relevant_plugins from regipy.registry import RegistryHive # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Initialize MCP server mcp = FastMCP("regipy-forensics") # Global state for loaded hives _loaded_hives: dict[str, RegistryHive] = {} _hive_directory: Optional[str] = None def _load_hives_from_directory(directory: str) -> dict[str, RegistryHive]: """ Auto-discover and load all registry hives from a directory. Skips transaction logs (.log, .log1, .log2) and only loads actual hive files. Returns a dict mapping file paths to RegistryHive objects. """ hives = {} directory_path = Path(directory) if not directory_path.exists(): logger.error(f"Hive directory does not exist: {directory}") return hives # Common registry hive file patterns skip_extensions = {".log", ".log1", ".log2", ".sav", ".regtrans-ms", ".blf", ".tmp"} for file_path in directory_path.iterdir(): if not file_path.is_file(): continue # Skip transaction logs and backup files if file_path.suffix.lower() in skip_extensions: continue try: logger.info(f"Attempting to load hive: {file_path}") hive = RegistryHive(str(file_path)) hives[str(file_path)] = hive logger.info(f"Successfully loaded {file_path.name} as {hive.hive_type}") except Exception as e: logger.warning(f"Failed to load {file_path}: {e}") continue return hives def _initialize_hives(hive_dir: Optional[str] = None): """ Initialize hives from the specified directory or environment variable. Priority: 1. Command-line argument (hive_dir parameter) 2. REGIPY_HIVE_DIRECTORY environment variable 3. If neither is set, hives must be loaded manually via set_hive_directory tool """ global _loaded_hives, _hive_directory # Try parameter first, then environment variable _hive_directory = hive_dir or os.getenv("REGIPY_HIVE_DIRECTORY") if not _hive_directory: logger.warning("No hive directory specified. Use set_hive_directory tool or pass --hives-dir argument.") return logger.info(f"Loading hives from: {_hive_directory}") _loaded_hives = _load_hives_from_directory(_hive_directory) logger.info(f"Loaded {len(_loaded_hives)} hive(s)") def _get_hives_by_type(hive_type: str) -> list[tuple[str, RegistryHive]]: """Get all loaded hives of a specific type.""" return [(path, hive) for path, hive in _loaded_hives.items() if hive.hive_type == hive_type] def _serialize_datetime(obj): """Convert datetime objects to ISO format strings.""" if isinstance(obj, datetime): return obj.isoformat() return obj def _serialize_plugin_results(results): """Recursively serialize plugin results, converting datetimes to strings.""" if isinstance(results, dict): return {k: _serialize_plugin_results(v) for k, v in results.items()} elif isinstance(results, list): return [_serialize_plugin_results(item) for item in results] elif isinstance(results, datetime): return results.isoformat() else: return results @mcp.tool() def set_hive_directory(directory: str) -> str: """ Set the directory containing registry hives and load them. Args: directory: Path to directory containing registry hive files Returns: Summary of loaded hives """ global _loaded_hives, _hive_directory _hive_directory = directory _loaded_hives = _load_hives_from_directory(directory) if not _loaded_hives: return f"No valid registry hives found in {directory}" # Group by hive type by_type = {} for path, hive in _loaded_hives.items(): hive_type = hive.hive_type or "unknown" if hive_type not in by_type: by_type[hive_type] = [] by_type[hive_type].append(Path(path).name) result = [f"Loaded {len(_loaded_hives)} hive(s) from {directory}:\n"] for hive_type, files in sorted(by_type.items()): result.append(f"\n{hive_type}:") for file in files: result.append(f" - {file}") return "\n".join(result) @mcp.tool() def list_available_hives() -> str: """ List all currently loaded registry hives and their types. Returns: Summary of available hives grouped by type """ if not _loaded_hives: return "No hives loaded. Use set_hive_directory to load hives." by_type = {} for path, hive in _loaded_hives.items(): hive_type = hive.hive_type or "unknown" if hive_type not in by_type: by_type[hive_type] = [] by_type[hive_type].append( {"filename": Path(path).name, "path": path, "root_key": hive.root.name if hive.root else "N/A"} ) result = [f"Available hives ({len(_loaded_hives)} total):\n"] for hive_type, hives in sorted(by_type.items()): result.append(f"\n{hive_type} ({len(hives)} file(s)):") for hive_info in hives: result.append(f" - {hive_info['filename']}") return "\n".join(result) @mcp.tool() def list_available_plugins(hive_type: Optional[str] = None) -> str: """ List all available regipy plugins, optionally filtered by hive type. Shows which plugins can run based on currently loaded hives. Args: hive_type: Optional hive type to filter by (e.g., 'SYSTEM', 'SOFTWARE', 'NTUSER') Returns: List of plugins with descriptions and compatibility info """ # Get loaded hive types loaded_types = {hive.hive_type for hive in _loaded_hives.values()} # Group plugins by hive type plugins_by_type = {} for plugin_class in PLUGINS: compat_hive = plugin_class.COMPATIBLE_HIVE or "unknown" # Filter by requested hive type if specified if hive_type and compat_hive != hive_type: continue if compat_hive not in plugins_by_type: plugins_by_type[compat_hive] = [] can_run = compat_hive in loaded_types plugins_by_type[compat_hive].append( {"name": plugin_class.NAME, "description": plugin_class.DESCRIPTION or "No description", "can_run": can_run} ) # Format output result = [] total_plugins = sum(len(plugins) for plugins in plugins_by_type.values()) runnable = sum(1 for plugins in plugins_by_type.values() for p in plugins if p["can_run"]) result.append(f"Total plugins: {total_plugins} ({runnable} can run with loaded hives)\n") for hive_type_name, plugins in sorted(plugins_by_type.items()): can_run_count = sum(1 for p in plugins if p["can_run"]) status = "✓ AVAILABLE" if can_run_count > 0 else "✗ No hive loaded" result.append(f"\n{hive_type_name} ({len(plugins)} plugins) - {status}:") for plugin in sorted(plugins, key=lambda x: x["name"]): status_icon = "✓" if plugin["can_run"] else "✗" result.append(f" {status_icon} {plugin['name']}") result.append(f" {plugin['description']}") return "\n".join(result) @mcp.tool() def run_plugin(plugin_name: str) -> dict: """ Run a specific regipy plugin on the appropriate hive. The plugin will automatically select the correct hive based on its COMPATIBLE_HIVE setting. For example, 'computer_name' will run on the SYSTEM hive, 'typed_urls' will run on NTUSER hives. Args: plugin_name: Name of the plugin to run (e.g., 'computer_name', 'timezone_data') Returns: Plugin results as a dictionary """ if not _loaded_hives: return {"error": "No hives loaded. Use set_hive_directory first."} # Find the plugin class plugin_class = None for p in PLUGINS: if plugin_name == p.NAME: plugin_class = p break if not plugin_class: available = [p.NAME for p in PLUGINS] return {"error": f"Plugin '{plugin_name}' not found", "available_plugins": sorted(available)} # Get hives of the compatible type compatible_hives = _get_hives_by_type(plugin_class.COMPATIBLE_HIVE) if not compatible_hives: return {"error": f"No {plugin_class.COMPATIBLE_HIVE} hive loaded", "required_hive_type": plugin_class.COMPATIBLE_HIVE} # Run plugin on all compatible hives all_results = {} for path, hive in compatible_hives: try: plugin = plugin_class(hive, as_json=True) if plugin.can_run(): plugin.run() results = _serialize_plugin_results(plugin.entries) all_results[Path(path).name] = results except Exception as e: logger.error(f"Error running {plugin_name} on {path}: {e}") all_results[Path(path).name] = {"error": str(e)} return { "plugin": plugin_name, "description": plugin_class.DESCRIPTION, "hive_type": plugin_class.COMPATIBLE_HIVE, "results": all_results, } @mcp.tool() def run_all_plugins_for_hive(hive_type: str) -> dict: """ Run all compatible plugins for a specific hive type. Args: hive_type: Type of hive (e.g., 'SYSTEM', 'SOFTWARE', 'NTUSER', 'SAM') Returns: Combined results from all plugins """ if not _loaded_hives: return {"error": "No hives loaded. Use set_hive_directory first."} compatible_hives = _get_hives_by_type(hive_type) if not compatible_hives: available_types = {h.hive_type for h in _loaded_hives.values()} return {"error": f"No {hive_type} hive loaded", "available_types": sorted(available_types)} # Run all plugins on all compatible hives all_results = {} for path, hive in compatible_hives: try: results = run_relevant_plugins(hive, as_json=True) serialized_results = _serialize_plugin_results(results) all_results[Path(path).name] = serialized_results except Exception as e: logger.error(f"Error running plugins on {path}: {e}") all_results[Path(path).name] = {"error": str(e)} return {"hive_type": hive_type, "files_analyzed": len(all_results), "results": all_results} @mcp.tool() def list_relevant_plugins(question: str) -> dict: """ List plugins relevant to a forensic question. Returns a list of available plugins with their descriptions that could help answer the given question. Use this to discover which plugins to run, then call run_plugin() for each one. Args: question: Natural language forensic question Returns: Dict with available plugins and their descriptions """ if not _loaded_hives: return {"error": "No hives loaded. Use set_hive_directory first."} # Get loaded hive types loaded_types = {hive.hive_type for hive in _loaded_hives.values()} # Build list of available plugins with their descriptions available_plugins = [] for plugin_class in PLUGINS: if plugin_class.COMPATIBLE_HIVE not in loaded_types: continue available_plugins.append( { "name": plugin_class.NAME, "description": plugin_class.DESCRIPTION or "No description", "hive_type": plugin_class.COMPATIBLE_HIVE, } ) return { "question": question, "available_plugins": available_plugins, "total_count": len(available_plugins), "instruction": "Based on the question, select relevant plugin(s) and call run_plugin() for each one.", } @mcp.tool() def get_registry_key(key_path: str, hive_type: Optional[str] = None) -> dict: """ Get a specific registry key and its values. Args: key_path: Path to the registry key (e.g., 'ControlSet001\\Control\\TimeZoneInformation') hive_type: Optional hive type to search in. If not specified, searches all loaded hives. Returns: Key information including values and subkeys """ if not _loaded_hives: return {"error": "No hives loaded. Use set_hive_directory first."} # Determine which hives to search if hive_type: hives_to_search = _get_hives_by_type(hive_type) else: hives_to_search = list(_loaded_hives.items()) if not hives_to_search: return {"error": "No hives available for search"} results = {} for path, hive in hives_to_search: try: subkey = hive.get_key(key_path) if subkey: results[Path(path).name] = { "found": True, "path": subkey.path, "timestamp": _serialize_datetime(subkey.timestamp), "values": [{"name": v.name, "value": v.value, "type": v.value_type} for v in (subkey.values or [])], "subkey_count": len(list(subkey.iter_subkeys())) if hasattr(subkey, "iter_subkeys") else 0, } except Exception as e: results[Path(path).name] = {"found": False, "error": str(e)} if not any(r.get("found") for r in results.values()): return {"key_path": key_path, "found": False, "searched_hives": [Path(p).name for p, _ in hives_to_search]} return {"key_path": key_path, "results": results} if __name__ == "__main__": import argparse # Parse command-line arguments parser = argparse.ArgumentParser(description="Regipy MCP Server - Windows Registry Analysis") parser.add_argument( "--hives-dir", type=str, help="Directory containing registry hive files (default: from REGIPY_HIVE_DIRECTORY env var)" ) args = parser.parse_args() # Initialize hives with command-line argument or environment variable _initialize_hives(args.hives_dir) # Run the MCP server mcp.run() else: # When imported as a module, initialize from environment variable _initialize_hives() ================================================ FILE: regipy_mcp_server/test_local.py ================================================ #!/usr/bin/env python3 """ Local test script for Regipy MCP Server Tests the server functions directly without Claude Desktop """ import io import os import sys # Fix Windows console encoding if sys.platform == "win32": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") # Set the hive directory os.environ["REGIPY_HIVE_DIRECTORY"] = r"C:\Users\marti\Downloads\hives" # Add server to path sys.path.insert(0, os.path.dirname(__file__)) print("=" * 70) print("Regipy MCP Server - Local Test") print("=" * 70) # Import after setting environment variable from server import ( # noqa: E402 PLUGINS, _initialize_hives, _loaded_hives, answer_forensic_question, list_available_hives, list_available_plugins, run_plugin, ) print("\n✓ Server imported successfully") print(f"✓ Discovered {len(PLUGINS)} plugins") # Initialize hives print("\n📂 Loading hives from: C:\\Users\\marti\\Downloads\\hives") _initialize_hives() if _loaded_hives: print(f"✓ Loaded {len(_loaded_hives)} hive file(s)") print("\n" + "=" * 70) print("TEST 1: List Available Hives") print("=" * 70) result = list_available_hives() print(result) print("\n" + "=" * 70) print("TEST 2: List Available Plugins (SYSTEM hive)") print("=" * 70) result = list_available_plugins(hive_type="SYSTEM") print(result[:500] + "..." if len(result) > 500 else result) print("\n" + "=" * 70) print("TEST 3: Run Computer Name Plugin") print("=" * 70) result = run_plugin("computer_name") print(result) print("\n" + "=" * 70) print("TEST 4: Answer Forensic Question - Hostname") print("=" * 70) result = answer_forensic_question("What is the hostname?") print(result) print("\n" + "=" * 70) print("TEST 5: Answer Forensic Question - Timezone") print("=" * 70) result = answer_forensic_question("What is the timezone?") print(result) print("\n" + "=" * 70) print("✅ All tests completed!") print("=" * 70) print("\nNext steps:") print("1. Configure Claude Desktop with the config from claude_desktop_config.example.json") print("2. Restart Claude Desktop") print("3. Ask Claude: 'What registry hives are available?'") else: print("\n❌ No hives loaded!") print("\nTroubleshooting:") print("1. Check that C:\\Users\\marti\\Downloads\\hives exists") print("2. Verify it contains registry hive files (SYSTEM, SOFTWARE, NTUSER.DAT, etc.)") print("3. Make sure files aren't corrupted") ================================================ FILE: regipy_tests/__init__.py ================================================ # flake8: noqa from .validation.validation_tests import shimcache_validation from .validation.validation_tests import ntuser_persistence_validation from .validation.validation_tests import software_persistence_validation from .validation.validation_tests import ntuser_userassist_validation from .validation.validation_tests import amcache_validation from .validation.validation_tests import bam_validation from .validation.validation_tests import word_wheel_query_ntuser_validation from .validation.validation_tests import computer_name_plugin_validation from .validation.validation_tests import uac_status_plugin_validation from .validation.validation_tests import software_classes_installer_plugin_validation from .validation.validation_tests import ntuser_classes_installer_plugin_validation from .validation.validation_tests import ras_tracing_plugin_validation from .validation.validation_tests import installed_programs_software_plugin_validation from .validation.validation_tests import last_logon_plugin_validation from .validation.validation_tests import typed_urls_plugin_validation from .validation.validation_tests import profile_list_plugin_validation from .validation.validation_tests import print_demon_plugin_validation from .validation.validation_tests import services_plugin_validation from .validation.validation_tests import local_sid_plugin_validation from .validation.validation_tests import boot_key_plugin_validation from .validation.validation_tests import host_domain_name_plugin_validation from .validation.validation_tests import domain_sid_plugin_validation from .validation.validation_tests import boot_entry_list_plugin_validation from .validation.validation_tests import wdigest_plugin_validation from .validation.validation_tests import winrar_plugin_validation from .validation.validation_tests import network_drives_plugin_validation from .validation.validation_tests import winscp_saved_sessions_plugin_validation from .validation.validation_tests import usbstor_plugin_validation from .validation.validation_tests import typed_paths_plugin_validation from .validation.validation_tests import shell_bag_ntuser_plugin_validation from .validation.validation_tests import shell_bag_usrclass_plugin_validation from .validation.validation_tests import network_data_plugin_validation from .validation.validation_tests import active_control_set_validation from .validation.validation_tests import backuprestore_plugin_validation from .validation.validation_tests import codepage_validation from .validation.validation_tests import crash_dump_validation from .validation.validation_tests import diag_sr_validation from .validation.validation_tests import disable_last_access_validation from .validation.validation_tests import disablesr_plugin_validation from .validation.validation_tests import image_file_execution_options_validation from .validation.validation_tests import installed_programs_ntuser_validation from .validation.validation_tests import previous_winver_plugin_validation from .validation.validation_tests import processor_architecture_validation from .validation.validation_tests import routes_validation from .validation.validation_tests import safeboot_configuration_validation from .validation.validation_tests import shutdown_validation from .validation.validation_tests import susclient_plugin_validation from .validation.validation_tests import spp_clients_plugin_validation from .validation.validation_tests import shutdown_validation from .validation.validation_tests import terminal_services_history_validation from .validation.validation_tests import timezone_data_validation from .validation.validation_tests import timezone_data2_validation from .validation.validation_tests import winrar_plugin_validation from .validation.validation_tests import winver_plugin_validation from .validation.validation_tests import wsl_plugin_validation from .validation.validation_tests import usb_devices_plugin_validation from .validation.validation_tests import mounted_devices_plugin_validation from .validation.validation_tests import pagefile_plugin_validation from .validation.validation_tests import lsa_packages_plugin_validation from .validation.validation_tests import app_paths_plugin_validation from .validation.validation_tests import networklist_plugin_validation from .validation.validation_tests import execution_policy_plugin_validation from .validation.validation_tests import windows_defender_plugin_validation from .validation.validation_tests import samparse_plugin_validation ================================================ FILE: regipy_tests/cli_tests.py ================================================ import json from tempfile import mktemp from click.testing import CliRunner from regipy.cli import parse_header, registry_dump, run_plugins def test_cli_registry_parse_header(ntuser_hive): runner = CliRunner() result = runner.invoke(parse_header, [ntuser_hive]) assert result.exit_code == 0 assert len(result.output.splitlines()) == 27 def test_cli_registry_dump(ntuser_hive): runner = CliRunner() start_date = "2012-04-03T00:00:00.000000" end_date = "2012-04-03T23:59:59.999999" output_file_path = mktemp() result = runner.invoke( registry_dump, [ntuser_hive, "-d", "-s", start_date, "-e", end_date, "-o", output_file_path], ) assert result.exit_code == 0 with open(output_file_path) as f: output = f.readlines() assert json.loads(output[0]) == { "subkey_name": ".Default", "path": "\\AppEvents\\EventLabels\\.Default", "timestamp": "2012-04-03T21:19:54.733216+00:00", "values_count": 2, "values": [], "actual_path": None, } assert json.loads(output[-1]) == { "subkey_name": "System", "path": "\\System", "timestamp": "2012-04-03T21:19:54.847482+00:00", "values_count": 0, "values": [], "actual_path": None, } # The output file contains one extra line, because of line breaks assert len(output) == 1490 assert result.output.strip().endswith("(1489 subkeys enumerated)") def test_cli_run_plugins(ntuser_hive): runner = CliRunner() output_file_path = mktemp() result = runner.invoke(run_plugins, [ntuser_hive, "-o", output_file_path]) assert result.exit_code == 0 assert result.output.strip() == "Loaded 75 plugins\nFinished: 13/75 plugins matched the hive type" with open(output_file_path) as f: output = json.loads(f.read()) assert set(output.keys()) == { "installed_programs_ntuser", "network_drives_plugin", "ntuser_classes_installer", "ntuser_persistence", "ntuser_shellbag_plugin", "terminal_services_history", "typed_paths", "typed_urls", "user_assist", "winrar_plugin", "winscp_saved_sessions", "word_wheel_query", "wsl", } ================================================ FILE: regipy_tests/conftest.py ================================================ import lzma import os from pathlib import Path from tempfile import mktemp import pytest def extract_lzma(path): tempfile_path = mktemp() with open(tempfile_path, "wb") as tmp, lzma.open(path) as f: tmp.write(f.read()) return tempfile_path @pytest.fixture() def temp_output_file(): tempfile_path = mktemp() yield tempfile_path os.remove(tempfile_path) @pytest.fixture(scope="module") def test_data_dir(): return str(Path(__file__).parent.joinpath("data")) @pytest.fixture(scope="module") def ntuser_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "NTUSER.DAT.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def software_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SOFTWARE.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def system_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def sam_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SAM.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def security_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SECURITY.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def amcache_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "amcache.hve.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def bcd_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "BCD.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def second_hive_path(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "NTUSER_modified.DAT.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def transaction_ntuser(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "transactions_NTUSER.DAT.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def transaction_log(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "transactions_ntuser.dat.log1.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def transaction_system(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM_B.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def system_tr_log_1(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM_B.LOG1.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def system_tr_log_2(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM_B.LOG2.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def transaction_usrclass(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "UsrClass.dat.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def usrclass_tr_log_1(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "UsrClass.dat.LOG1.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def usrclass_tr_log_2(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "UsrClass.dat.LOG2.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def ntuser_software_partial(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "ntuser_software_partial.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def corrupted_system_hive(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "corrupted_system_hive.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def system_devprop(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM_2.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def ntuser_hive_2(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "NTUSER_with_winscp.DAT.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def shellbags_ntuser(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "NTUSER_BAGMRU.DAT.xz")) yield temp_path os.remove(temp_path) @pytest.fixture(scope="module") def system_hive_with_filetime(test_data_dir): temp_path = extract_lzma(os.path.join(test_data_dir, "SYSTEM_WIN_10_1709.xz")) yield temp_path os.remove(temp_path) ================================================ FILE: regipy_tests/profiling.py ================================================ # flake8: noqa import cProfile import io import lzma import os import pstats from contextlib import contextmanager from pathlib import Path from pstats import SortKey from tempfile import mktemp import logging from regipy.registry import RegistryHive logger = logging.getLogger(__name__) @contextmanager def profiling(): pr = cProfile.Profile() pr.enable() yield pr.disable() s = io.StringIO() sortby = SortKey.CUMULATIVE ps = pstats.Stats(pr, stream=s).sort_stats(sortby) ps.print_stats() print(s.getvalue()) @contextmanager def get_file_from_tests(file_name): path = str(Path(__file__).parent.joinpath("data").joinpath(file_name)) tempfile_path = mktemp() with open(tempfile_path, "wb") as tmp: with lzma.open(path) as f: tmp.write(f.read()) yield tempfile_path os.remove(tempfile_path) registry_path = "SYSTEM_2.xz" print(f"Iterating over all subkeys in {registry_path}") with profiling(): with get_file_from_tests(registry_path) as reg: registry_hive = RegistryHive(reg) keys = [x for x in registry_hive.recurse_subkeys(fetch_values=False)] print(f"Done.") """ === Fetching subkey values: Iterating over all subkeys in SYSTEM_2.xz 6681282 function calls (6548808 primitive calls) in 21.406 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.009 0.009 21.440 21.440 /Users/martin/Projects/regipy/regipy_tests/profiling.py:47() 115672/20016 0.326 0.000 21.431 0.001 /Users/martin/Projects/regipy/regipy/registry.py:126(recurse_subkeys) 66844 1.067 0.000 19.524 0.000 /Users/martin/Projects/regipy/regipy/registry.py:413(iter_values) 49779 0.280 0.000 10.845 0.000 /Users/martin/Projects/regipy/regipy/registry.py:378(read_value) 1101844 10.603 0.000 10.603 0.000 {method 'read' of '_io.BytesIO' objects} 30464 0.357 0.000 5.337 0.000 /Users/martin/Projects/regipy/regipy/utils.py:149(try_decode_binary) 116294 3.587 0.000 5.021 0.000 {method 'decode' of 'bytes' objects} 150968 0.252 0.000 2.785 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:290(parse_stream) 187671/150968 0.117 0.000 2.359 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:311(_parsereport) 74151 0.051 0.000 1.779 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:786(_parse) 42243 0.024 0.000 1.434 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/encodings/utf_16_le.py:15(decode) 24363 0.039 0.000 1.413 0.000 /Users/martin/Projects/regipy/regipy/registry.py:322(iter_subkeys) 42243 1.410 0.000 1.410 0.000 {built-in method _codecs.utf_16_le_decode} 24367 0.116 0.000 1.333 0.000 /Users/martin/Projects/regipy/regipy/registry.py:350(_parse_subkeys) 49779 0.026 0.000 0.943 0.000 :79(parseall) 49779 0.579 0.000 0.917 0.000 :26(parse_struct_1) 20015 0.127 0.000 0.837 0.000 /Users/martin/Projects/regipy/regipy/registry.py:292(__init__) 20015 0.012 0.000 0.584 0.000 :124(parseall) 20015 0.378 0.000 0.572 0.000 :23(parse_struct_1) === Without fetching subkeys: Iterating over all subkeys in SYSTEM_2.xz 2068855 function calls (1973084 primitive calls) in 5.288 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.037 0.037 4.969 4.969 /Users/martin/Projects/regipy/regipy_tests/profiling.py:47() 115672/20016 0.462 0.000 4.932 0.000 /Users/martin/Projects/regipy/regipy/registry.py:126(recurse_subkeys) 24363 0.097 0.000 4.104 0.000 /Users/martin/Projects/regipy/regipy/registry.py:324(iter_subkeys) 24367 0.312 0.000 3.900 0.000 /Users/martin/Projects/regipy/regipy/registry.py:352(_parse_subkeys) 48737 0.322 0.000 3.121 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:290(parse_stream) 48737 0.119 0.000 2.640 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:311(_parsereport) 20015 0.343 0.000 2.370 0.000 /Users/martin/Projects/regipy/regipy/registry.py:294(__init__) 24372 0.038 0.000 2.334 0.000 /opt/anaconda3/envs/regipy/lib/python3.9/site-packages/construct/core.py:786(_parse) 20015 0.033 0.000 1.683 0.000 :124(parseall) 20015 1.123 0.000 1.650 0.000 :23(parse_struct_1) 4353 0.045 0.000 0.610 0.000 :76(parseall) """ ================================================ FILE: regipy_tests/test_packaging.py ================================================ """ Tests to verify package configuration and prevent packaging issues. These tests ensure that all Python packages are correctly declared in pyproject.toml, preventing ImportError when installed from PyPI. """ import pathlib import sys if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib def test_all_packages_included_in_pyproject(): """ Verify all Python packages with __init__.py are listed in pyproject.toml. This catches the case where a new subpackage is added but not included in the packages list, which would cause ImportError when installed from PyPI. """ # Find all packages (directories with __init__.py) regipy_root = pathlib.Path(__file__).parent.parent / "regipy" actual_packages = set() for init_file in regipy_root.rglob("__init__.py"): package_dir = init_file.parent # Convert path to dotted package name relative = package_dir.relative_to(regipy_root.parent) package_name = str(relative).replace("/", ".").replace("\\", ".") actual_packages.add(package_name) # Read packages from pyproject.toml pyproject_path = pathlib.Path(__file__).parent.parent / "pyproject.toml" with open(pyproject_path, "rb") as f: pyproject = tomllib.load(f) declared_packages = set(pyproject["tool"]["setuptools"]["packages"]) # Check for missing packages missing = actual_packages - declared_packages assert not missing, f"Packages missing from pyproject.toml: {missing}" # Check for extra packages (declared but don't exist) extra = declared_packages - actual_packages assert not extra, f"Packages in pyproject.toml but don't exist: {extra}" def test_all_packages_importable(): """Test that all declared packages can be imported.""" import importlib pyproject_path = pathlib.Path(__file__).parent.parent / "pyproject.toml" with open(pyproject_path, "rb") as f: pyproject = tomllib.load(f) packages = pyproject["tool"]["setuptools"]["packages"] for package in packages: try: importlib.import_module(package) except ImportError as e: raise AssertionError(f"Failed to import {package}: {e}") from e ================================================ FILE: regipy_tests/test_utils.py ================================================ """Unit tests for regipy.plugins.utils module""" from unittest.mock import MagicMock from regipy.plugins.utils import extract_values def _make_mock_value(name, value): """Helper to create a mock registry value""" mock = MagicMock() mock.name = name mock.value = value return mock def _make_mock_key(values): """Helper to create a mock registry key with values""" mock = MagicMock() mock.iter_values.return_value = [_make_mock_value(name, val) for name, val in values] return mock class TestExtractValues: """Tests for extract_values utility function""" def test_simple_rename(self): """Test simple string rename mapping""" key = _make_mock_key([("ProfileName", "MyNetwork")]) entry = {} extract_values(key, {"ProfileName": "profile_name"}, entry) assert entry == {"profile_name": "MyNetwork"} def test_multiple_simple_renames(self): """Test multiple string rename mappings""" key = _make_mock_key( [ ("ProfileName", "MyNetwork"), ("Description", "Home WiFi"), ] ) entry = {} extract_values( key, { "ProfileName": "profile_name", "Description": "description", }, entry, ) assert entry == { "profile_name": "MyNetwork", "description": "Home WiFi", } def test_callable_converter(self): """Test tuple with callable converter""" key = _make_mock_key([("Enabled", 1)]) entry = {} extract_values( key, { "Enabled": ("enabled", lambda v: v == 1), }, entry, ) assert entry == {"enabled": True} def test_callable_converter_false(self): """Test callable converter returning False""" key = _make_mock_key([("Enabled", 0)]) entry = {} extract_values( key, { "Enabled": ("enabled", lambda v: v == 1), }, entry, ) assert entry == {"enabled": False} def test_lookup_converter(self): """Test callable that performs lookup""" categories = {0: "Public", 1: "Private", 2: "Domain"} key = _make_mock_key([("Category", 1)]) entry = {} extract_values( key, { "Category": ("category", lambda v: categories.get(v, f"Unknown ({v})")), }, entry, ) assert entry == {"category": "Private"} def test_lookup_converter_unknown(self): """Test lookup converter with unknown value""" categories = {0: "Public", 1: "Private", 2: "Domain"} key = _make_mock_key([("Category", 99)]) entry = {} extract_values( key, { "Category": ("category", lambda v: categories.get(v, f"Unknown ({v})")), }, entry, ) assert entry == {"category": "Unknown (99)"} def test_unmapped_values_ignored(self): """Test that values not in value_map are ignored""" key = _make_mock_key( [ ("ProfileName", "MyNetwork"), ("UnknownField", "SomeValue"), ] ) entry = {} extract_values(key, {"ProfileName": "profile_name"}, entry) assert entry == {"profile_name": "MyNetwork"} assert "UnknownField" not in entry def test_preserves_existing_entry_values(self): """Test that existing entry values are preserved""" key = _make_mock_key([("ProfileName", "MyNetwork")]) entry = {"type": "profile", "key_path": "/some/path"} extract_values(key, {"ProfileName": "profile_name"}, entry) assert entry == { "type": "profile", "key_path": "/some/path", "profile_name": "MyNetwork", } def test_empty_value_map(self): """Test with empty value_map""" key = _make_mock_key([("ProfileName", "MyNetwork")]) entry = {} extract_values(key, {}, entry) assert entry == {} def test_empty_registry_key(self): """Test with registry key that has no values""" key = _make_mock_key([]) entry = {} extract_values(key, {"ProfileName": "profile_name"}, entry) assert entry == {} def test_mixed_simple_and_callable(self): """Test mixing simple rename and callable converters""" key = _make_mock_key( [ ("ProfileName", "MyNetwork"), ("Enabled", 1), ("Category", 2), ] ) entry = {} extract_values( key, { "ProfileName": "profile_name", "Enabled": ("enabled", lambda v: v != 0), "Category": ("category", lambda v: {0: "Public", 1: "Private", 2: "Domain"}.get(v)), }, entry, ) assert entry == { "profile_name": "MyNetwork", "enabled": True, "category": "Domain", } def test_converter_with_bytes(self): """Test converter that handles bytes (like MAC address)""" def format_mac(val): if isinstance(val, bytes) and len(val) == 6: return ":".join(f"{b:02X}" for b in val) return val key = _make_mock_key([("MacAddress", b"\x00\x1a\x2b\x3c\x4d\x5e")]) entry = {} extract_values( key, { "MacAddress": ("mac_address", format_mac), }, entry, ) assert entry == {"mac_address": "00:1A:2B:3C:4D:5E"} def test_converter_returns_none(self): """Test converter that returns None""" key = _make_mock_key([("DateCreated", b"\x00")]) entry = {} extract_values( key, { "DateCreated": ("date_created", lambda v: None if len(v) < 16 else "parsed"), }, entry, ) assert entry == {"date_created": None} def test_converter_with_integer_values(self): """Test various integer value conversions""" key = _make_mock_key( [ ("Bias", -300), ("DaylightBias", -60), ] ) entry = {} extract_values( key, { "Bias": ("bias_minutes", lambda v: v), "DaylightBias": ("daylight_bias_minutes", lambda v: v), }, entry, ) assert entry == { "bias_minutes": -300, "daylight_bias_minutes": -60, } ================================================ FILE: regipy_tests/tests.py ================================================ import json import os from tempfile import mkdtemp from regipy import NoRegistrySubkeysException from regipy.cli_utils import get_filtered_subkeys from regipy.hive_types import NTUSER_HIVE_TYPE from regipy.plugins.utils import dump_hive_to_json from regipy.recovery import apply_transaction_logs from regipy.regdiff import compare_hives from regipy.registry import NKRecord, RegistryHive def test_parse_header(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) assert isinstance(registry_hive, RegistryHive) assert registry_hive.header.primary_sequence_num == 749 assert registry_hive.header.secondary_sequence_num == 749 assert registry_hive.header.last_modification_time == 129782982453388850 assert registry_hive.header.major_version == 1 assert registry_hive.header.minor_version == 3 assert registry_hive.header.root_key_offset == 32 assert registry_hive.header.hive_bins_data_size == 733184 assert registry_hive.header.minor_version == 3 assert registry_hive.header.file_name == "?\\C:\\Users\\vibranium\\ntuser.dat" assert registry_hive.header.checksum == 476614345 def test_parse_root_key(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) assert isinstance(registry_hive, RegistryHive) assert isinstance(registry_hive.root, NKRecord) assert registry_hive.root.name == "CMI-CreateHive{6A1C4018-979D-4291-A7DC-7AED1C75B67C}" assert registry_hive.root.subkey_count == 11 assert dict(registry_hive.root.header) == { "access_bits": b"\x02\x00\x00\x00", "class_name_offset": 4294967295, "class_name_size": 0, "flags": { "KEY_COMP_NAME": True, "KEY_HIVE_ENTRY": True, "KEY_HIVE_EXIT": False, "KEY_NO_DELETE": True, "KEY_PREDEF_HANDLE": False, "KEY_SYM_LINK": False, "KEY_VOLATILE": False, }, "key_name_size": 52, "key_name_string": b"CMI-CreateHive{6A1C4018-979D-4291-A7DC-7AED1C75B67C}", "largest_sk_class_name": 0, "largest_sk_name": 40, "largest_value_name": 0, "last_modified": 129780243434537497, "largest_value_data": 0, "parent_key_offset": 1968, "security_key_offset": 1376, "subkey_count": 11, "subkeys_list_offset": 73760, "values_count": 0, "values_list_offset": 4294967295, "volatile_subkey_count": 0, "volatile_subkeys_list_offset": 4294967295, } def test_find_keys_ntuser(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) run_key = registry_hive.get_key(r"\Software\Microsoft\Windows\CurrentVersion\Run") assert run_key.name == "Run" assert run_key.header.last_modified == 129779615948377168 values = list(run_key.iter_values(as_json=True)) assert values[0].name == "Sidebar" assert values[0].value_type == "REG_EXPAND_SZ" def test_find_keys_partial_ntuser_hive(ntuser_software_partial): registry_hive = RegistryHive( ntuser_software_partial, hive_type=NTUSER_HIVE_TYPE, partial_hive_path=r"\Software", ) run_key = registry_hive.get_key(r"\Software\Microsoft\Windows\CurrentVersion\Run") assert run_key.name == "Run" assert run_key.header.last_modified == 132024690510209250 values = list(run_key.iter_values(as_json=True)) assert values[0].name == "OneDrive" assert values[0].value_type == "REG_SZ" def test_regdiff(ntuser_hive, second_hive_path): found_differences = compare_hives(ntuser_hive, second_hive_path, verbose=True) assert len(found_differences) == 7 assert len([x for x in found_differences if x[0] == "new_subkey"]) == 6 assert len([x for x in found_differences if x[0] == "new_value"]) == 1 def test_ntuser_emojis(transaction_ntuser): # There are some cases where the Registry stores utf-16 emojis as subkey names :) registry_hive = RegistryHive(transaction_ntuser) international = registry_hive.get_key(r"\Control Panel\International") subkeys = [x.name for x in international.iter_subkeys()] assert subkeys == ["Geo", "User Profile", "User Profile System Backup", "🌎🌏🌍"] def test_recurse_ntuser(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) value_types = { "REG_BINARY": 0, "REG_DWORD": 0, "REG_EXPAND_SZ": 0, "REG_MULTI_SZ": 0, "REG_NONE": 0, "REG_QWORD": 0, "REG_SZ": 0, } subkey_count = 0 values_count = 0 for subkey in registry_hive.recurse_subkeys(as_json=True): subkey_values = subkey.values subkey_count += 1 values_count += len(subkey_values or []) if subkey_values: for x in subkey_values: value_types[x["value_type"]] += 1 assert subkey_count == 1812 assert values_count == 4094 assert value_types == { "REG_BINARY": 531, "REG_DWORD": 1336, "REG_EXPAND_SZ": 93, "REG_MULTI_SZ": 303, "REG_NONE": 141, "REG_QWORD": 54, "REG_SZ": 1636, } def test_recurse_partial_ntuser(ntuser_software_partial): registry_hive = RegistryHive( ntuser_software_partial, hive_type=NTUSER_HIVE_TYPE, partial_hive_path=r"\Software", ) for subkey_count, subkey in enumerate(registry_hive.recurse_subkeys(as_json=True)): assert subkey.actual_path.startswith(registry_hive.partial_hive_path) assert subkey_count == 6395 def test_recurse_ntuser_without_fetching_values(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) for subkey_count, subkey in enumerate(registry_hive.recurse_subkeys(as_json=True, fetch_values=False)): assert subkey.values == [] assert subkey.values_count >= 0 assert subkey_count == 1811 def test_recurse_amcache(amcache_hive): registry_hive = RegistryHive(amcache_hive) value_types = { "REG_BINARY": 0, "REG_DWORD": 0, "REG_EXPAND_SZ": 0, "REG_MULTI_SZ": 0, "REG_NONE": 0, "REG_QWORD": 0, "REG_SZ": 0, } subkey_count = 0 values_count = 0 for subkey in registry_hive.recurse_subkeys(): subkey_count += 1 subkey_values = subkey.values values_count += len(subkey_values or []) if subkey_values: for x in subkey_values: value_types[x.value_type] += 1 assert subkey_count == 2105 assert values_count == 17539 assert value_types == { "REG_BINARY": 56, "REG_DWORD": 1656, "REG_EXPAND_SZ": 0, "REG_MULTI_SZ": 140, "REG_NONE": 0, "REG_QWORD": 1254, "REG_SZ": 14433, } def test_ntuser_apply_transaction_logs(transaction_ntuser, transaction_log): output_path = os.path.join(mkdtemp(), "recovered_hive.dat") restored_hive_path, recovered_dirty_pages_count = apply_transaction_logs( transaction_ntuser, transaction_log, restored_hive_path=output_path ) assert recovered_dirty_pages_count == 132 found_differences = compare_hives(transaction_ntuser, restored_hive_path) assert len(found_differences) == 588 assert len([x for x in found_differences if x[0] == "new_subkey"]) == 527 assert len([x for x in found_differences if x[0] == "new_value"]) == 60 def test_system_apply_transaction_logs(transaction_system, system_tr_log_1, system_tr_log_2): output_path = os.path.join(mkdtemp(), "recovered_hive.dat") restored_hive_path, recovered_dirty_pages_count = apply_transaction_logs( transaction_system, primary_log_path=system_tr_log_1, secondary_log_path=system_tr_log_2, restored_hive_path=output_path, ) assert recovered_dirty_pages_count == 315 found_differences = compare_hives(transaction_system, restored_hive_path) assert len(found_differences) == 2511 assert len([x for x in found_differences if x[0] == "new_subkey"]) == 2458 assert len([x for x in found_differences if x[0] == "new_value"]) == 53 def test_system_hive_devprop_structure(system_devprop): registry_hive = RegistryHive(system_devprop) subkey = registry_hive.get_key( "\\ControlSet001\\Enum\\ACPI\\ACPI0003\\0\\Properties\\{83da6326-97a6-4088-9453-a1923f573b29}\\0003" ) assert subkey.values_count == 1 value = subkey.get_values()[0] assert value.name == "(default)" assert value.value == "cmbatt.inf:db04a16c09a7808a:AcAdapter_Inst:6.3.9600.16384:ACPI\\ACPI0003" assert value.value_type == 18 def test_system_apply_transaction_logs_2(transaction_usrclass, usrclass_tr_log_1, usrclass_tr_log_2): output_path = os.path.join(mkdtemp(), "recovered_hive.dat") restored_hive_path, recovered_dirty_pages_count = apply_transaction_logs( transaction_usrclass, primary_log_path=usrclass_tr_log_1, secondary_log_path=usrclass_tr_log_2, restored_hive_path=output_path, ) assert recovered_dirty_pages_count == 158 found_differences = compare_hives(transaction_usrclass, restored_hive_path) assert len(found_differences) == 225 assert len([x for x in found_differences if x[0] == "new_subkey"]) == 93 assert len([x for x in found_differences if x[0] == "new_value"]) == 132 def test_hive_serialization(ntuser_hive, temp_output_file): registry_hive = RegistryHive(ntuser_hive) dump_hive_to_json(registry_hive, temp_output_file, registry_hive.root, verbose=False) counter = 0 with open(temp_output_file) as dumped_hive: for x in dumped_hive.readlines(): assert json.loads(x) counter += 1 assert counter == 1812 def test_get_key(software_hive): """ # Refers to https://github.com/mkorman90/regipy/issues/144 """ registry_hive = RegistryHive(software_hive) # We verify the registry headers are similar, because this is the same subkey. assert registry_hive.get_key("ODBC").header == registry_hive.root.get_subkey("ODBC").header assert registry_hive.root.get_subkey("ODBC").header == registry_hive.get_key("SOFTWARE\\ODBC").header def test_get_subkey_errors(software_hive): registry_hive = RegistryHive(software_hive) # Tests the NoRegistrySubkeysException that suppose to be raised try: registry_hive.get_key("ODBC").get_subkey("xyz") raise AssertionError() except NoRegistrySubkeysException: assert True # Tests value if raised_on_missing is set to False assert registry_hive.get_key("ODBC").get_subkey("xyz", raise_on_missing=False) is None def test_parse_security_info(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) run_key = registry_hive.get_key(r"\Software\Microsoft\Windows\CurrentVersion\Run") security_key_info = run_key.get_security_key_info() assert security_key_info["owner"] == "S-1-5-18" assert security_key_info["group"] == "S-1-5-18" assert len(security_key_info["dacl"]) == 4 assert security_key_info["dacl"][0] == { "access_mask": { "ACCESS_SYSTEM_SECURITY": False, "DELETE": True, "GENERIC_ALL": False, "GENERIC_EXECUTE": False, "GENERIC_READ": False, "GENERIC_WRITE": False, "MAXIMUM_ALLOWED": False, "READ_CONTROL": True, "SYNCHRONIZE": False, "WRITE_DAC": True, "WRITE_OWNER": True, }, "ace_type": "ACCESS_ALLOWED", "flags": { "CONTAINER_INHERIT_ACE": True, "INHERIT_ONLY_ACE": False, "NO_PROPAGATE_INHERIT_ACE": False, "OBJECT_INHERIT_ACE": True, }, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1673", } dacl_sids = [x["sid"] for x in security_key_info["dacl"]] assert dacl_sids == [ "S-1-5-21-2036804247-3058324640-2116585241-1673", "S-1-5-18", "S-1-5-32-544", "S-1-5-12", ] def test_parse_filetime_value(system_hive_with_filetime): registry_hive = RegistryHive(system_hive_with_filetime) subkey = registry_hive.get_key( r"\ControlSet001\Enum\USBSTOR\Disk&Ven_SanDisk&Prod_Cruzer&Rev_1.2" r"0\200608767007B7C08A6A&0\Properties\{83da6326-97a6-4088-9453-a1923f573b29}\0064" ) val = subkey.get_value("(default)", as_json=True) assert val == "2020-03-17T14:02:38.955490+00:00" def test_ntuser_filtered_timestamps_do_not_fetch_values(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) for subkey_count, entry in enumerate( get_filtered_subkeys( registry_hive, registry_hive.root, fetch_values=False, start_date="2012-04-03T00:00:00.000000", end_date="2012-04-03T23:59:59.999999", ) ): assert entry.values == [] assert subkey_count == 1489 def test_ntuser_filtered_timestamps_fetch_values(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) for subkey_count, entry in enumerate( get_filtered_subkeys( registry_hive, registry_hive.root, fetch_values=True, start_date="2012-04-03T00:00:00.000000", end_date="2012-04-03T23:59:59.999999", ) ): # values_count is parsed from the subkey header, so this test if effective. if entry.values_count > 0: assert len(entry.values) == entry.values_count assert subkey_count == 1489 def test_ntuser_filtered_timestamps_no_filter(ntuser_hive): registry_hive = RegistryHive(ntuser_hive) for subkey_count, entry in enumerate(get_filtered_subkeys(registry_hive, registry_hive.root, fetch_values=False)): assert entry.values == [] assert subkey_count == 1811 ================================================ FILE: regipy_tests/validation/plugin_validation.md ================================================ # Regipy plugin validation results ## Plugins with validation | plugin_name | plugin_description | plugin_class_name | test_case_name | success | |-------------------------------|------------------------------------------------------------------------------------------|---------------------------------|-----------------------------------------------|-----------| | active_control_set | Get information on SYSTEM hive control sets | ActiveControlSetPlugin | ActiveControlSetPluginValidationCase | True | | amcache | Parse Amcache | AmCachePlugin | AmCachePluginValidationCase | True | | app_paths | Parses application paths registry entries | AppPathsPlugin | AppPathsPluginValidationCase | True | | background_activity_moderator | Get the computer name | BAMPlugin | BamValidationCase | True | | backuprestore_plugin | Gets the contents of the FilesNotToSnapshot, KeysNotToRestore, and FilesNotToBackup keys | BackupRestorePlugin | BackupRestorePluginValidationCase | True | | boot_entry_list | List the Windows BCD boot entries | BootEntryListPlugin | BootEntryListPluginValidationCase | True | | bootkey | Get the Windows boot key | BootKeyPlugin | BootKeyPluginValidationCase | True | | codepage | Get codepage value | CodepagePlugin | CodepagePluginValidationCase | True | | computer_name | Get the computer name | ComputerNamePlugin | ComputerNamePluginValidationCase | True | | crash_dump | Get crash control information | CrashDumpPlugin | CrashDumpPluginValidationCase | True | | diag_sr | Get Diag\SystemRestore values and data | DiagSRPlugin | DiagSRPluginValidationCase | True | | disable_last_access | Get NTFSDisableLastAccessUpdate value | DisableLastAccessPlugin | DisableLastAccessPluginValidationCase | True | | disablesr_plugin | Gets the value that turns System Restore either on or off | DisableSRPlugin | DisableSRPluginValidationCase | True | | domain_sid | Get the machine domain name and SID | DomainSidPlugin | DomainSidPluginValidationCase | True | | execution_policy | Parses PowerShell and script execution policies | ExecutionPolicyPlugin | ExecutionPolicyPluginValidationCase | True | | host_domain_name | Get the computer host and domain names | HostDomainNamePlugin | HostDomainNamePluginValidationCase | True | | image_file_execution_options | Retrieve image file execution options - a persistence method | ImageFileExecutionOptions | ImageFileExecutionOptionsValidationCase | True | | installed_programs_ntuser | Retrieve list of installed programs and their install date from the NTUSER Hive | InstalledProgramsNTUserPlugin | InstalledProgramsNTUserPluginValidationCase | True | | installed_programs_software | Retrieve list of installed programs and their install date from the SOFTWARE Hive | InstalledProgramsSoftwarePlugin | InstalledProgramsSoftwarePluginValidationCase | True | | last_logon_plugin | Get the last logged on username | LastLogonPlugin | LastLogonPluginValidationCase | True | | local_sid | Get the machine local SID | LocalSidPlugin | LocalSidPluginValidationCase | True | | lsa_packages | Parses LSA security packages configuration | LSAPackagesPlugin | LSAPackagesPluginValidationCase | True | | mounted_devices | Parses mounted device information | MountedDevicesPlugin | MountedDevicesPluginValidationCase | True | | network_data | Get network data from many interfaces | NetworkDataPlugin | NetworkDataPluginValidationCase | True | | network_drives_plugin | Parse the user's mapped network drives | NetworkDrivesPlugin | NetworkDrivesPluginValidationCase | True | | networklist | Parses network connection history | NetworkListPlugin | NetworkListPluginValidationCase | True | | ntuser_classes_installer | List of installed software from NTUSER hive | NtuserClassesInstallerPlugin | NtuserClassesInstallerPluginValidationCase | True | | ntuser_persistence | Retrieve values from known persistence subkeys in NTUSER hive | NTUserPersistencePlugin | NTUserPersistenceValidationCase | True | | ntuser_shellbag_plugin | Parse NTUSER Shellbag items | ShellBagNtuserPlugin | ShellBagNtuserPluginValidationCase | True | | pagefile | Parses pagefile configuration | PagefilePlugin | PagefilePluginValidationCase | True | | previous_winver_plugin | Get previous relevant OS information | PreviousWinVersionPlugin | PreviousWinVersionPluginValidationCase | True | | print_demon_plugin | Get list of installed printer ports, as could be taken advantage by cve-2020-1048 | PrintDemonPlugin | PrintDemonPluginValidationCase | True | | processor_architecture | Get processor architecture info from the System's environment key | ProcessorArchitecturePlugin | ProcessorArchitecturePluginValidationCase | True | | profilelist_plugin | Parses information about user profiles found in the ProfileList key | ProfileListPlugin | ProfileListPluginValidationCase | True | | ras_tracing | Retrieve list of executables using ras | RASTracingPlugin | RASTracingPluginValidationCase | True | | routes | Get list of routes | RoutesPlugin | RoutesPluginValidationCase | True | | safeboot_configuration | Get safeboot configuration | SafeBootConfigurationPlugin | SafeBootConfigurationPluginValidationCase | True | | samparse | Parses user accounts from SAM hive | SAMParsePlugin | SAMParsePluginValidationCase | True | | services | Enumerate the services in the SYSTEM hive | ServicesPlugin | ServicesPluginValidationCase | True | | shimcache | Parse Shimcache artifact | ShimCachePlugin | AmCacheValidationCase | True | | shutdown | Get shutdown data | ShutdownPlugin | ShutdownPluginValidationCase | True | | software_classes_installer | List of installed software from SOFTWARE hive | SoftwareClassesInstallerPlugin | SoftwareClassesInstallerPluginValidationCase | True | | software_plugin | Retrieve values from known persistence subkeys in Software hive | SoftwarePersistencePlugin | SoftwarePersistenceValidationCase | True | | spp_clients_plugin | Determines volumes monitored by VSS | SppClientsPlugin | SppClientsPluginValidationCase | True | | susclient_plugin | Extracts SusClient* info, including HDD SN | SusclientPlugin | SusclientPluginValidationCase | True | | terminal_services_history | Retrieve history of RDP connections | TSClientPlugin | TSClientPluginValidationCase | True | | timezone_data | Get timezone data | TimezoneDataPlugin | TimezoneDataPluginValidationCase | True | | timezone_data2 | Get timezone data | TimezoneDataPlugin2 | TimezoneDataPlugin2ValidationCase | True | | typed_paths | Retrieve the typed Paths from the history | TypedPathsPlugin | TypedPathsPluginValidationCase | True | | typed_urls | Retrieve the typed URLs from IE history | TypedUrlsPlugin | TypedUrlsPluginValidationCase | True | | uac_plugin | Get the status of User Access Control | UACStatusPlugin | UACStatusPluginValidationCase | True | | usb_devices | Parses USB device connection history | USBDevicesPlugin | USBDevicesPluginValidationCase | True | | usbstor_plugin | Parse the connected USB devices history | USBSTORPlugin | USBSTORPluginValidationCase | True | | user_assist | Parse User Assist artifact | UserAssistPlugin | NTUserUserAssistValidationCase | True | | usrclass_shellbag_plugin | Parse USRCLASS Shellbag items | ShellBagUsrclassPlugin | ShellBagUsrclassPluginValidationCase | True | | wdigest | Get WDIGEST configuration | WDIGESTPlugin | WDIGESTPluginValidationCase | True | | windows_defender | Parses Windows Defender configuration and exclusions | WindowsDefenderPlugin | WindowsDefenderPluginValidationCase | True | | winrar_plugin | Parse the WinRAR archive history | WinRARPlugin | WinRARPluginValidationCase | True | | winscp_saved_sessions | Retrieve list of WinSCP saved sessions | WinSCPSavedSessionsPlugin | WinSCPSavedSessionsPluginValidationCase | True | | winver_plugin | Get relevant OS information | WinVersionPlugin | WinVersionPluginValidationCase | True | | word_wheel_query | Parse the word wheel query artifact | WordWheelQueryPlugin | WordWheelQueryPluginValidationCase | True | | wsl | Get WSL information | WSLPlugin | WSLPluginValidationCase | True | ## Plugins without validation **Starting regipy v5.0.0 - plugin validation replaces tests and is mandatary, being enforced by the build process** | plugin_name | plugin_description | plugin_class_name | test_case_name | success | |---------------------|---------------------------------------------------|-------------------------|------------------|-----------| | appcert_dlls | Parses AppCertDLLs persistence entries | AppCertDLLsPlugin | | False | | appcompat_flags | Parses application compatibility flags and layers | AppCompatFlagsPlugin | | False | | appinit_dlls | Parses AppInit_DLLs persistence entries | AppInitDLLsPlugin | | False | | appkeys | Parses application keyboard shortcuts | AppKeysPlugin | | False | | comdlg32 | Parses Open/Save dialog MRU lists | ComDlg32Plugin | | False | | muicache | Parses MUI Cache (application display names) | MUICachePlugin | | False | | pending_file_rename | Parses pending file rename operations | PendingFileRenamePlugin | | False | | powershell_logging | Parses PowerShell logging and execution policy | PowerShellLoggingPlugin | | False | | putty | Parses PuTTY sessions and SSH host keys | PuTTYPlugin | | False | | recentdocs | Parses recently opened documents | RecentDocsPlugin | | False | | runmru | Parses Run dialog MRU list | RunMRUPlugin | | False | | shares | Parses network share configuration | SharesPlugin | | False | | sysinternals | Parses Sysinternals tools EULA acceptance | SysinternalsPlugin | | False | ================================================ FILE: regipy_tests/validation/plugin_validation.py ================================================ import json import os import sys from collections import defaultdict from contextlib import contextmanager from dataclasses import asdict from pathlib import Path from tabulate import tabulate from regipy.plugins.plugin import PLUGINS from regipy.registry import RegistryHive from regipy_tests.validation.utils import extract_lzma from regipy_tests.validation.validation import ( VALIDATION_CASES, ValidationCase, ValidationResult, ) # Enable to raise exception on validation failure # As we are currently not enforcing validations - no raising exceptions by default ENFORCE_VALIDATION = True # Generate a template for all plugins that have missing validation tests GENERATE_MISSING_VALIDATION_TEST_TEMPLATES = False # It is possible to get an ipdb breakpoint once an exception is raised, useful for debugging plugin results # The user will be dropped into the validation case context, accessing all properties using `self`. SHOULD_DEBUG = False test_data_dir = str(Path(__file__).parent.parent.joinpath("data")) validation_results_output_file = str(Path(__file__).parent.joinpath("plugin_validation.md")) validated_plugins_json_file = str(Path(__file__).parent.parent.parent.joinpath("regipy/plugins/validated_plugins.json")) class PluginValidationCaseFailureException(Exception): """ raised when a plugin validation test case failed """ pass @contextmanager def load_hive(hive_file_name): temp_path = extract_lzma(os.path.join(test_data_dir, hive_file_name)) yield RegistryHive(temp_path) os.remove(temp_path) def validate_case(plugin_validation_case: ValidationCase, registry_hive: RegistryHive): try: plugin_validation_case_instance = plugin_validation_case(registry_hive) return plugin_validation_case_instance.validate() except AssertionError as ex: msg = f"Validation for {plugin_validation_case_instance.__class__.__name__} failed: {ex}" if ENFORCE_VALIDATION: if SHOULD_DEBUG: print(f"[!] [ENFORCED] [DEBUG]: {msg}") plugin_validation_case_instance.debug() raise PluginValidationCaseFailureException(msg) else: print(f"[!] [NOT ENFORCED]: {msg}") if SHOULD_DEBUG: plugin_validation_case_instance.debug() def run_validations_for_hive_file(hive_file_name, validation_cases) -> list[ValidationResult]: validation_results = [] with load_hive(hive_file_name) as registry_hive: for validation_case in validation_cases: validation_results.append(validate_case(validation_case, registry_hive)) return validation_results def main(): # Map all existing validation cases validation_cases_map: dict[str, ValidationCase] = {v.plugin.NAME: v for v in VALIDATION_CASES} plugins_without_validation: set = {p.NAME for p in PLUGINS}.difference(set(validation_cases_map.keys())) print(f"[*] Loaded {len(validation_cases_map)} validation cases") if len(sys.argv) == 2: plugin_name = sys.argv[1] if plugin_name in validation_cases_map: print(f"Running validation for plugin {plugin_name}") validation_case: ValidationCase = validation_cases_map[plugin_name] with load_hive(validation_case.test_hive_file_name) as registry_hive: validate_case(validation_case, registry_hive) return print(f"No ValidationCase for {plugin_name}") return # Map all plugins according to registry hive test file, for performance. # Also, warn about plugins without validation, this will be enforced in the future. registry_hive_map = defaultdict(list) for plugin in PLUGINS: plugin_name = plugin.NAME if plugin_name in validation_cases_map: print(f"[+] Plugin {plugin_name} has validation case") plugin_validation_case = validation_cases_map[plugin_name] # Get hive filename from file, in the future group plugin validation by hive file hive_file_name = plugin_validation_case.test_hive_file_name registry_hive_map[hive_file_name].append(plugin_validation_case) else: print(f"[!] {plugin_name} has NO validation case!") # Execute grouped by file, to save performance on extracting and loading the hive print("\n\nRunning Validations:") validation_results: list[ValidationResult] = [] for registry_hive_file_name, validation_cases in registry_hive_map.items(): print(f"\n\t[*] Validating {registry_hive_file_name} ({len(validation_cases)} validations):") validation_results.extend(run_validations_for_hive_file(registry_hive_file_name, validation_cases)) print() validation_results_dict = sorted([asdict(v) for v in validation_results], key=lambda x: x["plugin_name"]) print(f"\n[!] {len(validation_results_dict)}/{len(PLUGINS)} plugins have a validation case:") md_table_for_validation_results = tabulate(validation_results_dict, headers="keys", tablefmt="github") print(md_table_for_validation_results) if plugins_without_validation: print(f"\n[!] {len(plugins_without_validation)}/{len(PLUGINS)} plugins have no validation case!") # Create empty validation results for plugins without validation md_table_for_plugins_without_validation_results = tabulate( sorted( [ asdict( ValidationResult( plugin_name=p.NAME, plugin_description=p.DESCRIPTION, plugin_class_name=p.__name__, test_case_name=None, success=False, ) ) for p in PLUGINS if p.NAME in plugins_without_validation ], key=lambda x: x["plugin_name"], ), headers="keys", tablefmt="github", ) print(md_table_for_plugins_without_validation_results) if GENERATE_MISSING_VALIDATION_TEST_TEMPLATES: for p in PLUGINS: if p.NAME in plugins_without_validation: validation_template = f""" from regipy.plugins.{p.COMPATIBLE_HIVE}.{p.__name__.lower()} import {p.__name__} from regipy_tests.validation.validation import ValidationCase class {p.__name__}ValidationCase(ValidationCase): plugin = {p.__name__} test_hive_file_name = "{p.COMPATIBLE_HIVE}.xz" exact_expected_result = None """ plugin_name = p.NAME missing_test_target_path = str( Path(__file__).parent.joinpath("validation_tests").joinpath(f"{plugin_name}_validation.py") ) if not os.path.exists(missing_test_target_path): print(f"Creating template for {plugin_name} target path: {missing_test_target_path}") with open(missing_test_target_path, "w+") as f: f.write(validation_template) # If we are enforcing validation, raise on plugins without validation if not ENFORCE_VALIDATION and plugins_without_validation: # fmt: off raise PluginValidationCaseFailureException( f"{len(plugins_without_validation)} plugins are missing validation:" f" {[p.__name__ for p in PLUGINS if p.NAME in plugins_without_validation]}" ) # fmt: on # Generate markdown file `validation_results_output_file` markdown_content = f""" # Regipy plugin validation results ## Plugins with validation {md_table_for_validation_results} ## Plugins without validation **Starting regipy v5.0.0 - plugin validation replaces tests and is mandatary, being enforced by the build process** {md_table_for_plugins_without_validation_results} """ # Write the content to a Markdown file print(f" ** Updated the validation results in {validation_results_output_file} **") with open(validation_results_output_file, "w") as f: f.write(markdown_content) # Generate validated_plugins.json for the package validated_plugin_names = sorted(validation_cases_map.keys()) print(f" ** Updated validated plugins JSON in {validated_plugins_json_file} **") with open(validated_plugins_json_file, "w") as f: json.dump(validated_plugin_names, f, indent=4) if __name__ == "__main__": main() ================================================ FILE: regipy_tests/validation/utils.py ================================================ import lzma from tempfile import mktemp def extract_lzma(path): tempfile_path = mktemp() with open(tempfile_path, "wb") as tmp, lzma.open(path) as f: tmp.write(f.read()) return tempfile_path ================================================ FILE: regipy_tests/validation/validation.py ================================================ from dataclasses import dataclass from typing import Callable, Optional, Union from regipy.plugins.plugin import Plugin from regipy.registry import RegistryHive VALIDATION_CASES = set() @dataclass class ValidationResult: plugin_name: str plugin_description: Optional[str] plugin_class_name: str test_case_name: Optional[str] success: bool class ValidationCase: input_hive: RegistryHive = None plugin: Plugin = None plugin_instance: type[Plugin] = None # Will hold the output of the plugin execution plugin_output: Union[list, dict] = None # These entries will be tested for presence in the plugin output expected_entries: list[dict] = [] # The result here will be matched to the exact_expected_result: Optional[Union[dict, list]] = None # Optionally Implement a custom test for your plugin, which will be called during the validation step # This test can replace the validation of entries, but not the count. # The test must return True, or raise an AssertionError custom_test: Optional[Callable] = None # Expected entries count expected_entries_count: int = None def __init_subclass__(cls): VALIDATION_CASES.add(cls) def __init__(self, input_hive: RegistryHive) -> None: self.input_hive = input_hive def validate(self): print(f"\tStarting validation for {self.plugin.NAME} ({self.__class__.__name__})") self.plugin_instance = self.plugin(self.input_hive, as_json=True) self.plugin_instance.run() self.plugin_output = self.plugin_instance.entries assert self.exact_expected_result is not None or self.expected_entries is not None or self.custom_test is not None, ( "Some output must be tested!" ) entries_found = True for entry in self.expected_entries: if entry not in self.plugin_output: entries_found = False assert entries_found if self.exact_expected_result: assert self.plugin_output == self.exact_expected_result, "Expected exact plugin output!" if self.custom_test is not None: self.custom_test() # If we are verifying an exact result, there is no need to verify entries count if not self.exact_expected_result: output_entries_count = len(self.plugin_output) assert self.expected_entries_count == output_entries_count, ( f"No match for expected entries count: expected {self.expected_entries_count}, got {output_entries_count}" ) print(f"\tValidation passed for {self.plugin.NAME}") return ValidationResult( plugin_name=self.plugin.NAME, plugin_description=self.plugin.DESCRIPTION, plugin_class_name=self.plugin.__name__, test_case_name=self.__class__.__name__, success=True, ) def debug(self): import ipdb ipdb.set_trace() ================================================ FILE: regipy_tests/validation/validation_tests/__init_.py ================================================ ================================================ FILE: regipy_tests/validation/validation_tests/active_control_set_validation.py ================================================ from regipy.plugins.system.active_controlset import ActiveControlSetPlugin from regipy_tests.validation.validation import ValidationCase class ActiveControlSetPluginValidationCase(ValidationCase): plugin = ActiveControlSetPlugin test_hive_file_name = "SYSTEM_WIN_10_1709.xz" exact_expected_result = [ { "name": "Current", "value": 1, "value_type": "REG_DWORD", "is_corrupted": False, }, { "name": "Default", "value": 1, "value_type": "REG_DWORD", "is_corrupted": False, }, { "name": "Failed", "value": 0, "value_type": "REG_DWORD", "is_corrupted": False, }, { "name": "LastKnownGood", "value": 1, "value_type": "REG_DWORD", "is_corrupted": False, }, ] ================================================ FILE: regipy_tests/validation/validation_tests/amcache_validation.py ================================================ from regipy.plugins.amcache.amcache import AmCachePlugin from regipy_tests.validation.validation import ValidationCase class AmCachePluginValidationCase(ValidationCase): plugin = AmCachePlugin test_hive_file_name = "amcache.hve.xz" expected_entries = [ { "full_path": "C:\\Windows\\system32\\TPVMMondeu.dll", "last_modified_timestamp_2": "2017-03-17T05:06:04.002722+00:00", "program_id": "75a010066bb612ca7357ce31df8e9f0300000904", "sha1": "056f4b9d9ec9b5dc548e1b460da889e44089d76f", "timestamp": "2017-08-03T11:34:02.263418+00:00", } ] expected_entries_count = 1367 ================================================ FILE: regipy_tests/validation/validation_tests/app_paths_plugin_validation.py ================================================ from regipy.plugins.software.apppaths import AppPathsPlugin from regipy_tests.validation.validation import ValidationCase class AppPathsPluginValidationCase(ValidationCase): plugin = AppPathsPlugin test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "key_path": "\\Microsoft\\Windows\\CurrentVersion\\App Paths\\AcroRd32.exe", "application": "AcroRd32.exe", "architecture": "x64", "last_write": "2011-08-28T22:41:26.262844+00:00", "path": "C:\\Program Files\\Adobe\\Reader 10.0\\Reader\\AcroRd32.exe", "app_path": "C:\\Program Files\\Adobe\\Reader 10.0\\Reader\\", } ] expected_entries_count = 34 ================================================ FILE: regipy_tests/validation/validation_tests/backuprestore_plugin_validation.py ================================================ from regipy.plugins.system.backuprestore import BackupRestorePlugin from regipy_tests.validation.validation import ValidationCase def test_backup_restore_plugin_output(c: ValidationCase): assert c.plugin_output.keys() == { "\\ControlSet002\\Control\\BackupRestore\\FilesNotToBackup", "\\ControlSet002\\Control\\BackupRestore\\KeysNotToRestore", "\\ControlSet001\\Control\\BackupRestore\\KeysNotToRestore", "\\ControlSet002\\Control\\BackupRestore\\FilesNotToSnapshot", "\\ControlSet001\\Control\\BackupRestore\\FilesNotToSnapshot", "\\ControlSet001\\Control\\BackupRestore\\FilesNotToBackup", } assert set(c.plugin_output["\\ControlSet001\\Control\\BackupRestore\\FilesNotToBackup"].keys()) == { "BITS_metadata", "Temporary Files", "last_write", "MS Distributed Transaction Coordinator", "Kernel Dumps", "BITS_BAK", "Mount Manager", "WER", "VSS Service DB", "Memory Page File", "FVE_Log", "Power Management", "WUA", "FVE_Wipe", "Internet Explorer", "BITS_LOG", "FVE_Control", "Netlogon", "Offline Files Cache", "ETW", "VSS Service Alternate DB", "VSS Default Provider", "RAC", } class BackupRestorePluginValidationCase(ValidationCase): plugin = BackupRestorePlugin test_hive_file_name = "SYSTEM.xz" custom_test = test_backup_restore_plugin_output expected_entries_count = 6 ================================================ FILE: regipy_tests/validation/validation_tests/bam_validation.py ================================================ from regipy.plugins.system.bam import BAMPlugin from regipy_tests.validation.validation import ValidationCase class BamValidationCase(ValidationCase): plugin = BAMPlugin test_hive_file_name = "SYSTEM_WIN_10_1709.xz" expected_entries = [ { "sequence_number": 9, "version": 1, "sid": "S-1-5-90-0-1", "executable": "\\Device\\HarddiskVolume2\\Windows\\System32\\dwm.exe", "timestamp": "2020-04-19T09:09:35.731816+00:00", "key_path": "\\ControlSet001\\Services\\bam\\state\\UserSettings\\S-1-5-90-0-1", } ] expected_entries_count = 55 ================================================ FILE: regipy_tests/validation/validation_tests/boot_entry_list_plugin_validation.py ================================================ from regipy.plugins.bcd.boot_entry_list import BootEntryListPlugin from regipy_tests.validation.validation import ValidationCase class BootEntryListPluginValidationCase(ValidationCase): plugin = BootEntryListPlugin test_hive_file_name = "BCD.xz" exact_expected_result = [ { "guid": "{733b62de-f608-11eb-825c-c112f60133ab}", "type": "0x101FFFFF", "name": "Linux Boot Manager", "gpt_disk": "376e5397-7d1f-4e4f-a668-5a62c1269e60", "gpt_partition": "24e0e103-9bc2-477e-a5e2-3e42d2bb134f", "image_path": "\\EFI\\systemd\\systemd-bootx64.efi", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{733b62e2-f608-11eb-825c-c112f60133ab}", "type": "0x101FFFFF", "name": "UEFI OS", "gpt_disk": "376e5397-7d1f-4e4f-a668-5a62c1269e60", "gpt_partition": "24e0e103-9bc2-477e-a5e2-3e42d2bb134f", "image_path": "\\EFI\\BOOT\\BOOTX64.EFI", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{733b62e3-f608-11eb-825c-c112f60133ab}", "type": "0x101FFFFF", "name": "Windows Boot Manager", "gpt_disk": "376e5397-7d1f-4e4f-a668-5a62c1269e60", "gpt_partition": "24e0e103-9bc2-477e-a5e2-3e42d2bb134f", "image_path": "\\EFI\\Microsoft\\Boot\\bootmgfw.efi", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{733b62e4-f608-11eb-825c-c112f60133ab}", "type": "0x10200004", "name": "Windows Resume Application", "gpt_disk": "0b2394a9-095e-487d-8d48-719ecd4d78ca", "gpt_partition": "8e0f2c38-e4ea-47ba-b7fc-9d8c74dccf0b", "image_path": "\\Windows\\system32\\winresume.efi", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{733b62e5-f608-11eb-825c-c112f60133ab}", "type": "0x10200003", "name": "Windows 10", "gpt_disk": "0b2394a9-095e-487d-8d48-719ecd4d78ca", "gpt_partition": "8e0f2c38-e4ea-47ba-b7fc-9d8c74dccf0b", "image_path": "\\Windows\\system32\\winload.efi", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{733b62e6-f608-11eb-825c-c112f60133ab}", "type": "0x10200003", "name": "Windows Recovery Environment", "gpt_disk": "00000001-0090-0000-0500-000006000000", "gpt_partition": "00000003-0000-0000-0000-000000000000", "image_path": "\\windows\\system32\\winload.efi", "timestamp": "2021-08-09T02:13:30.976970+00:00", }, { "guid": "{9dea862c-5cdd-4e70-acc1-f32b344d4795}", "type": "0x10100002", "name": "Windows Boot Manager", "gpt_disk": "0b2394a9-095e-487d-8d48-719ecd4d78ca", "gpt_partition": "36be3955-63bf-4068-a6ab-00195cca3a22", "image_path": "\\EFI\\Microsoft\\Boot\\bootmgfw.efi", "timestamp": "2021-08-09T02:13:30.992594+00:00", }, { "guid": "{b2721d73-1db4-4c62-bf78-c548a880142d}", "type": "0x10200005", "name": "Windows Memory Diagnostic", "gpt_disk": "0b2394a9-095e-487d-8d48-719ecd4d78ca", "gpt_partition": "36be3955-63bf-4068-a6ab-00195cca3a22", "image_path": "\\EFI\\Microsoft\\Boot\\memtest.efi", "timestamp": "2021-08-09T02:13:30.976970+00:00", }, ] ================================================ FILE: regipy_tests/validation/validation_tests/boot_key_plugin_validation.py ================================================ from regipy.plugins.system.bootkey import BootKeyPlugin from regipy_tests.validation.validation import ValidationCase class BootKeyPluginValidationCase(ValidationCase): plugin = BootKeyPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = [ { "key": "e7f28d88f470cfed67dbcdb62ed1275b", "timestamp": "2012-04-04T11:47:46.203124+00:00", }, { "key": "e7f28d88f470cfed67dbcdb62ed1275b", "timestamp": "2012-04-04T11:47:46.203124+00:00", }, ] ================================================ FILE: regipy_tests/validation/validation_tests/codepage_validation.py ================================================ from regipy.plugins.system.codepage import CodepagePlugin from regipy_tests.validation.validation import ValidationCase class CodepagePluginValidationCase(ValidationCase): plugin = CodepagePlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Control\\Nls\\CodePage": { "last_write": "2009-07-14T04:37:09.460768+00:00", "ACP": "1252", }, "\\ControlSet002\\Control\\Nls\\CodePage": { "last_write": "2009-07-14T04:37:09.460768+00:00", "ACP": "1252", }, } ================================================ FILE: regipy_tests/validation/validation_tests/computer_name_plugin_validation.py ================================================ from regipy.plugins.system.computer_name import ComputerNamePlugin from regipy_tests.validation.validation import ValidationCase class ComputerNamePluginValidationCase(ValidationCase): plugin = ComputerNamePlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = [ {"name": "WKS-WIN732BITA", "timestamp": "2010-11-10T17:18:08.718750+00:00"}, {"name": "WIN-V5T3CSP8U4H", "timestamp": "2010-11-10T18:17:36.968750+00:00"}, ] ================================================ FILE: regipy_tests/validation/validation_tests/crash_dump_validation.py ================================================ from regipy.plugins.system.crash_dump import CrashDumpPlugin from regipy_tests.validation.validation import ValidationCase class CrashDumpPluginValidationCase(ValidationCase): plugin = CrashDumpPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Control\\CrashControl": { "last_write": "2012-04-04T11:47:36.984376+00:00", "CrashDumpEnabled": 2, "CrashDumpEnabledStr": "Kernel memory dump", "LogEvent": 1, "DumpFile": "%SystemRoot%\\MEMORY.DMP", "MinidumpDir": "%SystemRoot%\\Minidump", }, "\\ControlSet002\\Control\\CrashControl": { "last_write": "2012-04-04T11:47:36.984376+00:00", "CrashDumpEnabled": 2, "CrashDumpEnabledStr": "Kernel memory dump", "LogEvent": 1, "DumpFile": "%SystemRoot%\\MEMORY.DMP", "MinidumpDir": "%SystemRoot%\\Minidump", }, } ================================================ FILE: regipy_tests/validation/validation_tests/diag_sr_validation.py ================================================ from regipy.plugins.system.diag_sr import DiagSRPlugin from regipy_tests.validation.validation import ValidationCase class DiagSRPluginValidationCase(ValidationCase): plugin = DiagSRPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Services\\VSS\\Diag\\SystemRestore": { "last_write": "2012-03-31T04:00:22.998834+00:00", "SrCreateRp (Enter)": "2012-03-31 04:00:01", "SrCreateRp (Leave)": "2012-03-31 04:00:22", }, "\\ControlSet002\\Services\\VSS\\Diag\\SystemRestore": { "last_write": "2012-03-31T04:00:22.998834+00:00", "SrCreateRp (Enter)": "2012-03-31 04:00:01", "SrCreateRp (Leave)": "2012-03-31 04:00:22", }, } ================================================ FILE: regipy_tests/validation/validation_tests/disable_last_access_validation.py ================================================ from regipy.plugins.system.disablelastaccess import DisableLastAccessPlugin from regipy_tests.validation.validation import ValidationCase class DisableLastAccessPluginValidationCase(ValidationCase): plugin = DisableLastAccessPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Control\\FileSystem": { "last_write": "2009-07-14T04:37:09.429568+00:00", "NtfsDisableLastAccessUpdate": "1", "NtfsDisableLastAccessUpdateStr": "", }, "\\ControlSet002\\Control\\FileSystem": { "last_write": "2009-07-14T04:37:09.429568+00:00", "NtfsDisableLastAccessUpdate": "1", "NtfsDisableLastAccessUpdateStr": "", }, } ================================================ FILE: regipy_tests/validation/validation_tests/disablesr_plugin_validation.py ================================================ from regipy.plugins.software.disablesr import DisableSRPlugin from regipy_tests.validation.validation import ValidationCase class DisableSRPluginValidationCase(ValidationCase): plugin = DisableSRPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "\\Microsoft\\Windows NT\\CurrentVersion\\SystemRestore": {"last_write": "2012-03-31T04:00:23.006648+00:00"} } ================================================ FILE: regipy_tests/validation/validation_tests/domain_sid_plugin_validation.py ================================================ from regipy.plugins.security.domain_sid import DomainSidPlugin from regipy_tests.validation.validation import ValidationCase class DomainSidPluginValidationCase(ValidationCase): plugin = DomainSidPlugin test_hive_file_name = "SECURITY.xz" exact_expected_result = [ { "domain_name": "WORKGROUP", "domain_sid": None, "machine_sid": None, "timestamp": "2021-08-05T10:43:08.911000+00:00", } ] ================================================ FILE: regipy_tests/validation/validation_tests/execution_policy_plugin_validation.py ================================================ from regipy.plugins.software.execpolicy import ExecutionPolicyPlugin from regipy_tests.validation.validation import ValidationCase class ExecutionPolicyPluginValidationCase(ValidationCase): plugin = ExecutionPolicyPlugin test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "type": "powershell", "key_path": "\\Microsoft\\PowerShell\\1\\ShellIds\\Microsoft.PowerShell", "last_write": "2010-11-10T18:09:15.781250+00:00", "path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", }, { "type": "wsh", "key_path": "\\Microsoft\\Windows Script Host\\Settings", "last_write": "2009-07-14T04:37:08.306366+00:00", "display_logo": False, }, ] expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/host_domain_name_plugin_validation.py ================================================ from regipy.plugins.system.host_domain_name import HostDomainNamePlugin from regipy_tests.validation.validation import ValidationCase class HostDomainNamePluginValidationCase(ValidationCase): plugin = HostDomainNamePlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = [ { "hostname": "WKS-WIN732BITA", "domain": "shieldbase.local", "timestamp": "2011-09-17T13:43:23.770078+00:00", }, { "hostname": "WKS-WIN732BITA", "domain": "shieldbase.local", "timestamp": "2011-09-17T13:43:23.770078+00:00", }, ] ================================================ FILE: regipy_tests/validation/validation_tests/image_file_execution_options_validation.py ================================================ from regipy.plugins.software.image_file_execution_options import ( ImageFileExecutionOptions, ) from regipy_tests.validation.validation import ValidationCase class ImageFileExecutionOptionsValidationCase(ValidationCase): plugin = ImageFileExecutionOptions test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "name": "AcroRd32.exe", "timestamp": "2011-08-28T22:41:26.348786+00:00", "DisableExceptionChainValidation": 0, }, { "name": "AcroRd32Info.exe", "timestamp": "2011-08-28T22:41:26.342926+00:00", "DisableExceptionChainValidation": 0, }, { "name": "clview.exe", "timestamp": "2010-11-10T10:33:42.573040+00:00", "DisableExceptionChainValidation": 0, }, { "name": "cnfnot32.exe", "timestamp": "2010-11-10T10:33:43.369916+00:00", "DisableExceptionChainValidation": 0, }, { "name": "DllNXOptions", "timestamp": "2009-07-14T04:41:12.790008+00:00", "mscoree.dll": 1, "mscorwks.dll": 1, "mso.dll": 1, "msjava.dll": 1, "msci_uno.dll": 1, "jvm.dll": 1, "jvm_g.dll": 1, "javai.dll": 1, "vb40032.dll": 1, "vbe6.dll": 1, "ums.dll": 1, "main123w.dll": 1, "udtapi.dll": 1, "mscorsvr.dll": 1, "eMigrationmmc.dll": 1, "eProcedureMMC.dll": 1, "eQueryMMC.dll": 1, "EncryptPatchVer.dll": 1, "Cleanup.dll": 1, "divx.dll": 1, "divxdec.ax": 1, "fullsoft.dll": 1, "NSWSTE.dll": 1, "ASSTE.dll": 1, "NPMLIC.dll": 1, "PMSTE.dll": 1, "AVSTE.dll": 1, "NAVOPTRF.dll": 1, "DRMINST.dll": 1, "TFDTCTT8.dll": 1, "DJSMAR00.dll": 1, "xlmlEN.dll": 1, "ISSTE.dll": 1, "symlcnet.dll": 1, "ppw32hlp.dll": 1, "Apitrap.dll": 1, "Vegas60k.dll": 1, }, { "name": "dw20.exe", "timestamp": "2010-11-10T10:33:42.619916+00:00", "DisableExceptionChainValidation": 0, }, { "name": "dwtrig20.exe", "timestamp": "2010-11-10T10:33:42.619916+00:00", "DisableExceptionChainValidation": 0, }, { "name": "excel.exe", "timestamp": "2010-11-10T10:33:43.135540+00:00", "DisableExceptionChainValidation": 0, }, ] expected_entries_count = 32 ================================================ FILE: regipy_tests/validation/validation_tests/installed_programs_ntuser_validation.py ================================================ from regipy.plugins.ntuser.installed_programs_ntuser import ( InstalledProgramsNTUserPlugin, ) from regipy_tests.validation.validation import ValidationCase class InstalledProgramsNTUserPluginValidationCase(ValidationCase): plugin = InstalledProgramsNTUserPlugin test_hive_file_name = "NTUSER_with_winscp.DAT.xz" expected_entries = [ { "service_name": "ZoomUMX", "timestamp": "2022-02-28T09:05:31.141524+00:00", "registry_path": "\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "DisplayIcon": "C:\\Users\\tony\\AppData\\Roaming\\Zoom\\bin\\Zoom.exe", "DisplayName": "Zoom", "DisplayVersion": "5.9.3 (3169)", "EstimatedSize": 10000, "HelpLink": "https://support.zoom.us/home", "URLInfoAbout": "https://zoom.us", "URLUpdateInfo": "https://zoom.us", "Publisher": "Zoom Video Communications, Inc.", "UninstallString": '"C:\\Users\\tony\\AppData\\Roaming\\Zoom\\uninstall\\Installer.exe" /uninstall', "InstallLocation": "C:\\Users\\tony\\AppData\\Roaming\\Zoom\\bin", "NoModify": 1, "NoRepair": 1, }, ] expected_entries_count = 4 ================================================ FILE: regipy_tests/validation/validation_tests/installed_programs_software_plugin_validation.py ================================================ from regipy.plugins.software.installed_programs import InstalledProgramsSoftwarePlugin from regipy_tests.validation.validation import ValidationCase class InstalledProgramsSoftwarePluginValidationCase(ValidationCase): plugin = InstalledProgramsSoftwarePlugin test_hive_file_name = "SOFTWARE.xz" expected_entries_count = 67 expected_entries = [ { "registry_path": "\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "service_name": "AddressBook", "timestamp": "2009-07-14T04:41:12.758808+00:00", }, { "service_name": "Connection Manager", "timestamp": "2009-07-14T04:41:12.758808+00:00", "registry_path": "\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SystemComponent": 1, }, ] ================================================ FILE: regipy_tests/validation/validation_tests/last_logon_plugin_validation.py ================================================ from regipy.plugins.software.last_logon import LastLogonPlugin from regipy_tests.validation.validation import ValidationCase class LastLogonPluginValidationCase(ValidationCase): plugin = LastLogonPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "last_logged_on_provider": "{6F45DC1E-5384-457A-BC13-2CD81B0D28ED}", "last_logged_on_sam_user": "SHIELDBASE\\rsydow", "last_logged_on_user": "SHIELDBASE\\rsydow", "last_write": "2012-04-04T12:20:41.453654+00:00", "show_tablet_keyboard": 0, } expected_entries_count = 5 ================================================ FILE: regipy_tests/validation/validation_tests/local_sid_plugin_validation.py ================================================ from regipy.plugins.sam.local_sid import LocalSidPlugin from regipy_tests.validation.validation import ValidationCase class LocalSidPluginValidationCase(ValidationCase): plugin = LocalSidPlugin test_hive_file_name = "SAM.xz" exact_expected_result = [ { "machine_sid": "S-1-5-21-1760460187-1592185332-161725925", "timestamp": "2014-09-24T03:36:43.549302+00:00", } ] ================================================ FILE: regipy_tests/validation/validation_tests/lsa_packages_plugin_validation.py ================================================ from regipy.plugins.system.lsa_packages import LSAPackagesPlugin from regipy_tests.validation.validation import ValidationCase class LSAPackagesPluginValidationCase(ValidationCase): plugin = LSAPackagesPlugin test_hive_file_name = "SYSTEM.xz" expected_entries = [ { "key_path": "\\ControlSet001\\Control\\Lsa", "last_write": "2012-04-04T11:47:46.203124+00:00", "audit_base_objects": False, "audit_base_directories": False, "limit_blank_password_use": True, "notification_packages": ["scecli"], "security_packages": [ "kerberos", "msv1_0", "schannel", "wdigest", "tspkg", "pku2u", ], "authentication_packages": ["msv1_0"], "secure_boot": 1, } ] expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/mounted_devices_plugin_validation.py ================================================ from regipy.plugins.system.mountdev import MountedDevicesPlugin from regipy_tests.validation.validation import ValidationCase class MountedDevicesPluginValidationCase(ValidationCase): plugin = MountedDevicesPlugin test_hive_file_name = "SYSTEM.xz" expected_entries = [ { "key_path": "\\MountedDevices", "last_write": "2011-07-05T19:33:28.328124+00:00", "value_name": "\\DosDevices\\C:", "mount_point": "C:", "mount_type": "drive_letter", } ] expected_entries_count = 11 ================================================ FILE: regipy_tests/validation/validation_tests/network_data_plugin_validation.py ================================================ from regipy.plugins.system.network_data import NetworkDataPlugin from regipy_tests.validation.validation import ValidationCase class NetworkDataPluginValidationCase(ValidationCase): plugin = NetworkDataPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Services\\Tcpip\\Parameters\\Interfaces": { "timestamp": "2011-09-17T13:43:23.770078+00:00", "interfaces": [ { "interface_name": "{698E50A9-4F58-4D86-B61D-F42E58DCACF6}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": ["10.3.58.5"], "subnet_mask": ["255.255.255.0"], "default_gateway": ["10.3.58.1"], "name_server": "10.3.58.4", "domain": 0, }, { "interface_name": "{6AAFC9A9-0542-4DB2-8760-CCFFA953737C}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": ["192.168.1.123"], "subnet_mask": ["255.255.255.0"], "default_gateway": ["192.168.1.1"], "name_server": "192.168.1.112", "domain": 0, }, { "interface_name": "{e29ac6c2-7037-11de-816d-806e6f6e6963}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": None, "subnet_mask": None, "default_gateway": None, "name_server": None, "domain": None, }, ], }, "\\ControlSet002\\Services\\Tcpip\\Parameters\\Interfaces": { "timestamp": "2011-09-17T13:43:23.770078+00:00", "interfaces": [ { "interface_name": "{698E50A9-4F58-4D86-B61D-F42E58DCACF6}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": ["10.3.58.5"], "subnet_mask": ["255.255.255.0"], "default_gateway": ["10.3.58.1"], "name_server": "10.3.58.4", "domain": 0, }, { "interface_name": "{6AAFC9A9-0542-4DB2-8760-CCFFA953737C}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": ["192.168.1.123"], "subnet_mask": ["255.255.255.0"], "default_gateway": ["192.168.1.1"], "name_server": "192.168.1.112", "domain": 0, }, { "interface_name": "{e29ac6c2-7037-11de-816d-806e6f6e6963}", "last_modified": "2011-09-17T13:43:23.770078+00:00", "incomplete_data": False, "dhcp_enabled": False, "ip_address": None, "subnet_mask": None, "default_gateway": None, "name_server": None, "domain": None, }, ], }, } ================================================ FILE: regipy_tests/validation/validation_tests/network_drives_plugin_validation.py ================================================ from regipy.plugins.ntuser.network_drives import NetworkDrivesPlugin from regipy_tests.validation.validation import ValidationCase class NetworkDrivesPluginValidationCase(ValidationCase): plugin = NetworkDrivesPlugin test_hive_file_name = "NTUSER.DAT.xz" exact_expected_result = [ { "drive_letter": "p", "last_write": "2012-04-03T22:08:18.840132+00:00", "network_path": "\\\\controller\\public", } ] ================================================ FILE: regipy_tests/validation/validation_tests/networklist_plugin_validation.py ================================================ from regipy.plugins.software.networklist import NetworkListPlugin from regipy_tests.validation.validation import ValidationCase class NetworkListPluginValidationCase(ValidationCase): plugin = NetworkListPlugin test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "type": "profile", "key_path": "\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles\\{1EDE3981-5784-4C61-B5A7-CF328A10043E}", "profile_guid": "{1EDE3981-5784-4C61-B5A7-CF328A10043E}", "last_write": "2012-04-04T11:48:39.392750+00:00", "profile_name": "shieldbase.local", "description": "shieldbase.local", "managed": True, "category": "Domain", "date_created": None, "name_type": "Wired", "date_last_connected": None, } ] expected_entries_count = 6 ================================================ FILE: regipy_tests/validation/validation_tests/ntuser_classes_installer_plugin_validation.py ================================================ from regipy.plugins.ntuser.classes_installer import NtuserClassesInstallerPlugin from regipy_tests.validation.validation import ValidationCase class NtuserClassesInstallerPluginValidationCase(ValidationCase): plugin = NtuserClassesInstallerPlugin test_hive_file_name = "NTUSER_with_winscp.DAT.xz" expected_entries = [ { "identifier": "8A4152964845CF540BEAEBD27F7A8519", "is_hidden": False, "product_name": "Microsoft Visual C++ Compiler Package for Python 2.7", "timestamp": "2022-02-15T07:00:07.245646+00:00", } ] expected_entries_count = 1 ================================================ FILE: regipy_tests/validation/validation_tests/ntuser_persistence_validation.py ================================================ from regipy.plugins.ntuser.persistence import NTUserPersistencePlugin from regipy_tests.validation.validation import ValidationCase class NTUserPersistenceValidationCase(ValidationCase): plugin = NTUserPersistencePlugin test_hive_file_name = "NTUSER.DAT.xz" exact_expected_result = { "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run": { "timestamp": "2012-04-03T21:19:54.837716+00:00", "values": [ { "name": "Sidebar", "value_type": "REG_EXPAND_SZ", "value": "%ProgramFiles%\\Windows Sidebar\\Sidebar.exe /autoRun", "is_corrupted": False, } ], } } expected_entries_count = 1 ================================================ FILE: regipy_tests/validation/validation_tests/ntuser_userassist_validation.py ================================================ from regipy.plugins.ntuser.user_assist import UserAssistPlugin from regipy_tests.validation.validation import ValidationCase class NTUserUserAssistValidationCase(ValidationCase): plugin = UserAssistPlugin test_hive_file_name = "NTUSER.DAT.xz" expected_entries = [ { "focus_count": 1, "name": "%PROGRAMFILES(X86)%\\Microsoft Office\\Office14\\EXCEL.EXE", "run_counter": 4, "session_id": 0, "timestamp": "2012-04-04T15:43:14.785000+00:00", "total_focus_time_ms": 47673, }, { "focus_count": 9, "name": "Microsoft.Windows.RemoteDesktop", "run_counter": 8, "session_id": 0, "timestamp": "2012-04-03T22:06:58.124282+00:00", "total_focus_time_ms": 180000, }, ] expected_entries_count = 62 ================================================ FILE: regipy_tests/validation/validation_tests/pagefile_plugin_validation.py ================================================ from regipy.plugins.system.pagefile import PagefilePlugin from regipy_tests.validation.validation import ValidationCase class PagefilePluginValidationCase(ValidationCase): plugin = PagefilePlugin test_hive_file_name = "SYSTEM.xz" expected_entries = [ { "key_path": "\\ControlSet001\\Control\\Session Manager\\Memory Management", "last_write": "2012-04-04T11:47:44.093750+00:00", "clear_pagefile_at_shutdown": False, "paging_files": ["?:\\pagefile.sys"], "parsed_paging_files": [{"path": "?:\\pagefile.sys"}], "existing_page_files": ["\\??\\C:\\pagefile.sys"], } ] expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/previous_winver_plugin_validation.py ================================================ from regipy.plugins.system.previous_winver import PreviousWinVersionPlugin from regipy_tests.validation.validation import ValidationCase class PreviousWinVersionPluginValidationCase(ValidationCase): plugin = PreviousWinVersionPlugin test_hive_file_name = "SYSTEM_WIN_10_1709.xz" exact_expected_result = [ { "key": "\\Setup\\Source OS (Updated on 1/6/2019 02:18:37)", "update_date": "2019-01-06 02:18:37", "BuildLab": "15063.rs2_release.170317-1834", "BuildLabEx": "15063.0.amd64fre.rs2_release.170317-1834", "CompositionEditionID": "Professional", "CurrentBuild": "15063", "CurrentBuildNumber": "15063", "CurrentVersion": "6.3", "EditionID": "Professional", "InstallationType": "Client", "InstallDate": "2017-07-12 07:18:28", "ProductId": "00330-80111-62153-AA362", "ProductName": "Windows 10 Pro", "RegisteredOrganization": 0, "RegisteredOwner": "Windows User", }, { "key": "\\Setup\\Source OS (Updated on 5/16/2019 00:55:20)", "update_date": "2019-05-16 00:55:20", "BuildLab": "17134.rs4_release.180410-1804", "BuildLabEx": "17134.1.amd64fre.rs4_release.180410-1804", "CompositionEditionID": "Enterprise", "CurrentBuild": "17134", "CurrentBuildNumber": "17134", "CurrentVersion": "6.3", "EditionID": "Professional", "InstallationType": "Client", "InstallDate": "2019-01-27 10:39:32", "ProductId": "00330-80111-62153-AA442", "ProductName": "Windows 10 Pro", "RegisteredOrganization": 0, "RegisteredOwner": "Windows User", }, ] ================================================ FILE: regipy_tests/validation/validation_tests/print_demon_plugin_validation.py ================================================ from regipy.plugins.software.printdemon import PrintDemonPlugin from regipy_tests.validation.validation import ValidationCase class PrintDemonPluginValidationCase(ValidationCase): plugin = PrintDemonPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = [ { "parameters": ["9600", "n", "8", "1"], "port_name": "COM1:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": ["9600", "n", "8", "1"], "port_name": "COM2:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": ["9600", "n", "8", "1"], "port_name": "COM3:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": ["9600", "n", "8", "1"], "port_name": "COM4:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "FILE:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "LPT1:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "LPT2:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "LPT3:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "XPSPort:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "Ne00:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "Ne01:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, { "parameters": 0, "port_name": "nul:", "timestamp": "2010-11-10T10:35:02.448040+00:00", }, ] expected_entries_count = 12 ================================================ FILE: regipy_tests/validation/validation_tests/processor_architecture_validation.py ================================================ from regipy.plugins.system.processor_architecture import ProcessorArchitecturePlugin from regipy_tests.validation.validation import ValidationCase class ProcessorArchitecturePluginValidationCase(ValidationCase): plugin = ProcessorArchitecturePlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Control\\Session Manager\\Environment": { "PROCESSOR_ARCHITECTURE": "x86", "NUMBER_OF_PROCESSORS": 49, "PROCESSOR_IDENTIFIER": "x86 Family 16 Model 8 Stepping 0, AuthenticAMD", "PROCESSOR_REVISION": "0800", }, "\\ControlSet002\\Control\\Session Manager\\Environment": { "PROCESSOR_ARCHITECTURE": "x86", "NUMBER_OF_PROCESSORS": 49, "PROCESSOR_IDENTIFIER": "x86 Family 16 Model 8 Stepping 0, AuthenticAMD", "PROCESSOR_REVISION": "0800", }, } ================================================ FILE: regipy_tests/validation/validation_tests/profile_list_plugin_validation.py ================================================ from regipy.plugins.software.profilelist import ProfileListPlugin from regipy_tests.validation.validation import ValidationCase class ProfileListPluginValidationCase(ValidationCase): plugin = ProfileListPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = [ { "last_write": "2009-07-14T04:41:12.493608+00:00", "path": "%systemroot%\\system32\\config\\systemprofile", "flags": 12, "full_profile": None, "state": 0, "sid": "S-1-5-18", "load_time": None, "local_load_time": None, }, { "last_write": "2010-11-10T18:09:16.250000+00:00", "path": "C:\\Windows\\ServiceProfiles\\LocalService", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-19", "load_time": None, "local_load_time": None, }, { "last_write": "2010-11-10T18:09:16.250000+00:00", "path": "C:\\Windows\\ServiceProfiles\\NetworkService", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-20", "load_time": None, "local_load_time": None, }, { "last_write": "2010-11-10T17:22:52.109376+00:00", "path": "C:\\Users\\Pepper", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-100689374-1717798114-2601648136-1000", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2012-04-04T12:42:17.719834+00:00", "path": "C:\\Users\\SRL-Helpdesk", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-100689374-1717798114-2601648136-1001", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2011-08-21T00:51:19.820166+00:00", "path": "C:\\Users\\nfury", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1105", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2011-08-23T01:33:29.006350+00:00", "path": "C:\\Users\\mhill", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1106", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2011-09-17T13:33:17.372366+00:00", "path": "C:\\Users\\Tdungan", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1107", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2012-04-06T19:44:17.844274+00:00", "path": "C:\\Users\\nromanoff", "flags": 0, "full_profile": None, "state": 0, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1109", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2012-04-06T19:42:31.408714+00:00", "path": "C:\\Users\\rsydow", "flags": 0, "full_profile": None, "state": 256, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1114", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, { "last_write": "2012-04-06T19:22:20.845938+00:00", "path": "C:\\Users\\vibranium", "flags": 0, "full_profile": None, "state": 256, "sid": "S-1-5-21-2036804247-3058324640-2116585241-1673", "load_time": "1601-01-01T00:00:00+00:00", "local_load_time": None, }, ] expected_entries_count = 11 ================================================ FILE: regipy_tests/validation/validation_tests/ras_tracing_plugin_validation.py ================================================ from regipy.plugins.software.tracing import RASTracingPlugin from regipy_tests.validation.validation import ValidationCase class RASTracingPluginValidationCase(ValidationCase): plugin = RASTracingPlugin test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "key": "\\Microsoft\\Tracing", "name": "AcroRd32_RASAPI32", "timestamp": "2012-03-16T21:31:26.613878+00:00", }, { "key": "\\Microsoft\\Tracing", "name": "wmplayer_RASMANCS", "timestamp": "2012-03-12T20:58:55.476336+00:00", }, ] expected_entries_count = 70 ================================================ FILE: regipy_tests/validation/validation_tests/routes_validation.py ================================================ from regipy.plugins.system.routes import RoutesPlugin from regipy_tests.validation.validation import ValidationCase class RoutesPluginValidationCase(ValidationCase): plugin = RoutesPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Services\\Tcpip\\Parameters\\PersistentRoutes": { "timestamp": "2011-09-17T13:43:23.770078+00:00", "values": [ { "name": "0.0.0.0,0.0.0.0,192.168.1.1,-1", "value": 0, "value_type": "REG_SZ", "is_corrupted": False, }, { "name": "0.0.0.0,0.0.0.0,10.3.58.1,-1", "value": 0, "value_type": "REG_SZ", "is_corrupted": False, }, ], }, "\\ControlSet002\\Services\\Tcpip\\Parameters\\PersistentRoutes": { "timestamp": "2011-09-17T13:43:23.770078+00:00", "values": [ { "name": "0.0.0.0,0.0.0.0,192.168.1.1,-1", "value": 0, "value_type": "REG_SZ", "is_corrupted": False, }, { "name": "0.0.0.0,0.0.0.0,10.3.58.1,-1", "value": 0, "value_type": "REG_SZ", "is_corrupted": False, }, ], }, } ================================================ FILE: regipy_tests/validation/validation_tests/safeboot_configuration_validation.py ================================================ from regipy.plugins.system.safeboot_configuration import SafeBootConfigurationPlugin from regipy_tests.validation.validation import ValidationCase def test_safeboot_config_result(c: ValidationCase): assert {x["name"] for x in c.plugin_output["network"]} == { "volmgrx.sys", "PlugPlay", "NetBT", "{4D36E977-E325-11CE-BFC1-08002BE10318}", "mfefirek.sys", "SWPRV", "{4D36E965-E325-11CE-BFC1-08002BE10318}", "mfefire", "WudfPf", "Dhcp", "CryptSvc", "WudfSvc", "TabletInputService", "netprofm", "ndiscap", "mfevtp", "System Bus Extender", "NetDDEGroup", "{50DD5230-BA8A-11D1-BF5D-0000F805F530}", "MCODS", "Wlansvc", "mrxsmb20", "PNP_TDI", "nsiproxy.sys", "{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}", "Messenger", "TrustedInstaller", "rdsessmgr", "{4D36E97D-E325-11CE-BFC1-08002BE10318}", "DnsCache", "{4D36E980-E325-11CE-BFC1-08002BE10318}", "dfsc", "RpcSs", "volmgr.sys", "LanmanServer", "sacsvr", "File system", "WinDefend", "McMPFSvc", "NTDS", "{4D36E973-E325-11CE-BFC1-08002BE10318}", "LmHosts", "DcomLaunch", "PNP Filter", "NlaSvc", "Power", "Base", "ProfSvc", "{71A27CDD-812A-11D0-BEC7-08002BE2092F}", "mfehidk.sys", "NetworkProvider", "mrxsmb", "NDIS Wrapper", "{4D36E96B-E325-11CE-BFC1-08002BE10318}", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "NDIS", "Network", "WudfRd", "{4D36E969-E325-11CE-BFC1-08002BE10318}", "VDS", "Boot Bus Extender", "{D48179BE-EC20-11D1-B6B8-00C04FA372A7}", "PolicyAgent", "{4D36E972-E325-11CE-BFC1-08002BE10318}", "vga.sys", "Primary disk", "LanmanWorkstation", "Ndisuio", "PCI Configuration", "Dot3Svc", "Streams Drivers", "vmms", "{4D36E96A-E325-11CE-BFC1-08002BE10318}", "{6BDD1FC1-810F-11D0-BEC7-08002BE2092F}", "{4D36E967-E325-11CE-BFC1-08002BE10318}", "TDI", "WinMgmt", "WudfUsbccidDriver", "Boot file system", "mfehidk", "Browser", "BFE", "VaultSvc", "RpcEptMapper", "NetBIOS", "vgasave.sys", "SharedAccess", "AFD", "{4D36E96F-E325-11CE-BFC1-08002BE10318}", "bowser", "MPSSvc", "rdpencdd.sys", "mrxsmb10", "{D94EE5D8-D189-4994-83D2-F68D7D41B0E6}", "KeyIso", "Netlogon", "Eaphost", "TBS", "AppMgmt", "IKEEXT", "NativeWifiP", "{4D36E974-E325-11CE-BFC1-08002BE10318}", "MPSDrv", "rdbss", "EventLog", "Tcpip", "EFS", "mfefirek", "SCardSvr", "sermouse.sys", "{4D36E975-E325-11CE-BFC1-08002BE10318}", "Filter", "AppInfo", "NetBIOSGroup", "{533C5B84-EC70-11D2-9505-00C04F79DEAF}", "ipnat.sys", "SCSI Class", "NetMan", "{36FC9E60-C465-11CF-8056-444553540000}", "HelpSvc", "Nsi", } assert {x["name"] for x in c.plugin_output["minimal"]} == { "vga.sys", "{4D36E97D-E325-11CE-BFC1-08002BE10318}", "{4D36E980-E325-11CE-BFC1-08002BE10318}", "volmgrx.sys", "Primary disk", "RpcSs", "volmgr.sys", "PlugPlay", "sacsvr", "File system", "WinDefend", "{D94EE5D8-D189-4994-83D2-F68D7D41B0E6}", "PCI Configuration", "KeyIso", "NTDS", "Netlogon", "vmms", "{4D36E977-E325-11CE-BFC1-08002BE10318}", "{4D36E96A-E325-11CE-BFC1-08002BE10318}", "SWPRV", "DcomLaunch", "PNP Filter", "Power", "{4D36E965-E325-11CE-BFC1-08002BE10318}", "Base", "AppMgmt", "TBS", "{6BDD1FC1-810F-11D0-BEC7-08002BE2092F}", "ProfSvc", "{71A27CDD-812A-11D0-BEC7-08002BE2092F}", "WudfPf", "{4D36E967-E325-11CE-BFC1-08002BE10318}", "CryptSvc", "WudfSvc", "WinMgmt", "TabletInputService", "{4D36E96B-E325-11CE-BFC1-08002BE10318}", "Boot file system", "System Bus Extender", "EventLog", "{4D36E97B-E325-11CE-BFC1-08002BE10318}", "MCODS", "EFS", "sermouse.sys", "WudfRd", "Filter", "RpcEptMapper", "AppInfo", "vgasave.sys", "{4D36E969-E325-11CE-BFC1-08002BE10318}", "{533C5B84-EC70-11D2-9505-00C04F79DEAF}", "VDS", "{745A17A0-74D3-11D0-B6FE-00A0C90F57DA}", "SCSI Class", "Boot Bus Extender", "{D48179BE-EC20-11D1-B6B8-00C04FA372A7}", "{36FC9E60-C465-11CF-8056-444553540000}", "HelpSvc", "TrustedInstaller", "{4D36E96F-E325-11CE-BFC1-08002BE10318}", } class SafeBootConfigurationPluginValidationCase(ValidationCase): plugin = SafeBootConfigurationPlugin test_hive_file_name = "SYSTEM.xz" custom_test = test_safeboot_config_result expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/samparse_plugin_validation.py ================================================ from regipy.plugins.sam.samparse import SAMParsePlugin from regipy_tests.validation.validation import ValidationCase class SAMParsePluginValidationCase(ValidationCase): plugin = SAMParsePlugin test_hive_file_name = "SAM.xz" expected_entries = [ { "key_path": "\\SAM\\Domains\\Account\\Users\\000001F4", "rid": 500, "last_write": "2014-09-24T06:32:50.378042+00:00", "last_login": "2010-11-20T21:48:12.569244+00:00", "password_last_set": "2010-11-20T21:56:34.743687+00:00", "account_expires": None, "last_failed_login": None, "account_flags": 529, "account_flags_parsed": [ "Account Disabled", "Normal User Account", "Password Does Not Expire", ], "failed_login_count": 0, "login_count": 6, }, { "key_path": "\\SAM\\Domains\\Account\\Users\\000003E8", "rid": 1000, "last_write": "2014-09-30T02:59:34.316692+00:00", "last_login": "2014-09-30T02:59:34.316693+00:00", "password_last_set": "2014-09-24T03:35:45.844801+00:00", "account_expires": None, "last_failed_login": None, "account_flags": 16, "account_flags_parsed": ["Normal User Account"], "failed_login_count": 0, "login_count": 4, }, ] expected_entries_count = 3 ================================================ FILE: regipy_tests/validation/validation_tests/services_plugin_validation.py ================================================ from regipy.plugins.system.services import ServicesPlugin from regipy_tests.validation.validation import ValidationCase def test_service(c: ValidationCase): assert c.plugin_output["\\ControlSet001\\Services"]["services"][0] == { "last_modified": "2008-10-21T17:48:29.328124+00:00", "name": "Abiosdsk", "parameters": [], "values": [ { "is_corrupted": False, "name": "ErrorControl", "value": 0, "value_type": "REG_DWORD", }, { "is_corrupted": False, "name": "Group", "value": "Primary disk", "value_type": "REG_SZ", }, { "is_corrupted": False, "name": "Start", "value": 4, "value_type": "REG_DWORD", }, { "is_corrupted": False, "name": "Tag", "value": 3, "value_type": "REG_DWORD", }, { "is_corrupted": False, "name": "Type", "value": 1, "value_type": "REG_DWORD", }, ], } class ServicesPluginValidationCase(ValidationCase): plugin = ServicesPlugin test_hive_file_name = "corrupted_system_hive.xz" custom_test = test_service expected_entries_count = 1 ================================================ FILE: regipy_tests/validation/validation_tests/shell_bag_ntuser_plugin_validation.py ================================================ import datetime as dt from regipy.plugins.ntuser.shellbags_ntuser import ShellBagNtuserPlugin from regipy_tests.validation.validation import ValidationCase class ShellBagNtuserPluginValidationCase(ValidationCase): plugin = ShellBagNtuserPlugin test_hive_file_name = "NTUSER_BAGMRU.DAT.xz" expected_entries_count = 102 expected_entries = [ { "value": "rekall", "slot": "0", "reg_path": "\\Software\\Microsoft\\Windows\\Shell\\BagMRU\\2\\0", "value_name": "0", "node_slot": "11", "shell_type": "Directory", "path": "Search Folder\\tmp\\rekall", "creation_time": dt.datetime(2021, 8, 16, 9, 41, 32).isoformat(), "full path": None, "access_time": dt.datetime(2021, 8, 16, 9, 43, 22).isoformat(), "modification_time": dt.datetime(2021, 8, 16, 9, 41, 32).isoformat(), "last_write": "2021-08-16T09:44:39.333110+00:00", "location description": None, "mru_order": "0", "mru_order_location": 0, "first_interacted": None, } ] ================================================ FILE: regipy_tests/validation/validation_tests/shell_bag_usrclass_plugin_validation.py ================================================ from regipy.plugins.usrclass.shellbags_usrclass import ShellBagUsrclassPlugin from regipy_tests.validation.validation import ValidationCase class ShellBagUsrclassPluginValidationCase(ValidationCase): plugin = ShellBagUsrclassPlugin test_hive_file_name = "UsrClass.dat.xz" expected_entries_count = 29 expected_entries = [ { "value": "Dropbox", "slot": "9", "reg_path": "\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU", "value_name": "9", "node_slot": "20", "shell_type": "Root Folder", "path": "Dropbox", "creation_time": None, "full path": None, "access_time": None, "modification_time": None, "last_write": "2018-04-05T02:13:26.843024+00:00", "location description": None, "mru_order": "4-8-7-6-9-0-1-5-3-2", "mru_order_location": 4, } ] ================================================ FILE: regipy_tests/validation/validation_tests/shimcache_validation.py ================================================ from regipy.plugins.system.shimcache import ShimCachePlugin from regipy_tests.validation.validation import ValidationCase class AmCacheValidationCase(ValidationCase): plugin = ShimCachePlugin test_hive_file_name = "SYSTEM.xz" expected_entries = [ { "last_mod_date": "2011-01-12T12:08:00+00:00", "path": "\\??\\C:\\Program Files\\McAfee\\VirusScan Enterprise\\mfeann.exe", "exec_flag": "True", } ] expected_entries_count = 660 ================================================ FILE: regipy_tests/validation/validation_tests/shutdown_validation.py ================================================ from regipy.plugins.system.shutdown import ShutdownPlugin from regipy_tests.validation.validation import ValidationCase class ShutdownPluginValidationCase(ValidationCase): plugin = ShutdownPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = { "\\ControlSet001\\Control\\Windows": { "last_write": "2012-04-04T01:58:40.839250+00:00", "date": "2012-04-04 01:58:40", }, "\\ControlSet002\\Control\\Windows": { "last_write": "2012-04-04T01:58:40.839250+00:00", "date": "2012-04-04 01:58:40", }, } ================================================ FILE: regipy_tests/validation/validation_tests/software_classes_installer_plugin_validation.py ================================================ from regipy.plugins.software.classes_installer import SoftwareClassesInstallerPlugin from regipy_tests.validation.validation import ValidationCase def test_no_hidden_entries(c: ValidationCase): assert not any(x["is_hidden"] for x in c.plugin_output) class SoftwareClassesInstallerPluginValidationCase(ValidationCase): plugin = SoftwareClassesInstallerPlugin test_hive_file_name = "SOFTWARE.xz" expected_entries = [ { "identifier": "000041091A0090400000000000F01FEC", "is_hidden": False, "product_name": "Microsoft Office OneNote MUI (English) 2010", "timestamp": "2010-11-10T10:31:06.573040+00:00", } ] expected_entries_count = 26 custom_test = test_no_hidden_entries ================================================ FILE: regipy_tests/validation/validation_tests/software_persistence_validation.py ================================================ from regipy.plugins.software.persistence import SoftwarePersistencePlugin from regipy_tests.validation.validation import ValidationCase class SoftwarePersistenceValidationCase(ValidationCase): plugin = SoftwarePersistencePlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "\\Microsoft\\Windows\\CurrentVersion\\Run": { "timestamp": "2012-04-04T01:54:23.669836+00:00", "values": [ { "name": "VMware Tools", "value_type": "REG_SZ", "value": '"C:\\Program Files\\VMware\\VMware Tools\\VMwareTray.exe"', "is_corrupted": False, }, { "name": "VMware User Process", "value_type": "REG_SZ", "value": '"C:\\Program Files\\VMware\\VMware Tools\\VMwareUser.exe"', "is_corrupted": False, }, { "name": "Adobe ARM", "value_type": "REG_SZ", "value": '"C:\\Program Files\\Common Files\\Adobe\\ARM\\1.0\\AdobeARM.exe"', "is_corrupted": False, }, { "name": "McAfeeUpdaterUI", "value_type": "REG_SZ", "value": '"C:\\Program Files\\McAfee\\Common Framework\\udaterui.exe" /StartedFromRunKey', "is_corrupted": False, }, { "name": "ShStatEXE", "value_type": "REG_SZ", "value": '"C:\\Program Files\\McAfee\\VirusScan Enterprise\\SHSTAT.EXE" /STANDALONE', "is_corrupted": False, }, { "name": "McAfee Host Intrusion Prevention Tray", "value_type": "REG_SZ", "value": '"C:\\Program Files\\McAfee\\Host Intrusion Prevention\\FireTray.exe"', "is_corrupted": False, }, { "name": "svchost", "value_type": "REG_SZ", "value": "c:\\windows\\system32\\dllhost\\svchost.exe", "is_corrupted": False, }, ], } } expected_entries_count = 1 ================================================ FILE: regipy_tests/validation/validation_tests/spp_clients_plugin_validation.py ================================================ from regipy.plugins.software.spp_clients import SppClientsPlugin from regipy_tests.validation.validation import ValidationCase class SppClientsPluginValidationCase(ValidationCase): plugin = SppClientsPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "\\Microsoft\\Windows NT\\CurrentVersion\\SPP\\Clients": { "last_write": "2012-03-15T22:32:18.089574+00:00", "{09F7EDC5-294E-4180-AF6A-FB0E6A0E9513}": ["\\\\?\\Volume{656b1715-ecf6-11df-92e6-806e6f6e6963}\\:(C:)"], } } ================================================ FILE: regipy_tests/validation/validation_tests/susclient_plugin_validation.py ================================================ from regipy.plugins.software.susclient import SusclientPlugin from regipy_tests.validation.validation import ValidationCase class SusclientPluginValidationCase(ValidationCase): plugin = SusclientPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate": { "last_write": "2012-03-14T07:05:41.719626+00:00", "SusClientId": "50df98f2-964a-496d-976d-d95296e13929", "SusClientIdValidation": "", "LastRestorePointSetTime": "2012-03-14 07:05:41", } } ================================================ FILE: regipy_tests/validation/validation_tests/terminal_services_history_validation.py ================================================ from regipy.plugins.ntuser.tsclient import TSClientPlugin from regipy_tests.validation.validation import ValidationCase class TSClientPluginValidationCase(ValidationCase): plugin = TSClientPlugin # TDODO: Find registry sample with test data test_hive_file_name = "NTUSER_modified.DAT.xz" # For now, bypass validation with this ugly trick: exact_expected_result = None expected_entries_count = 0 ================================================ FILE: regipy_tests/validation/validation_tests/timezone_data2_validation.py ================================================ from regipy.plugins.system.timezone_data2 import TimezoneDataPlugin2 from regipy_tests.validation.validation import ValidationCase def test_tz2_plugin_output(c: ValidationCase): assert c.plugin_output["\\ControlSet001\\Control\\TimeZoneInformation"] == { "last_write": "2012-03-11T07:00:00.000642+00:00", "Bias": 300, "DaylightBias": -60, "DaylightName": "@tzres.dll,-111", "DaylightStart": "00000300020002000000000000000000", "StandardBias": 0, "StandardName": "@tzres.dll,-112", "StandardStart": "00000b00010002000000000000000000", # UTF-16-LE "Eastern Standard Time" followed by garbage bytes, converted to hex "TimeZoneKeyName": "4500610073007400650072006e0020005300740061006e0064006100720064002000540069006d00650000001963747614060136f408f10100000000000000000000000000000000940af101a80af10102000000040bf101000000007c0bf10100000000c80bf1010000000000000000d007f10110df0900d8717476d007f1010000000084e00900fb9373760200000000000000e98b73761406013610000000002000000200000000000000010000000000000000000000f4df090010a6f101000000000000000013000000304514001406013641919676a80af1012f000000d007f1010000000000000000020000000e0000001051140000000e00a0511400", "DynamicDaylightTimeDisabled": 0, "ActiveTimeBias": 240, } class TimezoneDataPlugin2ValidationCase(ValidationCase): plugin = TimezoneDataPlugin2 test_hive_file_name = "SYSTEM.xz" custom_test = test_tz2_plugin_output expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/timezone_data_validation.py ================================================ from regipy.plugins.system.timezone_data import TimezoneDataPlugin from regipy_tests.validation.validation import ValidationCase def test_timezone_data(c: ValidationCase): assert {(x["name"], x["value"]) for x in c.plugin_output["\\ControlSet001\\Control\\TimeZoneInformation"]} == { ("DaylightBias", 4294967236), ("Bias", 300), ("StandardBias", 0), ( "TimeZoneKeyName", "4500610073007400650072006e0020005300740061006e00640061007200640020" "00540069006d00650000001963747614060136f408f101000000000000000000000" "00000000000940af101a80af10102000000040bf101000000007c0bf10100000000" "c80bf1010000000000000000d007f10110df0900d8717476d007f101", ), ("ActiveTimeBias", 240), ("StandardStart", "00000b00010002000000000000000000"), ("DaylightStart", "00000300020002000000000000000000"), ("StandardName", "@tzres.dll,-112"), ("DynamicDaylightTimeDisabled", 0), ("DaylightName", "@tzres.dll,-111"), } class TimezoneDataPluginValidationCase(ValidationCase): plugin = TimezoneDataPlugin test_hive_file_name = "SYSTEM.xz" custom_test = test_timezone_data expected_entries_count = 2 ================================================ FILE: regipy_tests/validation/validation_tests/typed_paths_plugin_validation.py ================================================ from regipy.plugins.ntuser.typed_paths import TypedPathsPlugin from regipy_tests.validation.validation import ValidationCase class TypedPathsPluginValidationCase(ValidationCase): plugin = TypedPathsPlugin test_hive_file_name = "NTUSER_BAGMRU.DAT.xz" exact_expected_result = { "last_write": "2022-02-06T13:46:04.945080+00:00", "entries": [ {"url1": "cmd"}, {"url2": "C:\\Offline\\AD"}, {"url3": "git"}, {"url4": "powershell"}, {"url5": "C:\\Program Files"}, {"url6": "Network"}, {"url7": "\\\\wsl$\\Ubuntu\\projects\\CAD316_001\\partition_p1"}, {"url8": "\\\\wsl$\\Ubuntu\\projects"}, {"url9": "\\\\wsl$\\Ubuntu"}, {"url10": "C:\\Users\\tony\\Github"}, {"url11": "C:\\Users\\tony\\Github\\velocity-client-master"}, {"url12": "C:\\Users\\tony\\Github\\cogz"}, {"url13": "C:\\Users\\tony\\Github\\cogz\\cogz"}, {"url14": "Quick access"}, {"url15": "C:\\ProgramData\\chocolatey\\lib\\yara\\tools"}, {"url16": "C:\\Training\\MT01\\exercise"}, ], } ================================================ FILE: regipy_tests/validation/validation_tests/typed_urls_plugin_validation.py ================================================ from regipy.plugins.ntuser.typed_urls import TypedUrlsPlugin from regipy_tests.validation.validation import ValidationCase class TypedUrlsPluginValidationCase(ValidationCase): plugin = TypedUrlsPlugin test_hive_file_name = "NTUSER.DAT.xz" exact_expected_result = { "last_write": "2012-04-03T22:37:55.411500+00:00", "entries": [ {"url1": "http://199.73.28.114:53/"}, {"url2": "http://go.microsoft.com/fwlink/?LinkId=69157"}, ], } ================================================ FILE: regipy_tests/validation/validation_tests/uac_status_plugin_validation.py ================================================ from regipy.plugins.software.uac import UACStatusPlugin from regipy_tests.validation.validation import ValidationCase class UACStatusPluginValidationCase(ValidationCase): plugin = UACStatusPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "consent_prompt_admin": 5, "consent_prompt_user": 3, "enable_limited_user_accounts": 1, "enable_virtualization": 1, "filter_admin_token": 0, "last_write": "2011-08-30T18:47:10.734144+00:00", } ================================================ FILE: regipy_tests/validation/validation_tests/usb_devices_plugin_validation.py ================================================ from regipy.plugins.system.usb_devices import USBDevicesPlugin from regipy_tests.validation.validation import ValidationCase class USBDevicesPluginValidationCase(ValidationCase): plugin = USBDevicesPlugin test_hive_file_name = "SYSTEM.xz" expected_entries = [ { "key_path": "\\ControlSet001\\Enum\\USB\\ROOT_HUB\\5&391b2433&0", "vid_pid": "ROOT_HUB", "vid": None, "pid": None, "instance_id": "5&391b2433&0", "last_write": "2012-04-07T10:31:37.625246+00:00", "hardware_id": [ "USB\\ROOT_HUB&VID8086&PID7112&REV0000", "USB\\ROOT_HUB&VID8086&PID7112", "USB\\ROOT_HUB", ], "container_id": "{00000000-0000-0000-ffff-ffffffffffff}", "service": "usbhub", "class_guid": "{36fc9e60-c465-11cf-8056-444553540000}", "driver": "{36fc9e60-c465-11cf-8056-444553540000}\\0002", "class": "USB", "manufacturer": "(Standard USB Host Controller)", "device_desc": "USB Root Hub", } ] expected_entries_count = 14 ================================================ FILE: regipy_tests/validation/validation_tests/usbstor_plugin_validation.py ================================================ from regipy.plugins.system.usbstor import USBSTORPlugin from regipy_tests.validation.validation import ValidationCase class USBSTORPluginValidationCase(ValidationCase): plugin = USBSTORPlugin test_hive_file_name = "SYSTEM_WIN_10_1709.xz" exact_expected_result = [ { "device_name": "SanDisk Cruzer USB Device", "disk_guid": "{fc416b61-6437-11ea-bd0c-a483e7c21469}", "first_installed": "2020-03-17T14:02:38.955490+00:00", "key_path": "\\ControlSet001\\Enum\\USBSTOR\\Disk&Ven_SanDisk&Prod_Cruzer&Rev_1.20\\200608767007B7C08A6A&0", "last_connected": "2020-03-17T14:02:38.946628+00:00", "last_installed": "2020-03-17T14:02:38.955490+00:00", "last_removed": "2020-03-17T14:23:45.504690+00:00", "last_write": "2020-03-17T14:02:38.965050+00:00", "manufacturer": "Ven_SanDisk", "serial_number": "200608767007B7C08A6A&0", "title": "Prod_Cruzer", "version": "Rev_1.20", } ] ================================================ FILE: regipy_tests/validation/validation_tests/wdigest_plugin_validation.py ================================================ from regipy.plugins.system.wdigest import WDIGESTPlugin from regipy_tests.validation.validation import ValidationCase class WDIGESTPluginValidationCase(ValidationCase): plugin = WDIGESTPlugin test_hive_file_name = "SYSTEM.xz" exact_expected_result = [ { "subkey": "\\ControlSet001\\Control\\SecurityProviders\\WDigest", "timestamp": "2009-07-14T04:37:09.491968+00:00", "use_logon_credential": 1, }, { "subkey": "\\ControlSet002\\Control\\SecurityProviders\\WDigest", "timestamp": "2009-07-14T04:37:09.491968+00:00", "use_logon_credential": None, }, ] ================================================ FILE: regipy_tests/validation/validation_tests/windows_defender_plugin_validation.py ================================================ from regipy.plugins.software.defender import WindowsDefenderPlugin from regipy_tests.validation.validation import ValidationCase class WindowsDefenderPluginValidationCase(ValidationCase): plugin = WindowsDefenderPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = [ { "type": "configuration", "key_path": "\\Microsoft\\Windows Defender", "last_write": "2011-09-16T20:48:31.741870+00:00", "antispyware_disabled": True, "product_status": 0, } ] ================================================ FILE: regipy_tests/validation/validation_tests/winrar_plugin_validation.py ================================================ from regipy.plugins.ntuser.winrar import WinRARPlugin from regipy_tests.validation.validation import ValidationCase class WinRARPluginValidationCase(ValidationCase): plugin = WinRARPlugin test_hive_file_name = "NTUSER.DAT.xz" exact_expected_result = [ { "last_write": "2021-11-18T13:59:04.888952+00:00", "file_path": "C:\\Users\\tony\\Downloads\\RegistryFinder64.zip", "operation": "archive_opened", "value_name": "0", }, { "last_write": "2021-11-18T13:59:04.888952+00:00", "file_path": "C:\\temp\\token.zip", "operation": "archive_opened", "value_name": "1", }, { "last_write": "2021-11-18T13:59:50.023788+00:00", "file_name": "Tools.zip", "operation": "archive_created", "value_name": "0", }, { "last_write": "2021-11-18T13:59:50.023788+00:00", "file_name": "data.zip", "operation": "archive_created", "value_name": "1", }, { "last_write": "2021-11-18T14:00:44.180468+00:00", "file_path": "C:\\Users\\tony\\Downloads", "operation": "archive_extracted", "value_name": "0", }, { "last_write": "2021-11-18T14:00:44.180468+00:00", "file_path": "C:\\temp", "operation": "archive_extracted", "value_name": "1", }, ] ================================================ FILE: regipy_tests/validation/validation_tests/winscp_saved_sessions_plugin_validation.py ================================================ from regipy.plugins.ntuser.winscp_saved_sessions import WinSCPSavedSessionsPlugin from regipy_tests.validation.validation import ValidationCase class WinSCPSavedSessionsPluginValidationCase(ValidationCase): plugin = WinSCPSavedSessionsPlugin test_hive_file_name = "NTUSER_with_winscp.DAT.xz" expected_entries_count = 2 expected_entries = [ { "timestamp": "2022-04-25T09:53:58.125852+00:00", "hive_name": "HKEY_CURRENT_USER", "key_path": "HKEY_CURRENT_USER\\Software\\Martin Prikryl\\WinSCP 2\\Sessions\\Default%20Settings", } ] ================================================ FILE: regipy_tests/validation/validation_tests/winver_plugin_validation.py ================================================ from regipy.plugins.software.winver import WinVersionPlugin from regipy_tests.validation.validation import ValidationCase class WinVersionPluginValidationCase(ValidationCase): plugin = WinVersionPlugin test_hive_file_name = "SOFTWARE.xz" exact_expected_result = { "\\Microsoft\\Windows NT\\CurrentVersion": { "last_write": "2012-03-14T07:09:21.562500+00:00", "CurrentVersion": "6.1", "CurrentBuild": "7601", "InstallDate": "2010-11-10 16:28:55", "RegisteredOrganization": 0, "RegisteredOwner": "Windows User", "InstallationType": "Client", "EditionID": "Ultimate", "ProductName": "Windows 7 Ultimate", "ProductId": "00426-067-1817155-86250", "CurrentBuildNumber": "7601", "BuildLab": "7601.win7sp1_gdr.111118-2330", "BuildLabEx": "7601.17727.x86fre.win7sp1_gdr.111118-2330", "CSDVersion": "Service Pack 1", } } ================================================ FILE: regipy_tests/validation/validation_tests/word_wheel_query_ntuser_validation.py ================================================ from regipy.plugins.ntuser.word_wheel_query import WordWheelQueryPlugin from regipy_tests.validation.validation import ValidationCase class WordWheelQueryPluginValidationCase(ValidationCase): plugin = WordWheelQueryPlugin test_hive_file_name = "NTUSER.DAT.xz" expected_entries = [ { "last_write": "2012-04-04T15:45:18.551340+00:00", "mru_id": 1, "name": "alloy", "order": 0, } ] expected_entries_count = 6 ================================================ FILE: regipy_tests/validation/validation_tests/wsl_plugin_validation.py ================================================ from regipy.plugins.ntuser.wsl import WSLPlugin from regipy_tests.validation.validation import ValidationCase class WSLPluginValidationCase(ValidationCase): plugin = WSLPlugin test_hive_file_name = "NTUSER-WSL.DAT.xz" exact_expected_result = { "\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss": { "last_modified": "2024-11-26T23:13:44.535966+00:00", "number_of_distrib": 4, "default_distrib_GUID": "{e3986a51-3357-4c37-8a3e-1a83d995a3da}", "wsl_version": "WSL2", "nat_ip_address": None, "distributions": [ { "GUID": "AppxInstallerCache", "last_modified": "2024-11-26T22:25:52.048876+00:00", "wsl_distribution_source_location": None, "default_uid": None, "distribution_name": None, "default_environment": None, "flags": None, "kernel_command_line": None, "package_family_name": None, "state": None, "filesystem": "Unknown", }, { "GUID": "{17a1ba00-e5d7-44ba-af5f-194a2677fbb6}", "last_modified": "2024-11-26T23:13:35.817050+00:00", "wsl_distribution_source_location": "C:\\Users\\admin\\AppData\\Local\\Packages\\TheDebianProject.DebianGNULinux_76v4gfsz19hv4\\LocalState", "default_uid": 1000, "distribution_name": "Debian", "default_environment": [ "HOSTTYPE=x86_64", "LANG=en_US.UTF-8", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games", "TERM=xterm-256color", ], "flags": 7, "kernel_command_line": "BOOT_IMAGE=/kernel init=/init", "package_family_name": "TheDebianProject.DebianGNULinux_76v4gfsz19hv4", "state": "Normal", "filesystem": "wslfs", "enable_interop": True, "append_nt_path": True, "enable_drive_mounting": True, }, { "GUID": "{3f702114-dc8d-44dc-9903-642eb650ec4b}", "last_modified": "2024-11-26T23:12:56.145376+00:00", "wsl_distribution_source_location": "C:\\Users\\admin\\AppData\\Local\\Packages\\CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc\\LocalState", "default_uid": 1000, "distribution_name": "Ubuntu", "default_environment": [ "HOSTTYPE=x86_64", "LANG=en_US.UTF-8", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games", "TERM=xterm-256color", ], "flags": 7, "kernel_command_line": "BOOT_IMAGE=/kernel init=/init", "package_family_name": "CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc", "state": "Normal", "filesystem": "wslfs", "enable_interop": True, "append_nt_path": True, "enable_drive_mounting": True, }, { "GUID": "{e3986a51-3357-4c37-8a3e-1a83d995a3da}", "last_modified": "2024-11-26T23:06:39.553058+00:00", "wsl_distribution_source_location": "C:\\Users\\admin\\AppData\\Local\\Packages\\CanonicalGroupLimited.Ubuntu20.04onWindows_79rhkp1fndgsc\\LocalState", "default_uid": 0, "distribution_name": "Ubuntu-20.04", "default_environment": None, "flags": 7, "kernel_command_line": None, "package_family_name": "CanonicalGroupLimited.Ubuntu20.04onWindows_79rhkp1fndgsc", "state": "Normal", "filesystem": "wslfs", "enable_interop": True, "append_nt_path": True, "enable_drive_mounting": True, }, ], } } ================================================ FILE: renovate.json ================================================ { "extends": [ "config:base" ] } ================================================ FILE: requirements.txt ================================================ construct==2.10.70 attrs>=21.4.0 click==8.1.8 inflection==0.5.1 pytz tabulate==0.9.0 pytest==8.4.2 libfwsi-python==20240423 libfwps-python==20240417