From 5e53bcf4366a3d5e5626df7059351528333a5ffd Mon Sep 17 00:00:00 2001 From: Br4guette <92679326+Ston14@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:23:58 +0200 Subject: [PATCH 1/2] Dev (#51) * Enhance documentation for the project (#41) Co-authored-by: Br4guette * Add build docs * Add more docs (#42) * Enhance documentation for the project * Update docs --------- Co-authored-by: Br4guette * Add references (#43) * Enhance documentation for the project * Update docs * add references --------- Co-authored-by: Br4guette * Add references * Add title * Title 2 * add : mkdocs navigation references * document codes * fix typo * typo * fix typo * fix typo in utils * fix docs in tutorials.md * add : How to use documentation rename : tuto to installation add : documentation for windows fix : mkdocs add paths * Add other OS documentation * fix how to * fix : typo in docs * fix : typo * fix: typo in index * add : dark mode * add : colors on documentation * fix: diataxis * Fix colors * fix readme * Fix dumpfiles (#46) * fix : Dumpfile issue with parameters * test : Create test for dumpfiles * fix context builder * fix dumpfiles : dumpfiles can now be passed with arguments * tests: add tests for each parameters of dumpfiles fix: add markers on tests to easily execute a bunch of test instead of the complete file * fix : kwargs value in set_arguments was setted to int directly * add : Add test fonctions to test dumpfiles with a virtaddr but not able to test locally * add : add pytest decorator markers to pslist_pid --------- Co-authored-by: Br4guette * fix: fix error, function without parameter return an error * sorry * fix typing information (#47) * Fix/get plugins (#48) * fix : bad import on v3_plugins_mod fix : poetry lock modfied due to update dependacies fix : windows setargs * remover useless info --------- Co-authored-by: Br4guette * Fix: Correct dict.get() usage in TreeGrid_to_json renderer and remove debug print - Corrected the usage of dict.get() method by removing keyword arguments and using positional arguments instead. - Ensured the render method returns a dictionary as expected. - Updated the to_list method to properly call the render method and handle exceptions. - Improved the docstrings to reflect the correct return types and behaviors of the methods. - Removed a debug print statement introduced in a previous commit. This fixes the TypeError and ensures the TreeGrid is properly rendered to JSON format. * oops * fix to dataframe * add test for volatility (#49) Co-authored-by: Br4guette --------- Co-authored-by: Br4guette Co-authored-by: St0n14 Co-authored-by: Yann MAGNIN <42215723+YannMagnin@users.noreply.github.com> --- .github/workflows/mkdocs.yml | 1 + README.md | 2 +- docs/{tutorials.md => Usage/installation.md} | 15 +- docs/Usage/linux.md | 58 +++ docs/Usage/usage.md | 0 docs/Usage/windows.md | 45 ++ docs/how-to-guides.md | 1 - docs/index.md | 5 +- docs/reference/base.md | 3 + docs/reference/handler.md | 3 + docs/reference/reference.md | 9 +- docs/reference/renderer.md | 3 + docs/reference/test.md | 53 ++ docs/reference/utils.md | 3 + docs/reference/windows.md | 3 + docs/test.md | 8 - mkdocs.yml | 31 +- poetry.lock | 206 ++++---- pydfirram/__init__.py | 4 +- pydfirram/core/__init__.py | 3 + pydfirram/core/base.py | 316 +++++++----- pydfirram/core/handler.py | 43 +- pydfirram/core/renderer.py | 166 +++++-- pydfirram/core/utils.py | 20 +- pydfirram/modules/__init__.py | 3 + pydfirram/modules/windows.py | 134 ++++- pydfirram/py.typed | 3 + pyproject.toml | 8 +- tests/config.py | 2 +- tests/test_volatility_windows_function.py | 487 ++++++++++++++++++- 30 files changed, 1300 insertions(+), 338 deletions(-) rename docs/{tutorials.md => Usage/installation.md} (58%) create mode 100644 docs/Usage/linux.md create mode 100644 docs/Usage/usage.md create mode 100644 docs/Usage/windows.md delete mode 100644 docs/how-to-guides.md create mode 100644 docs/reference/base.md create mode 100644 docs/reference/handler.md create mode 100644 docs/reference/renderer.md create mode 100644 docs/reference/test.md create mode 100644 docs/reference/utils.md create mode 100644 docs/reference/windows.md delete mode 100644 docs/test.md create mode 100644 pydfirram/py.typed diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 0d1f86c..d81b17b 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - dev jobs: deploy: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 6ff634a..599663d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ print(output.to_df()) print(win.pslist().to_json()) ``` -All supported features are documented, check it out on [our documentation](https://pydfir.github.io/pyDFIRRam/) ! +All supported features are documented, check it out on [our documentation](https://pydfir.github.io/pyDFIRRam) ! ## Objectives diff --git a/docs/tutorials.md b/docs/Usage/installation.md similarity index 58% rename from docs/tutorials.md rename to docs/Usage/installation.md index 91ec975..e1f60ba 100644 --- a/docs/tutorials.md +++ b/docs/Usage/installation.md @@ -3,10 +3,9 @@ ## Quick installation ### Prerequisites -Install python3.10 -TODO -Install : poetry -TODO +- Python +- Poetry (for dev) +- pip ### From source On a standard Linux distribution : @@ -15,8 +14,14 @@ git clone https://github.com/pydfir/pydfirram poetry shell poetry install ``` -### From pip +### From pip stable ```shell pip install pydfirram ``` +### From pip dev + +```bash +pip install -i https://test.pypi.org/simple/ pydfirram +``` + diff --git a/docs/Usage/linux.md b/docs/Usage/linux.md new file mode 100644 index 0000000..9c87cf3 --- /dev/null +++ b/docs/Usage/linux.md @@ -0,0 +1,58 @@ +## Using pyDFIRRam for Linux or macOS + +### Introduction + +`pyDFIRRam` is a tool under development aimed at utilizing Volatility plugins for memory forensics on Linux and macOS systems. + +### Initial Setup + +1. **Installation**: + - Ensure Python 3.10 (or compatible version) is installed. + - Install `pyDFIRRam` using Poetry or manually. Example: + ``` + pip install pydfirram + ``` + +2. **Setting up a Profile**: + - Currently, there's no direct method via Python interface to add a profile. If you have a profile, place it in the Volatility symbols directory: + - For Linux/macOS: + ``` + $HOME/.local/lib/python3.10/site-packages/volatility3/symbols/ + ``` + - For Poetry virtual environments: + ``` + $HOME/.cache/pypoetry/virtualenvs/pydfirram-qv9SWnlF-py3.10/lib/python3.10/site-packages/volatility3/symbols/ + ``` + +### Using pyDFIRRam + +3. **Creating an Object**: + - Import necessary modules and create an object for your memory dump: + ```python + from pydfirram.core.base import Generic, OperatingSystem + from pathlib import Path + + os = OperatingSystem.LINUX # Set to OperatingSystem.MACOS for macOS + dumpfile = Path("dump.raw") # Replace with your actual memory dump path + generic = Generic(os, dumpfile) + ``` + +4. **Listing Available Functions**: + - To list all available Volatility plugins: + ```python + generic.get_all_plugins() + ``` + +5. **Using Plugins**: + - Refer to Volatility plugin documentation for parameters. Example using `pslist` plugin: + ```python + generic.pslist(pid=[4]).to_list() + ``` + +6. **Formatting Output**: + - The return from Volatility functions provides a `Rendering` class, allowing customization of output format. + +### Notes + +- Ensure your memory dump file (`dump.raw` in the example) is correctly specified. +- Adjust paths and settings based on your specific environment and Python setup. diff --git a/docs/Usage/usage.md b/docs/Usage/usage.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/Usage/windows.md b/docs/Usage/windows.md new file mode 100644 index 0000000..2e05f69 --- /dev/null +++ b/docs/Usage/windows.md @@ -0,0 +1,45 @@ +# How to Use pyDFIRRam for Windows + +This guide provides a brief and concise demonstration of how to use the pyDFIRRam tool for Windows. + +## Introduction + +Currently, the project is under development. To use the Volatility-related functions for Windows, follow these steps: + +### Initial Setup + +First, create an object for your memory dump: + +```python +from pydfirram.modules.windows import Windows +from pathlib import Path + +dump = Path("/home/dev/image.dump") +win = Windows(dump) +``` + +### Listing Available Functions + +The available functions are all the Volatility plugins (located in the Volatility plugin path). + +To list all available functions: + +```python +win.get_all_plugins() +``` + +You can use this function to retrieve all the plugins. + +### Using Parameters + +If you want to use Volatility parameters, refer to the plugin documentation. The parameters expected are generally the same with the same names. + +For example, to use the `pslist` plugin with a parameter: + +```python +win.pslist(pid=4).to_list() +``` + +### Note + +On the return of the Volatility functions, a `Rendering` class is retrieved. This allows us to format our output as desired. \ No newline at end of file diff --git a/docs/how-to-guides.md b/docs/how-to-guides.md deleted file mode 100644 index ffdccb3..0000000 --- a/docs/how-to-guides.md +++ /dev/null @@ -1 +0,0 @@ -# How to use pydfirram diff --git a/docs/index.md b/docs/index.md index 31166c0..29038b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,11 +8,10 @@ project documentation as described by Daniele Procida in the [Diátaxis documentation framework](https://diataxis.fr/) and consists of four separate parts: -1. [Tutorials](tutorials.md) -2. [How-To Guides](how-to-guides.md) +1. [Tutorials](./Usage/installation.md) +2. [How-To Guides](./Usage/usage.md) 3. [Reference](reference/reference.md) 4. [Explanation](explanation.md) -5. [Test](test.md) Quickly find what you're looking for depending on your use case by looking at the different pages. diff --git a/docs/reference/base.md b/docs/reference/base.md new file mode 100644 index 0000000..f06084c --- /dev/null +++ b/docs/reference/base.md @@ -0,0 +1,3 @@ +## Base + +::: pydfirram.core.base \ No newline at end of file diff --git a/docs/reference/handler.md b/docs/reference/handler.md new file mode 100644 index 0000000..573c337 --- /dev/null +++ b/docs/reference/handler.md @@ -0,0 +1,3 @@ +## Handler + +::: pydfirram.core.handler \ No newline at end of file diff --git a/docs/reference/reference.md b/docs/reference/reference.md index ce23b32..e2a7ced 100644 --- a/docs/reference/reference.md +++ b/docs/reference/reference.md @@ -1,5 +1,4 @@ - - + \ No newline at end of file diff --git a/docs/reference/renderer.md b/docs/reference/renderer.md new file mode 100644 index 0000000..66c3187 --- /dev/null +++ b/docs/reference/renderer.md @@ -0,0 +1,3 @@ +## Renderer + +::: pydfirram.core.renderer \ No newline at end of file diff --git a/docs/reference/test.md b/docs/reference/test.md new file mode 100644 index 0000000..490de9a --- /dev/null +++ b/docs/reference/test.md @@ -0,0 +1,53 @@ +# Test Documentation + +## Project Structure +The project is organized as follows: +```bash +. +├── __init__.py +├── config.py +├── data +│   └── dump.raw +├── test_core_base.py +├── test_core_rendering.py +└── test_volatility_windows_function.py +``` + +### Files Description + +- **config.py** + This file contains configuration settings. You need to set the path of your dump file here before running the tests. + +- **test_core_base.py** + This script tests the core functionalities used in `pydfirram/core/base.py`. + +- **test_core_rendering.py** + This script tests the core functionalities used in `pydfirram/core/renderer.py`. + +- **test_volatility_windows_function.py** + This script tests all(Not All configuration an plugins for the moment) plugins of Volatility. + +### Test Data +- **data/dump.raw** + This is where your test dump file should be located. + +## Running the Tests + +### Prerequisites +1. Download the Windows XP image from the Volatility Foundation: + [Win XP Image](https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz). + +2. Extract the downloaded image and place it in the `data` directory. Rename it to `dump.raw`. + +### Configuration +1. Open `config.py`. +2. Set the path of your dump file in the configuration. + +### Running the Tests +To run the tests, use the following command: +```bash +pytest +``` + +## Notes +- The current tests only support Windows architectures. Linux architectures are not supported yet. diff --git a/docs/reference/utils.md b/docs/reference/utils.md new file mode 100644 index 0000000..b282a47 --- /dev/null +++ b/docs/reference/utils.md @@ -0,0 +1,3 @@ +## Utils + +::: pydfirram.core.utils \ No newline at end of file diff --git a/docs/reference/windows.md b/docs/reference/windows.md new file mode 100644 index 0000000..333fcf3 --- /dev/null +++ b/docs/reference/windows.md @@ -0,0 +1,3 @@ +## Windows + +::: pydfirram.modules.windows \ No newline at end of file diff --git a/docs/test.md b/docs/test.md deleted file mode 100644 index b6fcf52..0000000 --- a/docs/test.md +++ /dev/null @@ -1,8 +0,0 @@ -# How to run test - -First of all you need to set the path of your dump file in test/config.py - -to run the tests : -```bash -pytest -``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 848b5f1..a6eb0f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,22 @@ repo_url: https://github.com/PyDFIR/PyDFIRRam edit_uri: edit/main/docs/ theme: + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode name: material + color_mode: auto + user_color_mode_toggle: true + locale: en features: - search.suggest - search.highlight @@ -39,11 +54,19 @@ markdown_extensions: nav: - index.md - - tutorials.md - - how-to-guides.md + - explanation.md + - Usage: + - Installation : Usage/installation.md + - Windows : Usage/windows.md + - Linux/Mac : Usage/linux.md - Reference: - Index: reference/reference.md - - explanation.md + - Base: reference/base.md + - Handler: reference/handler.md + - Renderer: reference/renderer.md + - Utils: reference/utils.md + - Windows : reference/windows.md + - Testing : reference/test.md extra: version: @@ -53,4 +76,4 @@ extra: link: https://github.com/PyDFIR/pyDFIRRam name: Github - icon: material/email - link: "mailto:alexis.debrito@ecole2600.com" + link: "mailto:alexis.debrito@ecole2600.com" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 57e69a1..4594282 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2460,114 +2460,110 @@ files = [ [[package]] name = "rpds-py" -version = "0.19.1" +version = "0.19.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:aaf71f95b21f9dc708123335df22e5a2fef6307e3e6f9ed773b2e0938cc4d491"}, - {file = "rpds_py-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca0dda0c5715efe2ab35bb83f813f681ebcd2840d8b1b92bfc6fe3ab382fae4a"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81db2e7282cc0487f500d4db203edc57da81acde9e35f061d69ed983228ffe3b"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8dfa125b60ec00c7c9baef945bb04abf8ac772d8ebefd79dae2a5f316d7850"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271accf41b02687cef26367c775ab220372ee0f4925591c6796e7c148c50cab5"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9bc4161bd3b970cd6a6fcda70583ad4afd10f2750609fb1f3ca9505050d4ef3"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0cf2a0dbb5987da4bd92a7ca727eadb225581dd9681365beba9accbe5308f7d"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b5e28e56143750808c1c79c70a16519e9bc0a68b623197b96292b21b62d6055c"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c7af6f7b80f687b33a4cdb0a785a5d4de1fb027a44c9a049d8eb67d5bfe8a687"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e429fc517a1c5e2a70d576077231538a98d59a45dfc552d1ac45a132844e6dfb"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2dbd8f4990d4788cb122f63bf000357533f34860d269c1a8e90ae362090ff3a"}, - {file = "rpds_py-0.19.1-cp310-none-win32.whl", hash = "sha256:e0f9d268b19e8f61bf42a1da48276bcd05f7ab5560311f541d22557f8227b866"}, - {file = "rpds_py-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:df7c841813f6265e636fe548a49664c77af31ddfa0085515326342a751a6ba51"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:902cf4739458852fe917104365ec0efbea7d29a15e4276c96a8d33e6ed8ec137"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d73022990ab0c8b172cce57c69fd9a89c24fd473a5e79cbce92df87e3d9c48"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3837c63dd6918a24de6c526277910e3766d8c2b1627c500b155f3eecad8fad65"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cdb7eb3cf3deb3dd9e7b8749323b5d970052711f9e1e9f36364163627f96da58"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26ab43b6d65d25b1a333c8d1b1c2f8399385ff683a35ab5e274ba7b8bb7dc61c"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75130df05aae7a7ac171b3b5b24714cffeabd054ad2ebc18870b3aa4526eba23"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34f751bf67cab69638564eee34023909380ba3e0d8ee7f6fe473079bf93f09b"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2671cb47e50a97f419a02cd1e0c339b31de017b033186358db92f4d8e2e17d8"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c73254c256081704dba0a333457e2fb815364018788f9b501efe7c5e0ada401"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4383beb4a29935b8fa28aca8fa84c956bf545cb0c46307b091b8d312a9150e6a"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbceedcf4a9329cc665452db1aaf0845b85c666e4885b92ee0cddb1dbf7e052a"}, - {file = "rpds_py-0.19.1-cp311-none-win32.whl", hash = "sha256:f0a6d4a93d2a05daec7cb885157c97bbb0be4da739d6f9dfb02e101eb40921cd"}, - {file = "rpds_py-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:c149a652aeac4902ecff2dd93c3b2681c608bd5208c793c4a99404b3e1afc87c"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:56313be667a837ff1ea3508cebb1ef6681d418fa2913a0635386cf29cff35165"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d1d7539043b2b31307f2c6c72957a97c839a88b2629a348ebabe5aa8b626d6b"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1dc59a5e7bc7f44bd0c048681f5e05356e479c50be4f2c1a7089103f1621d5"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8f78398e67a7227aefa95f876481485403eb974b29e9dc38b307bb6eb2315ea"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef07a0a1d254eeb16455d839cef6e8c2ed127f47f014bbda64a58b5482b6c836"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8124101e92c56827bebef084ff106e8ea11c743256149a95b9fd860d3a4f331f"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08ce9c95a0b093b7aec75676b356a27879901488abc27e9d029273d280438505"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b02dd77a2de6e49078c8937aadabe933ceac04b41c5dde5eca13a69f3cf144e"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4dd02e29c8cbed21a1875330b07246b71121a1c08e29f0ee3db5b4cfe16980c4"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c7042488165f7251dc7894cd533a875d2875af6d3b0e09eda9c4b334627ad1c"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f809a17cc78bd331e137caa25262b507225854073fd319e987bd216bed911b7c"}, - {file = "rpds_py-0.19.1-cp312-none-win32.whl", hash = "sha256:3ddab996807c6b4227967fe1587febade4e48ac47bb0e2d3e7858bc621b1cace"}, - {file = "rpds_py-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:32e0db3d6e4f45601b58e4ac75c6f24afbf99818c647cc2066f3e4b192dabb1f"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:747251e428406b05fc86fee3904ee19550c4d2d19258cef274e2151f31ae9d38"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc733d35f861f8d78abfaf54035461e10423422999b360966bf1c443cbc42705"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbda75f245caecff8faa7e32ee94dfaa8312a3367397975527f29654cd17a6ed"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd04d8cab16cab5b0a9ffc7d10f0779cf1120ab16c3925404428f74a0a43205a"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2d66eb41ffca6cc3c91d8387509d27ba73ad28371ef90255c50cb51f8953301"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdf4890cda3b59170009d012fca3294c00140e7f2abe1910e6a730809d0f3f9b"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1fa67ef839bad3815124f5f57e48cd50ff392f4911a9f3cf449d66fa3df62a5"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b82c9514c6d74b89a370c4060bdb80d2299bc6857e462e4a215b4ef7aa7b090e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7b07959866a6afb019abb9564d8a55046feb7a84506c74a6f197cbcdf8a208e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4f580ae79d0b861dfd912494ab9d477bea535bfb4756a2269130b6607a21802e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6d20c8896c00775e6f62d8373aba32956aa0b850d02b5ec493f486c88e12859"}, - {file = "rpds_py-0.19.1-cp313-none-win32.whl", hash = "sha256:afedc35fe4b9e30ab240b208bb9dc8938cb4afe9187589e8d8d085e1aacb8309"}, - {file = "rpds_py-0.19.1-cp313-none-win_amd64.whl", hash = "sha256:1d4af2eb520d759f48f1073ad3caef997d1bfd910dc34e41261a595d3f038a94"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:34bca66e2e3eabc8a19e9afe0d3e77789733c702c7c43cd008e953d5d1463fde"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:24f8ae92c7fae7c28d0fae9b52829235df83f34847aa8160a47eb229d9666c7b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71157f9db7f6bc6599a852852f3389343bea34315b4e6f109e5cbc97c1fb2963"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d494887d40dc4dd0d5a71e9d07324e5c09c4383d93942d391727e7a40ff810b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3661e6d4ba63a094138032c1356d557de5b3ea6fd3cca62a195f623e381c76"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97fbb77eaeb97591efdc654b8b5f3ccc066406ccfb3175b41382f221ecc216e8"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cc4bc73e53af8e7a42c8fd7923bbe35babacfa7394ae9240b3430b5dcf16b2a"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35af5e4d5448fa179fd7fff0bba0fba51f876cd55212f96c8bbcecc5c684ae5c"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3511f6baf8438326e351097cecd137eb45c5f019944fe0fd0ae2fea2fd26be39"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:57863d16187995c10fe9cf911b897ed443ac68189179541734502353af33e693"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9e318e6786b1e750a62f90c6f7fa8b542102bdcf97c7c4de2a48b50b61bd36ec"}, - {file = "rpds_py-0.19.1-cp38-none-win32.whl", hash = "sha256:53dbc35808c6faa2ce3e48571f8f74ef70802218554884787b86a30947842a14"}, - {file = "rpds_py-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:8df1c283e57c9cb4d271fdc1875f4a58a143a2d1698eb0d6b7c0d7d5f49c53a1"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e76c902d229a3aa9d5ceb813e1cbcc69bf5bda44c80d574ff1ac1fa3136dea71"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de1f7cd5b6b351e1afd7568bdab94934d656abe273d66cda0ceea43bbc02a0c2"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fc5a84777cb61692d17988989690d6f34f7f95968ac81398d67c0d0994a897"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74129d5ffc4cde992d89d345f7f7d6758320e5d44a369d74d83493429dad2de5"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e360188b72f8080fefa3adfdcf3618604cc8173651c9754f189fece068d2a45"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13e6d4840897d4e4e6b2aa1443e3a8eca92b0402182aafc5f4ca1f5e24f9270a"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f09529d2332264a902688031a83c19de8fda5eb5881e44233286b9c9ec91856d"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d4b52811dcbc1aba08fd88d475f75b4f6db0984ba12275d9bed1a04b2cae9b5"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd635c2c4043222d80d80ca1ac4530a633102a9f2ad12252183bcf338c1b9474"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f35b34a5184d5e0cc360b61664c1c06e866aab077b5a7c538a3e20c8fcdbf90b"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d4ec0046facab83012d821b33cead742a35b54575c4edfb7ed7445f63441835f"}, - {file = "rpds_py-0.19.1-cp39-none-win32.whl", hash = "sha256:f5b8353ea1a4d7dfb59a7f45c04df66ecfd363bb5b35f33b11ea579111d4655f"}, - {file = "rpds_py-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:1fb93d3486f793d54a094e2bfd9cd97031f63fcb5bc18faeb3dd4b49a1c06523"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d5c7e32f3ee42f77d8ff1a10384b5cdcc2d37035e2e3320ded909aa192d32c3"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:89cc8921a4a5028d6dd388c399fcd2eef232e7040345af3d5b16c04b91cf3c7e"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca34e913d27401bda2a6f390d0614049f5a95b3b11cd8eff80fe4ec340a1208"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5953391af1405f968eb5701ebbb577ebc5ced8d0041406f9052638bafe52209d"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:840e18c38098221ea6201f091fc5d4de6128961d2930fbbc96806fb43f69aec1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d8b735c4d162dc7d86a9cf3d717f14b6c73637a1f9cd57fe7e61002d9cb1972"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce757c7c90d35719b38fa3d4ca55654a76a40716ee299b0865f2de21c146801c"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9421b23c85f361a133aa7c5e8ec757668f70343f4ed8fdb5a4a14abd5437244"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3b823be829407393d84ee56dc849dbe3b31b6a326f388e171555b262e8456cc1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:5e58b61dcbb483a442c6239c3836696b79f2cd8e7eec11e12155d3f6f2d886d1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39d67896f7235b2c886fb1ee77b1491b77049dcef6fbf0f401e7b4cbed86bbd4"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8b32cd4ab6db50c875001ba4f5a6b30c0f42151aa1fbf9c2e7e3674893fb1dc4"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c32e41de995f39b6b315d66c27dea3ef7f7c937c06caab4c6a79a5e09e2c415"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a129c02b42d46758c87faeea21a9f574e1c858b9f358b6dd0bbd71d17713175"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:346557f5b1d8fd9966059b7a748fd79ac59f5752cd0e9498d6a40e3ac1c1875f"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31e450840f2f27699d014cfc8865cc747184286b26d945bcea6042bb6aa4d26e"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01227f8b3e6c8961490d869aa65c99653df80d2f0a7fde8c64ebddab2b9b02fd"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69084fd29bfeff14816666c93a466e85414fe6b7d236cfc108a9c11afa6f7301"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d2b88efe65544a7d5121b0c3b003ebba92bfede2ea3577ce548b69c5235185"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ea961a674172ed2235d990d7edf85d15d8dfa23ab8575e48306371c070cda67"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:5beffdbe766cfe4fb04f30644d822a1080b5359df7db3a63d30fa928375b2720"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:720f3108fb1bfa32e51db58b832898372eb5891e8472a8093008010911e324c5"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c2087dbb76a87ec2c619253e021e4fb20d1a72580feeaa6892b0b3d955175a71"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ddd50f18ebc05ec29a0d9271e9dbe93997536da3546677f8ca00b76d477680c"}, - {file = "rpds_py-0.19.1.tar.gz", hash = "sha256:31dd5794837f00b46f4096aa8ccaa5972f73a938982e32ed817bb520c465e520"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:fb37bd599f031f1a6fb9e58ec62864ccf3ad549cf14bac527dbfa97123edcca4"}, + {file = "rpds_py-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3384d278df99ec2c6acf701d067147320b864ef6727405d6470838476e44d9e8"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54548e0be3ac117595408fd4ca0ac9278fde89829b0b518be92863b17ff67a2"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8eb488ef928cdbc05a27245e52de73c0d7c72a34240ef4d9893fdf65a8c1a955"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5da93debdfe27b2bfc69eefb592e1831d957b9535e0943a0ee8b97996de21b5"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79e205c70afddd41f6ee79a8656aec738492a550247a7af697d5bd1aee14f766"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959179efb3e4a27610e8d54d667c02a9feaa86bbabaf63efa7faa4dfa780d4f1"}, + {file = "rpds_py-0.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6e605bb9edcf010f54f8b6a590dd23a4b40a8cb141255eec2a03db249bc915b"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9133d75dc119a61d1a0ded38fb9ba40a00ef41697cc07adb6ae098c875195a3f"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd36b712d35e757e28bf2f40a71e8f8a2d43c8b026d881aa0c617b450d6865c9"}, + {file = "rpds_py-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354f3a91718489912f2e0fc331c24eaaf6a4565c080e00fbedb6015857c00582"}, + {file = "rpds_py-0.19.0-cp310-none-win32.whl", hash = "sha256:ebcbf356bf5c51afc3290e491d3722b26aaf5b6af3c1c7f6a1b757828a46e336"}, + {file = "rpds_py-0.19.0-cp310-none-win_amd64.whl", hash = "sha256:75a6076289b2df6c8ecb9d13ff79ae0cad1d5fb40af377a5021016d58cd691ec"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6d45080095e585f8c5097897313def60caa2046da202cdb17a01f147fb263b81"}, + {file = "rpds_py-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5c9581019c96f865483d031691a5ff1cc455feb4d84fc6920a5ffc48a794d8a"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1540d807364c84516417115c38f0119dfec5ea5c0dd9a25332dea60b1d26fc4d"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e65489222b410f79711dc3d2d5003d2757e30874096b2008d50329ea4d0f88c"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da6f400eeb8c36f72ef6646ea530d6d175a4f77ff2ed8dfd6352842274c1d8b"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37f46bb11858717e0efa7893c0f7055c43b44c103e40e69442db5061cb26ed34"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071d4adc734de562bd11d43bd134330fb6249769b2f66b9310dab7460f4bf714"}, + {file = "rpds_py-0.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9625367c8955e4319049113ea4f8fee0c6c1145192d57946c6ffcd8fe8bf48dd"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e19509145275d46bc4d1e16af0b57a12d227c8253655a46bbd5ec317e941279d"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d438e4c020d8c39961deaf58f6913b1bf8832d9b6f62ec35bd93e97807e9cbc"}, + {file = "rpds_py-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90bf55d9d139e5d127193170f38c584ed3c79e16638890d2e36f23aa1630b952"}, + {file = "rpds_py-0.19.0-cp311-none-win32.whl", hash = "sha256:8d6ad132b1bc13d05ffe5b85e7a01a3998bf3a6302ba594b28d61b8c2cf13aaf"}, + {file = "rpds_py-0.19.0-cp311-none-win_amd64.whl", hash = "sha256:7ec72df7354e6b7f6eb2a17fa6901350018c3a9ad78e48d7b2b54d0412539a67"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:5095a7c838a8647c32aa37c3a460d2c48debff7fc26e1136aee60100a8cd8f68"}, + {file = "rpds_py-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f2f78ef14077e08856e788fa482107aa602636c16c25bdf59c22ea525a785e9"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7cc6cb44f8636fbf4a934ca72f3e786ba3c9f9ba4f4d74611e7da80684e48d2"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf902878b4af334a09de7a45badbff0389e7cf8dc2e4dcf5f07125d0b7c2656d"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:688aa6b8aa724db1596514751ffb767766e02e5c4a87486ab36b8e1ebc1aedac"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57dbc9167d48e355e2569346b5aa4077f29bf86389c924df25c0a8b9124461fb"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4cf5a9497874822341c2ebe0d5850fed392034caadc0bad134ab6822c0925b"}, + {file = "rpds_py-0.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a790d235b9d39c70a466200d506bb33a98e2ee374a9b4eec7a8ac64c2c261fa"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d16089dfa58719c98a1c06f2daceba6d8e3fb9b5d7931af4a990a3c486241cb"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bc9128e74fe94650367fe23f37074f121b9f796cabbd2f928f13e9661837296d"}, + {file = "rpds_py-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c8f77e661ffd96ff104bebf7d0f3255b02aa5d5b28326f5408d6284c4a8b3248"}, + {file = "rpds_py-0.19.0-cp312-none-win32.whl", hash = "sha256:5f83689a38e76969327e9b682be5521d87a0c9e5a2e187d2bc6be4765f0d4600"}, + {file = "rpds_py-0.19.0-cp312-none-win_amd64.whl", hash = "sha256:06925c50f86da0596b9c3c64c3837b2481337b83ef3519e5db2701df695453a4"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:52e466bea6f8f3a44b1234570244b1cff45150f59a4acae3fcc5fd700c2993ca"}, + {file = "rpds_py-0.19.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e21cc693045fda7f745c790cb687958161ce172ffe3c5719ca1764e752237d16"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b31f059878eb1f5da8b2fd82480cc18bed8dcd7fb8fe68370e2e6285fa86da6"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dd46f309e953927dd018567d6a9e2fb84783963650171f6c5fe7e5c41fd5666"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a01a4490e170376cd79258b7f755fa13b1a6c3667e872c8e35051ae857a92b"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcf426a8c38eb57f7bf28932e68425ba86def6e756a5b8cb4731d8e62e4e0223"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68eea5df6347d3f1378ce992d86b2af16ad7ff4dcb4a19ccdc23dea901b87fb"}, + {file = "rpds_py-0.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dab8d921b55a28287733263c0e4c7db11b3ee22aee158a4de09f13c93283c62d"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6fe87efd7f47266dfc42fe76dae89060038f1d9cb911f89ae7e5084148d1cc08"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:535d4b52524a961d220875688159277f0e9eeeda0ac45e766092bfb54437543f"}, + {file = "rpds_py-0.19.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8b1a94b8afc154fbe36978a511a1f155f9bd97664e4f1f7a374d72e180ceb0ae"}, + {file = "rpds_py-0.19.0-cp38-none-win32.whl", hash = "sha256:7c98298a15d6b90c8f6e3caa6457f4f022423caa5fa1a1ca7a5e9e512bdb77a4"}, + {file = "rpds_py-0.19.0-cp38-none-win_amd64.whl", hash = "sha256:b0da31853ab6e58a11db3205729133ce0df26e6804e93079dee095be3d681dc1"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5039e3cef7b3e7a060de468a4a60a60a1f31786da94c6cb054e7a3c75906111c"}, + {file = "rpds_py-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab1932ca6cb8c7499a4d87cb21ccc0d3326f172cfb6a64021a889b591bb3045c"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2afd2164a1e85226fcb6a1da77a5c8896c18bfe08e82e8ceced5181c42d2179"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1c30841f5040de47a0046c243fc1b44ddc87d1b12435a43b8edff7e7cb1e0d0"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f757f359f30ec7dcebca662a6bd46d1098f8b9fb1fcd661a9e13f2e8ce343ba1"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15e65395a59d2e0e96caf8ee5389ffb4604e980479c32742936ddd7ade914b22"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb0f6eb3a320f24b94d177e62f4074ff438f2ad9d27e75a46221904ef21a7b05"}, + {file = "rpds_py-0.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b228e693a2559888790936e20f5f88b6e9f8162c681830eda303bad7517b4d5a"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2575efaa5d949c9f4e2cdbe7d805d02122c16065bfb8d95c129372d65a291a0b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5c872814b77a4e84afa293a1bee08c14daed1068b2bb1cc312edbf020bbbca2b"}, + {file = "rpds_py-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850720e1b383df199b8433a20e02b25b72f0fded28bc03c5bd79e2ce7ef050be"}, + {file = "rpds_py-0.19.0-cp39-none-win32.whl", hash = "sha256:ce84a7efa5af9f54c0aa7692c45861c1667080814286cacb9958c07fc50294fb"}, + {file = "rpds_py-0.19.0-cp39-none-win_amd64.whl", hash = "sha256:1c26da90b8d06227d7769f34915913911222d24ce08c0ab2d60b354e2d9c7aff"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:75969cf900d7be665ccb1622a9aba225cf386bbc9c3bcfeeab9f62b5048f4a07"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8445f23f13339da640d1be8e44e5baf4af97e396882ebbf1692aecd67f67c479"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5a7c1062ef8aea3eda149f08120f10795835fc1c8bc6ad948fb9652a113ca55"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:462b0c18fbb48fdbf980914a02ee38c423a25fcc4cf40f66bacc95a2d2d73bc8"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3208f9aea18991ac7f2b39721e947bbd752a1abbe79ad90d9b6a84a74d44409b"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3444fe52b82f122d8a99bf66777aed6b858d392b12f4c317da19f8234db4533"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb4bac7185a9f0168d38c01d7a00addece9822a52870eee26b8d5b61409213"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b130bd4163c93798a6b9bb96be64a7c43e1cec81126ffa7ffaa106e1fc5cef5"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a707b158b4410aefb6b054715545bbb21aaa5d5d0080217290131c49c2124a6e"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dc9ac4659456bde7c567107556ab065801622396b435a3ff213daef27b495388"}, + {file = "rpds_py-0.19.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:81ea573aa46d3b6b3d890cd3c0ad82105985e6058a4baed03cf92518081eec8c"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f148c3f47f7f29a79c38cc5d020edcb5ca780020fab94dbc21f9af95c463581"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0906357f90784a66e89ae3eadc2654f36c580a7d65cf63e6a616e4aec3a81be"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f629ecc2db6a4736b5ba95a8347b0089240d69ad14ac364f557d52ad68cf94b0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6feacd1d178c30e5bc37184526e56740342fd2aa6371a28367bad7908d454fc"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b6068ee374fdfab63689be0963333aa83b0815ead5d8648389a8ded593378"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d57546bad81e0da13263e4c9ce30e96dcbe720dbff5ada08d2600a3502e526"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b6683a37338818646af718c9ca2a07f89787551057fae57c4ec0446dc6224b"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e8481b946792415adc07410420d6fc65a352b45d347b78fec45d8f8f0d7496f0"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bec35eb20792ea64c3c57891bc3ca0bedb2884fbac2c8249d9b731447ecde4fa"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:aa5476c3e3a402c37779e95f7b4048db2cb5b0ed0b9d006983965e93f40fe05a"}, + {file = "rpds_py-0.19.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19d02c45f2507b489fd4df7b827940f1420480b3e2e471e952af4d44a1ea8e34"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3e2fd14c5d49ee1da322672375963f19f32b3d5953f0615b175ff7b9d38daed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:93a91c2640645303e874eada51f4f33351b84b351a689d470f8108d0e0694210"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b9fc03bf76a94065299d4a2ecd8dfbae4ae8e2e8098bbfa6ab6413ca267709"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a4b07cdf3f84310c08c1de2c12ddadbb7a77568bcb16e95489f9c81074322ed"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba0ed0dc6763d8bd6e5de5cf0d746d28e706a10b615ea382ac0ab17bb7388633"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:474bc83233abdcf2124ed3f66230a1c8435896046caa4b0b5ab6013c640803cc"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329c719d31362355a96b435f4653e3b4b061fcc9eba9f91dd40804ca637d914e"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef9101f3f7b59043a34f1dccbb385ca760467590951952d6701df0da9893ca0c"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0121803b0f424ee2109d6e1f27db45b166ebaa4b32ff47d6aa225642636cd834"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8344127403dea42f5970adccf6c5957a71a47f522171fafaf4c6ddb41b61703a"}, + {file = "rpds_py-0.19.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:443cec402ddd650bb2b885113e1dcedb22b1175c6be223b14246a714b61cd521"}, + {file = "rpds_py-0.19.0.tar.gz", hash = "sha256:4fdc9afadbeb393b4bbbad75481e0ea78e4469f2e1d713a90811700830b553a9"}, ] [[package]] @@ -2989,4 +2985,4 @@ test = [] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d321613b2c63b5e51cdb48d970ccf066662de3961a96fa2f1329914755417319" +content-hash = "426cb72a8cc3bc6b230dc3b6cb484869351a5328c0138678a90673d33f6025e7" diff --git a/pydfirram/__init__.py b/pydfirram/__init__.py index 2685eeb..89cce0e 100644 --- a/pydfirram/__init__.py +++ b/pydfirram/__init__.py @@ -1 +1,3 @@ -#For poetry builds +""" +pydfirram - simplify and enhance memory forensics tasks +""" diff --git a/pydfirram/core/__init__.py b/pydfirram/core/__init__.py index e69de29..36a2465 100644 --- a/pydfirram/core/__init__.py +++ b/pydfirram/core/__init__.py @@ -0,0 +1,3 @@ +""" +pydfirram.core - pydfirram core +""" diff --git a/pydfirram/core/base.py b/pydfirram/core/base.py index b9752b9..7799a9d 100644 --- a/pydfirram/core/base.py +++ b/pydfirram/core/base.py @@ -23,14 +23,16 @@ >>> plugin = generic.get_plugin("Banners") >>> generic.run_plugin(plugin) - OR : +Example: + Or it can be used as follow : + $ python3 >>> from pydfirram.core.base import Generic, OperatingSystem >>> from pathlib import Path >>> os = OperatingSystem.WINDOWS >>> dumpfile = Path("tests/data/dump.raw") >>> generic = Generic(dumpfile) - >>> plugin = generic.pslist().to_dict() + >>> plugin = generic.pslist().to_df() >>> print(plugin) """ @@ -38,14 +40,42 @@ from dataclasses import dataclass from enum import Enum from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Optional, cast +from collections.abc import Callable + from loguru import logger -from volatility3 import framework, plugins # type: ignore -from volatility3.framework import automagic, contexts # type: ignore -from volatility3.framework import exceptions as VolatilityExceptions -from volatility3.framework import interfaces -from volatility3.framework.plugins import construct_plugin +from volatility3.framework import ( # type: ignore + import_files as v3_framework_import_files, + list_plugins as v3_framework_list_plugins, +) + +from volatility3 import ( + plugins as v3_framework_plugins_mod, + +) +from volatility3.framework.contexts import ( # type: ignore + Context as V3Context, +) +from volatility3.framework.interfaces.plugins import ( # type: ignore + PluginInterface as V3PluginInterface, +) +from volatility3.framework.exceptions import ( # type: ignore + UnsatisfiedException as V3UnsatisfiedException, +) +from volatility3.framework.plugins import ( # type: ignore + construct_plugin as v3_construct_plugin, +) +from volatility3.framework.interfaces.automagic import ( # type: ignore + AutomagicInterface as V3AutomagicInterface, +) +from volatility3.framework.automagic import ( # type: ignore + available as v3_automagic_available, + choose_automagic as v3_automagic_choose, +) +from volatility3.framework.automagic.stacker import ( # type: ignore + choose_os_stackers as v3_choose_os_stackers, +) from pydfirram.core.handler import create_file_handler from pydfirram.core.renderer import Renderer @@ -65,7 +95,7 @@ class OperatingSystem(Enum): MACOS = "mac" @staticmethod - def to_list() -> List[str]: + def to_list() -> list[str]: """Returns a list of supported operating systems. Returns: List[str]: List of supported operating systems. @@ -87,33 +117,34 @@ class PluginType(Enum): @dataclass -class PluginEntry: +class PluginEntry(): """A plugin entry. - The interface allows to directly interact with the plugin from volatility3 functions. + The interface allows to directly interact with the plugin from + volatility3 functions. Attributes: type: PluginType: The plugin type. name: str: The plugin name. - interface: volatility3.framework.interfaces.plugins.PluginInterface: The plugin interface. + interface: PluginInterface: The (volatility3) plugin interface. """ type: PluginType name: str - interface: interfaces.plugins.PluginInterface + interface: V3PluginInterface def __repr__(self) -> str: """Returns a string representation of the plugin entry.""" return f"PluginEntry({self.type}, {self.name}, {self.interface})" -class Context: +class Context(): """Context for a volatility3 plugin. Attributes: os: OperatingSystem: The operating system. dump_file: Path: The dump file path. - context: volatility3.framework.contexts.Context: The volatility3 context. + context: V3Context: The volatility3 context. plugin: PluginEntry: The plugin entry. Constants: @@ -141,47 +172,63 @@ def __init__( """ self.os = operating_system self.dump_file = dump_file - self.context = contexts.Context() + self.context = V3Context() self.plugin = plugin + self.automag: Any = None + + def set_context(self) -> None: + """ setup the current context """ + dump_file_location = self.get_dump_file_location() + self.context.config[self.KEY_STACKERS] = self.os_stackers() + self.context.config[self.KEY_SINGLE_LOCATION] = dump_file_location - def build(self) -> interfaces.plugins.PluginInterface: + def set_automagic(self) -> None: + """ setup the automagics """ + self.automag = self.automagics() + + def build(self) -> V3PluginInterface: """Build a basic context for the provided plugin. Returns: interfaces.plugins.PluginInterface: The built plugin interface. Raises: - VolatilityExceptions.UnsatisfiedException: If the plugin cannot be built. + V3UnsatisfiedException: If the plugin cannot be built. """ plugin = self.plugin.interface - automagics = self.automagics() - dump_file_location = self.get_dump_file_location() base_config_path = "plugins" file_handler = create_file_handler(os.getcwd()) - self.context.config[self.KEY_STACKERS] = self.os_stackers() - self.context.config[self.KEY_SINGLE_LOCATION] = dump_file_location try: # Construct the plugin, clever magic figures out how to # fulfill each requirement that might not be fulfilled - constructed = construct_plugin( + # @notes + # - As many volatility3 internals, some of the argument mismatch + # because type awaiting by the framework is, for exemple, + # `type[PluginInterface]` and we give a `PluginInterface` wich + # is the same thing...So, lets cast to `Any` to avoid embrouille + constructed = v3_construct_plugin( self.context, - automagics, - plugin, # type: ignore + self.automag, + cast(Any, plugin), base_config_path, None, # no progress callback for now file_handler, ) - except VolatilityExceptions.UnsatisfiedException as e: - logger.error(f"Failed to build plugin: {e}") - raise e - + except V3UnsatisfiedException as err: + logger.error(f"Failed to build plugin: {err}") + raise err return constructed - def add_arguments(self,context: contexts.Context(), kwargs: Dict[str, Any]) -> contexts.Context(): - """Handle keyword arguments and set them as context config attributes. + def add_arguments( + self, + context: V3Context, + kwargs: dict[str, Any] + ) -> V3Context: + """ + Handle keyword arguments and set them as context config attributes. Args: - kwargs (Dict[str, Any]): The keyword arguments. + kwargs (dict[str, Any]): The keyword arguments. Raises: AttributeError: If the attribute does not exist. @@ -191,37 +238,51 @@ def add_arguments(self,context: contexts.Context(), kwargs: Dict[str, Any]) -> c return context - def get_available_automagics(self) -> List[interfaces.automagic.AutomagicInterface]: + def get_available_automagics(self) -> list[V3AutomagicInterface]: """Returns a list of available volatility3 automagics. Returns: - List[interfaces.automagic.AutomagicInterface]: A list of available automagics. + List[V3AutomagicInterface]: A list of available automagics. """ - return automagic.available(self.context) + return cast( + list[V3AutomagicInterface], + v3_automagic_available(self.context), + ) - def automagics(self) -> List[interfaces.automagic.AutomagicInterface]: + def automagics(self) -> list[V3AutomagicInterface]: """Returns a list of volatility3 automagics. Returns: - List[interfaces.automagic.AutomagicInterface]: A list of automagics. + List[V3AutomagicInterface]: A list of automagics. Raises: - VolatilityExceptions.UnsatisfiedException: If no automagic can be chosen. + V3UnsatisfiedException: If no automagic can be chosen. """ available_automagics = self.get_available_automagics() - - return automagic.choose_automagic( - available_automagics, # type: ignore - self.plugin.interface, # type: ignore + # @notes + # It seems that `choose_automagic` require weird typing information + # that should match what we give to this bastard, but it's not + # since, for example, our `PluginInterface` do not match the + # `type[PluginInterface]` awaited...even if its the same type :pouce: + # So, let's cast all argument to Any to avoid typing collision + return cast( + list[V3AutomagicInterface], + v3_automagic_choose( + cast(Any, available_automagics), + cast(Any, self.plugin.interface), + ), ) - def os_stackers(self) -> List[interfaces.automagic.AutomagicInterface]: + def os_stackers(self) -> list[V3AutomagicInterface]: """Returns a list of stackers for the OS. Returns: - List[interfaces.automagic.AutomagicInterface]: A list of stackers. + List[V3AutomagicInterface]: A list of (volatility3) stackers. """ - return automagic.stacker.choose_os_stackers(self.plugin.interface) + return cast( + list[V3AutomagicInterface], + v3_choose_os_stackers(cast(Any,self.plugin.interface)), + ) def get_dump_file_location(self) -> str: """Returns the dump file location. @@ -232,14 +293,15 @@ def get_dump_file_location(self) -> str: return "file://" + self.dump_file.absolute().as_posix() -class Generic: +class Generic(): """Generic OS wrapper to be used with volatility3 This class provides a way to interact with volatility3 plugins in a more abstract way. It allows to automatically get all available plugins for a specific OS and run them with the required arguments. - It aims to be inherited by specific OS wrappers like Windows, Linux or MacOS. + It aims to be inherited by specific OS wrappers like Windows, Linux or + MacOS. Attributes: os (OperatingSystem): The operating system. @@ -248,6 +310,10 @@ class Generic: context (Context): The context. """ + #--- + # Magic methods + #--- + def __init__(self, operating_system: OperatingSystem, dump_file: Path): """Initializes a generic OS. @@ -261,22 +327,25 @@ def __init__(self, operating_system: OperatingSystem, dump_file: Path): FileNotFoundError: If the dump file does not exist. """ self.validate_dump_file(dump_file) - self.os = operating_system - self.plugins: List[PluginEntry] = self.get_all_plugins() + self.plugins: list[PluginEntry] = self.get_all_plugins() self.dump_file = dump_file - self.context = None + self.context: Optional[Context] = None self.temp_data = None - self.tmp_plugin: PluginEntry = None + self.tmp_plugin: Optional[PluginEntry] = None logger.info(f"Generic OS initialized: {self.os}") - def __getattr__(self, key: str,**kwargs: Dict) -> Renderer : + def __getattr__( + self, + key: str, + **kwargs: dict[str, Any] + ) -> Callable[...,Renderer]: """ Handle attribute access for commands. - This method is called when an attribute that - matches a command name is accessed. It returns a lambda function + This method is called when an attribute that + matches a command name is accessed. It returns a lambda function that calls the __run_commands method with the corresponding key. :param key: The attribute name (command name). @@ -291,13 +360,74 @@ def __getattr__(self, key: str,**kwargs: Dict) -> Renderer : plugin: PluginEntry = self.get_plugin(key) except Exception as exc: raise ValueError(f"Unable to handle {key}") from exc - def parse_data_function(**kwargs): + def parse_data_function(**kwargs: dict[str,Any]) -> Renderer: return Renderer( - data= self.run_plugin(plugin,**kwargs) - ) + data = self.run_plugin(plugin,**kwargs) + ) return parse_data_function - def run_plugin(self, plugin: PluginEntry, **kwargs: Any) -> Any: + #--- + # Internals methods + #--- + + def _get_plugins_list(self) -> dict[str,Any]: + """Get a list of available volatility3 plugins for the OS. + + Returns: + dict[str,Any]: A dictionary of plugins. + """ + failures = v3_framework_import_files( + base_module = v3_framework_plugins_mod, + ignore_errors = True + ) + if failures: + logger.warning(f"Failed to import some plugins: {failures}") + return cast(dict[str,Any], v3_framework_list_plugins()) + + def _parse_plugins_list( + self, + plugin_list: dict[str, Any], + ) -> list[PluginEntry]: + """Parse the list of available volatility3 plugins. + + The plugin list is a dictionary where the key is the plugin name + and the value is the plugin interface. + + Args: + plugin_list (Dict[str, Any]): The plugin list. + + Returns: + List[PluginEntry]: A list of PluginEntry. + """ + parsed: list[PluginEntry] = [] + for plugin in plugin_list: + interface = plugin_list[plugin] + elements = plugin.split(".") + platform = elements[0] + name = elements[-1] + name = name.lower() + if platform not in OperatingSystem.to_list(): + type_ = PluginType.GENERIC + elif platform == self.os.value: + type_ = PluginType.SPECIFIC + else: + continue + parsed.append( + PluginEntry(type_, name, interface), + ) + logger.info(f"Found {len(parsed)} plugins for {self.os}") + return parsed + + #--- + # Public methods + #--- + + # (todo) : more explicit return type + def run_plugin( + self, + plugin: PluginEntry, + **kwargs: dict[str,Any], + ) -> Any: """Run a volatility3 plugin with the given arguments. Args: @@ -310,14 +440,18 @@ def run_plugin(self, plugin: PluginEntry, **kwargs: Any) -> Any: Raises: ValueError: If the context is not built. """ + # (todo) : move `context.set_*()` in `Context.__init__()` ? self.context = Context(self.os, self.dump_file, plugin) # type: ignore - context = self.context.build() # type: ignore - + self.context.set_automagic() + self.context.set_context() + builded_context = self.context.build() # type: ignore if kwargs: - context = self.context.add_arguments(context,kwargs) + runable_context = self.context.add_arguments(builded_context,kwargs) + else: + runable_context = builded_context if self.context is None: raise ValueError("Context not built.") - return context.run() + return runable_context.run() def validate_dump_file(self, dump_file: Path) -> bool: """Validate dump file location. @@ -331,9 +465,9 @@ def validate_dump_file(self, dump_file: Path) -> bool: Raises: FileNotFoundError: If the file does not exist. """ - if not dump_file.is_file(): - raise FileNotFoundError(f"The file {dump_file} does not exist.") - return True + if dump_file.is_file(): + return True + raise FileNotFoundError(f"The file {dump_file} does not exist.") def get_plugin(self, name: str) -> PluginEntry: """Fetches a plugin by its name from the list of plugins. @@ -351,10 +485,9 @@ def get_plugin(self, name: str) -> PluginEntry: for plugin in self.plugins: if plugin.name == name: return plugin - raise ValueError(f"Plugin {name} not found for {self.os}") - def get_all_plugins(self) -> List[PluginEntry]: + def get_all_plugins(self) -> list[PluginEntry]: """Get all available plugins for the specified OS. Returns: @@ -366,53 +499,4 @@ def get_all_plugins(self) -> List[PluginEntry]: """ plugin_list = self._get_plugins_list() parsed_plugins = self._parse_plugins_list(plugin_list) - return parsed_plugins - - def _get_plugins_list(self) -> Dict[str, Any]: - """Get a list of available volatility3 plugins for the OS. - - Returns: - Dict[str, Any]: A dictionary of plugins. - """ - failures = framework.import_files(plugins, True) - if failures: - logger.warning(f"Failed to import some plugins: {failures}") - - plugin_list = framework.list_plugins() - - return plugin_list - - def _parse_plugins_list(self, plugin_list: Dict[str, Any]) -> List[PluginEntry]: - """Parse the list of available volatility3 plugins. - - The plugin list is a dictionary where the key is the plugin name - and the value is the plugin interface. - - Args: - plugin_list (Dict[str, Any]): The plugin list. - - Returns: - List[PluginEntry]: A list of PluginEntry. - """ - parsed: List[PluginEntry] = list() - - for plugin in plugin_list: - interface = plugin_list[plugin] - elements = plugin.split(".") - platform = elements[0] - name = elements[-1] - name = name.lower() - if platform not in OperatingSystem.to_list(): - type_ = PluginType.GENERIC - elif platform == self.os.value: - type_ = PluginType.SPECIFIC - else: - continue - - plugin = PluginEntry(type_, name, interface) - parsed.append(plugin) # type: ignore - - logger.info(f"Found {len(parsed)} plugins for {self.os}") - - return parsed diff --git a/pydfirram/core/handler.py b/pydfirram/core/handler.py index 690e2c4..54f0569 100644 --- a/pydfirram/core/handler.py +++ b/pydfirram/core/handler.py @@ -23,9 +23,11 @@ import os import tempfile -from typing import Optional +from typing import Optional, Any -from volatility3.framework import interfaces +from volatility3.framework.interfaces.plugins import ( # type: ignore + FileHandlerInterface as V3FileHandlerInterface, +) def create_file_handler(output_dir: Optional[str]) -> type: @@ -38,9 +40,9 @@ def create_file_handler(output_dir: Optional[str]) -> type: type: A file handler class that saves files directly to disk. """ - class CLIFileHandler(interfaces.plugins.FileHandlerInterface): - """The FileHandler from Volatility3 CLI.""" - + class CLIFileHandler(V3FileHandlerInterface): # type: ignore + """The FileHandler from Volatility3 CLI. + """ def _get_final_filename(self) -> str: """Gets the final filename for the saved file.""" if output_dir is None: @@ -63,16 +65,23 @@ def _get_final_filename(self) -> str: return output_filename - class CLIDirectFileHandler(CLIFileHandler): - """A file handler class that saves files directly to disk.""" + def close(self) -> None: + """ V3FileHandlerInterface require to implement this method """ - def __init__(self, filename: str): + class CLIDirectFileHandler(CLIFileHandler): + """A file handler class that saves files directly to disk. + """ + def __init__(self, filename: str) -> None: fd, temp_name = tempfile.mkstemp( - suffix=".vol3", prefix="tmp_", dir=output_dir + suffix = ".vol3", + prefix = "tmp_", + dir = output_dir, ) + # allow `io.open()` without using `with` context + # pylint: disable=R1732 self._file = io.open(fd, mode="w+b") - CLIFileHandler.__init__(self, filename) + CLIFileHandler.__init__(self, filename) # type: ignore for attr in dir(self._file): if not attr.startswith("_") and attr not in [ @@ -85,25 +94,29 @@ def __init__(self, filename: str): self._name = temp_name - def __getattr__(self, item): + def __getattr__(self, item: Any) -> Any: return getattr(self._file, item) + ## properties + @property - def closed(self): + def closed(self) -> bool: """Returns whether the file is closed.""" return self._file.closed @property - def mode(self): + def mode(self) -> str: """Returns the mode of the file.""" return self._file.mode @property - def name(self): + def name(self) -> str: """Returns the name of the file.""" return self._file.name - def close(self): + ## methods + + def close(self) -> None: """Closes and commits the file by moving the temporary file to the correct name. """ diff --git a/pydfirram/core/renderer.py b/pydfirram/core/renderer.py index eba677b..e54c2c2 100644 --- a/pydfirram/core/renderer.py +++ b/pydfirram/core/renderer.py @@ -1,49 +1,78 @@ -"""todo""" +""" +This module provides utilities for rendering data in various formats, +specifically focusing on rendering Volatility framework data into JSON and +pandas DataFrames. + +Classes: + TreeGrid_to_json: A class for rendering Volatility TreeGrid data into + JSON format. + Renderer: A class for rendering data into human-readable formats such + as lists, JSON strings, and pandas DataFrames. +""" import datetime -from json import JSONEncoder,dumps,loads -from typing import Any, Tuple, List, Dict +from json import dumps +from typing import Any import pandas as pd from loguru import logger - -from volatility3.framework.renderers import format_hints -from volatility3.framework import interfaces -from volatility3.cli import ( - text_renderer, +from volatility3.framework.interfaces.renderers import ( # type: ignore + Disassembly as V3Disassembly, + BaseAbsentValue as V3BaseAbsentValue, + RenderOption as V3RenderOption, + TreeGrid as V3TreeGrid, + TreeNode as V3TreeNode, +) +from volatility3.framework.renderers.format_hints import ( # type: ignore + HexBytes as V3HexBytes, + MultiTypeData as V3MultiTypeData, +) +from volatility3.cli.text_renderer import ( # type: ignore + CLIRenderer as V3CLIRenderer, + optional as v3_optional, + quoted_optional as v3_quoted_optional, + hex_bytes_as_text as v3_hex_bytes_as_text, + display_disassembly as v3_display_disassembly, + multitypedata_as_text as v3_multitypedata_as_text, ) -class TreeGrid_to_json(text_renderer.CLIRenderer): - _type_renderers = { - format_hints.HexBytes: lambda x: text_renderer.quoted_optional( - text_renderer.hex_bytes_as_text +# allow no PascalCase naming style and "lambda may not be necessary" +# pylint: disable=W0108,C0103 +# (todo) : switch to PascalCase +class TreeGrid_to_json(V3CLIRenderer): # type: ignore + """ simple TreeGrid to JSON + """ + _type_renderers: Any = { + V3HexBytes: lambda x: v3_quoted_optional( + v3_hex_bytes_as_text, )(x), - interfaces.renderers.Disassembly: lambda x: text_renderer.quoted_optional( - text_renderer.display_disassembly + V3Disassembly: lambda x: v3_quoted_optional( + v3_display_disassembly, )(x), - format_hints.MultiTypeData: lambda x: text_renderer.quoted_optional( - text_renderer.multitypedata_as_text + V3MultiTypeData: lambda x: v3_quoted_optional( + v3_multitypedata_as_text, )(x), - bytes: lambda x: text_renderer.optional( + bytes: lambda x: v3_optional( lambda x: " ".join([f"{b:02x}" for b in x]) )(x), - datetime.datetime: lambda x: x.isoformat() - if not isinstance(x, interfaces.renderers.BaseAbsentValue) - else None, + datetime.datetime: lambda x: ( + x.isoformat() if not isinstance(x, V3BaseAbsentValue) else None + ), "default": lambda x: x, } name = "JSON" structured_output = True - def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + def get_render_options(self) -> list[V3RenderOption]: """ Get render options. """ - pass + return [] - def render(self, grid: interfaces.renderers.TreeGrid) -> Dict: + # (fixme): This method should return nothing as defined in V3CLIRenderer + def render(self, grid: V3TreeGrid) -> dict[str, Any]: """ Render the TreeGrid to JSON format. @@ -53,36 +82,40 @@ def render(self, grid: interfaces.renderers.TreeGrid) -> Dict: Returns: Dict: The JSON representation of the TreeGrid. """ - final_output: Tuple[ - Dict[str, List[interfaces.renderers.TreeNode]], - List[interfaces.renderers.TreeNode], + final_output: tuple[ + dict[str, dict[str, Any]], + list[dict[str, Any]], ] = ({}, []) + def visitor( - node: interfaces.renderers.TreeNode, - accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], - ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: - + node: V3TreeNode, + accumulator: tuple[dict[str, Any], list[dict[str, Any]]], + ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """ A visitor function to process each node in the TreeGrid. Args: - node (interfaces.renderers.TreeNode): The current node being visited. - accumulator (Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]): + node (V3TreeNode): The current node being visited. + accumulator (Tuple[Dict[str, Any], List[Dict[str, Any]]]): The accumulator containing the accumulated results. Returns: - Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: The updated accumulator. + Tuple[Dict[str, Any], List[Dict[str, Any]]]: The updated + accumulator. """ - acc_map, final_tree = accumulator - node_dict: Dict[str, Any] = {"__children": []} + acc_map = accumulator[0] + final_tree = accumulator[1] + node_dict: dict[str, Any] = {"__children": []} - for column_index in range(len(grid.columns)): - column = grid.columns[column_index] + for column_index, column in enumerate(grid.columns): renderer = self._type_renderers.get( - column.type, self._type_renderers["default"] + column.type, + self._type_renderers["default"] + ) + data = renderer( + list(node.values)[column_index], ) - data = renderer(list(node.values)[column_index]) - if isinstance(data, interfaces.renderers.BaseAbsentValue): + if isinstance(data, V3BaseAbsentValue): data = None node_dict[column.name] = data @@ -96,16 +129,20 @@ def visitor( if not grid.populated: grid.populate(visitor, final_output) else: - grid.visit(node=None, function=visitor, initial_accumulator=final_output) - return final_output[1] + grid.visit( + node=None, + function=visitor, + initial_accumulator=final_output, + ) + return {"data": final_output[1]} -class Renderer: +class Renderer(): """ Class for rendering data in various formats. - This class provides methods to render data into human-readable formats such as lists, - JSON strings, and pandas DataFrames. + This class provides methods to render data into human-readable formats + such as lists, JSON strings, and pandas DataFrames. Attributes: data (Any): The input data to be rendered. @@ -120,12 +157,33 @@ def __init__(self, data: Any) -> None: """ self.data = data - def to_list(self) -> Dict: + def to_list(self): + """ + Convert the data to a list format. + + This method attempts to render the input data using the + TreeGrid_to_json class, and convert it to a dictionary. + + Returns: + Dict: The rendered data in list format. + + Raises: + Exception: If rendering the data fails. + """ + try: + # (fixme) : `render()` should return nothing + parsed_data : dict[str, Any] = TreeGrid_to_json().render(self.data) + return parsed_data.get("data") + except Exception as e: + logger.error("Impossible to render data in dictionary form.") + raise e + + def file_render(self)-> None: """ Convert the data to a list format. - This method attempts to render the input data using the TreeGrid_to_json class, - and convert it to a dictionary. + This method attempts to render the input data using the + TreeGrid_to_json class, and convert it to a dictionary. Returns: Dict: The rendered data in list format. @@ -134,8 +192,8 @@ def to_list(self) -> Dict: Exception: If rendering the data fails. """ try: - formatted = TreeGrid_to_json().render(self.data) - return formatted + # (fixme) : `render()` return nothing + TreeGrid_to_json().render(self.data) except Exception as e: logger.error("Impossible to render data in dictionary form.") raise e @@ -144,8 +202,8 @@ def to_json(self) -> str: """ Convert the data to a JSON string. - This method first converts the data to a list format, and then serializes it - to a JSON string. + This method first converts the data to a list format, and then + serializes it to a JSON string. Returns: str: The data serialized as a JSON string. @@ -164,8 +222,8 @@ def to_df(self,max_row: bool = False) -> pd.DataFrame: """ Convert the data to a pandas DataFrame. - This method first converts the data to a list format, and then constructs - a pandas DataFrame from it. + This method first converts the data to a list format, and then + constructs a pandas DataFrame from it. Returns: pd.DataFrame: The data as a pandas DataFrame. diff --git a/pydfirram/core/utils.py b/pydfirram/core/utils.py index 66ee982..c156c61 100644 --- a/pydfirram/core/utils.py +++ b/pydfirram/core/utils.py @@ -1,4 +1,9 @@ -""" Todo: Add module docstring. """ +""" +This module provides utilities for hashing files. + +Functions: + get_hash(path: Path) -> str: Calculates and returns the SHA-256 hash of the specified file. +""" import hashlib @@ -9,13 +14,15 @@ def get_hash(path: Path) -> str: """ Get the hash of a file. - This method opens the specified file in binary mode and calculates the SHA-256 hash by traversing the file in - blocks of 4096 bytes. The hash is updated at each iteration to include the contents of the processed block. + This method opens the specified file in binary mode and calculates the + SHA-256 hash by traversing the file inblocks of 4096 bytes. The hash is + updated at each iteration to include the contents of the processed block. - Once the entire file has been processed, the method returns the SHA-256 hash value in hexadecimal format. + Once the entire file has been processed, the method returns the SHA-256 + hash value in hexadecimal format. - Note: This method is intended for internal use by the specific code and must not be called - directly from other parts of the code. + Note: This method is intended for internal use by the specific code and + must not be called directly from other parts of the code. Args: path (Path): Path to the file. @@ -27,5 +34,4 @@ def get_hash(path: Path) -> str: hash_obj = hashlib.sha256() for chunk in iter(lambda: f.read(4096), b""): hash_obj.update(chunk) - return hash_obj.hexdigest() diff --git a/pydfirram/modules/__init__.py b/pydfirram/modules/__init__.py index e69de29..8eb6759 100644 --- a/pydfirram/modules/__init__.py +++ b/pydfirram/modules/__init__.py @@ -0,0 +1,3 @@ +""" +pydfirram.modules - pydfirram special modules +""" diff --git a/pydfirram/modules/windows.py b/pydfirram/modules/windows.py index 5356bfa..4892465 100644 --- a/pydfirram/modules/windows.py +++ b/pydfirram/modules/windows.py @@ -1,8 +1,134 @@ -from pydfirram.core.base import Generic, OperatingSystem +"""Create generic volatility3 OS wrappers. + +This module provides a way to interact with Volatility3 plugins in a more +abstract way. It allows to automatically get all available plugins for a +specific OS and run them with the required arguments. + +Classes: + Windows + +Example: + The module can be used as follows: + + $ python3 + >>> from pydfirram.modules.windows import Windows + >>> from pathlib import Path + >>> dumpfile = Path("tests/data/dump.raw") + >>> generic = Windows(dumpfile) + >>> plugin = generic.pslist().to_list() + + OR : + $ python3 + >>> from pydfirram.modules.windows import Windows + >>> from pathlib import Path + >>> dumpfile = Path("tests/data/dump.raw") + >>> generic = Windows(dumpfile) + >>> plugin = generic.pslist(pid=[4]).to_df() + >>> print(plugin) +""" +from typing import Any +from pathlib import Path + +from pydfirram.core.base import Generic, OperatingSystem,Context +from pydfirram.core.renderer import Renderer class Windows(Generic): - """todo: add docstring here""" + """ + A wrapper class for utilizing Windows-specific functionalities around + the base methods. + + This class serves as a simplified interface for interacting with + Windows operating system dumps. It inherits from the Generic class and + initializes with Windows as the operating system. + + Attributes: + ----------- + dumpfile : str + The path to the memory dump file. + + Methods: + -------- + __init__(dumpfile) + Initializes the Windows class with the given dump file. + """ + def __init__(self, dumpfile: str|Path) -> None: + """ + Initializes the Windows class. + + Parameters: + ----------- + dumpfile : str + The path to the memory dump file. + + Example: + -------- + >>> windows = Windows("path/to/dump.raw": Path) + """ + if isinstance(dumpfile,str): + dumpfile = Path(dumpfile) + self.dump_files = dumpfile + super().__init__( + operating_system = OperatingSystem.WINDOWS, + dump_file = dumpfile, + ) + + # (todo) : seems to be a boilerplate from `Context` + # (todo) : add typing information + def _set_argument(self, context, prefix, kwargs): + for k, v in kwargs.items(): + print(k,v) + context.config[prefix+k] = v + return context + + def dumpfiles(self, **_kwargs: dict[str,Any]) -> None: + """ + Dump memory files based on provided parameters. + + This method utilizes the "dumpfiles" plugin to create memory + dumps from a Windows operating system context. The memory dumps + can be filtered based on the provided arguments. If no + parameters are provided, the method will dump the entire + system by default. + + Parameters: + ----------- + physaddr : int, optional + The physical address offset for the memory dump. + virtaddr : int, optional + The virtual address offset for the memory dump. + pid : int, optional + The process ID for which the memory dump should be + generated. + + Notes: + ------ + - The method sets up the context with the operating system + and dump files. + - Automagic and context settings are configured before + building the context. + - If additional keyword arguments are provided, they are + added as arguments to the context. + - The resulting context is executed and rendered to a file + using the Renderer class. + - If no parameters are provided, the method will dump the + entire system by default. - def __init__(self, dumpfile): - super().__init__(OperatingSystem.WINDOWS, dumpfile) + Returns: + -------- + None + """ + plugin = self.get_plugin("dumpfiles") + context = Context( + operating_system = OperatingSystem.WINDOWS, + dump_file = self.dump_files, + plugin = plugin, + ) + context.set_automagic() + context.set_context() + builded_context = context.build() + if _kwargs: + runable_context = context.add_arguments(builded_context,_kwargs) + else: + runable_context = builded_context + Renderer(runable_context.run()).file_render() diff --git a/pydfirram/py.typed b/pydfirram/py.typed new file mode 100644 index 0000000..e920333 --- /dev/null +++ b/pydfirram/py.typed @@ -0,0 +1,3 @@ +# Marker file for PEP 561 +# @note +# - "pydfirram" use uses inline types. diff --git a/pyproject.toml b/pyproject.toml index 559f44d..6627113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,13 +43,7 @@ test = [ ] dev = [ - "tox", - "pre-commit", - "virtualenv", - "pip", - "twine", - "toml", - "pandas-stubs", + "tox", "pre-commit", "virtualenv", "pip", "twine", "toml","pandas-stubs", ] doc = [ diff --git a/tests/config.py b/tests/config.py index 7f5f8df..42546aa 100644 --- a/tests/config.py +++ b/tests/config.py @@ -1,3 +1,3 @@ from pathlib import Path -DUMP_FILE = Path("/home/braguette/dataset_memory/ch2.dmp") \ No newline at end of file +DUMP_FILE = Path("./data/dump.raw") diff --git a/tests/test_volatility_windows_function.py b/tests/test_volatility_windows_function.py index ee4f905..fb4da57 100644 --- a/tests/test_volatility_windows_function.py +++ b/tests/test_volatility_windows_function.py @@ -1,6 +1,7 @@ import pytest from pathlib import Path from pydfirram.core.base import Generic, OperatingSystem +from pydfirram.modules.windows import Windows from pydfirram.core.renderer import Renderer from loguru import logger from .config import DUMP_FILE @@ -17,6 +18,13 @@ def generic_instance() -> Generic: dumpfile = Path(DUMP_FILE) return Generic(os, dumpfile) +@pytest.fixture +def windows_instance() -> Windows : + dumpfile = Path(DUMP_FILE) + return Windows(dumpfile) + + +@pytest.mark.pslist def test_volatility_pslist(generic_instance: Generic) -> None: """ Test the volatility PsList function @@ -25,11 +33,12 @@ def test_volatility_pslist(generic_instance: Generic) -> None: output: Renderer = generic_instance.pslist() assert isinstance(output, Renderer), "Output is not an instance of Renderer" pslist_content: List[Any] = output.to_list() - print(type(pslist_content)) assert isinstance(pslist_content, list), "Output content is not a list" assert len(pslist_content) > 0, "Output list is empty" logger.success("TEST PASSED!") +@pytest.mark.pslist +@pytest.mark.pslist_pid def test_volatilty_pslist_with_args_pid(generic_instance : Generic) -> None : logger.opt(colors=True).info("pslist with args from volatility is running") output : Renderer = generic_instance.pslist(pid=[4]) @@ -39,12 +48,14 @@ def test_volatilty_pslist_with_args_pid(generic_instance : Generic) -> None : assert len(pslist_content) == 1 logger.success("TEST PASSED !") +@pytest.mark.banners def test_volatility_banners(generic_instance : Generic) -> None : logger.opt(colors=True).info("banners from volatility is running") output : Renderer = generic_instance.banners(pid=[4]) assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.cmdline def test_volatility_cmdline(generic_instance : Generic) -> None : logger.opt(colors=True).info("cmdline from volatility is running") output : Renderer = generic_instance.cmdline() @@ -54,6 +65,10 @@ def test_volatility_cmdline(generic_instance : Generic) -> None : assert isinstance(cmdline_content,list),"Not a list" assert len(cmdline_content) > 0 +# Add tests for cmdline(pid) + + +@pytest.mark.dlllist def test_volatility_dlllist(generic_instance : Generic) -> None : logger.opt(colors=True).info("dlllist from volatility is running") output : Renderer = generic_instance.dlllist() @@ -62,61 +77,90 @@ def test_volatility_dlllist(generic_instance : Generic) -> None : assert isinstance(dllist_content,list),"Not a list" assert len(dllist_content) > 0 logger.success("TEST PASSED !") +# add tests for dlllist with these args +# --pid [PID ...] Process IDs to include (all other processes are excluded) +# --offset OFFSET Process offset in the physical address space +# --name NAME Specify a regular expression to match dll name(s) +# --base BASE Specify a base virtual address in process memory +# --ignore-case Specify case insensitivity for the regular expression name matching +# --dump Extract listed DLLs + +@pytest.mark.bigpools def test_bigpools(generic_instance : Generic) -> None : logger.opt(colors=True).info("bigpools from volatility is running") output : Renderer = generic_instance.bigpools() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +# add tests for dlllist with these args +# --tags TAGS Comma separated list of pool tags to filter pools returned +# --show-free Show freed regions (otherwise only show allocations in use) + + +@pytest.mark.callbacks def test_callbacks(generic_instance : Generic) -> None : logger.opt(colors=True).info("callbacks from volatility is running") output : Renderer = generic_instance.callbacks() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.certificates def test_certificates(generic_instance : Generic) -> None : logger.opt(colors=True).info("certificate from volatility is running") output : Renderer = generic_instance.certificates() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +# add tests for certificates with these args +# dump + +@pytest.mark.configwriter def test_configwriter(generic_instance : Generic) -> None : logger.opt(colors=True).info("configwriter from volatility is running") output : Renderer = generic_instance.configwriter() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +# add tests for configwriter with these args +# extra + +@pytest.mark.crashinfo def test_crashinfo(generic_instance : Generic) -> None : logger.opt(colors=True).info("crashinfo from volatility is running") with pytest.raises(Exception): generic_instance.crashinfo() logger.success("TEST PASSED !") +@pytest.mark.devicetree def test_devicetree(generic_instance : Generic) -> None : logger.opt(colors=True).info("devicetree from volatility is running") output : Renderer = generic_instance.devicetree() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.driverirp def test_driverirp(generic_instance : Generic) -> None : logger.opt(colors=True).info("driverirp from volatility is running") output : Renderer = generic_instance.driverirp() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.drivermodules def test_drivermodule(generic_instance : Generic) -> None : logger.opt(colors=True).info("drivermodule from volatility is running") output : Renderer = generic_instance.drivermodule() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.driverscan def test_driverscan(generic_instance : Generic) -> None : logger.opt(colors=True).info("driverscan from volatility is running") output : Renderer = generic_instance.driverscan() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.envars def test_envars(generic_instance : Generic) -> None : logger.opt(colors=True).info("envars from volatility is running") output : Renderer = generic_instance.envars() @@ -126,30 +170,46 @@ def test_envars(generic_instance : Generic) -> None : assert len(envars_content) > 0 logger.success("TEST PASSED !") +# add tests for envars with these args +# pid +# slient + + +@pytest.mark.hivelist def test_hivelist(generic_instance : Generic) -> None : logger.opt(colors=True).info("hivelist from volatility is running") output : Renderer = generic_instance.hivelist() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +# add tests for hivelist with these args +# filter +# dump + +@pytest.mark.hivescan def test_hivescan(generic_instance : Generic) -> None : logger.opt(colors=True).info("hivescan from volatility is running") output : Renderer = generic_instance.hivescan() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.iat def test_iat(generic_instance : Generic) -> None : logger.opt(colors=True).info("iat from volatility is running") output : Renderer = generic_instance.iat() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +# add tests for iat with these args +# pid +@pytest.mark.info def test_info(generic_instance : Generic) -> None : logger.opt(colors=True).info("info from volatility is running") output : Renderer = generic_instance.info() assert isinstance(output, Renderer), "Error during function execution" logger.success("TEST PASSED !") +@pytest.mark.pstree def test_pstree(generic_instance : Generic) -> None : logger.opt(colors=True).info("pstree from volatility is running") output : Renderer = generic_instance.pstree() @@ -158,3 +218,428 @@ def test_pstree(generic_instance : Generic) -> None : assert isinstance(cmdline_content,list),"Not a list" assert len(cmdline_content) > 0 logger.success("TEST PASSED !") +# add tests for pstree with these args +# pid +# physical + + +@pytest.mark.dumpfile_pid +@pytest.mark.dumpfiles +def test_dumpfile_with_args_pid(windows_instance : Windows): + current_directory = Path.cwd() + initial_files = set(current_directory.glob("file.*")) + new_files = set() + + try: + result = windows_instance.dumpfiles(pid=4) + assert result is None, "The dumpfile method should return a non-null result" + new_files = set(current_directory.glob("file.*")) - initial_files + assert len(new_files) >= 1, f"Expected exactly one new file starting with 'file.', but found {len(new_files)}" + logger.opt(colors=True).info(f"number of file dumped {len(new_files)}") + except Exception as e: + pytest.fail(f"An exception should not be raised: {e}") + finally: + for new_file in new_files: + try: + new_file.unlink() + except Exception as cleanup_error: + print(f"Failed to delete {new_file}: {cleanup_error}") + +@pytest.mark.dumpfile_physaddr +@pytest.mark.dumpfiles +def test_dumpfile_with_args_physaddr(windows_instance : Windows): + current_directory = Path.cwd() + initial_files = set(current_directory.glob("file.*")) + new_files = set() + + try: + result = windows_instance.dumpfiles(physaddr=533517296) + assert result is None, "The dumpfile method should return a non-null result" + # Check if new files starting with 'file.' are created + new_files = set(current_directory.glob("file.*")) - initial_files + assert len(new_files) == 1, f"Expected exactly one new file starting with 'file.', but found {len(new_files)}" + except Exception as e: + pytest.fail(f"An exception should not be raised: {e}") + + finally: + # Clean up any new files created during the test + for new_file in new_files: + try: + new_file.unlink() + except Exception as cleanup_error: + print(f"Failed to delete {new_file}: {cleanup_error}") + +#Not able to test virtaddr locally +@pytest.mark.dumpfiles +@pytest.mark.dumpfile_virtaddr +def test_dumpfile_with_args_virtaddr(windows_instance : Windows): + current_directory = Path.cwd() + initial_files = set(current_directory.glob("file.*")) + new_files = set() + value = 2274855800 + try: + result = windows_instance.dumpfiles(virtaddr=value) + # Check if new files starting with 'file.' with the value in hex are created + file_created = "file." + hex(value) + new_files = set(current_directory.glob(file_created)) - initial_files + assert len(new_files) == 1, f"Expected exactly one new file starting with 'file.', but found {len(new_files)}" + + except Exception as e: + pytest.fail(f"An exception should not be raised: {e}") + + finally: + # Clean up any new files created# windows.crashinfo.Crashinfo during the test + for new_file in new_files: + try: + new_file.unlink() + except Exception as cleanup_error: + print(f"Failed to delete {new_file}: {cleanup_error}") + +# windows.filescan.FileScan +@pytest.mark.filescan +def test_filescan(windows_instance: Windows): + logger.opt(colors=True).info("filescan from volatility is running") + output : Renderer = windows_instance.pstree() + assert isinstance(output, Renderer), "Error during function execution" + cmdline_content : list = output.to_list() + assert isinstance(cmdline_content,list),"Not a list" + assert len(cmdline_content) > 0 + logger.success("TEST PASSED !") + +# windows.getservicesids.GetServiceSIDs +@pytest.mark.getservicesids +def test_getservicesids(windows_instance: Windows): + logger.opt(colors=True).info("getservicesids from volatility is running") + output : Renderer = windows_instance.getservicesids() + assert isinstance(output, Renderer), "Error during function execution" + getservicesids : list = output.to_list() + assert isinstance(getservicesids,list),"Not a list" + assert len(getservicesids) > 0 + logger.success("TEST PASSED !") + +# windows.getsids.GetSIDs +@pytest.mark.getstids +def test_getsids(windows_instance: Windows): + logger.opt(colors=True).info("getsids from volatility is running") + output : Renderer = windows_instance.getsids() + assert isinstance(output, Renderer), "Error during function execution" + getsids : list = output.to_list() + assert isinstance(getsids,list),"Not a list" + assert len(getsids) > 0 + logger.success("TEST PASSED !") + +# windows.joblinks.JobLinks +@pytest.mark.joblinks +def test_joblinks(windows_instance: Windows): + logger.opt(colors=True).info("joblinks from volatility is running") + output : Renderer = windows_instance.joblinks() + assert isinstance(output, Renderer), "Error during function execution" + joblinks : list = output.to_list() + assert isinstance(joblinks,list),"Not a list" + assert len(joblinks) > 0 + logger.success("TEST PASSED !") + +@pytest.mark.new +@pytest.mark.handles +def test_handles(windows_instance: Windows): + logger.opt(colors=True).info("handles from volatility is running") + output : Renderer = windows_instance.handles() + assert isinstance(output, Renderer), "Error during function execution" + handles : list = output.to_list() + assert isinstance(handles,list),"Not a list" + assert len(handles) > 0 + logger.success("TEST PASSED !") + +# ----- NEW + +# windows.malfind.Malfind +@pytest.mark.new +@pytest.mark.malfind +def test_malfind(windows_instance: Windows): + logger.opt(colors=True).info("malfind from volatility is running") + output : Renderer = windows_instance.malfind() + assert isinstance(output, Renderer), "Error during function execution" + malfind : list = output.to_list() + assert isinstance(malfind,list),"Not a list" + assert len(malfind) > 0 + logger.success("TEST PASSED !") + +@pytest.mark.new +@pytest.mark.memmap +def test_memmap(windows_instance: Windows): + logger.opt(colors=True).info("memmap from volatility is running") + output : Renderer = windows_instance.memmap() + assert isinstance(output, Renderer), "Error during function execution" + memmap : list = output.to_list() + assert isinstance(memmap,list),"Not a list" + assert len(memmap) > 0 + logger.success("TEST PASSED !") + +@pytest.mark.new +@pytest.mark.modules +def test_modules(windows_instance: Windows): + logger.opt(colors=True).info("modules from volatility is running") + output : Renderer = windows_instance.modules() + assert isinstance(output, Renderer), "Error during function execution" + modules : list = output.to_list() + assert isinstance(modules,list),"Not a list" + assert len(modules) > 0 + logger.success("TEST PASSED !") + +# windows.mbrscan.MBRScan +@pytest.mark.new +@pytest.mark.mbrscan +def test_mbrscan(windows_instance: Windows): + logger.opt(colors=True).info("mbrscan from volatility is running") + output : Renderer = windows_instance.mbrscan() + assert isinstance(output, Renderer), "Error during function execution" + mbrscan : list = output.to_list() + assert isinstance(mbrscan,list),"Not a list" + assert len(mbrscan) > 0 + logger.success("TEST PASSED !") + + +# windows.modscan.ModScan +@pytest.mark.new +@pytest.mark.modscan +def test_modscan(windows_instance: Windows): + logger.opt(colors=True).info("modscan from volatility is running") + output : Renderer = windows_instance.modscan() + assert isinstance(output, Renderer), "Error during function execution" + modscan : list = output.to_list() + assert isinstance(modscan,list),"Not a list" + assert len(modscan) > 0 + logger.success("TEST PASSED !") + +# windows.mutantscan.MutantScan +@pytest.mark.new +@pytest.mark.mutantscan +def test_mutantscan(windows_instance: Windows): + logger.opt(colors=True).info("mutantscan from volatility is running") + output : Renderer = windows_instance.mutantscan() + assert isinstance(output, Renderer), "Error during function execution" + mutantscan : list = output.to_list() + assert isinstance(mutantscan,list),"Not a list" + assert len(mutantscan) > 0 + logger.success("TEST PASSED !") + +# windows.netscan.NetScan +@pytest.mark.new +@pytest.mark.netscan +def test_netscan(windows_instance: Windows): + logger.opt(colors=True).info("netscan from volatility is running") + output : Renderer = windows_instance.netscan() + assert isinstance(output, Renderer), "Error during function execution" + netscan : list = output.to_list() + assert isinstance(netscan,list),"Not a list" + assert len(netscan) > 0 + logger.success("TEST PASSED !") + +# windows.netstat.NetStat +@pytest.mark.new +@pytest.mark.netstat +def test_netstat(windows_instance: Windows): + logger.opt(colors=True).info("netscan from volatility is running") + output : Renderer = windows_instance.netstat() + assert isinstance(output, Renderer), "Error during function execution" + netstat : list = output.to_list() + assert isinstance(netstat,list),"Not a list" + assert len(netstat) > 0 + logger.success("TEST PASSED !") + +# windows.poolscanner.PoolScanner +@pytest.mark.new +@pytest.mark.poolscanner +def test_poolscanner(windows_instance: Windows): + logger.opt(colors=True).info("poolscanner from volatility is running") + output : Renderer = windows_instance.poolscanner() + assert isinstance(output, Renderer), "Error during function execution" + poolscanner : list = output.to_list() + assert isinstance(poolscanner,list),"Not a list" + assert len(poolscanner) > 0 + logger.success("TEST PASSED !") + + +# windows.privileges.Privs +@pytest.mark.new +@pytest.mark.privs +def test_privs(windows_instance: Windows): + logger.opt(colors=True).info("privs from volatility is running") + output : Renderer = windows_instance.privs() + assert isinstance(output, Renderer), "Error during function execution" + privs : list = output.to_list() + assert isinstance(privs,list),"Not a list" + assert len(privs) > 0 + logger.success("TEST PASSED !") + + +# windows.psscan.PsScan +@pytest.mark.new +@pytest.mark.psscan +def test_psscan(windows_instance: Windows): + logger.opt(colors=True).info("psscan from volatility is running") + output : Renderer = windows_instance.psscan() + assert isinstance(output, Renderer), "Error during function execution" + psscan : list = output.to_list() + assert isinstance(psscan,list),"Not a list" + assert len(psscan) > 0 + logger.success("TEST PASSED !") + +# windows.registry.printkey.PrintKey +@pytest.mark.new +@pytest.mark.printkey +def test_printkey(windows_instance: Windows): + logger.opt(colors=True).info("printkey from volatility is running") + output : Renderer = windows_instance.printkey() + assert isinstance(output, Renderer), "Error during function execution" + printkey : list = output.to_list() + assert isinstance(printkey,list),"Not a list" + assert len(printkey) > 0 + logger.success("TEST PASSED !") + +# windows.statistics.Statistics +@pytest.mark.new +@pytest.mark.statistics +def test_statistics(windows_instance: Windows): + logger.opt(colors=True).info("statistics from volatility is running") + output : Renderer = windows_instance.statistics() + assert isinstance(output, Renderer), "Error during function execution" + statistics : list = output.to_list() + assert isinstance(statistics,list),"Not a list" + assert len(statistics) > 0 + logger.success("TEST PASSED !") + +# windows.sessions.Sessions +@pytest.mark.new +@pytest.mark.sessions +def test_sessions(windows_instance: Windows): + logger.opt(colors=True).info("sessions from volatility is running") + output : Renderer = windows_instance.sessions() + assert isinstance(output, Renderer), "Error during function execution" + sessions : list = output.to_list() + assert isinstance(sessions,list),"Not a list" + assert len(sessions) > 0 + logger.success("TEST PASSED !") + +# windows.ssdt.SSDT Lists the system call table. +@pytest.mark.new +@pytest.mark.ssdt +def test_ssdt(windows_instance: Windows): + logger.opt(colors=True).info("ssdt from volatility is running") + output : Renderer = windows_instance.ssdt() + assert isinstance(output, Renderer), "Error during function execution" + ssdt : list = output.to_list() + assert isinstance(ssdt,list),"Not a list" + assert len(ssdt) > 0 + logger.success("TEST PASSED !") + +# windows.thrdscan.ThrdScan +@pytest.mark.new +@pytest.mark.thrdscan +def test_thrdscan(windows_instance: Windows): + logger.opt(colors=True).info("thrdscan from volatility is running") + output : Renderer = windows_instance.thrdscan() + assert isinstance(output, Renderer), "Error during function execution" + thrdscan : list = output.to_list() + assert isinstance(thrdscan,list),"Not a list" + assert len(thrdscan) > 0 + logger.success("TEST PASSED !") + +# windows.threads.Threads +@pytest.mark.new +@pytest.mark.threads +def test_threads(windows_instance: Windows): + logger.opt(colors=True).info("threads from volatility is running") + output : Renderer = windows_instance.threads() + assert isinstance(output, Renderer), "Error during function execution" + threads : list = output.to_list() + assert isinstance(threads,list),"Not a list" + assert len(threads) > 0 + logger.success("TEST PASSED !") + +# windows.vadinfo.VadInfo +@pytest.mark.new +@pytest.mark.vadinfo +def test_vadinfo(windows_instance: Windows): + logger.opt(colors=True).info("vadinfo from volatility is running") + output : Renderer = windows_instance.vadinfo() + assert isinstance(output, Renderer), "Error during function execution" + vadinfo : list = output.to_list() + assert isinstance(vadinfo,list),"Not a list" + assert len(vadinfo) > 0 + logger.success("TEST PASSED !") + +# windows.verinfo.VerInfo +@pytest.mark.new +@pytest.mark.verinfo +def test_verinfo(windows_instance: Windows): + logger.opt(colors=True).info("verinfo from volatility is running") + output : Renderer = windows_instance.verinfo() + assert isinstance(output, Renderer), "Error during function execution" + verinfo : list = output.to_list() + assert isinstance(verinfo,list),"Not a list" + assert len(verinfo) > 0 + logger.success("TEST PASSED !") + +# windows.virtmap.VirtMap +@pytest.mark.new +@pytest.mark.virtmap +def test_virtmap(windows_instance: Windows): + logger.opt(colors=True).info("virtmap from volatility is running") + output : Renderer = windows_instance.virtmap() + assert isinstance(output, Renderer), "Error during function execution" + virtmap : list = output.to_list() + assert isinstance(virtmap,list),"Not a list" + assert len(virtmap) > 0 + logger.success("TEST PASSED !") + + +# NOT ABLE TO TEST AT THIS TIME +# windows.registry.getcellroutine.GetCellRoutine +# windows.registry.userassist.UserAssist +# windows.symlinkscan.SymlinkScan +# windows.skeleton_key_check.Skeleton_Key_Check +# windows.strings.Strings +# windows.suspicious_threads.SupsiciousThreads +# windows.truecrypt.Passphrase +## windows.cachedump.Cachedump + + +#@pytest.mark.new +#@pytest.mark.cachedump +#def test_cachedump(windows_instance: Windows): +# logger.opt(colors=True).info("cachedump from volatility is running") +# output : Renderer = windows_instance.cachedump() +# assert isinstance(output, Renderer), "Error during function execution" +# cache_content : list = output.to_list() +# assert isinstance(cache_content,list),"Not a list" +# assert len(cache_content) > 0 +# logger.success("TEST PASSED !") +# +## windows.hashdump.Hashdump +## Not able to test this functionnality at this time +#@pytest.mark.new +#@pytest.mark.hashdump +#def test_hashdump(windows_instance: Windows): +# logger.opt(colors=True).info("hashdump from volatility is running") +# output : Renderer = windows_instance.hashdump() +# assert isinstance(output, Renderer), "Error during function execution" +# hashdump : list = output.to_list() +# assert isinstance(hashdump,list),"Not a list" +# assert len(hashdump) > 0 +# logger.success("TEST PASSED !") +# +# +## windows.hollowprocesses.HollowProcesses +## Not able to test this functionnality at this time +#@pytest.mark.new +#@pytest.mark.hollowprocesses +#def test_hollowprocesses(windows_instance: Windows): +# logger.opt(colors=True).info("hollowprocess from volatility is running") +# output : Renderer = windows_instance.hollowprocesses() +# assert isinstance(output, Renderer), "Error during function execution" +# hollowprocesses : list = output.to_list() +# assert isinstance(hollowprocesses,list),"Not a list" +# assert len(hollowprocesses) > 0 +# logger.success("TEST PASSED !") +# \ No newline at end of file From 7eae85fc2d629062c946df1cf4d3a1b909e87f8f Mon Sep 17 00:00:00 2001 From: Br4guette <92679326+Ston14@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:30:23 +0200 Subject: [PATCH 2/2] Dev (#52) * Enhance documentation for the project (#41) Co-authored-by: Br4guette * Add build docs * Add more docs (#42) * Enhance documentation for the project * Update docs --------- Co-authored-by: Br4guette * Add references (#43) * Enhance documentation for the project * Update docs * add references --------- Co-authored-by: Br4guette * Add references * Add title * Title 2 * add : mkdocs navigation references * document codes * fix typo * typo * fix typo * fix typo in utils * fix docs in tutorials.md * add : How to use documentation rename : tuto to installation add : documentation for windows fix : mkdocs add paths * Add other OS documentation * fix how to * fix : typo in docs * fix : typo * fix: typo in index * add : dark mode * add : colors on documentation * fix: diataxis * Fix colors * fix readme * Fix dumpfiles (#46) * fix : Dumpfile issue with parameters * test : Create test for dumpfiles * fix context builder * fix dumpfiles : dumpfiles can now be passed with arguments * tests: add tests for each parameters of dumpfiles fix: add markers on tests to easily execute a bunch of test instead of the complete file * fix : kwargs value in set_arguments was setted to int directly * add : Add test fonctions to test dumpfiles with a virtaddr but not able to test locally * add : add pytest decorator markers to pslist_pid --------- Co-authored-by: Br4guette * fix: fix error, function without parameter return an error * sorry * fix typing information (#47) * Fix/get plugins (#48) * fix : bad import on v3_plugins_mod fix : poetry lock modfied due to update dependacies fix : windows setargs * remover useless info --------- Co-authored-by: Br4guette * Fix: Correct dict.get() usage in TreeGrid_to_json renderer and remove debug print - Corrected the usage of dict.get() method by removing keyword arguments and using positional arguments instead. - Ensured the render method returns a dictionary as expected. - Updated the to_list method to properly call the render method and handle exceptions. - Improved the docstrings to reflect the correct return types and behaviors of the methods. - Removed a debug print statement introduced in a previous commit. This fixes the TypeError and ensures the TreeGrid is properly rendered to JSON format. * oops * fix to dataframe * add test for volatility (#49) Co-authored-by: Br4guette * fix: poetry lock --------- Co-authored-by: Br4guette Co-authored-by: St0n14 Co-authored-by: Yann MAGNIN <42215723+YannMagnin@users.noreply.github.com>