├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── codecov.yml ├── docs ├── Makefile ├── changelog.rst ├── commandline.rst ├── conf.py ├── dependencies.rst ├── index.rst ├── installation.rst ├── make.bat ├── python.rst └── specifics.rst ├── examples └── simple_cli.py ├── imagemounter ├── __init__.py ├── _util.py ├── cli │ ├── __init__.py │ ├── imount.py │ └── shell.py ├── dependencies.py ├── disk.py ├── exceptions.py ├── filesystems.py ├── parser.py ├── unmounter.py ├── volume.py └── volume_system.py ├── imount.py ├── requirements.txt ├── setup.py ├── shell.py └── tests ├── __init__.py ├── cli_test.py ├── dependencies_test.py ├── full_image_test.py ├── images ├── bdetest.E01 ├── lvm.raw ├── test.cramfs ├── test.ext3 ├── test.fat12 ├── test.iso ├── test.mbr ├── test.minix ├── test.ntfs ├── test.sqsh └── test.zip ├── parser_test.py ├── requirements.txt ├── volume_system_test.py └── volume_test.py /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/images/* filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] 12 | 13 | steps: 14 | # - uses: actions/checkout@v2 15 | # with: 16 | # lfs: true 17 | - uses: nschloe/action-cached-lfs-checkout@v1 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install system dependencies 24 | run: | 25 | sudo apt-get install -y xmount ewf-tools afflib-tools sleuthkit lvm2 mdadm cryptsetup libmagic1 avfs disktype squashfs-tools mtd-tools vmfs-tools 26 | sudo modprobe fuse 27 | - name: Install Python dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | pip install -r tests/requirements.txt 32 | 33 | - name: Critical lint with flake8 34 | # stop the build if there are Python syntax errors or undefined names 35 | run: | 36 | flake8 imagemounter --count --select=E9,F63,F7,F82 --show-source --statistics 37 | - name: Lint with flake8 38 | continue-on-error: true 39 | run: | 40 | flake8 imagemounter --ignore=E128,W503 --count --max-line-length=120 --statistics 41 | 42 | - name: Test with pytest 43 | run: | 44 | sudo $(which pytest) --cov=imagemounter --cov-report=xml --cov-branch 45 | 46 | - uses: codecov/codecov-action@v2 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | /.idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ralph Broenink. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include docs * 4 | recursive-include test * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | imagemounter 3 | ============ 4 | 5 | .. image:: https://github.com/ralphje/imagemounter/actions/workflows/test.yml/badge.svg?branch=master&event=push 6 | :target: https://github.com/ralphje/imagemounter/actions/workflows/test.yml 7 | .. image:: https://codecov.io/gh/ralphje/imagemounter/branch/master/graph/badge.svg 8 | :target: https://codecov.io/gh/ralphje/imagemounter 9 | .. image:: https://readthedocs.org/projects/imagemounter/badge/?version=latest 10 | :target: http://imagemounter.readthedocs.io/en/latest/?badge=latest 11 | 12 | imagemounter is a command-line utility and Python package to ease the mounting and unmounting of EnCase, Affuse, vmdk 13 | and dd disk images (and other formats supported by supported tools). It supports mounting disk images using xmount (with 14 | optional RW cache), affuse, ewfmount and vmware-mount; detecting DOS, BSD, Sun, Mac and GPT volume systems; mounting 15 | FAT, Ext, XFS UFS, HFS+, LUKS and NTFS volumes, in addition to some less known filesystems; detecting (nested) LVM 16 | volume systems and mounting its subvolumes; and reconstructing Linux Software RAID arrays. 17 | 18 | In its default mode, imagemounter will try to start mounting the base image on a temporary mount point, 19 | detect the volume system and then mount each volume seperately. If it fails finding a volume system, 20 | it will try to mount the entire image as a whole if it succeeds in detecting what it actually is. 21 | 22 | This package supports Python 3.6+. 23 | 24 | Example 25 | ======= 26 | A very basic example of a valid mount is as follows. The command-line utility has much more features, but results vary 27 | wildly depending on the exact type of disk you are trying to mount:: 28 | 29 | # imount lvm_containing_dos_volumesystem_containing_ext4 30 | [+] Mounting image lvm_containing_dos_volumesystem_containing_ext4 using auto... 31 | [+] Mounted raw image [1/1] 32 | [+] Mounted volume 2.0 GiB 4.0.2:Ext4 / [Linux] on /tmp/im_4.0.2_8l86mZ. 33 | >>> Press [enter] to unmount the volume, or ^C to keep mounted... 34 | [+] Parsed all volumes! 35 | [+] Analysis complete, unmounting... 36 | [+] All cleaned up 37 | 38 | If you want to see for yourself, you could try executing ``imount /dev/sda`` first. 39 | 40 | Documentation 41 | ============= 42 | Full documentation of this project is available from http://imagemounter.readthedocs.org/ or in the ``docs/`` directory. 43 | 44 | Installation 45 | ============ 46 | This package does not require other packages, though ``termcolor`` is recommended and ``pytsk3`` is needed if you wish to 47 | use this package for volume detection. 48 | 49 | Just perform the following commands for a basic installation:: 50 | 51 | apt-get install xmount ewf-tools afflib-tools sleuthkit 52 | pip3 install imagemounter 53 | imount --check 54 | 55 | Use ``imount --check`` to verify which packages are (not) installed. Install additional packages as needed. 56 | 57 | Contributing 58 | ============ 59 | Since imagemounter is an open source project, contributions of many forms are welcomed. Examples of possible 60 | contributions include: 61 | 62 | * Bug patches 63 | * New features 64 | * Documentation improvements 65 | * Bug reports and reviews of pull requests 66 | 67 | We use GitHub to keep track of issues and pull requests. You can always 68 | `submit an issue `_ when you encounter something out of the ordinary. 69 | 70 | Not all combinations of file and volume systems have been tested. If you encounter an issue, please try to change some 71 | of your arguments first, before creating a new GitHub issue. 72 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/imagemounter.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/imagemounter.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/imagemounter" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/imagemounter" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Release notes 2 | ============= 3 | 4 | We try to reduce backwards compatibility breakage only to major version releases, i.e. X.0.0. Minor releases (1.X.0) may include new features, whereas patch releases (1.0.X) will generally be used to fix bugs. Not all versions have followed these simple rules to date (and sometimes new features may creep up in patch releases), but we try to adhere them as much as possible :). 5 | 6 | Release history 7 | ~~~~~~~~~~~~~~~ 8 | 4.0.0 (unreleased) 9 | ------------------ 10 | This release contains some major improvements in the API of imagemounter, making large if-statements obsolete and 11 | obtaining more 'Pythonic' code where possible. It is a continuation of the efforts in version 3.0 and includes fixes 12 | from many contributors. 13 | 14 | New features: 15 | 16 | * Drop support for Python 2, bump version requirement for Python to 3.6. 17 | * Allowed programmatic access to dependency checks (contributed by gtback). This enables you to write code that checks 18 | whether certain dependencies are satisfied. 19 | * Changed the way file system types work. Large if-statements in the code have been replaced by class-based 20 | filesystem types. **Backwards incompatible** 21 | * The ``fstype`` attribute has been replaced with the ``filesystem`` attribute, but a backwards-compatible 22 | attribute is still available. 23 | * The ``_paths`` attribute is no longer available, but replaced by attributes on the :cls:`FileSystem` classes 24 | * The class-based filesystems implement a form of heuristic to determine the filesystem type. 25 | 26 | .. note:: 27 | 28 | This change may result in file systems that were previously correctly detected suddenly not being identified 29 | correctly. If you encounter such an error, please submit a bug report stating the outputs of the different 30 | identifiers, the actual result and the expected result. You can obtain this information using ``-vvvv`` on the 31 | command line. 32 | 33 | * carve and volume shadow copies are now also subvolumes. 34 | 35 | * Add support for VBox disk images (vdi) (contributed by ruzzle) 36 | * Add support for VHD volumes (contributed by Jarmo van Lenthe) 37 | 38 | Bugfixes: 39 | 40 | * Renamed nbd to qemu-nbd (contributed by Jarmo van Lenthe) 41 | * Support for segmented filenames higher than 999 (contributed by jdossett) 42 | * Fixed ``xmount`` command order (contributed by Jarmo van Lenthe) 43 | 44 | 3.1.0 (2017-08-06) 45 | ------------------ 46 | New features: 47 | 48 | * Support for exFAT filesystems (contributed by ldgriffin) 49 | * Addition of ``force_clean`` method to :class:`ImageParser` 50 | * Addition of :option:`--skip`, to complement :option:`--only-mount` 51 | * Improved support for UFS / BSD volumes 52 | 53 | Bugfixes: 54 | 55 | * Updated :option:`--keys` to not parse commas (as these may be part of the key themselves) and properly support 56 | the asterisk key. 57 | * Several fixes for ``fsstat`` call, including actually killing it after 3 seconds and optimizations by passing 58 | in the correct FS type, making mounting actually a lot faster. 59 | * Fixes for path expanding (contributed by ruzzle) 60 | * Re-index loopbacks prior to unmounting in the :class:`Unmounter` class (contributed by ldgriffin) 61 | * Use ``--sizelimit`` for various mounts, for support in newer kernel versions (contributed by ldgriffin) 62 | * Improved support for non-ASCII volume labels 63 | * The parted detection method does not hang anymore when parted requests input 64 | * Fix for communication of LUKS keys to the ``cryptsetup`` command 65 | * Fixes for reconstruction when multiple roots exist 66 | 67 | 68 | 3.0.1 (2017-04-08) 69 | ------------------ 70 | * Add support for qcow2 (contributed by Jarmo van Lenthe) 71 | * Allow use of lowercase e01 file extension when mounting a directory in imount CLI (contributed by sourcex) 72 | * Add ADS support for NTFS volumes (contributed by Patrick Leedom) 73 | * Ability to lazily call fusermount -uz when unmounting (contributed by Patrick Leedom) 74 | 75 | * Fix regression in mounting LV volumes; the path was incorrectly detected in :func:`get_raw_path` for these volumes. 76 | * Fix regression in detection of single volumes that would be detected as DOS/MBR based on file type. 77 | 78 | 3.0.0 (2016-12-11) 79 | ------------------ 80 | This new release includes several backwards-incompatible changes, mostly because features were removed from the 81 | public API or have been renamed to obtain a more consistent API. 82 | 83 | It was released after a long time of development, and does not even contain all features that were originally 84 | planned to go into this release, but it contains some important bugfixes that warranted a release. 85 | 86 | New major features: 87 | 88 | * Add volume shadow copy support for NTFS 89 | * Add BDE (Bitlocker) support 90 | * Addition of :option:`--keys` CLI argument and corresponding argument to Volume class, allowing to specify key 91 | material for crypto mounts, supporting both BDE and LUKS. 92 | * (Experimental) support for volume systems inside a volume. This is useful when e.g. a LVM volume contains in 93 | itself a MBR. 94 | * A split between detection and initialization of volumes has been made. The basic way to access volumes as calling 95 | :func:`init`, but that mounted all volumes immediately. Now, ``detect_*`` methods have been added. 96 | * Support ``blkid`` to retrieve FS type info 97 | * Support for Linux RAID volumes 98 | * (Still in development) interactive console, which will eventually become the primary means to interact with 99 | imagemounter. 100 | 101 | Bugfixes: 102 | 103 | * Calling :func:`init` will not automatically mount the volume when it is not ``alloc``. 104 | * Fix a bug where ``.e01`` files (lowercase) would not be recognized as Encase 105 | * Fixed support for newer versions of ``mmls`` 106 | * Fixed support for pytsk3 under Python 3 (contributed by insomniacslk) 107 | * Fixed support for EnCase v7 (EX01) image files (contributed by pix) 108 | * Improved detection of several volume types 109 | * :attr:`index` is now always ``str`` 110 | * :attr:`Volume.size` is now always ``int`` 111 | * Improved the unmounter with generic loopback support 112 | 113 | Removed and modified features: 114 | 115 | * Stopped providing :const:`None` and :const:`False` results when things go wrong for most methods. Instead, 116 | numerous exceptions have been added. These exceptions should be catched instead, or when using ``mount_volumes`` 117 | or ``init``, you can specify ``swallow_exceptions`` (default) to restore previous behaviour. This is useful, since 118 | iteration will continue regardless of exceptions. 119 | * Moved the attributes ``fstypes``, ``vstypes``, ``keys``, ``mountdir`` and ``pretty`` to the ``ImageParser`` instance, 120 | so it does not need to get passed down through the ``*args`` hack anymore. For instance, ``fstypes`` has been moved; 121 | the dict will be inspected upon Volume instantiation and stored in the ``fstype`` attribute. Other arguments and 122 | attributes have been eliminated completely, or have been replaced by arguments to specific methods. 123 | * Added an intermediary class :class:`VolumeSystem`. Both :class:`Volume` and :class:`Disk` now use this (iterable) 124 | base class in their :attr:`volumes` attribute. If you relied on :attr:`volumes` being a ``list``, you should now 125 | use ``list(volumes)``. If you relied on indexing of the attribute, you could now also use ``disk[0]`` or ``volume[0]`` 126 | for finding the correct volume index. :attr:`volume_source` was moved to this class, as have :attr:`vstype` and 127 | :attr:`volume_detector`. 128 | 129 | * Changes to the CLI: 130 | * Removed :option:`--fsforce` and :option:`--fsfallback`. Use ``*`` and ``?`` as fstypes instead for the same effect. 131 | This should make the CLI more sensible, especially regarding the :option:`--fsforce` argument. The default FS 132 | fallback is still ``unknown``, which can only be overridden by specifying ``--fstypes=?=none``. (You can now 133 | specify ``--fstypes=TYPE``, which equals to ``--fstypes=*=TYPE``) 134 | * Removed ``--stats`` and ``--no-stats``. These only complicated things and ``fsstat`` has been working fine for 135 | years now. 136 | * Removed ``--raid`` and ``--no-raid`` (due to Volume RAID support) 137 | * Removed ``--disktype`` and ``--no-disktype``. 138 | * Renamed ``--method`` to ``--disk-mounter``. 139 | * Renamed ``--detection`` to ``--volume-detector``. 140 | * Renamed ``--vstype`` to ``--vstypes``, now accepting a dict, similar to ``--fstypes`` 141 | * Moved the ``imount.py`` file into a new ``cli`` module, where also a new experimental shell-style CLI is under 142 | development. 143 | 144 | * Changes specific to :class:`ImageParser`: 145 | * Added ``add_disk`` and made ``paths`` optional in constructor. 146 | * Added indexing of the `ImageParser` and added ``get_volume_by_index`` method. 147 | * Removed ``mount_single_volume`` and ``mount_multiple_volumes``. Use ``init_volumes`` instead, or use a custom 148 | loop for more control. 149 | * Dropped support for a single string argument for ``paths`` in ``__init__``. Additionally, dropped the ``paths`` 150 | attribute entirely. 151 | 152 | * Changes specific to :class:`Disk`: 153 | * Renamed ``method`` to ``disk_mounter`` (see also CLI) 154 | * Removed ``name``, ``avfs_mountpoint`` and ``md_device`` from public API. 155 | * Removed Linux RAID Disk support. Instead, mount as a single volume, with the type of this volume being RAID. 156 | This greatly simplifies the :class:`Disk` class. (This means that :attr:`loopback` has also been dropped from Disk) 157 | * Added ``detect_volumes`` method, which can be used to detect volumes. 158 | * Removed most ``mount_*`` methods. Moved ``mount_volumes`` to ``init_volumes``. Functionality from the other methods 159 | can be restored with only a few lines of code. 160 | * Removed the need for the rather obsure ``multifile`` attribute of ``mount``. Only ``xmount`` actually required 161 | this, so we just implicitly use it there. 162 | * Moved the ``type`` attribute to a method ``get_disk_type``. 163 | 164 | * Changes specific to :class:`Volume`: 165 | * Renamed ``get_raw_base_path`` to ``get_raw_path`` 166 | * Renamed ``get_size_gib`` to ``get_formatted_size`` 167 | * Removed ``get_magic_type``, ``fill_stats``, ``open_jffs2``, ``find_lvm_volumes`` and ``open_luks_container`` 168 | from public API. 169 | * Removed the ``*_path``, ``carvepoint`` and ``bindmountpoint`` attributes from the public API. For ``carvepoint``, 170 | the ``carve`` method now returns the path to the carvepoint. All data has been moved to the private ``_paths`` 171 | attribute. The ``mountpoint`` and ``loopback`` attributes are kept. 172 | * Removed ``fsforce`` and ``fsfallback`` arguments and attributes from Volume (see also CLI) 173 | * Added ``init_volume``, which only mounts the single volume. It is used by ``init`` and the preferred way of 174 | mounting a single volume (instead of using ``mount``) 175 | * Moved several attributes of :class:`Volume` to a new :attr:`info` attribute, which is publicly accessible, but 176 | its contents are not part of a stable public API. 177 | 178 | * Changes specific to :class:`VolumeSystem` (if you consider it on par with the functionality moved from Disk): 179 | * Renamed ``detection`` to ``volume_detector`` (see also CLI) 180 | * Added a :func:`VolumeSystem.detect_volumes` iterable, which is the basic functionality of this class. 181 | * Moved ``mount_single_volume`` code from :class:`Disk` to this class, adding the ``single`` volume detection 182 | method. The directory detection method has been incorporated in this new method. 183 | 184 | * Dropped support for Python 3.2, since everyone seems to be doing that these days. 185 | 186 | 2.0.4 (2016-03-15) 187 | ------------------ 188 | * Add HFS+ support 189 | 190 | 2.0.3 (2015-08-02) 191 | ------------------ 192 | * Remove error prefix (``[-]``) from some of the warnings 193 | * Do not warn about using unknown as fsfallback anymore 194 | * Also work properly with the ``python-magic`` system package (in addition to the totally different ``python-magic`` 195 | PyPI package) 196 | * *vmware-mount* Add ``-r`` to vmware-mount for readonly mounts 197 | * *ntfs* Add force to mount options 198 | 199 | 2.0.2 (2015-06-17) 200 | ------------------ 201 | * Bugfix in :option:`--check` regarding the ``python-magic`` module 202 | * *vmware-mount* Fix vmware-mount support 203 | 204 | 2.0.1 (2015-06-17) 205 | ------------------ 206 | * Changed the default ``fsfallback`` to ``unknown``, instead of ``none``. 207 | 208 | 2.0.0 (2015-06-17) 209 | ------------------ 210 | * Introduce support for XFS, ISO, JFFS2, FAT, SquashFS, CramFS, VMFS, UDF and Minix (cheers martinvw!) 211 | * Add ability to read the disk GUID using disktype, and read the filesystem magic for better detection of filesystems 212 | (cheers martinvw!) 213 | * Add support for 'mounting' directories and compressed files using avfs (cheers martinvw!) 214 | * Add support for detecting volumes using parted 215 | * Introduce facility to carve filesystems for removed files, even in unallocated spaces 216 | * Add :option:`--no-interaction` for scripted access to the CLI 217 | * Add :option:`--check` for access to an overview of all dependencies of imagemounter 218 | * Add :option:`--casename` (and corresponding Python argument) to easily recognize and organize multiple mounts on 219 | the same system 220 | * Change :option:`--clean` to :option:`--unmount`, supporting arguments such as :option:`--mountdir` and 221 | :option:`--pretty`, and made the code more robust and easier to read and extend 222 | * Detect terminal color support and show color by default 223 | 224 | 225 | * BSD is now called UFS 226 | * :option:`--stats` is now the default in the Python script 227 | * NTFS mount now also shows the system files by default 228 | * Do not stop when not running as root, but warn and probably fail miserably later on 229 | * :attr:`fstype` now stores the detected file system type, instead of the :attr:`fstype` as determined by 230 | :func:`fill_stats` 231 | * Logging now properly uses the Python logging framework, and there are now 4 verbosity levels 232 | * Changes to how the pretty names are formatted 233 | * Some Py2/Py3 compatibility fixes 234 | 235 | 1.5.3 (2015-04-08) 236 | ------------------ 237 | * Add support for ``vmware-mount`` 238 | 239 | 1.5.2 (2015-04-08) 240 | ------------------ 241 | * Ensure ``Volume.size`` is always int 242 | * Fixed a GPT/DOS bug caused by TSK 243 | * Add FAT support 244 | 245 | 1.5.1 (2014-05-22) 246 | ------------------ 247 | * Add disk index for multi-disk mounts 248 | 249 | 1.5.0 (2014-05-14) 250 | ------------------ 251 | * Add support for volume detection using mmls 252 | * Python 3 support 253 | * Bugfix in luksOpen 254 | 255 | 1.4.3 (2014-04-26) 256 | ------------------ 257 | * Experimental LUKS support 258 | 259 | 1.4.2 (2014-04-26) 260 | ------------------ 261 | * Bugfix that would prevent proper unmounting 262 | 263 | 1.4.1 (2014-02-10) 264 | ------------------ 265 | * Initial Py3K support 266 | * Included script is now called ``imount`` instead of ``mount_images`` 267 | 268 | 1.4.0 (2014-02-03) 269 | ------------------ 270 | * :class:`Disk` is now a seperate class 271 | * Some huge refactoring 272 | * Numerous bugfixes, including resolving issues with unmounting 273 | * Rename ``image_mounter`` to ``imagemounter`` 274 | * Remove ``mount_images`` alias 275 | 276 | 1.3.1 (2014-01-23) 277 | ------------------ 278 | * More verbosity with respect to failing mounts 279 | 280 | 1.3.0 (2014-01-23) 281 | ------------------ 282 | * Add support for single volume mounts 283 | * Add support for dummy base mounting 284 | * Add support for RAID detection and mounting 285 | 286 | 1.2.9 (2014-01-21) 287 | ------------------ 288 | * Improve support for some types of disk images 289 | * Some changes in the way some command-line arguments work (removed :option:`-vs`, :option:`-fs` and :option:`-fsf`) 290 | 291 | 1.2.8 (2014-01-08) 292 | ------------------ 293 | * Make :option:`--stats the default 294 | * Print the volume size and offset in verbose mode in the CLI 295 | * Add imount as command line utility name 296 | 297 | 1.2.7 (2014-01-08) 298 | ------------------ 299 | * Add :option:`--keep` 300 | 301 | 1.2.6 (2014-01-08) 302 | ------------------ 303 | * Use fallback commands for base image mounting if the normal one fails 304 | * Add multifile option to Volume to control whether multifile argument passing should be attempted 305 | * Fix error in backwards compatibility of mount_partitions 306 | * Copy the label of a volume to the last mountpoint if it looks like a mountpoint 307 | 308 | 1.2.5 (2014-01-07) 309 | ------------------ 310 | * Ability to automatically detect the mountpoint based on files in the filesystem 311 | 312 | 1.2.4 (2013-12-16) 313 | ------------------ 314 | * Partition is now Volume 315 | * Store the volume flag (alloc, unalloc, meta) 316 | 317 | 1.2.3 (2013-12-10) 318 | ------------------ 319 | * Add support for pretty mount point names 320 | 321 | 1.2.2 (2013-12-09) 322 | ------------------ 323 | * Fix issue where 'extended' is detected as ext (again) 324 | 325 | 1.2.1 (2013-12-09) 326 | ------------------ 327 | * Fix issue where 'extended' is detected as ext 328 | * ImagePartition is now Volume 329 | 330 | 1.2.0 (2013-12-05) 331 | ------------------ 332 | * ImagePartition is now responsible for mounting and obtaining its stats, and detecting lvm volumes 333 | * LVM partitions are now mounted using this new mount method 334 | * Utilize the partition size for disk size, which is more reliable 335 | * Renamed ImagePartition to Volume (no backwards compatibility is provided) 336 | * Add unknown mount type, for use with :option:`--fstype`, which mounts without knowing anything 337 | * Support mounting a directory containing \*.001/\*.E01 files 338 | 339 | 1.1.2 (2013-12-05) 340 | ------------------ 341 | * Resolve bug with respect to determining free loopback device 342 | 343 | 1.1.1 (2013-12-04) 344 | ------------------ 345 | * Improve :option:`--clean` by showing the commands to be executed beforehand 346 | 347 | 1.1.0 (2013-12-04) 348 | ------------------ 349 | * Do not add sudo to internal commands anymore 350 | * :option:`--loopback` is removed, detects it automatically now 351 | * :option:`--clean` is added; will remove all traces of an unsuccessful previous run 352 | 353 | 1.0.4 (2013-12-03) 354 | ------------------ 355 | * Add the any vstype 356 | * Fix some errors in the ``mount_images`` script 357 | 358 | 1.0.3 (2013-12-02) 359 | ------------------ 360 | * Support forcing the fstype 361 | * Improved LVM support 362 | * Added some warnings to CLI 363 | 364 | 1.0.2 (2013-11-28) 365 | ------------------ 366 | * Improved NTFS support 367 | 368 | 1.0.1 (2013-11-28) 369 | ------------------ 370 | * ``command_exists`` now works properly 371 | 372 | 1.0.0 (2013-11-28) 373 | ------------------ 374 | * Now includes proper setup.py and versioning 375 | * Add support for reconstructing the filesystem using bindmounts 376 | * More reliable use of fsstat 377 | * Overhauled Python API with more transparency and less CLI requirements 378 | 379 | * Store yielded information in a ImagePartition 380 | * Remove dependency on args and add them to the class explicitly 381 | * Do not depend on user interaction or CLI output in ImageParser or util, but do CLI in ``__main__`` 382 | 383 | * Support for LVM 384 | * Support for ewfmount 385 | * Retrieve stats more reliably 386 | * New CLI arguments: 387 | 388 | * Colored output with :option:`--color` 389 | * Wait for warnings with :option:`--wait` 390 | * Support for automatic method with ``--method=auto`` 391 | * Specify custom mount dir with :option:`--mountdir` 392 | * Specify explicit volume system type with :option:`--vstype` 393 | * Specify explicit file system type with :option:`--fstype` 394 | * Specify loopback device with :option:`--loopback` (required by LVM support) 395 | -------------------------------------------------------------------------------- /docs/commandline.rst: -------------------------------------------------------------------------------- 1 | Command-line usage 2 | ================== 3 | 4 | One of the core functionalities of :mod:`imagemounter` is the command-line utility :command:`imount` that eases the mounting and unmounting of different types of disks and volumes. In its most basic form, the utility accepts a positional argument pointing to a disk image, disk or volume, e.g.:: 5 | 6 | imount disk.E01 7 | 8 | Multiple files can be passed to this command, allowing the mounting of volume systems that span multiple disks, which can be useful for those wishing to reconstruct a system that entailed multiple disks or for reconstructing RAID arrays. 9 | 10 | By default, :command:`imount` will mount each single volume in :file:`/tmp` and wait until you confirm an unmount operation. Common usage is therefore to keep :command:`imount` running in a separate window and perform other operations in a second window. 11 | 12 | Arguments 13 | --------- 14 | 15 | The :command:`imount` utility requires one (or more) positional arguments and offers the ability to pass several optional arguments. 16 | 17 | .. cmdoption:: [ ...] 18 | 19 | The positional argument(s) should provide the path(s) to the disk images you want to mount. Many different formats are supported, including the EnCase evidence format, split dd files, mounted hard drives, etc. In the case of split files, you can refer to the folder containing these files. 20 | 21 | If you specify more than one file, all files are considered to be part of the same originating system, which is relevant for the :option:`--reconstruct` command-line option. 22 | 23 | Arguments that immediately exit 24 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | Some useful facilities. 26 | 27 | .. cmdoption:: --help 28 | -h 29 | 30 | Shows a help message and exits. 31 | 32 | .. cmdoption:: --version 33 | 34 | Shows the current version and exits. 35 | 36 | .. cmdoption:: --check 37 | 38 | Shows which third-party utilities you have installed for a correct functioning of imagemounter. 39 | 40 | .. cmdoption:: --unmount 41 | -u 42 | 43 | Option that will try to identify leftover files from previous :command:`imount` executions and try to delete these. This will, for instance, clean leftover :file:`/tmp/im_{...}` mounts and mountpoints. This command will allow you to review the actions that will be taken before they are done. 44 | 45 | Can be combined with :option:`--casename`, :option:`--mountdir` and :option:`--pretty` to specify which mount points to delete. 46 | 47 | CLI behaviour 48 | ^^^^^^^^^^^^^ 49 | The next four command-line options alter the behaviour of the :command:`imount` utility, but does not affect the behaviour of the underlying :mod:`imagemounter` module. 50 | 51 | .. cmdoption:: --wait 52 | -w 53 | 54 | Pauses the execution of the program on all warnings. 55 | 56 | .. cmdoption:: --keep 57 | -k 58 | 59 | Skips the unmounting at the end of the program. 60 | 61 | .. cmdoption:: --no-interaction 62 | 63 | Never ask for input from the user, implies :option:`--keep`. 64 | 65 | .. cmdoption:: --only-mount 66 | 67 | Comma-separated list of volume indexes you want to mount. Other volumes are skipped. 68 | 69 | .. cmdoption:: --skip 70 | 71 | Comma-separated list of volume indexes you do not want to mount. 72 | 73 | .. cmdoption:: --verbose 74 | -v 75 | 76 | Show verbose output. Repeat for more verbosity (up to 4). 77 | 78 | .. cmdoption:: --color 79 | --no-color 80 | 81 | Force toggle colorizing the output. Verbose message will be colored blue, for instance. Requires the :mod:`termcolor` package. 82 | 83 | 84 | Additional features 85 | ^^^^^^^^^^^^^^^^^^^ 86 | This command-line option enables an additional and useful feature. 87 | 88 | .. cmdoption:: --reconstruct 89 | -r 90 | 91 | Attempts to reconstruct the full filesystem tree by identifying the last mountpoint of each identified volume and bindmounting this in the previous root directory. For instance, if volumes have previously been mounted at :file:`/` , :file:`/var` and :file:`/home` ; :file:`/var` and :file:`/home` will be bind-mounted in :file:`/` , providing you with a single filesystem tree in the mount location of :file:`/` that is easily traversible. 92 | 93 | This only works with Linux-based filesystems and only if :file:`/` can be identified. 94 | 95 | Implies :option:`--stats`. 96 | 97 | .. cmdoption:: --carve 98 | 99 | Carves the filesystem for missing files. 100 | 101 | .. cmdoption:: --vshadow 102 | 103 | Also mounts volume shadow copies 104 | 105 | Mount behaviour 106 | ^^^^^^^^^^^^^^^ 107 | These arguments alter some pieces of the mount behaviour of :mod:`imagemounter`, mostly to ease your work. 108 | 109 | .. cmdoption:: --mountdir 110 | -md 111 | 112 | Specifies the directory to place volume mounts. Defaults to a temporary directory. 113 | 114 | .. cmdoption:: --pretty 115 | -p 116 | 117 | Uses pretty names for volume mount points. This is useful in combination with :option:`--mountdir`, but you should be careful using this option. It does not provide a fallback when the mount point is not available or other issues arise. It can also not be cleaned with :option:`--clean`. 118 | 119 | .. cmdoption:: --casename 120 | -cn 121 | 122 | Use to specify the case name, which is used in pretty mounts, but also for the location of the mountdir. Useful if you want to be able to identify the mountpoints later. 123 | 124 | .. cmdoption:: --read-write 125 | -rw 126 | 127 | Will use read-write mounts. Written data will be stored using a local write cache. 128 | 129 | Implies :option:`--method xmount`. 130 | 131 | Advanced options 132 | ^^^^^^^^^^^^^^^^ 133 | While :mod:`imagemounter` will try to automatically detect as much as possible, there are some cases where you may wish to override the automatically detected options. You can specify which detection methods should be used and override the volume system and file system types if needed. 134 | 135 | .. cmdoption:: --disk-mounter 136 | -m 137 | 138 | Specifies the method to use to mount the base image(s). Defaults to automatic detection, though different methods deliver different results. Available options are `xmount`, `affuse` and `ewfmount` (defaulting to `auto`). 139 | 140 | If you provide `dummy`, the base is not mounted but used directly. 141 | 142 | .. cmdoption:: --volume-detector 143 | -d 144 | 145 | Specifies the volume detection method. Available options are `pytsk3`, `mmls`, `parted` and `auto`, which is the default. Though `pytsk3` and `mmls` should in principle deliver identical results, `pytsk3` can be considered more reliable as this uses the C API of The Sleuth Kit (TSK). However, it also requires :mod:`pytsk3` to be installed, which is not possible with Py3K. 146 | 147 | .. cmdoption:: --vstypes 148 | 149 | Specifies the type of the volume system, defaulting to `detect`. However, detection may not always succeed and valid options are `dos`, `bsd`, `sun`, `mac`, `gpt` and `dbfiller`, though the exact available options depend on the detection method and installed modules on the operating system. 150 | 151 | .. cmdoption:: --fstypes 152 | 153 | Specifies the filesystem of a volume to use. Available options include `ext`, `ufs`, `ntfs`, `luks`, `lvm` and `unknown`, with the latter simply mounting the volume without specifying type. See the command-line help for all available volume types. 154 | 155 | Filesystem types are specified for each volume separately. You can use subvolumes, examples including:: 156 | 157 | 1=ntfs 158 | 2=luks,2.0=lvm,2.0.1=ext 159 | 160 | If you wish to specify a fallback to use if automatic detection fails, you can use the special question mark (?) volume index. If you wish to override automatic detection at all for all unspecified volumes, you can use the asterisk (*) volume type. There is no point is specifying both a question mark and an asterisk. 161 | 162 | .. cmdoption:: --keys 163 | 164 | Allows the specification of key information for each volume separately. This is similar to :option:`--fstypes`, except that you can only specify one key per argument (i.e. a comma is not interpreted as special). The format of the specifc value depends on the volume type: 165 | 166 | For BDE, you can use a single letter, followed by a colon, followed by the value. This leads to the following accepted formats, similar to how the :command:`bdemount` command interprets input:: 167 | 168 | k:full volume encryption and tweak key 169 | p:passphrase 170 | r:recovery password 171 | s:file to startup key (.bek) 172 | 173 | For LUKS, you can use a similar format:: 174 | 175 | p:passphrase 176 | f:key-file 177 | m:master-key-file 178 | 179 | 180 | .. cmdoption:: --lazy-unmount 181 | 182 | Enables to unmount the volumes and disk lazily when the direct unmounting of the volumes fails. 183 | 184 | Advanced toggles 185 | ^^^^^^^^^^^^^^^^ 186 | :command:`imount` has some facilities that automatically detect some types of disks and volumes. However, these facilities may sometimes fail and can be disabled if needed. 187 | 188 | .. cmdoption:: --single 189 | --no-single 190 | 191 | :command:`imount` will, by default, try to detect whether the disk that is being mounted, contains an entire volume system, or only a single volume. If you know your volumes are not single volumes, or you know they are, use :option:`--no-single` and :option:`--single` respectively. 192 | 193 | Where :option:`--single` forces the mounting of the disk as a single volume, :option:`--no-single` will prevent the identification of the disk as a single volume if no volume system is found. 194 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # imagemounter documentation build configuration file, created by 4 | # sphinx-quickstart on Mon May 19 14:34:55 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix of source filenames. 39 | source_suffix = '.rst' 40 | 41 | # The encoding of source files. 42 | #source_encoding = 'utf-8-sig' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = u'imagemounter' 49 | copyright = u'2014, Ralph Broenink' 50 | 51 | # The version info for the project you're documenting, acts as replacement for 52 | # |version| and |release|, also used in various other places throughout the 53 | # built documents. 54 | # 55 | # The short X.Y version. 56 | version = '1.5' 57 | # The full version, including alpha/beta/rc tags. 58 | release = '1.5.1' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | #language = None 63 | 64 | # There are two options for replacing |today|: either, you set today to some 65 | # non-false value, then it is used: 66 | #today = '' 67 | # Else, today_fmt is used as the format for a strftime call. 68 | #today_fmt = '%B %d, %Y' 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | exclude_patterns = ['_build'] 73 | 74 | # The reST default role (used for this markup: `text`) to use for all 75 | # documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | # If true, keep warnings as "system message" paragraphs in the built documents. 96 | #keep_warnings = False 97 | 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | html_theme = 'default' 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | #html_theme_options = {} 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | #html_short_title = None 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | #html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'imagemounterdoc' 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | latex_documents = [ 201 | ('index', 'imagemounter.tex', u'imagemounter Documentation', 202 | u'Ralph Broenink, Peter Wagenaar', 'manual'), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | #latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | #latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | #latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | #latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | #latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | #latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [ 231 | ('index', 'imagemounter', u'imagemounter Documentation', 232 | [u'Ralph Broenink, Peter Wagenaar'], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | autoclass_content = 'both' 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'imagemounter', u'imagemounter Documentation', 247 | u'Ralph Broenink, Peter Wagenaar', 'imagemounter', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | 263 | 264 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org 265 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 266 | 267 | if not on_rtd: # only import and set the theme if we're building docs locally 268 | import sphinx_rtd_theme 269 | html_theme = 'sphinx_rtd_theme' 270 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 271 | 272 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it -------------------------------------------------------------------------------- /docs/dependencies.rst: -------------------------------------------------------------------------------- 1 | Dependencies 2 | ============ 3 | 4 | The ``imagemounter.dependencies`` module defines several optional and required 5 | dependencies to use ``imagemounter``. This data is used by the ``imount --check`` 6 | command, and can also be accessed via Python code: 7 | 8 | .. code-block:: python 9 | 10 | from imagemounter.dependencies import ewfmount 11 | 12 | ... 13 | 14 | if ewfmount.is_available: 15 | do_something_with_ewfmount() 16 | else: 17 | print(ewfmount.printable_status) 18 | 19 | 20 | Full list of dependencies: 21 | 22 | - ``imagemounter.dependencies.affuse`` 23 | - ``imagemounter.dependencies.bdemount`` 24 | - ``imagemounter.dependencies.blkid`` 25 | - ``imagemounter.dependencies.cryptsetup`` 26 | - ``imagemounter.dependencies.disktype`` 27 | - ``imagemounter.dependencies.ewfmount`` 28 | - ``imagemounter.dependencies.file`` 29 | - ``imagemounter.dependencies.fsstat`` 30 | - ``imagemounter.dependencies.lvm`` 31 | - ``imagemounter.dependencies.magic`` 32 | - ``imagemounter.dependencies.mdadm`` 33 | - ``imagemounter.dependencies.mmls`` 34 | - ``imagemounter.dependencies.mount_jffs2`` 35 | - ``imagemounter.dependencies.mount_ntfs`` 36 | - ``imagemounter.dependencies.mount_squashfs`` 37 | - ``imagemounter.dependencies.mount_xfs`` 38 | - ``imagemounter.dependencies.mountavfs`` 39 | - ``imagemounter.dependencies.parted`` 40 | - ``imagemounter.dependencies.pytsk3`` 41 | - ``imagemounter.dependencies.qemu_nbd`` 42 | - ``imagemounter.dependencies.vmfs_fuse`` 43 | - ``imagemounter.dependencies.vmware_mount`` 44 | - ``imagemounter.dependencies.vshadowmount`` 45 | - ``imagemounter.dependencies.xmount`` 46 | 47 | API 48 | --- 49 | 50 | The following classes are how dependencies are represented within imagemounter: 51 | 52 | .. autoclass:: Dependency 53 | :members: 54 | .. autoclass:: CommandDependency 55 | :members: 56 | .. autoclass:: PythonModuleDependency 57 | :members: 58 | .. autoclass:: MagicDependency 59 | :members: 60 | .. autoclass:: DependencySection 61 | :members: 62 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | imagemounter 2 | ============ 3 | imagemounter is a command-line utility and Python package to ease the mounting and unmounting of EnCase, Affuse, vmdk 4 | and dd disk images (and other formats supported by supported tools). It supports mounting disk images using xmount (with 5 | optional RW cache), affuse, ewfmount, vmware-mount and qemu-nbd; detecting DOS, BSD, Sun, Mac and GPT volume systems; 6 | mounting FAT, Ext, XFS UFS, HFS+, LUKS and NTFS volumes, in addition to some less known filesystems; detecting (nested) 7 | LVM volume systems and mounting its subvolumes; and reconstructing Linux Software RAID arrays. 8 | 9 | In its default mode, imagemounter will try to start mounting the base image on a temporary mount point, 10 | detect the volume system and then mount each volume seperately. If it fails finding a volume system, 11 | it will try to mount the entire image as a whole if it succeeds in detecting what it actually is. 12 | 13 | .. note:: 14 | Not all combinations of file and volume systems have been tested. If you encounter an issue, please try to change 15 | some of your arguments first, before creating a new GitHub issue. 16 | 17 | .. warning:: 18 | Mounting disks and volumes from unknown sources may pose an important security risk (especially since you probably 19 | need to run imagemounter as root). 20 | 21 | Contents 22 | -------- 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | 27 | installation 28 | dependencies 29 | commandline 30 | python 31 | specifics 32 | changelog 33 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | If you need an installation with basic support, you are suggested to run the following commands:: 5 | 6 | apt-get install xmount ewf-tools afflib-tools sleuthkit 7 | pip3 install imagemounter 8 | imount --check 9 | 10 | The latter command will list all other packages you could install to expand the capabilities of imagemounter. 11 | 12 | Python packages 13 | --------------- 14 | This package does not require other packages, though the :mod:`termcolor` package is recommended if you are using the 15 | :command:`imount` command line utility with the :option:`--color` argument. 16 | 17 | If you wish to use :mod:`pytsk3` support, you require *python-dev* and *libtsk-dev*. For compilation, the *build-essential* 18 | package from your distribution is also required. After that, you can easily install the :mod:`pytsk3` package from PyPI 19 | (:command:`pip` requires the :option:`--pre` flag to allow installing the package). 20 | 21 | Other dependencies 22 | ------------------ 23 | This package highly depends on other utilities to be present on your system. For a full installation, you require more 24 | tools. You can run ``imount --check`` to get a full list of all required tools. 25 | 26 | A basic installation contains at least one of the mount tools. Highly recommended is also ``fsstat``, others are required 27 | for specific file system types. 28 | 29 | You can install ``vmware-mount`` by installing VMware Workstation on your system. 30 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\imagemounter.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\imagemounter.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/python.rst: -------------------------------------------------------------------------------- 1 | Python interface 2 | ================ 3 | 4 | While :command:`imount` heavily utilizes the Python API of :mod:`imagemounter`, this API is also available for other classes. 5 | 6 | Data structure 7 | -------------- 8 | 9 | The basic structure of :mod:`imagemounter` is the :class:`imagemounter.ImageParser` class, which provides access to underlying :class:`imagemounter.Disk` and :class:`imagemounter.Volume` objects. Each file name passed to a new :class:`imagemounter.ImageParser` object results in one :class:`imagemounter.Disk` object. :class:`imagemounter.Volume` objects are created by analysis of the :class:`Disk` object (each volume generates one object, even if it is not mountable), and each :class:`imagemounter.Volume` can have one or more subvolumes. 10 | 11 | For instance, a LUKS volume may contain a LVM system that contains a Ext volume. This would create a :class:`Disk` with a :class:`Volume` containing a :class:`Volume` which contains the actual Ext :class:`Volume`. Subvolumes are managed through :class:`imagemounter.VolumeSystem`s, which is used by both the :class:`Volume` and :class:`Disk` classes. 12 | 13 | Most operations are managed on a :class:`Volume` level, although individual disk file mounting (and volume detection) is performed on a :class:`Disk` level and reconstruction is performed on a :class:`ImageParser` level. This means the following main parts make up the Python package: 14 | 15 | - :class:`imagemounter.ImageParser`, maintaining a list of Disks, providing several methods that are carried out on all disks (e.g. mount) and reconstruct. 16 | - :class:`imagemounter.Disk`, which represents a single disk iamge and can be mounted, and maintain volumes. It is also responsible for maintaining the write cache. Although a Disk is able to detect volumes, a Volume has similar capabilities. 17 | - :class:`imagemounter.Volume`, which can detect its own type and fill its stats, can be mounted, and maintain subvolumes. 18 | - :class:`imagemounter.VolumeSystem`, which is used to manage subvolumes and can detect volumes from a volume system. 19 | 20 | All three classes maintain an ``init()`` method that yields the volumes below it. You should call ``clean()`` on the parser as soon as you are done; you may also call ``unmount()`` on separate volumes or disks, which will also unmount all volumes below it. Warning: unmounting one of the RAID volumes in a RAID array, causes the entire array to be unmounted. 21 | 22 | Reference 23 | --------- 24 | .. module:: imagemounter 25 | 26 | If you utilize the API, you typically only require the :class:`ImageParser` object, e.g.:: 27 | 28 | parser = ImageParser(['/path/to/disk']) 29 | for v in parser.init(): 30 | print v.size 31 | root = parser.reconstruct() 32 | print root.mountpoint 33 | parser.clean() 34 | 35 | The best example of the use of the Python interface is the :command:`imount` command. The entirety of all methods and attributes is documented below. 36 | 37 | ImageParser 38 | ^^^^^^^^^^^ 39 | 40 | .. autoclass:: ImageParser 41 | 42 | .. automethod:: add_disk 43 | .. automethod:: init 44 | .. automethod:: init_volumes 45 | .. automethod:: reconstruct 46 | .. automethod:: clean 47 | .. automethod:: force_clean 48 | 49 | Most methods above, especially :func:`init`, handle most complicated tasks. However, you may need some more fine-grained control over the mount process, which may require you to use the following methods. Each of these methods passes their activities down to all disks in the parser and return whether it succeeded. 50 | 51 | .. automethod:: rw_active 52 | .. automethod:: get_volumes 53 | .. automethod:: get_by_index 54 | 55 | .. automethod:: mount_disks 56 | .. automethod:: mount_volumes 57 | 58 | For completeness, this is a list of all attributes of :class:`ImageParser`: 59 | 60 | .. attribute:: disks 61 | 62 | List of all :class:`Disk` objects. 63 | 64 | .. attribute:: paths 65 | casename 66 | fstypes 67 | keys 68 | vstypes 69 | mountdir 70 | pretty 71 | 72 | See the constructor of :class:`ImageParser`. 73 | 74 | Disk 75 | ^^^^ 76 | 77 | .. autoclass:: Disk 78 | 79 | .. automethod:: init 80 | .. automethod:: mount 81 | .. automethod:: detect_volumes 82 | .. automethod:: init_volumes 83 | .. automethod:: unmount 84 | 85 | The following methods are only required if you want some fine-grained control, typically if you are not using :func:`init`. 86 | 87 | .. automethod:: get_disk_type 88 | .. automethod:: rw_active 89 | .. automethod:: get_fs_path 90 | .. automethod:: get_raw_path 91 | .. automethod:: get_volumes 92 | 93 | The following attributes are also available: 94 | 95 | .. attribute:: index 96 | 97 | Disk index. May be None if it is the only disk of this type. 98 | 99 | .. attribute:: mountpoint 100 | 101 | The mountpoint of the disk, after a call to :func:`mount`. 102 | 103 | .. attribute:: rwpath 104 | 105 | The path to the read-write cache, filled after a call to :func:`mount`. 106 | 107 | .. attribute:: volumes 108 | 109 | :class:`VolumeSystem` of all direct child volumes of this disk, excluding all subvolumes. See :func:`get_volumes`. 110 | 111 | .. attribute:: method 112 | 113 | Used to store the base mount method. If it is set to ``auto``, this value will be overwritten with the actually used 114 | mount method after calling :func:`mount`. 115 | 116 | See also the constructor of :class:`Disk`. 117 | 118 | .. attribute:: parser 119 | paths 120 | offset 121 | read_write 122 | disk_mounter 123 | 124 | See the constructor of :class:`Disk`. 125 | 126 | Volume 127 | ^^^^^^ 128 | 129 | .. autoclass:: Volume 130 | 131 | 132 | .. automethod:: init 133 | .. automethod:: init_volume 134 | .. automethod:: unmount 135 | 136 | The following methods offer some more information about the volume: 137 | 138 | .. automethod:: get_description 139 | .. automethod:: get_safe_label 140 | .. automethod:: get_formatted_size 141 | .. automethod:: get_volumes 142 | 143 | These functions offer access to some internals: 144 | 145 | .. automethod:: determine_fs_type 146 | .. automethod:: get_raw_path 147 | .. automethod:: mount 148 | .. automethod:: bindmount 149 | .. automethod:: carve 150 | .. automethod:: detect_volume_shadow_copies 151 | .. automethod:: detect_mountpoint 152 | 153 | The following details may also be available as attributes: 154 | 155 | .. attribute:: size 156 | 157 | The size of the volume in bytes. 158 | 159 | .. attribute:: offset 160 | 161 | The offset of the volume in the disk in bytes. 162 | 163 | .. attribute:: index 164 | 165 | The index of the volume in the disk. If there are subvolumes, the index is separated by periods, though the exact 166 | format depends on the detection method and its format. 167 | 168 | .. attribute:: slot 169 | 170 | Internal slot number of the volume. 171 | 172 | .. attribute:: flag 173 | 174 | Indicates whether this volume is allocated (*alloc*), unallocated (*unalloc*) or a meta volume (*meta*). 175 | 176 | .. attribute:: block_size 177 | 178 | The block size of this volume. 179 | 180 | .. attribute:: fstype 181 | 182 | The volume file system type used internally as determined by :func:`determine_fs_type`. 183 | 184 | .. attribute:: key 185 | 186 | The key used by some crypto methods. 187 | 188 | .. attribute:: info 189 | 190 | A dict containing information about the volume. Not all keys are always available. Some common keys include: 191 | 192 | * fsdescription -- A description of the file system type, usually set by the detection method 193 | * lastmountpoint -- The last mountpoint of this volume. Set by :func:`load_fsstat_data` or 194 | :func:`detect_mountpoint` and only available for UFS and Ext volumes 195 | * label -- The volume label as detected by :func:`load_fsstat_data` 196 | * version -- The volume version as detected by :func:`load_fsstat_data` 197 | * statfstype -- The volume file system type as detected by :func:`load_fsstat_data` 198 | * guid -- The volume GUID 199 | * volume_group -- Used for LVM support 200 | 201 | The contents of the info dict are not considered part of a stable API and are subject to change in the future. 202 | 203 | .. attribute:: mountpoint 204 | 205 | The mountpoint of the volume after :func:`mount` has been called. 206 | 207 | .. attribute:: loopback 208 | 209 | The loopback device used by the volume after :func:`mount` (or related methods) has been called. 210 | 211 | .. attribute:: was_mounted 212 | is_mounted 213 | 214 | Booleans indicating that the volume has successfully been mounted during its lifetime, and is currently mounted 215 | 216 | .. attribute:: volumes 217 | parent 218 | 219 | :attr:`volumes` contains a :class:`VolumeSystem` of all subvolumes of this volume; :attr:`parent` contains the parent volume (if 220 | any). 221 | 222 | .. attribute:: disk 223 | stats 224 | fstypes 225 | pretty 226 | mountdir 227 | args 228 | 229 | See the constructor of :class:`Volume`. 230 | 231 | VolumeSystem 232 | ^^^^^^^^^^^^ 233 | 234 | .. autoclass:: VolumeSystem 235 | 236 | .. automethod:: detect_volumes 237 | .. automethod:: preload_volume_data 238 | 239 | .. automethod:: __iter__ 240 | .. automethod:: __getitem__ 241 | 242 | .. attribute:: volumes 243 | 244 | The list of all volumes in this system. 245 | 246 | .. attribute:: volume_source 247 | 248 | The source of the volumes of this system, either *single* or *multi*. 249 | 250 | .. attribute:: has_detected 251 | 252 | Boolean indicating whether this volume already ran its detection. 253 | 254 | .. attribute:: vstype 255 | detection 256 | args 257 | 258 | See the constructor of :class:`VolumeSystem`. 259 | 260 | Unmounter 261 | ^^^^^^^^^ 262 | 263 | .. autoclass:: Unmounter 264 | 265 | .. automethod:: preview_unmount 266 | .. automethod:: unmount 267 | 268 | .. automethod:: find_bindmounts 269 | .. automethod:: find_mounts 270 | .. automethod:: find_base_images 271 | .. automethod:: find_volume_groups 272 | .. automethod:: find_loopbacks 273 | .. automethod:: find_clean_dirs 274 | 275 | .. automethod:: unmount_bindmounts 276 | .. automethod:: unmount_mounts 277 | .. automethod:: unmount_base_images 278 | .. automethod:: unmount_volume_groups 279 | .. automethod:: unmount_loopbacks 280 | .. automethod:: clean_dirs 281 | 282 | .. attribute:: re_pattern 283 | 284 | The regex pattern used to look for volume mountpoints. 285 | 286 | .. attribute:: glob_pattern 287 | 288 | The glob pattern used to look for volume mountpoints. Always used in conjunction with the :attr:`re_pattern`. 289 | 290 | .. attribute:: orig_re_pattern 291 | 292 | The regex pattern used to look for base mountpoints. 293 | 294 | .. attribute:: orig_glob_pattern 295 | 296 | The glob pattern used to look for base mountpoints. Always used in conjunction with the :attr:`orig_re_pattern`. 297 | 298 | .. attribute:: be_greedy 299 | 300 | If set, some more volumes and mountpoints may be found. 301 | -------------------------------------------------------------------------------- /docs/specifics.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | File and volume system specifics 3 | ================================ 4 | 5 | This section contains specifics on different file systems and volume systems supported by imagemounter. This section is not complete and does not cover every edge case. You are invited to amend this section with additional details. 6 | 7 | File systems 8 | ============ 9 | 10 | ext 11 | --- 12 | ext2, ext3 and ext4 are all supported by imagemounter. All mounts use the ext4 drivers, allowing us to specify the ``noload`` argument to the mount subsystem. 13 | 14 | UFS 15 | --- 16 | Supported is UFS2, as we explicitly pass in the UFS2 driver type in the mount call. This may result in some UFS volumes not mounting. There is currently no workaround for that. 17 | 18 | Normally, a volume can be detected as both a UFS volume, as well as a BSD partition table. When the former happens, it may appear as if there is only a single volume, as this hides the other UFS volumes. Although detections have been amended for this, it is important to note that you can explicitly override detection using :option:`--fstypes` if detection fails for some reason. 19 | 20 | (See BSD volume system below for more information.) 21 | 22 | Depending on your OS, you may need to run ``modprobe ufs`` to enable UFS support in your kernel. 23 | 24 | NTFS 25 | ---- 26 | For mounting NTFS, you may need the *ntfs-3g* package. The ``show_sys_files`` option is enabled by default. 27 | 28 | This file system type may (accidentally) be detected when a BDE volume should be used instead. 29 | 30 | HFS / HFS+ 31 | ---------- 32 | No additional details. 33 | 34 | LUKS 35 | ---- 36 | For mounting LUKS volumes, the :command:`cryptsetup` command is used. At this point, it is not possible to specify more options to the cryptsetup subsystem. To specify keys, use :option:`--keys`. The following values are accepted:: 37 | 38 | p:passphrase 39 | f:key-file 40 | m:master-key-file 41 | 42 | Note that you can't provide multiple keys using a single --keys argument. Repeat the argument to accomplish this, e.g. ``--keys 0=p:passphrase --keys 1=p:passphrase``. 43 | 44 | To determine whether a volume is a LUKS volume, ``cryptsetup isLuks`` is called. This method should return true; if it doesn't, imagemounter will also not be able to mount the volume. The next step is to create a loopback device that is used to call ``cryptsetup luksOpen ``, where name is of the form ``image_mounter_luks_``. Additional details of the volume are extracted by using ``cryptsetup status``. The actual dd image of the volume is mounted in ``/dev/mapper/`` by the OS. 45 | 46 | The LUKS volume will get a subvolume at index 0 with the file system description ``LUKS Volume``. When this volume is a LVM volume that is not be properly recognized by imagemounter, you could use something like the following to amend this:: 47 | 48 | imount image.E01 --fstypes=1=luks,1.0=lvm,1.0.0=ext --keys=1=p:passphrase 49 | 50 | LUKS volumes are automatically unmounted by ending the script normally, but can't be unmounted by :option:`--unmount`. 51 | 52 | The LUKS volume type may not be automatically detected in some cases. 53 | 54 | BDE (Bitlocker) 55 | --------------- 56 | Bitlocker Drive Encrypted volumes are mounted using the :command:`bdemount` command. imagemounter allows you to provide the crypto material to the command using :option:`--keys`. Examples of valid commands are:: 57 | 58 | imount image.E01 --fstypes=2=bde --keys=2=p:passphrase 59 | imount image.E01 --fstypes=2=bde --keys=2=r:recovery_password 60 | 61 | See the manpage for :command:`bdemount` for all valid arguments that can be passed to the subsystem (crypto material is provided by replacing ``:`` with ``- ``). 62 | 63 | The BDE volume will get a subvolume at index 0 with the file system description ``BDE Volume``. imagemounter should normally correctly detect this subvolume to be a NTFS volume. 64 | 65 | BDE volumes are automatically unmounted by ending the script normally, but in some cases may not be properly unmounted by :option:`--unmount`. 66 | 67 | The BDE volume type may not be properly recognized and may instead by recognized as NTFS volume. You can override this by explicitly stating the volume type as in the examples above. 68 | 69 | LVM 70 | --- 71 | LVM systems host multiple volumes inside a single volume. imagemounter is able to detect these volumes on most occassions, though it may not always be possible to detect the file system type of the volumes inside the LVM. 72 | 73 | Mounting an LVM is done by mounting the volume to a loopback device and running ``lvm pvscan``. This should return a list of all LVMs on the system, but by matching the mount point of the base image, the scirpt should be able to identify the volume group name. This name is then used to enable the LVM by running ``vgchange -a y ``. Using ``lvdisplay ``, the volumes inside the volume group are extracted. The volume themselves are found at the LV Path provided by this command. 74 | 75 | Volumes inside a LVM are given the FS description ``Logical Volume``. The file system types should be recognized properly by the detection methods, and otherwise ``unknown`` should work, but otherwise you could explicitly specify the file system type as follows:: 76 | 77 | imount image.E01 --fstypes=1=lvm,1.0=ext 78 | 79 | Please note that many Linux based operating systems will try to mount LVMs for you. Although imagemounter tries to circumvent this automation, if you are unable to properly unmount, you should try to unmount through the interface of your OS first. Another useful command is ``vgchange -a n`` to disable all LVMs currently active (only use if you are not using a LVM for your own OS!). 80 | 81 | Unmounting LVMs is supported both by properly closing from the script as well as by using :option:`--unmount` 82 | 83 | Linux Software RAID 84 | ------------------- 85 | Linux RAID volume support is provided by the ``mdadm`` command. A volume is added to a RAID array incrementally; the ``mdadm`` command is responsible for adding the volume to the correct array. The location of the RAID array is captured by imagemounter so it can be unmounted again. A subvolume will be added with the description ``RAID volume`` at index 0. 86 | 87 | If the RAID volume can not be started directly after adding the volume, mounting will have succeeded, but the mountpoint will not be available yet. When another volume is added to the same RAID array, it will get the same (identical) subvolume as the original RAID volume. You should not mount it again. ``init`` will take care of both cases for you. 88 | 89 | .. warning:: 90 | 91 | If, for any reason, you have multiple RAID volumes in the same RAID array, unmounting one of the volumes will also immediately unmount all other RAID volumes in the same array. Because of this, you should ensure that you keep all RAID volumes mounted until you are done building and examining a specific array. 92 | 93 | RAID volumes are sometimes correctly detected, but there are also cases where the volume appears to *successfully* mount as another volume type. You should be very careful with this. 94 | 95 | .. note:: 96 | 97 | A disk leveraging full disk RAID can be mounted as a single volume with the RAID filesystem type. 98 | 99 | XFS 100 | --- 101 | XFS is supported through the *xfsprogs* package. 102 | 103 | ISO (ISO9660) 104 | ------------- 105 | No additional details. 106 | 107 | UDF 108 | --- 109 | No additional details. 110 | 111 | FAT 112 | --- 113 | FAT volumes, independent of type, are mounted through the VFAT driver. 114 | 115 | exFAT 116 | ----- 117 | exFAT volumes are mounted by teh exFAT driver. Note that exFAT volumes are sometimes recognized as NTFS volumes. 118 | 119 | Another quirk may be that parted recognizes a single exFAT volume as a DOS partition table with some free space (also see `this comment `_). Use another detection method or an explicit :option:`--single` to amend this. 120 | 121 | VMFS 122 | ---- 123 | VMFS is supported through the *vmfs-tools* package. Mounting is performed by finding a loopback device and using the ``vmfs-fuse`` command to mount this loopback on the mountpoint. 124 | 125 | SquashFS 126 | -------- 127 | SquashFS is supported through the *squashfs-tools* package. 128 | 129 | JFFS2 130 | ----- 131 | JFFS2 is supported through the *mtd-tools* package. JFFS2 is sometimes used by BIOS images and the like. 132 | 133 | The following commands are executed to open a JFFS2 image, where ```` is given a buffer of 1.2 times the size of the volume:: 134 | 135 | modprobe -v mtd 136 | modprobe -v jffs2 137 | modprobe -v mtdram total_size= erase_size=256 138 | modprobe -v mtdblock 139 | dd if= of=/dev/mtd0 140 | mount -t jffs2 /dev/mtdblock0 141 | 142 | .. warning:: 143 | 144 | This filesystem type may not work while mounting multiple images of the same type at the same time. 145 | 146 | Unmounting for this filesystem type is not fully supported. 147 | 148 | CramFS 149 | ------ 150 | No additional details. 151 | 152 | Minix 153 | ----- 154 | No additional details. 155 | 156 | Dir 157 | --- 158 | The dir filesystem type is not an actual mount type, but is used by imagemounter to indicate directories. This can be used in conjunction with the AVFS mount method, but basically just symlinks a directory to the mount location. It is provided for abstraction purposes. 159 | 160 | Unknown 161 | ------- 162 | The unknown filesystem type is not an actual mount type, but used by imagemounter to indicate that the volume should be mounted without specifying the volume type. This is less specific and does not work in most cases (since it lacks the ability to provide additional options to the mount subsystem) but may result in the volume actually being able to be used. 163 | 164 | The unknown filesystem type is used as fallback by default, and is for instance used if no specific volume type is provided by any of the detection methods other than 'Linux'. If you wish to override this default, and choose skipping mounting instead, you can also use the ``none`` filesystem type:: 165 | 166 | imount image.dd --fstypes=?=none 167 | 168 | 169 | Volume systems 170 | ============== 171 | 172 | DOS (MBR) 173 | --------- 174 | In some cases, the DOS volume system is recognized as either a DOS or a GPT volume system. This appears to be a bug in The Sleuth Kit used by some detection methods. imagemounter works around this by choosing in this case for the GPT volume system and will log a warning. In the case that this is not the right choice, you must use :option:`--vstype` to explicitly provide the correct volume system. 175 | 176 | In the case you have picked the wrong volume system, you can easily spot this. If you see ``GPT Safety Partition`` popping up, you should have chosen GPT. 177 | 178 | GPT 179 | --- 180 | See the DOS/MBR volume system. 181 | 182 | BSD 183 | --- 184 | The BSD volume system (BSD disklabel) is commonly used in conjunction with UFS. 185 | 186 | BSD volume c (BSD disk label uses letters to indicate the volumes, imagemounter will number this as volume 3) may appear to contain the entire volume set, and have the same offset as UFS volume a. The correct volume is volume a, and you should skip volume c. This is currently not fixed by imagemounter. 187 | 188 | Sun 189 | --- 190 | No additional details. 191 | 192 | MAC 193 | --- 194 | No additional details. 195 | 196 | Detect 197 | ------ 198 | Lets the subsystem automatically decide the correct volume system type. 199 | -------------------------------------------------------------------------------- /examples/simple_cli.py: -------------------------------------------------------------------------------- 1 | """This is a very basic CLI interface. This CLI is only provided as a concrete example for how to use the Python 2 | interface of imagemounter. This is by no means a complete representation of the feature set of imagemounter. More 3 | advanced options are available in the mount_images script. 4 | 5 | However, the mount_images has become bloated, with additional checks for commands, manual overrides, etc. You 6 | should use that script if you need some control, but this script should work perfectly too for very basic 7 | functions. 8 | 9 | Note that it doesn't unmount. 10 | """ 11 | 12 | import os 13 | import sys 14 | 15 | sys.path.append(os.path.dirname(os.path.dirname(__file__))) 16 | 17 | import argparse 18 | import logging 19 | import sys 20 | from imagemounter import ImageParser, __version__ 21 | from imagemounter.exceptions import ImageMounterError 22 | from termcolor import colored 23 | 24 | 25 | def main(): 26 | # We use argparse to parse arguments from the command line. 27 | parser = argparse.ArgumentParser(description='Simple CLI to mount disk images.') 28 | parser.add_argument('--version', action='version', version=__version__, help='display version and exit') 29 | parser.add_argument("-i", "--in", action='append', required=True, metavar='IN', 30 | help="path(s) to the files you want to mount", dest='images') 31 | parser.add_argument("-o", "--out", help="directory to mount the volumes in", dest="mountdir") 32 | parser.add_argument("-c", "--casename", help="the name of the case (this is appended to the output dir)") 33 | parser.add_argument("-r", "--restore", action="store_true", help="carve unallocated space", dest="carve") 34 | parser.add_argument("-s", "--shadow", action="store_true", help="mount volume shadow copies", dest="vshadow") 35 | parser.add_argument("-v", "--verbose", action="store_true", help="enable verbose output") 36 | args = parser.parse_args() 37 | 38 | # This sets up the logger. This is somewhat huge part of this example 39 | class ImageMounterFormatter(logging.Formatter): 40 | def format(self, record): 41 | msg = record.getMessage() 42 | if args.verbose and record.exc_info: 43 | if not record.exc_text: 44 | record.exc_text = self.formatException(record.exc_info) 45 | if msg[-1:] != "\n": 46 | msg += "\n" 47 | msg += record.exc_text 48 | if record.levelno >= logging.WARNING: 49 | return colored("[-] " + msg, 'cyan') 50 | elif record.levelno == logging.INFO: 51 | return colored("[+] " + msg, 'cyan') 52 | elif msg.startswith('$'): 53 | return colored(" " + msg, 'cyan') 54 | else: 55 | return colored(" " + msg, 'cyan') 56 | 57 | # Set logging level for internal Python 58 | handler = logging.StreamHandler() 59 | handler.setFormatter(ImageMounterFormatter()) 60 | logger = logging.getLogger("imagemounter") 61 | logger.setLevel(logging.WARNING if not args.verbose else logging.DEBUG) 62 | logger.addHandler(handler) 63 | 64 | # This is the basic parser. 65 | parser = ImageParser(args.images, pretty=True, **vars(args)) 66 | for volume in parser.init(): 67 | # parser.init() loops over all volumes and mounts them 68 | if volume.mountpoint: 69 | # If the mountpoint is set, we have successfully mounted it 70 | print('[+] Mounted volume {0} on {1}.'.format(colored(volume.get_description(), attrs=['bold']), 71 | colored(volume.mountpoint, 'green', attrs=['bold']))) 72 | 73 | elif volume.loopback: 74 | # If the mountpoint is not set, but a loopback is used, this is probably something like an LVM that did 75 | # not work properly. 76 | print('[+] Mounted volume {0} as loopback on {1}.'.format(colored(volume.get_description(), attrs=['bold']), 77 | colored(volume.loopback, 'green', attrs=['bold']))) 78 | print(colored('[-] Could not detect further volumes in the loopback device.', 'red')) 79 | 80 | elif volume.exception and volume.size is not None and volume.size <= 1048576: 81 | # If an exception occurred, but the volume is small, this is just a warning 82 | print(colored('[-] Exception while mounting small volume {0}'.format(volume.get_description()), 'yellow')) 83 | 84 | elif volume.exception: 85 | # Other exceptions are a bit troubling. Should never happen, actually. 86 | print(colored('[-] Exception while mounting {0}'.format(volume.get_description()), 'red')) 87 | 88 | elif volume.flag != 'meta' or args.verbose: 89 | # Meta volumes are not interesting enough to always show a warning about 90 | # Other volumes, we just print a warning that we couldn't mount it. 91 | print(colored('[-] Could not mount volume {0}'.format(volume.get_description()), 'yellow')) 92 | 93 | if args.carve and volume.flag != 'meta': 94 | # Carving is not neccesary on meta volumes, other volumes are carved for their unallocated space 95 | # or their entire space, depending on whether we could mount it. 96 | sys.stdout.write("[+] Carving volume...\r") 97 | sys.stdout.flush() 98 | try: 99 | path = volume.carve(freespace=not volume.mountpoint) 100 | except ImageMounterError: 101 | print(colored('[-] Carving failed.', 'red')) 102 | else: 103 | print('[+] Carved data is available at {0}.'.format(colored(path, 'green', attrs=['bold']))) 104 | 105 | if args.vshadow and volume.filesystem.type == 'ntfs': 106 | sys.stdout.write("[+] Mounting volume shadow copies...\r") 107 | sys.stdout.flush() 108 | try: 109 | volumes = volume.detect_volume_shadow_copies() 110 | except ImageMounterError: 111 | print(colored('[-] Volume shadow copies could not be mounted.', 'red')) 112 | else: 113 | for v in volumes: 114 | try: 115 | v.init_volume() 116 | except ImageMounterError: 117 | print(colored('[-] Volume shadow copy {} not mounted'.format(v), 'red')) 118 | else: 119 | print('[+] Volume shadow copy available at {0}.'.format(colored(v.mountpoint, 'green', attrs=['bold']))) 120 | else: 121 | continue # we do not need the unmounting sequence 122 | 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /imagemounter/__init__.py: -------------------------------------------------------------------------------- 1 | __ALL__ = ['Volume', 'VolumeSystem', 'Disk', 'ImageParser', 'Unmounter'] 2 | __version__ = '3.1.0' 3 | 4 | BLOCK_SIZE = 512 5 | DISK_MOUNTERS = ('xmount', 'affuse', 'ewfmount', 'vmware-mount', 'avfs', 'qemu-nbd', 'auto', 'dummy') 6 | VOLUME_SYSTEM_TYPES = ('detect', 'dos', 'bsd', 'sun', 'mac', 'gpt') 7 | 8 | 9 | from imagemounter.filesystems import FILE_SYSTEM_TYPES # NOQA 10 | from imagemounter.parser import ImageParser # NOQA 11 | from imagemounter.disk import Disk # NOQA 12 | from imagemounter.volume import Volume # NOQA 13 | from imagemounter.unmounter import Unmounter # NOQA 14 | from imagemounter.volume_system import VolumeSystem # NOQA 15 | -------------------------------------------------------------------------------- /imagemounter/_util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import subprocess 4 | import re 5 | import glob 6 | import os 7 | import sys 8 | import locale 9 | 10 | from imagemounter.exceptions import SubsystemError, CleanupError, NoNetworkBlockAvailableError 11 | 12 | logger = logging.getLogger(__name__) 13 | encoding = locale.getdefaultlocale()[1] 14 | 15 | 16 | def clean_unmount(cmd, mountpoint, tries=5, rmdir=True): 17 | cmd.append(mountpoint) 18 | 19 | # AVFS mounts are not actually unmountable, but are symlinked. 20 | if os.path.exists(os.path.join(mountpoint, 'avfs')): 21 | os.remove(os.path.join(mountpoint, 'avfs')) 22 | # noinspection PyProtectedMember 23 | logger.debug("Removed {}".format(os.path.join(mountpoint, 'avfs'))) 24 | elif os.path.islink(mountpoint): 25 | pass # if it is a symlink, we can simply skip to removing it 26 | elif not os.path.ismount(mountpoint): 27 | pass # if is not a mount point, we can simply skip to removing it 28 | else: 29 | # Perform unmount 30 | # noinspection PyBroadException 31 | try: 32 | check_call_(cmd) 33 | except Exception as e: 34 | raise SubsystemError(e) 35 | 36 | # Remove mountpoint only if needed 37 | if not rmdir: 38 | return 39 | 40 | for _ in range(tries): 41 | if not os.path.ismount(mountpoint): 42 | # Unmount was successful, remove mountpoint 43 | try: 44 | if os.path.islink(mountpoint): 45 | os.unlink(mountpoint) 46 | else: 47 | os.rmdir(mountpoint) 48 | except FileNotFoundError: 49 | pass # this is what we are looking for! 50 | break 51 | else: 52 | time.sleep(1) 53 | 54 | if os.path.isdir(mountpoint): 55 | raise CleanupError() 56 | 57 | 58 | def is_encase(path): 59 | return re.match(r'^.*\.[Ee][Xx]?\d\d$', path) 60 | 61 | 62 | def is_compressed(path): 63 | return re.match(r'^.*\.((zip)|(rar)|((t(ar\.)?)?gz))$', path) 64 | 65 | 66 | def is_vmware(path): 67 | return re.match(r'^.*\.vmdk', path) 68 | 69 | 70 | def is_qcow2(path): 71 | return re.match(r'.*\.qcow2', path) 72 | 73 | 74 | def is_vbox(path): 75 | return re.match(r'.*\.vdi', path) 76 | 77 | 78 | def expand_path(path): 79 | """ 80 | Expand the given path to either an Encase image or a dd image 81 | i.e. if path is '/path/to/image.E01' then the result of this method will be 82 | /path/to/image.E*' 83 | and if path is '/path/to/image.001' then the result of this method will be 84 | '/path/to/image.[0-9][0-9]?' 85 | """ 86 | if is_encase(path): 87 | return glob.glob(path[:-2] + '??') or [path] 88 | ext_match = re.match(r'^.*\.(\d{2,})$', path) 89 | if ext_match is not None: 90 | ext_size = len(ext_match.groups()[-1]) 91 | return glob.glob(path[:-ext_size] + '[0-9]' * ext_size) or [path] 92 | else: 93 | return [path] 94 | 95 | 96 | def command_exists(cmd): 97 | fpath, fname = os.path.split(cmd) 98 | if fpath: 99 | return os.path.isfile(cmd) and os.access(cmd, os.X_OK) 100 | else: 101 | for p in os.environ['PATH'].split(os.pathsep): 102 | p = p.strip('"') 103 | fp = os.path.join(p, cmd) 104 | if os.path.isfile(fp) and os.access(fp, os.X_OK): 105 | return True 106 | 107 | return False 108 | 109 | 110 | def module_exists(mod): 111 | import importlib 112 | try: 113 | importlib.import_module(mod) 114 | return True 115 | except ImportError: 116 | return False 117 | 118 | 119 | def check_call_(cmd, wrap_error=False, *args, **kwargs): 120 | logger.debug('$ {0}'.format(' '.join(cmd))) 121 | try: 122 | return subprocess.check_call(cmd, *args, **kwargs) 123 | except Exception as e: 124 | if wrap_error: 125 | raise SubsystemError(e) 126 | else: 127 | raise 128 | 129 | 130 | def check_output_(cmd, *args, **kwargs): 131 | logger.debug('$ {0}'.format(' '.join(cmd))) 132 | try: 133 | result = subprocess.check_output(cmd, *args, **kwargs) 134 | if result: 135 | result = result.decode(encoding) 136 | logger.debug('< {0}'.format(result)) 137 | return result 138 | except subprocess.CalledProcessError as e: 139 | logger.debug("< return code {}".format(e.returncode)) 140 | if e.output: 141 | result = e.output.decode(encoding) 142 | logger.debug('< {0}'.format(result)) 143 | raise 144 | 145 | 146 | def get_free_nbd_device(): 147 | for nbd_path in glob.glob("/sys/class/block/nbd*"): 148 | try: 149 | if check_output_(["cat", "{0}/size".format(nbd_path), ]).strip() == "0": 150 | return "/dev/{}".format(os.path.basename(nbd_path)) 151 | except subprocess.CalledProcessError as e: 152 | if e.output: 153 | result = e.output.decode(encoding) 154 | logger.debug("< {0}".format(result)) 155 | raise NoNetworkBlockAvailableError() 156 | 157 | 158 | def determine_slot(table, slot): 159 | if int(table) >= 0: 160 | return int(table) * 4 + int(slot) + 1 161 | else: 162 | return int(slot) + 1 163 | 164 | 165 | def terminal_supports_color(): 166 | return (sys.platform != 'Pocket PC' and (sys.platform != 'win32' or 'ANSICON' in os.environ) 167 | and hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()) 168 | -------------------------------------------------------------------------------- /imagemounter/cli/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | from imagemounter import _util, dependencies 4 | 5 | 6 | class CheckAction(argparse.Action): 7 | """Action that checks the current state of the system according to the command requirements that imount has.""" 8 | 9 | # noinspection PyShadowingNames 10 | def __call__(self, parser, namespace, values, option_string=None): 11 | print("The following commands are used by imagemounter internally. Without most commands, imagemounter " 12 | "works perfectly fine, but may lack some detection or mounting capabilities.") 13 | 14 | for section in dependencies.ALL_SECTIONS: 15 | print(section.printable_status) 16 | 17 | parser.exit() 18 | 19 | 20 | class AppendDictAction(argparse.Action): 21 | """argparse method that parses a command-line dict to an actual dict:: 22 | 23 | a=1 -> {'a': '1'} 24 | a=1,b=2 -> {'a': '1', 'b': '2'} 25 | 123 -> {'*': '123'} 26 | 27 | """ 28 | 29 | def __init__(self, allow_commas=True, *args, **kwargs): 30 | self.allow_commas = allow_commas 31 | super(AppendDictAction, self).__init__(*args, **kwargs) 32 | 33 | def __call__(self, parser, namespace, values, option_string=None): 34 | items = getattr(namespace, self.dest, {}) or {} 35 | if ',' not in values and '=' not in values: 36 | items['*'] = values 37 | else: 38 | try: 39 | if self.allow_commas: 40 | vals = values.split(',') 41 | for t in vals: 42 | k, v = t.split('=', 1) 43 | items[k] = v 44 | else: 45 | k, v = values.split('=', 1) 46 | items[k] = v 47 | except ValueError: 48 | parser.error("could not parse {}".format(self.dest)) 49 | setattr(namespace, self.dest, items) 50 | 51 | 52 | class ImageMounterStreamHandler(logging.StreamHandler): 53 | terminator = "\n" 54 | 55 | def __init__(self, colored_func=None, verbosity=0, *args, **kwargs): 56 | super(ImageMounterStreamHandler, self).__init__(*args, **kwargs) 57 | self.setFormatter(ImageMounterFormatter(colored_func, verbosity=verbosity)) 58 | 59 | def emit(self, record): 60 | if record.getMessage().startswith("<") and self.formatter.verbosity <= 3: 61 | return 62 | return super(ImageMounterStreamHandler, self).emit(record) 63 | 64 | 65 | class ImageMounterFormatter(logging.Formatter): 66 | """Formats logging messages according to ImageMounter's format.""" 67 | 68 | def __init__(self, colored_func, verbosity=0): 69 | super(ImageMounterFormatter, self).__init__() 70 | self.colored_func = colored_func 71 | self.verbosity = verbosity 72 | 73 | def format(self, record): 74 | msg = record.getMessage() 75 | if self.verbosity >= 4 and record.exc_info: 76 | if not record.exc_text: 77 | record.exc_text = self.formatException(record.exc_info) 78 | if msg[-1:] != "\n": 79 | msg += "\n" 80 | msg += record.exc_text 81 | if record.levelno >= logging.WARNING: 82 | return self.colored_func("[-] " + msg, 'cyan') 83 | elif record.levelno == logging.INFO: 84 | return self.colored_func("[+] " + msg, 'cyan') 85 | elif msg.startswith('$'): 86 | return self.colored_func(" " + msg, 'cyan') 87 | elif msg.startswith('<'): 88 | if self.verbosity >= 4: 89 | return self.colored_func(" " + "\n < ".join(msg.splitlines()), 'cyan') 90 | else: 91 | return "" 92 | else: 93 | return self.colored_func(" " + msg, 'cyan') 94 | 95 | 96 | def get_coloring_func(color=False, no_color=False): 97 | # Colorize the output by default if the terminal supports it 98 | if not color and no_color: 99 | color = False 100 | elif color: 101 | color = True 102 | else: 103 | color = _util.terminal_supports_color() 104 | 105 | if not color: 106 | # noinspection PyUnusedLocal,PyShadowingNames 107 | def col(s, *args, **kwargs): 108 | return s 109 | return col 110 | else: 111 | from termcolor import colored 112 | return colored 113 | -------------------------------------------------------------------------------- /imagemounter/cli/shell.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import cmd 3 | import logging 4 | import os 5 | import shlex 6 | 7 | import pickle 8 | 9 | from imagemounter import __version__, DISK_MOUNTERS, FILE_SYSTEM_TYPES 10 | from imagemounter.cli import CheckAction, get_coloring_func, ImageMounterStreamHandler 11 | from imagemounter.disk import Disk 12 | from imagemounter.parser import ImageParser 13 | 14 | 15 | SAVE_PATH = os.path.expanduser("~/.imountshell") 16 | 17 | 18 | class ShellArgumentParser(argparse.ArgumentParser): 19 | _exit = False 20 | 21 | def exit(self, status=0, message=None): 22 | if message: 23 | self._print_message(message) 24 | raise Exception() # stop the loop and lookup. 25 | 26 | 27 | class ArgumentParsedShell(cmd.Cmd): 28 | def __init__(self): 29 | cmd.Cmd.__init__(self) 30 | self._make_argparser() 31 | 32 | def _make_argparser(self): 33 | """Makes a new argument parser.""" 34 | self.argparser = ShellArgumentParser(prog='') 35 | subparsers = self.argparser.add_subparsers() 36 | 37 | for name in self.get_names(): 38 | if name.startswith('parser_'): 39 | parser = subparsers.add_parser(name[7:]) 40 | parser.set_defaults(func=getattr(self, 'arg_' + name[7:])) 41 | getattr(self, name)(parser) 42 | 43 | self.argparser_completer = None 44 | 45 | try: 46 | import argcomplete 47 | except ImportError: 48 | pass 49 | else: 50 | os.environ.setdefault("_ARGCOMPLETE_COMP_WORDBREAKS", " \t\"'") 51 | self.argparser_completer = argcomplete.CompletionFinder(self.argparser) 52 | 53 | def postcmd(self, stop, line): 54 | self._make_argparser() # load argparser again to reload options 55 | return stop 56 | 57 | def complete(self, text, state): 58 | """Overridden to reset the argument parser after every completion (argcomplete fails :()""" 59 | result = cmd.Cmd.complete(self, text, state) 60 | if self.argparser_completer: 61 | self._make_argparser() 62 | # argparser screws up with internal states, this is the best way to fix it for now 63 | return result 64 | 65 | def default(self, line): 66 | """Overriding default to get access to any argparse commands we have specified.""" 67 | 68 | if any((line.startswith(x) for x in self.argparse_names())): 69 | try: 70 | args = self.argparser.parse_args(shlex.split(line)) 71 | except Exception: # intentionally catches also other errors in argparser 72 | pass 73 | else: 74 | args.func(args) 75 | else: 76 | cmd.Cmd.default(self, line) 77 | 78 | def completedefault(self, text, line, begidx, endidx): 79 | """Accessing the argcompleter if available.""" 80 | if self.argparser_completer and any((line.startswith(x) for x in self.argparse_names())): 81 | self.argparser_completer.rl_complete(line, 0) 82 | return [x[begidx:] for x in self.argparser_completer._rl_matches] 83 | else: 84 | return [] 85 | 86 | def argparse_names(self, prefix=""): 87 | return [a[4:] for a in self.get_names() if a.startswith("arg_" + prefix)] 88 | 89 | def completenames(self, text, *ignored): 90 | """Patched to also return argparse commands""" 91 | return sorted(cmd.Cmd.completenames(self, text, *ignored) + self.argparse_names(text)) 92 | 93 | def do_help(self, arg): 94 | """Patched to show help for arparse commands""" 95 | if not arg or arg not in self.argparse_names(): 96 | cmd.Cmd.do_help(self, arg) 97 | else: 98 | try: 99 | self.argparser.parse_args([arg, '--help']) 100 | except Exception: 101 | pass 102 | 103 | def print_topics(self, header, cmds, cmdlen, maxcol): 104 | """Patched to show all argparse commands as being documented""" 105 | if header == self.doc_header: 106 | cmds.extend(self.argparse_names()) 107 | cmd.Cmd.print_topics(self, header, sorted(cmds), cmdlen, maxcol) 108 | 109 | 110 | class ImageMounterShell(ArgumentParsedShell): 111 | prompt = '(imount) ' 112 | file = None 113 | parser = None 114 | args = None 115 | saved = False 116 | 117 | def save(self): 118 | with open(SAVE_PATH, 'wb') as f: 119 | pickle.dump(self.parser, f) 120 | self.saved = True 121 | 122 | def load(self): 123 | with open(SAVE_PATH, 'rb') as f: 124 | self.parser = pickle.load(f) 125 | 126 | def preloop(self): 127 | """if the parser is not already set, loads the parser.""" 128 | if not self.parser: 129 | self.stdout.write("Welcome to imagemounter {version}".format(version=__version__)) 130 | self.stdout.write("\n") 131 | 132 | self.parser = ImageParser() 133 | for p in self.args.paths: 134 | self.onecmd('disk "{}"'.format(p)) 135 | 136 | def onecmd(self, line): 137 | """Do not crash the entire program when a single command fails.""" 138 | try: 139 | return cmd.Cmd.onecmd(self, line) 140 | except Exception as e: 141 | print("Critical error.", e) 142 | 143 | def error(self, error): 144 | """Writes an error to the console""" 145 | self.stdout.write('*** %s\n' % error) 146 | 147 | def _get_all_indexes(self): 148 | """Returns all indexes available in the parser""" 149 | if self.parser: 150 | return [v.index for v in self.parser.get_volumes()] + [d.index for d in self.parser.disks] 151 | else: 152 | return None 153 | 154 | def _get_by_index(self, index): 155 | """Returns a volume,disk tuple for the specified index""" 156 | volume_or_disk = self.parser.get_by_index(index) 157 | volume, disk = (volume_or_disk, None) if not isinstance(volume_or_disk, Disk) else (None, volume_or_disk) 158 | return volume, disk 159 | 160 | ###################################################################### 161 | # disk command 162 | ###################################################################### 163 | 164 | def parser_disk(self, parser): 165 | parser.description = "Add a disk to the current parser" 166 | p = parser.add_argument('path', help='path to the disk image that you want to mount') 167 | try: 168 | from argcomplete.completers import FilesCompleter 169 | p.completer = FilesCompleter([".dd", ".e01", ".aff", ".DD", ".E01", ".AFF"]) 170 | except ImportError: 171 | pass 172 | parser.add_argument("--mounter", choices=DISK_MOUNTERS, help="the method to mount with") 173 | 174 | def arg_disk(self, args): 175 | args.path = os.path.expanduser(args.path) 176 | if not os.path.exists(args.path): 177 | return self.error("The path {path} does not exist".format(path=args.path)) 178 | disk = self.parser.add_disk(args.path, disk_mounter=args.mounter) 179 | disk.mount() 180 | for _ in disk.detect_volumes(): 181 | pass 182 | print("Added {path} to the image mounter as index {index}".format(path=args.path, index=disk.index)) 183 | 184 | ###################################################################### 185 | # mount command 186 | ###################################################################### 187 | 188 | def parser_mount(self, parser): 189 | parser.description = "Mount a volume or disk by its index" 190 | parser.add_argument('index', help='volume or disk index', choices=self._get_all_indexes()) 191 | parser.add_argument('-r', '--recursive', action='store_true', 192 | help='recursively mount all volumes under this index') 193 | parser.add_argument('-f', '--fstype', default=None, choices=FILE_SYSTEM_TYPES, 194 | help='specify the file system type for the volume') 195 | parser.add_argument('-k', '--key', default=None, 196 | help='specify the key for the volume') 197 | 198 | def arg_mount(self, args): 199 | col = get_coloring_func() 200 | volume, disk = self._get_by_index(args.index) 201 | 202 | if not args.recursive: 203 | if disk: 204 | if not disk.is_mounted: 205 | try: 206 | disk.mount() 207 | except Exception: 208 | pass 209 | else: 210 | print(col("Disk {} is already mounted.".format(disk.index), 'red')) 211 | else: 212 | if not volume.is_mounted: 213 | try: 214 | if args.key is not None: 215 | volume.key = args.key 216 | # TODO: do something with args.fstype 217 | volume.init_volume() 218 | if volume.is_mounted: 219 | if volume.mountpoint: 220 | print("Mounted volume {index} at {path}" 221 | .format(path=col(volume.mountpoint, "green", attrs=['bold']), 222 | index=volume.index)) 223 | else: 224 | print("Mounted volume {index} (no mountpoint available)".format(index=volume.index)) 225 | else: 226 | print("Refused to mount volume {index}.".format(index=volume.index)) 227 | except Exception as e: 228 | print(col("An error occurred while mounting volume {index}: {type}: {args}" 229 | .format(type=type(e).__name__, 230 | args=" ".join(map(str, e.args)), 231 | index=volume.index), "red")) 232 | else: 233 | if volume.mountpoint: 234 | print(col("Volume {} is already mounted at {}.".format(volume.index, volume.mountpoint), 'red')) 235 | else: 236 | print(col("Volume {} is already mounted.".format(volume.index), 'red')) 237 | else: 238 | if disk: 239 | if not disk.is_mounted: 240 | try: 241 | disk.mount() 242 | except Exception: 243 | pass 244 | it = disk.init_volumes 245 | else: 246 | it = volume.init 247 | for v in it(): 248 | if v.mountpoint: 249 | print("Mounted volume {index} at {path}" 250 | .format(path=col(v.mountpoint, "green", attrs=['bold']), 251 | index=v.index)) 252 | elif v.exception: 253 | e = v.exception 254 | print(col("An error occurred while mounting volume {index}: {type}: {args}" 255 | .format(type=type(e).__name__, 256 | args=" ".join(map(str, e.args)), 257 | index=v.index), "red")) 258 | 259 | ###################################################################### 260 | # unmount command 261 | ###################################################################### 262 | 263 | def parser_unmount(self, parser): 264 | parser.description = "Unmount a disk or volume by its index. Is recursive by design." 265 | parser.add_argument('index', help='volume index', nargs='?', choices=self._get_all_indexes()) 266 | 267 | def arg_unmount(self, args): 268 | if args.index: 269 | volume = self.parser.get_by_index(args.index) 270 | volume.unmount() 271 | print("Unmounted {index}".format(index=volume.index)) 272 | else: 273 | self.parser.clean() 274 | print("Unmounted everything") 275 | 276 | ###################################################################### 277 | # show command 278 | ###################################################################### 279 | 280 | def parser_show(self, parser): 281 | parser.description = "Without arguments, displays a tree view of all known volumes and disks. With an " \ 282 | "argument, it provides details about the referenced volume or disk." 283 | parser.add_argument('index', help='volume index', nargs='?', choices=self._get_all_indexes()) 284 | 285 | def arg_show(self, args): 286 | if not args.index: 287 | self._show_tree() 288 | else: 289 | col = get_coloring_func() 290 | volume, disk = self._get_by_index(args.index) 291 | 292 | # displays a volume's details 293 | if volume: 294 | print(col("Details of volume {description}".format(description=volume.get_description()), 295 | attrs=['bold'])) 296 | if volume.is_mounted: 297 | print(col("Currently mounted", 'green')) 298 | 299 | print("\nAttributes:") 300 | for attr in ['size', 'offset', 'slot', 'flag', 'block_size', 'fstype', 'key']: 301 | print(" - {name:<15} {value}".format(name=attr, value=getattr(volume, attr))) 302 | 303 | if volume.mountpoint or volume.loopback or volume._paths: 304 | print("\nPaths:") 305 | if volume.mountpoint: 306 | print(" - mountpoint {value}".format(value=volume.mountpoint)) 307 | if volume.loopback: 308 | print(" - loopback {value}".format(value=volume.loopback)) 309 | 310 | if volume.info: 311 | print("\nAdditional information:") 312 | for k, v in volume.info.items(): 313 | print(" - {name:<15} {value}".format(name=k, value=v)) 314 | 315 | else: # displays a disk's details 316 | print(col("Details of disk {description}".format(description=disk.paths[0]), attrs=['bold'])) 317 | if disk.is_mounted: 318 | print(col("Currently mounted", 'green')) 319 | 320 | print("\nAttributes:") 321 | for attr in ['offset', 'block_size', 'read_write', 'disk_mounter']: 322 | print(" - {name:<15} {value}".format(name=attr, value=getattr(disk, attr))) 323 | 324 | if disk.mountpoint or disk.rwpath or disk._paths: 325 | print("\nPaths:") 326 | if disk.mountpoint: 327 | print(" - mountpoint {value}".format(value=disk.mountpoint)) 328 | if disk.rwpath: 329 | print(" - rwpath {value}".format(value=disk.rwpath)) 330 | for k, v in disk._paths.items(): 331 | print(" - {name:<15} {value}".format(name=k, value=v)) 332 | 333 | def _show_tree(self): 334 | col = get_coloring_func() 335 | 336 | for disk in self.parser.disks: 337 | print("- {index:<5} {type} {filename}".format( 338 | index=col("{:<5}".format(disk.index), 'green' if disk.is_mounted else None, attrs=['bold']), 339 | type=col("{:<10}".format(disk.volumes.vstype), attrs=['dark']), 340 | filename=disk.paths[0] 341 | )) 342 | 343 | def _show_volume_system(volumes, level=0): 344 | level += 1 345 | for i, v in enumerate(volumes): 346 | level_str = " " * level + ("└ " if i == len(volumes) - 1 else "├ ") 347 | tp = v.volumes.vstype if v.filesystem.type == 'volumesystem' else v.filesystem.type if v.flag == 'alloc' else v.flag 348 | 349 | print("{level_str}{index} {type} {size:<10} {description}".format( 350 | level_str=level_str, 351 | index=col("{:<5}".format(v.index), 'green' if v.is_mounted else None, attrs=['bold']), 352 | type=col("{:<10}".format(str(tp)), attrs=['dark']), 353 | description=v.get_description(with_index=False, with_size=False)[:30], 354 | size=v.get_formatted_size() 355 | )) 356 | _show_volume_system(v.volumes, level) 357 | 358 | _show_volume_system(disk.volumes) 359 | 360 | ###################################################################### 361 | # set command 362 | ###################################################################### 363 | 364 | def parser_set(self, parser): 365 | parser.description = "Modifies a property of a volume or disk. This is for advanced usage only." 366 | parser.add_argument('index', help='volume index', choices=self._get_all_indexes()) 367 | parser.add_argument('name', help='property name', choices=['size', 'offset', 'slot', 'flag', 'block_size', 368 | 'fstype', 'key', 'disk_mounter']) 369 | parser.add_argument('value', help='property value') 370 | 371 | def arg_set(self, args): 372 | col = get_coloring_func() 373 | volume, disk = self._get_by_index(args.index) 374 | 375 | if volume: 376 | if args.name in ['size', 'offset', 'block_size']: 377 | try: 378 | setattr(volume, args.name, int(args.value)) 379 | except ValueError: 380 | print(col("Invalid value provided for {}".format(args.name), 'red')) 381 | else: 382 | print(col("Updated value for {}".format(args.name), 'green')) 383 | elif args.name in ['slot', 'flag', 'key']: 384 | setattr(volume, args.name, args.value) 385 | print(col("Updated value for {}".format(args.name), 'green')) 386 | elif args.name == 'fstype': 387 | try: 388 | setattr(volume, args.name, FILE_SYSTEM_TYPES[args.value]) 389 | except KeyError: 390 | print(col("No file system type for {}".format(args.value), 'red')) 391 | else: 392 | print(col("Updated value for {}".format(args.name), 'green')) 393 | else: 394 | print(col("Property {} can't be set for a volume".format(args.name), 'red')) 395 | 396 | else: 397 | if args.name in ['offset', 'block_size']: 398 | try: 399 | setattr(disk, args.name, int(args.value)) 400 | except ValueError: 401 | print(col("Invalid value provided for {}".format(args.name), 'red')) 402 | else: 403 | print(col("Updated value for {}".format(args.name), 'green')) 404 | elif args.name in ['disk_mounter']: 405 | setattr(disk, args.name, args.value) 406 | print(col("Updated value for {}".format(args.name), 'green')) 407 | else: 408 | print(col("Property {} can't be set for a disk".format(args.name), 'red')) 409 | 410 | ###################################################################### 411 | # quit command 412 | ###################################################################### 413 | 414 | def do_save(self, arg): 415 | self.save() 416 | 417 | def do_EOF(self, arg): 418 | self.save() 419 | return True 420 | 421 | def do_quit(self, arg): 422 | """Quits the program.""" 423 | if self.saved: 424 | self.save() 425 | else: 426 | self.parser.clean() 427 | return True 428 | 429 | 430 | def main(): 431 | parser = argparse.ArgumentParser(description='Shell to mount disk images locally.') 432 | parser.add_argument('--version', action='version', version=__version__, help='display version and exit') 433 | parser.add_argument('--check', action=CheckAction, nargs=0, 434 | help='do a system check and list which tools are installed') 435 | parser.add_argument('-v', '--verbose', action='count', default=False, help='enable verbose output') 436 | parser.add_argument('-i', '--interactive', action='store_true', default=False, help='ignored option') 437 | parser.add_argument('paths', nargs='*', help='path(s) to the image(s) that you want to mount') 438 | 439 | args = parser.parse_args() 440 | col = get_coloring_func() 441 | 442 | print(col("WARNING: the interactive console is still in active development.", attrs=['dark'])) 443 | 444 | handler = ImageMounterStreamHandler(col, args.verbose) 445 | logger = logging.getLogger("imagemounter") 446 | logger.setLevel({0: logging.CRITICAL, 1: logging.WARNING, 2: logging.INFO}.get(args.verbose, logging.DEBUG)) 447 | logger.handlers = [] 448 | logger.addHandler(handler) 449 | 450 | shell = ImageMounterShell() 451 | shell.args = args 452 | if os.path.exists(SAVE_PATH): 453 | try: 454 | inp = raw_input 455 | except NameError: 456 | inp = input 457 | if 'n' not in inp('>>> Do wish to continue your previous session? [Y/n] ').lower(): 458 | shell.load() 459 | os.unlink(SAVE_PATH) 460 | 461 | while True: 462 | try: 463 | shell.cmdloop() 464 | except KeyboardInterrupt: 465 | print("Please exit using the 'quit' command.") 466 | else: 467 | break 468 | 469 | 470 | if __name__ == '__main__': 471 | main() 472 | -------------------------------------------------------------------------------- /imagemounter/dependencies.py: -------------------------------------------------------------------------------- 1 | """Dependencies (optional and required) for the imagemounter package.""" 2 | import subprocess 3 | from builtins import NotImplementedError 4 | 5 | from imagemounter import _util 6 | from imagemounter.exceptions import PrerequisiteFailedError, CommandNotFoundError, ModuleNotFoundError 7 | import functools 8 | 9 | 10 | def require(*requirements, none_on_failure=False): 11 | """Decorator that can be used to require requirements. 12 | 13 | :param requirements: List of requirements that should be verified 14 | :param none_on_failure: If true, does not raise a PrerequisiteFailedError, but instead returns None 15 | """ 16 | 17 | def inner(f): 18 | @functools.wraps(f) 19 | def wrapper(*args, **kwargs): 20 | for req in requirements: 21 | if none_on_failure: 22 | if not req.is_available: 23 | return None 24 | else: 25 | req.require() 26 | return f(*args, **kwargs) 27 | return wrapper 28 | return inner 29 | 30 | 31 | class Dependency: 32 | """An abstract class representing external tools imagemounter depends on. 33 | 34 | Subclasses of this class should define :attr:`is_available` and :attr:`status_message`. 35 | 36 | :param str name: name for the dependency; used in status messages and as the ``__str__()`` representation. 37 | :param str package: the package that can be installed to provide this dependency. 38 | :param str why: description of why this dependency is useful; used in status messages to help user decide if they 39 | need to install the dependency package. 40 | """ 41 | 42 | def __init__(self, name, package="", why=""): 43 | self.name = name 44 | self.package = package 45 | self.why = why 46 | 47 | def __str__(self): 48 | return self.name 49 | 50 | @property 51 | def is_available(self): 52 | """Returns whether the dependency is available on the system. 53 | 54 | "Available" generally means that the system has this dependency installed and configured correctly to be used. 55 | 56 | :rtype: bool 57 | """ 58 | raise NotImplementedError() 59 | 60 | @property 61 | def status_message(self): 62 | """Detailed message about whether the dependency is installed. 63 | 64 | This message may specify it is installed or not, how and why to install it, etc. 65 | 66 | The string may contain format specifiers; the ``printable_status`` property interpolates the current 67 | dependency object as ``{0}``. 68 | 69 | :rtype: str 70 | """ 71 | if self.is_available: 72 | return "INSTALLED {0!s}" 73 | elif self.why and self.package: 74 | return "MISSING {0!s:<20}needed for {0.why}, part of the {0.package} package" 75 | elif self.why: 76 | return "MISSING {0!s:<20}needed for {0.why}" 77 | elif self.package: 78 | return "MISSING {0!s:<20}part of the {0.package} package" 79 | else: 80 | return "MISSING {0!s:<20}" 81 | 82 | def require(self, *a, **kw): 83 | """Raises an error when the specified requirement is not available. 84 | """ 85 | if not self.is_available: 86 | raise PrerequisiteFailedError(str(self)) 87 | 88 | @property 89 | def printable_status(self): 90 | """A printable message about the status of the dependency. 91 | 92 | :return: the status of the dependency 93 | :rtype: str 94 | """ 95 | return self.status_message.format(self) 96 | 97 | 98 | class CommandDependency(Dependency): 99 | """A dependency on a CLI command""" 100 | 101 | def require(self): 102 | if not self.is_available: 103 | raise CommandNotFoundError(str(self)) 104 | 105 | @property 106 | def is_available(self): 107 | return _util.command_exists(self.name) 108 | 109 | 110 | class FileSystemTypeDependency(Dependency): 111 | """A dependency on the fact that an entry is available in /proc/filesystems or the fact that a kernel module exists 112 | that can be dynamically loaded to support this. In other words, ``mount -t xxx`` should work. 113 | """ 114 | 115 | def _is_loaded(self): 116 | """check the filesystem is loaded directly""" 117 | with open("/proc/filesystems", "r") as f: 118 | for l in f: 119 | if l.split()[-1] == self.name: 120 | return True 121 | return False 122 | 123 | def _is_module(self): 124 | """check the kernel module exists by executing modinfo and checking for an exception""" 125 | try: 126 | _util.check_call_(['modinfo', "fs-" + self.name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 127 | except Exception: 128 | return False 129 | else: 130 | return True 131 | 132 | def _is_helper(self): 133 | """check a helper exists for this mount type""" 134 | return _util.command_exists('mount.' + self.name) 135 | 136 | @property 137 | def is_available(self): 138 | return any((x() for x in (self._is_loaded, self._is_module, self._is_helper))) 139 | 140 | 141 | class PythonModuleDependency(Dependency): 142 | def require(self): 143 | if not self.is_available: 144 | raise ModuleNotFoundError(str(self)) 145 | 146 | @property 147 | def is_available(self): 148 | return _util.module_exists(self.name) 149 | 150 | @property 151 | def status_message(self): 152 | if self.is_available: 153 | return "INSTALLED {0!s}" 154 | elif self.why: 155 | return "MISSING {0!s:<20}needed for {0.why}, install using pip" 156 | else: 157 | return "MISSING {0!s:<20}install using pip" 158 | 159 | 160 | class MagicDependency(PythonModuleDependency): 161 | """A special case of PythonModuleDependency 162 | 163 | The ``magic`` Python module can be provided by either the ``python-magic`` PyPI package 164 | or the ``python-magic`` apt package (on debian-based systems). They have 165 | different APIs, but ``imagemounter`` supports either. 166 | """ 167 | 168 | @property 169 | def _importable(self): 170 | """Check if there is an importable module named 'magic'. 171 | 172 | Don't rely on the is_available property, since if the source of the 173 | 'magic' module is unknown, we should consider it missing 174 | """ 175 | return _util.module_exists('magic') 176 | 177 | @property 178 | def is_available(self): 179 | """Whether an acceptable version of the ``magic`` module is available on the system. 180 | 181 | :rtype: bool 182 | """ 183 | return self.is_python_package or self.is_system_package 184 | 185 | @property 186 | def is_python_package(self): 187 | """Whether the ``magic`` module is provided by the ``python-magic`` PyPI package. 188 | 189 | :rtype: bool 190 | """ 191 | if not self._importable: 192 | return False 193 | 194 | import magic 195 | return hasattr(magic, 'from_file') 196 | 197 | @property 198 | def is_system_package(self): 199 | """Whether the ``magic`` module is provided by the ``python-magic`` system package. 200 | 201 | :rtype: bool 202 | """ 203 | if not self._importable: 204 | return False 205 | 206 | import magic 207 | return hasattr(magic, 'open') 208 | 209 | @property 210 | def status_message(self): 211 | if self.is_python_package: 212 | return "INSTALLED {0!s:<20}(Python package)" 213 | elif self.is_system_package: 214 | return "INSTALLED {0!s:<20}(system package)" 215 | elif self._importable: 216 | return "ERROR {0!s:<20}expecting {0}, found other module named magic" 217 | else: 218 | return "MISSING {0!s:<20}install using pip" 219 | 220 | 221 | class DependencySection: 222 | """Group of dependencies that are displayed together in ``imount --check``. 223 | 224 | :param str name: name for the group 225 | :param str description: explanation of which dependencies in the group are needed. 226 | :param list[Dependency] deps: dependencies that are part of this group. 227 | """ 228 | 229 | def __init__(self, name, description, deps): 230 | self.name = name 231 | self.description = description 232 | self.deps = deps 233 | 234 | @property 235 | def printable_status(self): 236 | lines = [ 237 | "-- {0.name} ({0.description}) --".format(self) 238 | ] 239 | for dep in self.deps: 240 | lines.append(" " + dep.printable_status) 241 | return "\n".join(lines) 242 | 243 | 244 | xmount = CommandDependency("xmount", "xmount", "several types of disk images") 245 | ewfmount = CommandDependency("ewfmount", "ewf-tools", "EWF images (partially covered by xmount)") 246 | affuse = CommandDependency("affuse", "afflib-tools", "AFF images (partially covered by xmount)") 247 | vmware_mount = CommandDependency("vmware-mount", why="VMWare disks") 248 | mountavfs = CommandDependency("mountavfs", "avfs", "compressed disk images") 249 | qemu_nbd = CommandDependency("qemu-nbd", "qemu-utils", "Qcow2 images") 250 | 251 | mmls = CommandDependency("mmls", "sleuthkit") 252 | pytsk3 = PythonModuleDependency("pytsk3") 253 | parted = CommandDependency("parted", "parted") 254 | 255 | fsstat = CommandDependency("fsstat", "sleuthkit") 256 | file = CommandDependency("file", "libmagic1") 257 | blkid = CommandDependency("blkid") 258 | magic = MagicDependency('python-magic') 259 | disktype = CommandDependency("disktype", "disktype") 260 | 261 | mount_xfs = FileSystemTypeDependency("xfs", "xfsprogs", "XFS volumes") 262 | mount_ntfs = FileSystemTypeDependency("ntfs", "ntfs-3g", "NTFS volumes") 263 | lvm = CommandDependency("lvm", "lvm2", "LVM volumes") 264 | vmfs_fuse = CommandDependency("vmfs-fuse", "vmfs-tools", "VMFS volumes") 265 | mount_jffs2 = FileSystemTypeDependency("jffs2", "mtd-tools", "JFFS2 volumes") 266 | mount_squashfs = FileSystemTypeDependency("squashfs", "squashfs-tools", "SquashFS volumes") 267 | mdadm = CommandDependency("mdadm", "mdadm", "RAID volumes") 268 | cryptsetup = CommandDependency("cryptsetup", "cryptsetup", "LUKS containers") 269 | bdemount = CommandDependency("bdemount", "libbde-utils", "Bitlocker Drive Encryption volumes") 270 | vshadowmount = CommandDependency("vshadowmount", "libvshadow-utils", "NTFS volume shadow copies") 271 | photorec = CommandDependency("photorec", "testdisk", "carving free space") 272 | 273 | mount_images = DependencySection(name="Mounting base disk images", 274 | description="at least one required, first three recommended", 275 | deps=[xmount, ewfmount, affuse, vmware_mount, mountavfs, qemu_nbd]) 276 | 277 | detect_volumes = DependencySection(name="Detecting volumes and volume types", 278 | description="at least one required", 279 | deps=[mmls, pytsk3, parted]) 280 | 281 | detect_volume_types = DependencySection(name="Detecting volume types", 282 | description="all recommended, first two highly recommended", 283 | deps=[fsstat, file, blkid, magic, disktype]) 284 | 285 | mount_volumes = DependencySection(name="Mounting volumes", 286 | description="install when needed", 287 | deps=[mount_xfs, mount_ntfs, lvm, vmfs_fuse, mount_jffs2, 288 | mount_squashfs, mdadm, cryptsetup, bdemount, vshadowmount, photorec]) 289 | 290 | ALL_SECTIONS = [ 291 | mount_images, 292 | detect_volumes, 293 | detect_volume_types, 294 | mount_volumes, 295 | ] 296 | -------------------------------------------------------------------------------- /imagemounter/disk.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import glob 4 | import logging 5 | import os 6 | import subprocess 7 | import tempfile 8 | import time 9 | 10 | from imagemounter import _util, BLOCK_SIZE 11 | from imagemounter.exceptions import ImageMounterError, ArgumentError, MountpointEmptyError, MountError, \ 12 | NoNetworkBlockAvailableError, SubsystemError 13 | from imagemounter.volume_system import VolumeSystem 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class Disk: 19 | """Representation of a disk, image file or anything else that can be considered a disk. """ 20 | 21 | # noinspection PyUnusedLocal 22 | def __init__(self, parser, path, index=None, offset=0, block_size=BLOCK_SIZE, read_write=False, vstype='', 23 | disk_mounter='auto', volume_detector='auto'): 24 | """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the 25 | :func:`init` method for this. 26 | 27 | Only use arguments offset and further as keyword arguments. 28 | 29 | :param parser: the parent parser 30 | :type parser: :class:`ImageParser` 31 | :param str path: the path of the Disk 32 | :param str index: the base index of this Disk 33 | :param int offset: offset of the disk where the volume (system) resides 34 | :param int block_size: 35 | :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled 36 | :param str vstype: the volume system type to use. 37 | :param str disk_mounter: the method to mount the base image with 38 | :param str volume_detector: the volume system detection method to use 39 | """ 40 | 41 | self.parser = parser 42 | 43 | # Find the type and the paths 44 | path = os.path.expandvars(os.path.expanduser(path)) 45 | self.paths = sorted(_util.expand_path(path)) 46 | 47 | self.offset = offset 48 | self.block_size = block_size 49 | self.read_write = read_write 50 | self.disk_mounter = disk_mounter or 'auto' 51 | self.index = index 52 | 53 | self._name = os.path.split(path)[1] 54 | self._paths = {} 55 | self.rwpath = "" 56 | self.mountpoint = "" 57 | self.volumes = VolumeSystem(parent=self, volume_detector=volume_detector, vstype=vstype) 58 | 59 | self.was_mounted = False 60 | self.is_mounted = False 61 | 62 | self._disktype = defaultdict(dict) 63 | 64 | def __str__(self): 65 | return self._name 66 | 67 | def __getitem__(self, item): 68 | return self.volumes[item] 69 | 70 | def get_disk_type(self): 71 | if _util.is_encase(self.paths[0]): 72 | return 'encase' 73 | elif _util.is_vmware(self.paths[0]): 74 | return 'vmdk' 75 | elif _util.is_compressed(self.paths[0]): 76 | return 'compressed' 77 | elif _util.is_qcow2(self.paths[0]): 78 | return 'qcow2' 79 | elif _util.is_vbox(self.paths[0]): 80 | return 'vdi' 81 | else: 82 | return 'dd' 83 | 84 | def _get_mount_methods(self, disk_type): 85 | """Finds which mount methods are suitable for the specified disk type. Returns a list of all suitable mount 86 | methods. 87 | """ 88 | if self.disk_mounter == 'auto': 89 | methods = [] 90 | 91 | def add_method_if_exists(method): 92 | if (method == 'avfs' and _util.command_exists('avfsd')) or \ 93 | _util.command_exists(method): 94 | methods.append(method) 95 | 96 | if self.read_write: 97 | add_method_if_exists('xmount') 98 | else: 99 | if disk_type == 'encase': 100 | add_method_if_exists('ewfmount') 101 | elif disk_type == 'vmdk': 102 | add_method_if_exists('vmware-mount') 103 | add_method_if_exists('affuse') 104 | elif disk_type == 'dd': 105 | add_method_if_exists('affuse') 106 | elif disk_type == 'compressed': 107 | add_method_if_exists('avfs') 108 | elif disk_type == 'qcow2': 109 | add_method_if_exists('qemu-nbd') 110 | elif disk_type == 'vdi': 111 | add_method_if_exists('qemu-nbd') 112 | add_method_if_exists('xmount') 113 | else: 114 | methods = [self.disk_mounter] 115 | return methods 116 | 117 | def _mount_avfs(self): 118 | """Mounts the AVFS filesystem.""" 119 | 120 | self._paths['avfs'] = tempfile.mkdtemp(prefix='image_mounter_avfs_') 121 | 122 | # start by calling the mountavfs command to initialize avfs 123 | _util.check_call_(['avfsd', self._paths['avfs'], '-o', 'allow_other'], stdout=subprocess.PIPE) 124 | 125 | # no multifile support for avfs 126 | avfspath = self._paths['avfs'] + '/' + os.path.abspath(self.paths[0]) + '#' 127 | targetraw = os.path.join(self.mountpoint, 'avfs') 128 | 129 | os.symlink(avfspath, targetraw) 130 | logger.debug("Symlinked {} with {}".format(avfspath, targetraw)) 131 | raw_path = self.get_raw_path() 132 | logger.debug("Raw path to avfs is {}".format(raw_path)) 133 | if raw_path is None: 134 | raise MountpointEmptyError() 135 | 136 | def mount(self): 137 | """Mounts the base image on a temporary location using the mount method stored in :attr:`method`. If mounting 138 | was successful, :attr:`mountpoint` is set to the temporary mountpoint. 139 | 140 | If :attr:`read_write` is enabled, a temporary read-write cache is also created and stored in :attr:`rwpath`. 141 | 142 | :return: whether the mounting was successful 143 | :rtype: bool 144 | """ 145 | 146 | if self.parser.casename: 147 | self.mountpoint = tempfile.mkdtemp(prefix='image_mounter_', suffix='_' + self.parser.casename) 148 | else: 149 | self.mountpoint = tempfile.mkdtemp(prefix='image_mounter_') 150 | 151 | if self.read_write: 152 | self.rwpath = tempfile.mkstemp(prefix="image_mounter_rw_cache_")[1] 153 | 154 | disk_type = self.get_disk_type() 155 | methods = self._get_mount_methods(disk_type) 156 | 157 | cmds = [] 158 | for method in methods: 159 | if method == 'avfs': # avfs does not participate in the fallback stuff, unfortunately 160 | self._mount_avfs() 161 | self.disk_mounter = method 162 | self.was_mounted = True 163 | self.is_mounted = True 164 | return 165 | 166 | elif method == 'dummy': 167 | os.rmdir(self.mountpoint) 168 | self.mountpoint = "" 169 | logger.debug("Raw path to dummy is {}".format(self.get_raw_path())) 170 | self.disk_mounter = method 171 | self.was_mounted = True 172 | self.is_mounted = True 173 | return 174 | 175 | elif method == 'xmount': 176 | cmds.append(['xmount', ]) 177 | if self.read_write: 178 | cmds[-1].extend(['--cache', self.rwpath]) 179 | cmds[-1].extend(['--in', 'ewf' if disk_type == 'encase' else 'dd']) 180 | cmds[-1].extend(self.paths) # specify all paths, xmount needs this :( 181 | cmds[-1].append(self.mountpoint) 182 | 183 | elif method == 'affuse': 184 | cmds.extend([['affuse', '-o', 'allow_other', self.paths[0], self.mountpoint], 185 | ['affuse', self.paths[0], self.mountpoint]]) 186 | 187 | elif method == 'ewfmount': 188 | cmds.extend([['ewfmount', '-X', 'allow_other', self.paths[0], self.mountpoint], 189 | ['ewfmount', self.paths[0], self.mountpoint]]) 190 | 191 | elif method == 'vmware-mount': 192 | cmds.append(['vmware-mount', '-r', '-f', self.paths[0], self.mountpoint]) 193 | 194 | elif method == 'qemu-nbd': 195 | _util.check_output_(['modprobe', 'nbd', 'max_part=63']) # Load nbd driver 196 | try: 197 | self._paths['nbd'] = _util.get_free_nbd_device() # Get free nbd device 198 | except NoNetworkBlockAvailableError: 199 | logger.warning("No free network block device found.", exc_info=True) 200 | raise 201 | cmds.extend([['qemu-nbd', '--read-only', '-c', self._paths['nbd'], self.paths[0]]]) 202 | 203 | else: 204 | raise ArgumentError("Unknown mount method {0}".format(self.disk_mounter)) 205 | 206 | for cmd in cmds: 207 | # noinspection PyBroadException 208 | try: 209 | _util.check_call_(cmd, stdout=subprocess.PIPE) 210 | # mounting does not seem to be instant, add a timer here 211 | time.sleep(.1) 212 | except Exception: 213 | logger.warning('Could not mount {0}, trying other method'.format(self.paths[0]), exc_info=True) 214 | continue 215 | else: 216 | raw_path = self.get_raw_path() 217 | logger.debug("Raw path to disk is {}".format(raw_path)) 218 | self.disk_mounter = cmd[0] 219 | 220 | if raw_path is None: 221 | raise MountpointEmptyError() 222 | self.was_mounted = True 223 | self.is_mounted = True 224 | return 225 | 226 | logger.error('Unable to mount {0}'.format(self.paths[0])) 227 | os.rmdir(self.mountpoint) 228 | self.mountpoint = "" 229 | raise MountError() 230 | 231 | def get_raw_path(self): 232 | """Returns the raw path to the mounted disk image, i.e. the raw :file:`.dd`, :file:`.raw` or :file:`ewf1` 233 | file. 234 | 235 | :rtype: str 236 | """ 237 | 238 | if self.disk_mounter == 'dummy': 239 | return self.paths[0] 240 | else: 241 | if self.disk_mounter == 'avfs' and os.path.isdir(os.path.join(self.mountpoint, 'avfs')): 242 | logger.debug("AVFS mounted as a directory, will look in directory for (random) file.") 243 | # there is no support for disks inside disks, so this will fail to work for zips containing 244 | # E01 files or so. 245 | searchdirs = (os.path.join(self.mountpoint, 'avfs'), self.mountpoint) 246 | else: 247 | searchdirs = (self.mountpoint, ) 248 | 249 | raw_path = [] 250 | if self._paths.get('nbd'): 251 | raw_path.append(self._paths['nbd']) 252 | 253 | for searchdir in searchdirs: 254 | # avfs: apparently it is not a dir 255 | for pattern in ['*.dd', '*.iso', '*.raw', '*.dmg', 'ewf1', 'flat', 'avfs']: 256 | raw_path.extend(glob.glob(os.path.join(searchdir, pattern))) 257 | 258 | if not raw_path: 259 | logger.warning("No viable mount file found in {}.".format(searchdirs)) 260 | return None 261 | return raw_path[0] 262 | 263 | def get_fs_path(self): 264 | """Returns the path to the filesystem. Most of the times this is the image file, but may instead also return 265 | the MD device or loopback device the filesystem is mounted to. 266 | 267 | :rtype: str 268 | """ 269 | 270 | if self._paths.get('md'): 271 | return self._paths['md'] 272 | else: 273 | return self.get_raw_path() 274 | 275 | def detect_volumes(self, single=None): 276 | """Generator that detects the volumes from the Disk, using one of two methods: 277 | 278 | * Single volume: the entire Disk is a single volume 279 | * Multiple volumes: the Disk is a volume system 280 | 281 | :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`. 282 | If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None, 283 | :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume` 284 | if no volumes were detected. 285 | """ 286 | # prevent adding the same volumes twice 287 | if self.volumes.has_detected: 288 | yield from self.volumes 289 | 290 | elif single: 291 | yield from self.volumes.detect_volumes(method='single') 292 | 293 | else: 294 | # if single == False or single == None, loop over all volumes 295 | amount = 0 296 | try: 297 | for v in self.volumes.detect_volumes(): 298 | amount += 1 299 | yield v 300 | except ImageMounterError: 301 | pass # ignore and continue to single mount 302 | 303 | # if single == None and no volumes were mounted, use single_volume 304 | if single is None and amount == 0: 305 | logger.info("Detecting as single volume instead") 306 | yield from self.volumes.detect_volumes(method='single', force=True) 307 | 308 | def init(self, single=None, only_mount=None, skip_mount=None, swallow_exceptions=True): 309 | """Calls several methods required to perform a full initialisation: :func:`mount`, and 310 | :func:`mount_volumes` and yields all detected volumes. 311 | 312 | :param bool|None single: indicates whether the disk should be mounted as a single disk, not as a single disk or 313 | whether it should try both (defaults to :const:`None`) 314 | :param list only_mount: If set, must be a list of volume indexes that are only mounted. 315 | :param list skip_mount: If set, must be a list of volume indexes tat should not be mounted. 316 | :param bool swallow_exceptions: If True, Exceptions are not raised but rather set on the instance. 317 | :rtype: generator 318 | """ 319 | 320 | self.mount() 321 | self.volumes.preload_volume_data() 322 | 323 | yield from self.init_volumes(single, only_mount=only_mount, skip_mount=skip_mount, 324 | swallow_exceptions=swallow_exceptions) 325 | 326 | def init_volumes(self, single=None, only_mount=None, skip_mount=None, swallow_exceptions=True): 327 | """Generator that detects and mounts all volumes in the disk. 328 | 329 | :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`. 330 | If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None, 331 | :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume` 332 | if no volumes were detected. 333 | :param list only_mount: If set, must be a list of volume indexes that are only mounted. 334 | :param list skip_mount: If set, must be a list of volume indexes tat should not be mounted. 335 | :param bool swallow_exceptions: If True, Exceptions are not raised but rather set on the instance. 336 | """ 337 | 338 | for volume in self.detect_volumes(single=single): 339 | yield from volume.init(only_mount=only_mount, skip_mount=skip_mount, 340 | swallow_exceptions=swallow_exceptions) 341 | 342 | def get_volumes(self): 343 | """Gets a list of all volumes in this disk, including volumes that are contained in other volumes.""" 344 | 345 | volumes = [] 346 | for v in self.volumes: 347 | volumes.extend(v.get_volumes()) 348 | return volumes 349 | 350 | def rw_active(self): 351 | """Indicates whether anything has been written to a read-write cache.""" 352 | 353 | return self.rwpath and os.path.getsize(self.rwpath) 354 | 355 | def unmount(self, remove_rw=False, allow_lazy=False): 356 | """Removes all ties of this disk to the filesystem, so the image can be unmounted successfully. 357 | 358 | :raises SubsystemError: when one of the underlying commands fails. Some are swallowed. 359 | :raises CleanupError: when actual cleanup fails. Some are swallowed. 360 | """ 361 | 362 | for m in list(sorted(self.volumes, key=lambda v: v.mountpoint or "", reverse=True)): 363 | try: 364 | m.unmount(allow_lazy=allow_lazy) 365 | except ImageMounterError: 366 | logger.warning("Error unmounting volume {0}".format(m.mountpoint)) 367 | 368 | if self._paths.get('nbd'): 369 | _util.check_call_(['qemu-nbd', '-d', self._paths['nbd']]) 370 | 371 | if self.mountpoint: 372 | try: 373 | _util.clean_unmount(['fusermount', '-u'], self.mountpoint) 374 | except SubsystemError: 375 | if not allow_lazy: 376 | raise 377 | _util.clean_unmount(['fusermount', '-uz'], self.mountpoint) 378 | 379 | if self._paths.get('avfs'): 380 | try: 381 | _util.clean_unmount(['fusermount', '-u'], self._paths['avfs']) 382 | except SubsystemError: 383 | if not allow_lazy: 384 | raise 385 | _util.clean_unmount(['fusermount', '-uz'], self._paths['avfs']) 386 | 387 | if self.rw_active() and remove_rw: 388 | os.remove(self.rwpath) 389 | 390 | self.is_mounted = False 391 | -------------------------------------------------------------------------------- /imagemounter/exceptions.py: -------------------------------------------------------------------------------- 1 | class ImageMounterError(Exception): 2 | """Base class for all exceptions:: 3 | 4 | ImageMounterError 5 | |- PrerequisiteFailedError 6 | |- CommandNotFoundError 7 | |- ModuleNotFoundError 8 | |- ArgumentError 9 | |- NotMountedError 10 | |- MountError 11 | |- MountpointEmptyError 12 | |- KeyInvalidError 13 | |- FilesystemError 14 | |- UnsupportedFilesystemError 15 | |- IncorrectFilesystemError 16 | |- AvailabilityError 17 | |- NoMountpointAvailableError 18 | |- NoLoopbackAvailableError 19 | |- NoNetworkBlockAvailableError 20 | |- CleanupError 21 | |- NoRootFoundError 22 | |- DiskIndexError 23 | |- SubsystemError 24 | 25 | """ 26 | 27 | pass 28 | 29 | 30 | class NoRootFoundError(ImageMounterError): 31 | """Used by reconstruct when no root could be found.""" 32 | pass 33 | 34 | 35 | class DiskIndexError(ImageMounterError): 36 | """Used by add_disk if a disk is being added when the previous disk had no index.""" 37 | pass 38 | 39 | 40 | class PrerequisiteFailedError(ImageMounterError): 41 | """Base class for several errors that are thrown when a method could not execute due to failing prerequisites 42 | e.g. specific arguments, a specific state or a specific command/module. 43 | """ 44 | pass 45 | 46 | 47 | class NotMountedError(PrerequisiteFailedError): 48 | """Used by methods that require a volume or disk to be mounted.""" 49 | pass 50 | 51 | 52 | class CommandNotFoundError(PrerequisiteFailedError): 53 | """Raised by methods that require a specific command to be available, but isn't.""" 54 | pass 55 | 56 | 57 | class ModuleNotFoundError(PrerequisiteFailedError): 58 | """Raised by methods that require a specific module to be available, but isn't.""" 59 | 60 | 61 | class ArgumentError(PrerequisiteFailedError): 62 | """Raised when a method requires a specific argument to be present, but isn't, or the format is incorrect.""" 63 | pass 64 | 65 | 66 | class MountError(ImageMounterError): 67 | """Raised when the mount failed, but it is the result of multiple non-critical errors, or it is unclear why it 68 | exactly failed (e.g. the mountpoint is empty after all calls where successful. 69 | Typically a SubsystemError is raised instead. 70 | """ 71 | pass 72 | 73 | 74 | class MountpointEmptyError(MountError): 75 | """Raised when a mountpoint is empty but shouldn't be.""" 76 | pass 77 | 78 | 79 | class KeyInvalidError(MountError): 80 | """Raised when a key is invalid.""" 81 | pass 82 | 83 | 84 | class FilesystemError(MountError): 85 | """Base class for several filesystem errors.""" 86 | pass 87 | 88 | 89 | class UnsupportedFilesystemError(FilesystemError): 90 | """Raised when a filesystem is attempted to be mounted, but the filesystem is unsupported.""" 91 | pass 92 | 93 | 94 | class IncorrectFilesystemError(FilesystemError): 95 | """Raised when an incorrect filesystem type is attempted to be mounted.""" 96 | pass 97 | 98 | 99 | class AvailabilityError(MountError): 100 | """Base class for NoMountpointAvailableError and NoLoopbackAvailableError""" 101 | pass 102 | 103 | 104 | class NoMountpointAvailableError(AvailabilityError): 105 | """Raised when a mountpoint is required, but could not be created, or could not be accessed.""" 106 | pass 107 | 108 | 109 | class NoLoopbackAvailableError(AvailabilityError): 110 | """Raised when a loopback device is required, but could not be found, or could not be accessed.""" 111 | pass 112 | 113 | 114 | class NoNetworkBlockAvailableError(AvailabilityError): 115 | """Raised when a network block device is required, but could not be found, or could not be accessed.""" 116 | pass 117 | 118 | 119 | class SubsystemError(ImageMounterError): 120 | """Generic exception raised by methods when an unknown error occurs in one of the subsystems.""" 121 | 122 | 123 | class CleanupError(ImageMounterError): 124 | """Raised by the unmounter when cleaning failed.""" 125 | pass 126 | -------------------------------------------------------------------------------- /imagemounter/parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import tempfile 4 | 5 | import time 6 | 7 | from imagemounter.disk import Disk 8 | from imagemounter.exceptions import NoRootFoundError, ImageMounterError, DiskIndexError 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | # noinspection PyShadowingNames 14 | class ImageParser: 15 | """Root object of the :mod:`imagemounter` Python interface. This class should be sufficient allowing access to the 16 | underlying functions of this module. 17 | 18 | """ 19 | 20 | def __init__(self, paths=(), force_disk_indexes=False, 21 | casename=None, read_write=False, disk_mounter='auto', 22 | volume_detector='auto', vstypes=None, 23 | fstypes=None, keys=None, mountdir=None, pretty=False, **args): 24 | """Instantiation of this class does not automatically mount, detect or analyse :class:`Disk` s, though it 25 | initialises each provided path as a new :class:`Disk` object. 26 | 27 | :param paths: list of paths to base images that should be mounted 28 | :type paths: iterable 29 | :param force_disk_indexes: if True, a Disk index is always included. If False, will only use Disk indexes if 30 | more than 1 Disk is provided to the paths 31 | :param casename: the name of the case, used when prettifying names 32 | :param bool read_write: indicates whether disks should be mounted with a read-write cache enabled 33 | :param str disk_mounter: the method to mount the base images with 34 | :param dict fstypes: dict mapping volume indices to file system types to use; use * and ? as volume indexes for 35 | additional control. Only when ?=none, unknown will not be used as fallback. 36 | :param dict keys: dict mapping volume indices to key material 37 | :param str mountdir: location where mountpoints are created, defaulting to a temporary location 38 | :param bool pretty: indicates whether pretty names should be used for the mountpoints 39 | :param args: ignored 40 | """ 41 | 42 | from imagemounter import __version__ 43 | logger.debug("imagemounter version %s", __version__) 44 | 45 | # Store other arguments 46 | self.casename = casename 47 | 48 | self.fstypes = {str(k): v for k, v in fstypes.items()} if fstypes else {'?': 'unknown'} 49 | if '?' in self.fstypes and (not self.fstypes['?'] or self.fstypes['?'] == 'none'): 50 | self.fstypes['?'] = None 51 | self.keys = {str(k): v for k, v in keys.items()} if keys else {} 52 | self.vstypes = {str(k): v for k, v in vstypes.items()} if vstypes else {} 53 | 54 | self.mountdir = mountdir 55 | if self.casename: 56 | self.mountdir = os.path.join(mountdir or tempfile.gettempdir(), self.casename) 57 | self.pretty = pretty 58 | 59 | # Add disks 60 | self.disks = [] 61 | for path in paths: 62 | self.add_disk(path, len(paths) > 1 or force_disk_indexes, 63 | read_write=read_write, disk_mounter=disk_mounter, volume_detector=volume_detector) 64 | 65 | def __getitem__(self, item): 66 | item = str(item) 67 | for d in self.disks: 68 | if d.index == item: 69 | return d 70 | raise KeyError(item) 71 | 72 | def add_disk(self, path, force_disk_indexes=True, **args): 73 | """Adds a disk specified by the path to the ImageParser. 74 | 75 | :param path: The path to the disk volume 76 | :param force_disk_indexes: If true, always uses disk indexes. If False, only uses disk indexes if this is the 77 | second volume you add. If you plan on using this method, always leave this True. 78 | If you add a second disk when the previous disk has no index, an error is raised. 79 | :param args: Arguments to pass to the constructor of the Disk. 80 | """ 81 | if self.disks and self.disks[0].index is None: 82 | raise DiskIndexError("First disk has no index.") 83 | 84 | if force_disk_indexes or self.disks: 85 | index = len(self.disks) + 1 86 | else: 87 | index = None 88 | disk = Disk(self, path, index=str(index) if index else None, **args) 89 | self.disks.append(disk) 90 | return disk 91 | 92 | def init(self, single=None, swallow_exceptions=True): 93 | """Handles all important disk-mounting tasks, i.e. calls the :func:`Disk.init` function on all underlying 94 | disks. It yields every volume that is encountered, including volumes that have not been mounted. 95 | 96 | :param single: indicates whether the :class:`Disk` should be mounted as a single disk, not as a single disk or 97 | whether it should try both (defaults to :const:`None`) 98 | :type single: bool|None 99 | :param swallow_exceptions: specify whether you want the init calls to swallow exceptions 100 | :rtype: generator 101 | """ 102 | for d in self.disks: 103 | yield from d.init(single, swallow_exceptions=swallow_exceptions) 104 | 105 | def mount_disks(self): 106 | """Mounts all disks in the parser, i.e. calling :func:`Disk.mount` on all underlying disks. You probably want to 107 | use :func:`init` instead. 108 | 109 | :return: whether all mounts have succeeded 110 | :rtype: bool""" 111 | 112 | result = True 113 | for disk in self.disks: 114 | result = disk.mount() and result 115 | return result 116 | 117 | def rw_active(self): 118 | """Indicates whether a read-write cache is active in any of the disks. 119 | 120 | :rtype: bool""" 121 | result = False 122 | for disk in self.disks: 123 | result = disk.rw_active() or result 124 | return result 125 | 126 | def init_volumes(self, single=None, only_mount=None, skip_mount=None, swallow_exceptions=True): 127 | """Detects volumes (as volume system or as single volume) in all disks and yields the volumes. This calls 128 | :func:`Disk.init_volumes` on all disks and should be called after :func:`mount_disks`. 129 | 130 | :rtype: generator""" 131 | 132 | for disk in self.disks: 133 | logger.info("Mounting volumes in {0}".format(disk)) 134 | yield from disk.init_volumes(single, only_mount, skip_mount, swallow_exceptions=swallow_exceptions) 135 | 136 | def get_by_index(self, index): 137 | """Returns a Volume or Disk by its index.""" 138 | 139 | try: 140 | return self[index] 141 | except KeyError: 142 | for v in self.get_volumes(): 143 | if v.index == str(index): 144 | return v 145 | raise KeyError(index) 146 | 147 | def get_volumes(self): 148 | """Gets a list of all volumes of all disks, concatenating :func:`Disk.get_volumes` of all disks. 149 | 150 | :rtype: list""" 151 | 152 | volumes = [] 153 | for disk in self.disks: 154 | volumes.extend(disk.get_volumes()) 155 | return volumes 156 | 157 | def clean(self, remove_rw=False, allow_lazy=False): 158 | """Cleans all volumes of all disks (:func:`Volume.unmount`) and all disks (:func:`Disk.unmount`). Volume errors 159 | are ignored, but returns immediately on disk unmount error. 160 | 161 | :param bool remove_rw: indicates whether a read-write cache should be removed 162 | :param bool allow_lazy: indicates whether lazy unmounting is allowed 163 | :raises SubsystemError: when one of the underlying commands fails. Some are swallowed. 164 | :raises CleanupError: when actual cleanup fails. Some are swallowed. 165 | """ 166 | 167 | # To ensure clean unmount after reconstruct, we sort across all volumes in all our disks to provide a proper 168 | # order 169 | volumes = list(sorted(self.get_volumes(), key=lambda v: v.mountpoint or "", reverse=True)) 170 | for v in volumes: 171 | try: 172 | v.unmount(allow_lazy=allow_lazy) 173 | except ImageMounterError: 174 | logger.error("Error unmounting volume {0}".format(v.mountpoint)) 175 | 176 | # Now just clean the rest. 177 | for disk in self.disks: 178 | disk.unmount(remove_rw, allow_lazy=allow_lazy) 179 | 180 | def force_clean(self, remove_rw=False, allow_lazy=False, retries=5, sleep_interval=0.5): 181 | """Attempts to call the clean method, but will retry automatically if an error is raised. When the attempts 182 | run out, it will raise the last error. 183 | 184 | Note that the method will only catch :class:`ImageMounterError` exceptions. 185 | 186 | :param bool remove_rw: indicates whether a read-write cache should be removed 187 | :param bool allow_lazy: indicates whether lazy unmounting is allowed 188 | :param retries: Maximum amount of retries while unmounting 189 | :param sleep_interval: The sleep interval between attempts. 190 | :raises SubsystemError: when one of the underlying commands fails. Some are swallowed. 191 | :raises CleanupError: when actual cleanup fails. Some are swallowed. 192 | """ 193 | 194 | while True: 195 | try: 196 | self.clean(remove_rw=remove_rw, allow_lazy=allow_lazy) 197 | except ImageMounterError: 198 | if retries == 0: 199 | raise 200 | retries -= 1 201 | time.sleep(sleep_interval) 202 | else: 203 | return 204 | 205 | def reconstruct(self): 206 | """Reconstructs the filesystem of all volumes mounted by the parser by inspecting the last mount point and 207 | bind mounting everything. 208 | 209 | :raises: NoRootFoundError if no root could be found 210 | :return: the root :class:`Volume` 211 | """ 212 | volumes = list(sorted((v for v in self.get_volumes() if v.mountpoint and v.info.get('lastmountpoint')), 213 | key=lambda v: v.numeric_index)) 214 | 215 | try: 216 | root = list(filter(lambda x: x.info.get('lastmountpoint') == '/', volumes))[0] 217 | except IndexError: 218 | logger.error("Could not find / while reconstructing, aborting!") 219 | raise NoRootFoundError() 220 | 221 | volumes.remove(root) 222 | 223 | for v in volumes: 224 | if v.info.get('lastmountpoint') == root.info.get('lastmountpoint'): 225 | logger.debug("Skipping volume %s as it has the same root as %s", v, root) 226 | continue 227 | v.bindmount(os.path.join(root.mountpoint, v.info.get('lastmountpoint')[1:])) 228 | return root 229 | -------------------------------------------------------------------------------- /imagemounter/unmounter.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import re 4 | import tempfile 5 | from imagemounter import _util 6 | 7 | 8 | class Unmounter: 9 | """Allows easy unmounting of left-overs of ImageParser calls.""" 10 | 11 | def __init__(self, casename=None, pretty=False, mountdir=None, allow_greedy=True, *args, **kwargs): 12 | """Instantiation of this class automatically indexes the mountpoints and loopbacks currently on the system. 13 | However, in the time between calling any ``find`` function and actually unmounting anything, the system may 14 | change. This can be especially painful when using :func:`preview_unmount`. 15 | 16 | :param str casename: The casename to be unmounted, see :class:`ImageParser` 17 | :param bool pretty: Whether the volumes were mounted using pretty mount, see :class:`Volume` 18 | :param str mountdir: The mountdir wheret he volumes were mounted, see :class:`Volume` 19 | :param bool allow_greedy: When none of the parameters are specified, by default, a greedy method will try to 20 | find as much possible mount points as possible. 21 | """ 22 | 23 | self.mountpoints = {} 24 | self.loopbacks = {} 25 | 26 | # if any details provided, do not try to be greedy 27 | self.be_greedy = allow_greedy and not (casename or pretty or mountdir) 28 | 29 | mountdir = mountdir or tempfile.gettempdir() 30 | if casename: 31 | mountdir = os.path.join(mountdir, casename) 32 | 33 | if pretty: 34 | self.re_pattern = re.escape(mountdir) + r"/.*[0-9.]+-.+" 35 | self.glob_pattern = mountdir + "/*" 36 | else: 37 | self.re_pattern = re.escape(mountdir) + r"/im_[0-9.]+_.+" 38 | self.glob_pattern = mountdir + "/im_*" 39 | 40 | if casename: 41 | self.orig_re_pattern = re.escape(tempfile.gettempdir()) + r"/image_mounter_.*_" + re.escape(casename) 42 | self.orig_glob_pattern = tempfile.gettempdir() + "/image_mounter_*_" + casename 43 | else: 44 | self.orig_re_pattern = re.escape(tempfile.gettempdir()) + r"/image_mounter_.*" 45 | self.orig_glob_pattern = tempfile.gettempdir() + "/image_mounter_*" 46 | 47 | self._index_loopbacks() 48 | self._index_mountpoints() 49 | 50 | def preview_unmount(self): 51 | """Returns a list of all commands that would be executed if the :func:`unmount` method would be called. 52 | 53 | Note: any system changes between calling this method and calling :func:`unmount` aren't listed by this command. 54 | """ 55 | 56 | commands = [] 57 | for mountpoint in self.find_bindmounts(): 58 | commands.append('umount {0}'.format(mountpoint)) 59 | for mountpoint in self.find_mounts(): 60 | commands.append('umount {0}'.format(mountpoint)) 61 | commands.append('rm -Rf {0}'.format(mountpoint)) 62 | for vgname, pvname in self.find_volume_groups(): 63 | commands.append('lvchange -a n {0}'.format(vgname)) 64 | commands.append('losetup -d {0}'.format(pvname)) 65 | for device in self.find_loopbacks(): 66 | commands.append('losetup -d {0}'.format(device)) 67 | for mountpoint in self.find_base_images(): 68 | commands.append('fusermount -u {0}'.format(mountpoint)) 69 | commands.append('rm -Rf {0}'.format(mountpoint)) 70 | for folder in self.find_clean_dirs(): 71 | cmd = 'rm -Rf {0}'.format(folder) 72 | if cmd not in commands: 73 | commands.append(cmd) 74 | return commands 75 | 76 | def unmount(self): 77 | """Calls all unmount methods in the correct order.""" 78 | 79 | self.unmount_bindmounts() 80 | self.unmount_mounts() 81 | self.unmount_volume_groups() 82 | self.unmount_loopbacks() 83 | self.unmount_base_images() 84 | self.clean_dirs() 85 | 86 | def _index_mountpoints(self): 87 | """Finds all mountpoints and stores them in :attr:`mountpoints`""" 88 | 89 | # find all mountponits 90 | self.mountpoints = {} 91 | # noinspection PyBroadException 92 | try: 93 | result = _util.check_output_(['mount']) 94 | for line in result.splitlines(): 95 | m = re.match(r'(.+) on (.+) type (.+) \((.+)\)', line) 96 | if m: 97 | self.mountpoints[m.group(2)] = (m.group(1), m.group(3), m.group(4)) 98 | except Exception: 99 | pass 100 | 101 | def _index_loopbacks(self): 102 | """Finds all loopbacks and stores them in :attr:`loopbacks`""" 103 | 104 | self.loopbacks = {} 105 | try: 106 | result = _util.check_output_(['losetup', '-a']) 107 | for line in result.splitlines(): 108 | m = re.match(r'(.+): (.+) \((.+)\).*', line) 109 | if m: 110 | self.loopbacks[m.group(1)] = m.group(3) 111 | except Exception: 112 | pass 113 | 114 | def find_bindmounts(self): 115 | """Finds all bind mountpoints that are inside mounts that match the :attr:`re_pattern`""" 116 | 117 | for mountpoint, (orig, fs, opts) in self.mountpoints.items(): 118 | if 'bind' in opts and re.match(self.re_pattern, mountpoint): 119 | yield mountpoint 120 | 121 | def find_mounts(self): 122 | """Finds all mountpoints that are mounted to a directory matching :attr:`re_pattern` or originate from a 123 | directory matching :attr:`orig_re_pattern`. 124 | """ 125 | 126 | for mountpoint, (orig, fs, opts) in self.mountpoints.items(): 127 | if 'bind' not in opts and (re.match(self.orig_re_pattern, orig) 128 | or (self.be_greedy and re.match(self.re_pattern, mountpoint))): 129 | yield mountpoint 130 | 131 | def find_base_images(self): 132 | """Finds all mountpoints that are mounted to a directory matching :attr:`orig_re_pattern`.""" 133 | 134 | for mountpoint, _ in self.mountpoints.items(): 135 | if re.match(self.orig_re_pattern, mountpoint): 136 | yield mountpoint 137 | 138 | def find_volume_groups(self): 139 | """Finds all volume groups that are mounted through a loopback originating from :attr:`orig_re_pattern`. 140 | 141 | Generator yields tuples of vgname, pvname 142 | """ 143 | 144 | os.environ['LVM_SUPPRESS_FD_WARNINGS'] = '1' 145 | 146 | # find volume groups 147 | try: 148 | result = _util.check_output_(['pvdisplay']) 149 | pvname = vgname = None 150 | for line in result.splitlines(): 151 | if '--- Physical volume ---' in line: 152 | pvname = vgname = None 153 | elif "PV Name" in line: 154 | pvname = line.replace("PV Name", "").strip() 155 | elif "VG Name" in line: 156 | vgname = line.replace("VG Name", "").strip() 157 | 158 | if pvname and vgname: 159 | try: 160 | # unmount volume groups with a physical volume originating from a disk image 161 | if re.match(self.orig_re_pattern, self.loopbacks[pvname]): 162 | yield vgname, pvname 163 | except Exception: 164 | pass 165 | pvname = vgname = None 166 | 167 | except Exception: 168 | pass 169 | 170 | def find_loopbacks(self): 171 | """Finds all loopbacks originating from :attr:`orig_re_pattern`. 172 | 173 | Generator yields device names 174 | """ 175 | 176 | for dev, source in self.loopbacks.items(): 177 | if re.match(self.orig_re_pattern, source): 178 | yield dev 179 | 180 | def unmount_bindmounts(self): 181 | """Unmounts all bind mounts identified by :func:`find_bindmounts`""" 182 | 183 | for mountpoint in self.find_bindmounts(): 184 | _util.clean_unmount(['umount'], mountpoint, rmdir=False) 185 | 186 | def unmount_mounts(self): 187 | """Unmounts all mounts identified by :func:`find_mounts`""" 188 | 189 | for mountpoint in self.find_mounts(): 190 | _util.clean_unmount(['umount'], mountpoint) 191 | 192 | def unmount_base_images(self): 193 | """Unmounts all mounts identified by :func:`find_base_images`""" 194 | 195 | for mountpoint in self.find_base_images(): 196 | _util.clean_unmount(['fusermount', '-u'], mountpoint) 197 | 198 | def unmount_volume_groups(self): 199 | """Unmounts all volume groups and related loopback devices as identified by :func:`find_volume_groups`""" 200 | 201 | for vgname, pvname in self.find_volume_groups(): 202 | _util.check_output_(['lvchange', '-a', 'n', vgname]) 203 | _util.check_output_(['losetup', '-d', pvname]) 204 | 205 | def unmount_loopbacks(self): 206 | """Unmounts all loopback devices as identified by :func:`find_loopbacks`""" 207 | 208 | # re-index loopback devices 209 | self._index_loopbacks() 210 | 211 | for dev in self.find_loopbacks(): 212 | _util.check_output_(['losetup', '-d', dev]) 213 | 214 | def find_clean_dirs(self): 215 | """Finds all (temporary) directories according to the glob and re patterns that should be cleaned.""" 216 | 217 | for folder in glob.glob(self.glob_pattern): 218 | if re.match(self.re_pattern, folder): 219 | yield folder 220 | for folder in glob.glob(self.orig_glob_pattern): 221 | if re.match(self.orig_re_pattern, folder): 222 | yield folder 223 | 224 | def clean_dirs(self): 225 | """Does a final cleaning of the (temporary) directories according to :func:`find_clean_dirs`.""" 226 | 227 | for folder in self.find_clean_dirs(): 228 | try: 229 | os.rmdir(folder) 230 | except Exception: 231 | pass 232 | -------------------------------------------------------------------------------- /imount.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # backwards compatibility 3 | from imagemounter.cli.imount import main 4 | 5 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #pytsk3>=0.0.dev0 # allow dev releases 2 | termcolor 3 | #python-magic 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup 4 | 5 | 6 | def get_metadata(): 7 | import re 8 | with open(os.path.join("imagemounter", "__init__.py")) as f: 9 | return dict(re.findall("__([a-z]+)__ = ['\"]([^'\"]+)['\"]", f.read())) 10 | 11 | metadata = get_metadata() 12 | 13 | try: 14 | long_description = open("README.rst", "r").read() 15 | except Exception: 16 | long_description = None 17 | 18 | setup( 19 | name='imagemounter', 20 | version=metadata['version'], 21 | license='MIT', 22 | packages=['imagemounter', 'imagemounter.cli'], 23 | author='Peter Wagenaar, Ralph Broenink', 24 | author_email='ralph@ralphbroenink.net', 25 | url='https://github.com/ralphje/imagemounter', 26 | download_url='https://github.com/ralphje/imagemounter/tarball/v' + metadata['version'], 27 | description='Command line utility and Python package to ease the (un)mounting of forensic disk images.', 28 | long_description=long_description, 29 | entry_points={'console_scripts': ['imount = imagemounter.cli.imount:main']}, 30 | install_requires=['termcolor>=1.0.0'], 31 | extras={"magic": ["python-magic>=0.4"]}, 32 | keywords=['encase', 'aff', 'dd', 'disk image', 'ewfmount', 'affuse', 'xmount', 'imount'], 33 | classifiers=[ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Environment :: Console', 36 | 'Intended Audience :: Developers', 37 | 'Intended Audience :: Legal Industry', 38 | 'Intended Audience :: System Administrators', 39 | 'License :: OSI Approved :: MIT License', 40 | 'Operating System :: POSIX :: Linux', 41 | 'Programming Language :: Python :: 3.6', 42 | 'Programming Language :: Python :: 3.7', 43 | 'Programming Language :: Python :: 3.8', 44 | 'Programming Language :: Python :: 3.9', 45 | 'Programming Language :: Python :: 3.10', 46 | 'Topic :: Scientific/Engineering :: Information Analysis', 47 | 'Topic :: System :: Filesystems', 48 | 'Topic :: Terminals', 49 | 'Topic :: Utilities', 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # backwards compatibility 3 | from imagemounter.cli.shell import main 4 | 5 | main() 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("imagemounter") 4 | logger.setLevel(logging.DEBUG) 5 | logger.addHandler(logging.StreamHandler()) 6 | -------------------------------------------------------------------------------- /tests/cli_test.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import pytest 4 | 5 | from imagemounter.cli import AppendDictAction 6 | 7 | 8 | class TestAppendDictAction: 9 | @pytest.mark.parametrize("args,result", [ 10 | (["--test", "x"], {"*": 'x'}), 11 | (["--test", "y=x"], {"y": 'x'}), 12 | (["--test", "y=x,z=a"], {"y": 'x', "z": 'a'}), 13 | (["--test", "*=x"], {"*": 'x'}), 14 | 15 | (["--test", "*=x", "--test", "y=a"], {"*": 'x', 'y': 'a'}), 16 | (["--test", "x=x", "--test", "x=y"], {'x': 'y'}), 17 | (["--test", "*=x", "--test", "y"], {"*": 'y'}), 18 | (["--test", "y", "--test", "*=3"], {"*": '3'}), 19 | ]) 20 | def test_with_comma(self, args, result): 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument('--test', action=AppendDictAction) 23 | 24 | assert parser.parse_args(args).test == result 25 | 26 | @pytest.mark.parametrize("args", [ 27 | (["--test", "y=x,z"]), 28 | (["--test", "y=x,z", "--test", "x"]), 29 | ]) 30 | def test_with_comma_error(self, args): 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument('--test', action=AppendDictAction) 33 | 34 | with pytest.raises(SystemExit): 35 | parser.parse_args(args) 36 | 37 | @pytest.mark.parametrize("args,result", [ 38 | (["--test", "x"], {"*": 'x'}), 39 | (["--test", "y=x"], {"y": 'x'}), 40 | (["--test", "y=x,z=a"], {"y": 'x,z=a'}), 41 | (["--test", "*=x"], {"*": 'x'}), 42 | (["--test", "y=x,z"], {"y": 'x,z'}), 43 | 44 | (["--test", "x", "--test", "y"], {"*": 'y'}), 45 | (["--test", "y=x", "--test", "x=y"], {"y": 'x', 'x': 'y'}), 46 | (["--test", "y=x,z=a", "--test", "b=c"], {"y": 'x,z=a', 'b': 'c'}), 47 | ]) 48 | def test_without_comma(self, args, result): 49 | parser = argparse.ArgumentParser() 50 | parser.add_argument('--test', action=AppendDictAction, allow_commas=False) 51 | 52 | assert parser.parse_args(args).test == result 53 | 54 | def test_with_default(self): 55 | parser = argparse.ArgumentParser() 56 | parser.add_argument('--test', action=AppendDictAction, default={"aa": "bb"}) 57 | 58 | assert parser.parse_args(["--test", "x"]).test == {"*": 'x', 'aa': 'bb'} 59 | -------------------------------------------------------------------------------- /tests/dependencies_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from imagemounter.dependencies import CommandDependency, DependencySection, MagicDependency, PythonModuleDependency, \ 6 | require, FileSystemTypeDependency 7 | from imagemounter.exceptions import CommandNotFoundError, ModuleNotFoundError, PrerequisiteFailedError 8 | 9 | 10 | class TestDependencyDecorator: 11 | @pytest.mark.parametrize("none_on_failure", [True, False]) 12 | def test_existing_dependency(self, none_on_failure): 13 | dep = CommandDependency('ls') 14 | 15 | @require(dep, none_on_failure=none_on_failure) 16 | def test(x, y): 17 | return x + y 18 | 19 | assert test(1, 2) == 3 20 | 21 | def test_missing_dependency(self): 22 | dep = CommandDependency('lsxxxx') 23 | 24 | @require(dep) 25 | def test(x, y): 26 | return x + y 27 | 28 | with pytest.raises(CommandNotFoundError): 29 | test() 30 | 31 | def test_missing_dependency_none_on_failure(self): 32 | dep = CommandDependency('lsxxxx') 33 | 34 | @require(dep, none_on_failure=True) 35 | def test2(x, y): 36 | return x + y 37 | 38 | assert test2(1, 2) is None 39 | 40 | 41 | class TestCommandDependency: 42 | def test_existing_dependency(self): 43 | dep = CommandDependency('ls') 44 | assert dep.is_available 45 | dep.require() 46 | 47 | def test_missing_dependency(self): 48 | dep = CommandDependency('lsxxxx') 49 | assert not dep.is_available 50 | with pytest.raises(CommandNotFoundError): 51 | dep.require() 52 | 53 | def test_status_message_existing(self, mocker): 54 | util = mocker.patch('imagemounter.dependencies._util') 55 | util.command_exists.return_value = True 56 | 57 | dep = CommandDependency('lsxxxx') 58 | assert dep.is_available 59 | assert dep.printable_status == "INSTALLED lsxxxx" 60 | 61 | @pytest.mark.parametrize("args,result", [ 62 | ({}, "MISSING ls"), 63 | (dict(package="core-utils"), "MISSING ls part of the core-utils package"), 64 | (dict(why="listing files"), "MISSING ls needed for listing files"), 65 | (dict(package="core-utils", why="listing files"), 66 | "MISSING ls needed for listing files, part of the core-utils package"), 67 | ]) 68 | def test_status_message_missing(self, mocker, args, result): 69 | util = mocker.patch('imagemounter.dependencies._util') 70 | util.command_exists.return_value = False 71 | 72 | dep = CommandDependency('ls', **args) 73 | assert not dep.is_available 74 | assert dep.printable_status.strip() == result 75 | 76 | 77 | class TestFilesystemDependency: 78 | def test_existing_dependency(self): 79 | dep = FileSystemTypeDependency('tmpfs') 80 | assert dep.is_available 81 | dep.require() 82 | 83 | def test_existing_unloaded_dependency(self, mocker): 84 | # this assumes iso9660 is a kernel dependency available 85 | dep = FileSystemTypeDependency('iso9660') 86 | dep._is_loaded = mocker.Mock(return_value=False) 87 | assert dep.is_available 88 | dep.require() 89 | 90 | def test_missing_dependency(self): 91 | dep = CommandDependency('foobar') 92 | assert not dep.is_available 93 | with pytest.raises(PrerequisiteFailedError): 94 | dep.require() 95 | 96 | 97 | class TestPythonModuleDependency: 98 | def test_existing_dependency(self): 99 | dep = PythonModuleDependency('sys') 100 | assert dep.is_available 101 | dep.require() 102 | 103 | def test_missing_dependency(self): 104 | dep = PythonModuleDependency('foobarnonexistent') 105 | assert not dep.is_available 106 | with pytest.raises(ModuleNotFoundError): 107 | dep.require() 108 | 109 | def test_status_message_existing(self, mocker): 110 | util = mocker.patch('imagemounter.dependencies._util') 111 | util.module_exists.return_value = True 112 | 113 | dep = PythonModuleDependency('requests2') 114 | assert dep.is_available 115 | assert dep.printable_status == "INSTALLED requests2" 116 | 117 | @pytest.mark.parametrize("args,result", [ 118 | ({}, "MISSING sys install using pip"), 119 | (dict(why="system functions"), "MISSING sys needed for system functions, install using pip"), 120 | ]) 121 | def test_status_message_missing(self, mocker, args, result): 122 | util = mocker.patch('imagemounter.dependencies._util') 123 | util.module_exists.return_value = False 124 | 125 | dep = PythonModuleDependency('sys', **args) 126 | assert not dep.is_available 127 | assert dep.printable_status.strip() == result 128 | 129 | 130 | class TestMagicDependency: 131 | @pytest.fixture 132 | def magic_dep(self): 133 | return MagicDependency("python-magic") 134 | 135 | @pytest.fixture 136 | def magic_pypi(self, mocker): 137 | sys.modules['magic'] = mocker.Mock(['from_file']) 138 | yield sys.modules['magic'] 139 | del sys.modules['magic'] 140 | 141 | @pytest.fixture 142 | def magic_system(self, mocker): 143 | sys.modules['magic'] = mocker.Mock(['open']) 144 | yield sys.modules['magic'] 145 | del sys.modules['magic'] 146 | 147 | @pytest.fixture 148 | def magic_unknown(self, mocker): 149 | sys.modules['magic'] = mocker.Mock([]) 150 | yield sys.modules['magic'] 151 | del sys.modules['magic'] 152 | 153 | def test_not_exists(self, mocker, magic_dep): 154 | util = mocker.patch('imagemounter.dependencies._util') 155 | util.module_exists.return_value = False 156 | 157 | assert not magic_dep.is_available 158 | assert not magic_dep._importable 159 | assert magic_dep.printable_status == "MISSING python-magic install using pip" 160 | 161 | def test_exists_pypi(self, magic_dep, magic_pypi): 162 | assert magic_dep.is_available 163 | assert magic_dep.is_python_package 164 | assert not magic_dep.is_system_package 165 | assert magic_dep.printable_status == "INSTALLED python-magic (Python package)" 166 | 167 | def test_exists_system(self, magic_dep, magic_system): 168 | assert magic_dep.is_available 169 | assert not magic_dep.is_python_package 170 | assert magic_dep.is_system_package 171 | assert magic_dep.printable_status == "INSTALLED python-magic (system package)" 172 | 173 | def test_exists_unknown(self, magic_dep, magic_unknown): 174 | assert magic_dep._importable 175 | assert not magic_dep.is_available 176 | assert not magic_dep.is_python_package 177 | assert not magic_dep.is_system_package 178 | assert magic_dep.printable_status \ 179 | == "ERROR python-magic expecting python-magic, found other module named magic" 180 | 181 | 182 | class TestDependencySection: 183 | def test_section_no_deps(self): 184 | section = DependencySection(name="empty section", 185 | description='not needed', 186 | deps=[]) 187 | assert section.printable_status == "-- empty section (not needed) --" 188 | 189 | def test_section_printable_status(self, mocker): 190 | mock_dependency = mocker.Mock() 191 | mock_dependency.printable_status = "I'm just a mock" 192 | section = DependencySection(name="fake section", 193 | description='needed for stuff', 194 | deps=[mock_dependency]) 195 | 196 | assert section.printable_status == "-- fake section (needed for stuff) --\n I'm just a mock" 197 | -------------------------------------------------------------------------------- /tests/full_image_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from imagemounter import ImageParser, dependencies 6 | 7 | 8 | def supportfs(fs): 9 | return dependencies.FileSystemTypeDependency(fs).is_available 10 | 11 | 12 | def fullpath(fn): 13 | return os.path.join(os.path.dirname(os.path.realpath(__file__)), fn) 14 | 15 | 16 | @pytest.mark.parametrize("type,filename", [ 17 | pytest.param('cramfs', 'images/test.cramfs', marks=pytest.mark.skipif(not supportfs("cramfs"), reason="cramfs not supported")), 18 | pytest.param('ext', 'images/test.ext3', marks=pytest.mark.skipif(not supportfs("ext3"), reason="ext3 not supported")), 19 | pytest.param('fat', 'images/test.fat12', marks=pytest.mark.skipif(not supportfs("vfat"), reason="vfat not supported")), 20 | pytest.param('iso', 'images/test.iso', marks=pytest.mark.skipif(not supportfs("iso9660"), reason="iso not supported")), 21 | pytest.param('minix', 'images/test.minix', marks=pytest.mark.skipif(not supportfs("minix"), reason="minix not supported")), 22 | pytest.param('ntfs', 'images/test.ntfs', marks=pytest.mark.skipif(not supportfs("ntfs"), reason="ntfs not supported")), 23 | pytest.param('squashfs', 'images/test.sqsh', marks=[pytest.mark.xfail(reason="squashfs support is currently broken"), 24 | pytest.mark.skipif(not supportfs("squashfs"), reason="squashfs not supported")]), 25 | pytest.param('iso', 'images/test.zip', marks=pytest.mark.skipif(not supportfs("iso9660"), reason="iso not supported")), 26 | ]) 27 | def test_direct_mount(type, filename): 28 | volumes = [] 29 | parser = ImageParser([fullpath(filename)]) 30 | for v in parser.init(): 31 | if v.flag == "alloc": 32 | assert v.mountpoint is not None 33 | volumes.append(v) 34 | 35 | parser.force_clean() 36 | 37 | assert len(volumes) == 1 38 | assert volumes[0].filesystem.type == type 39 | 40 | 41 | @pytest.mark.skipif(not supportfs("ntfs"), reason="ntfs not supported") 42 | @pytest.mark.skipif(not dependencies.bdemount.is_available, reason="bdemount not available") 43 | # identifier should be B8992A09-3AE9-47E3-8FD0-4A5B8389B0A4 44 | @pytest.mark.parametrize("key", ["p:test1234", "r:391798-523787-614746-034969-107921-412302-401654-479457"]) 45 | def test_bde_mount(key): 46 | parser = ImageParser([fullpath('images/bdetest.E01')], keys={"0": key}) 47 | for i, v in enumerate(parser.init()): 48 | assert v.mountpoint is not None 49 | assert v.flag == "alloc" 50 | assert v.filesystem.type == "ntfs" 51 | assert v.index == "0.0" 52 | assert i == 0 # ensures we only have a single item in this iteration 53 | 54 | parser.force_clean() 55 | 56 | 57 | @pytest.mark.skipif(not dependencies.lvm.is_available, reason="lvm not available") 58 | def test_lvm_mount(): 59 | parser = ImageParser([fullpath('images/lvm.raw')]) 60 | 61 | volumes = [] 62 | for v in parser.init(): 63 | volumes.append(v) 64 | 65 | assert len(volumes) == 2 66 | assert volumes[0].mountpoint is not None 67 | assert volumes[0].flag == "alloc" 68 | assert volumes[0].filesystem.type == "ext" 69 | assert volumes[0].index == "0.0" 70 | 71 | parser.force_clean() 72 | 73 | 74 | def test_filesystem_mount(): 75 | filename = 'images/test.mbr' 76 | volumes = [] 77 | parser = ImageParser([fullpath(filename)]) 78 | for v in parser.init(): 79 | if v.flag == "alloc" and v.index != "4": 80 | assert v.mountpoint is not None 81 | volumes.append(v) 82 | 83 | parser.force_clean() 84 | 85 | assert len(volumes) == 13 86 | 87 | assert volumes[0].flag == "meta" 88 | assert volumes[1].flag == "unalloc" 89 | assert volumes[2].filesystem.type == "fat" 90 | assert volumes[3].filesystem.type == "ext" 91 | assert volumes[4].filesystem.type == "unknown" 92 | assert volumes[5].flag == "meta" 93 | assert volumes[6].flag == "meta" 94 | assert volumes[7].flag == "unalloc" 95 | assert volumes[8].filesystem.type == "ext" 96 | assert volumes[9].flag == "meta" 97 | assert volumes[10].flag == "meta" 98 | assert volumes[11].flag == "unalloc" 99 | assert volumes[12].filesystem.type == "fat" 100 | -------------------------------------------------------------------------------- /tests/images/bdetest.E01: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:003cd15edd4c7270c11ec512954a37e9acbd7bf7a6d7b813cc0148040bf49c7b 3 | size 67676551 4 | -------------------------------------------------------------------------------- /tests/images/lvm.raw: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ff44276701b4bf56117f199508396a762a8642b6b50c60c5acaa8ce7561b60e2 3 | size 10485760 4 | -------------------------------------------------------------------------------- /tests/images/test.cramfs: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4e1a2641051f4488b728c58b1421fdf3222dd914f7c343d93533afa7c72be267 3 | size 4096 4 | -------------------------------------------------------------------------------- /tests/images/test.ext3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b5f542287d59b9151a964978fd89a6d635aade6253476455c4c12ee29a382fe8 3 | size 204800 4 | -------------------------------------------------------------------------------- /tests/images/test.fat12: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5db96b7abc68e6663abf632a4fd1604ad8843bce7c4ab6e7c6482b3881675fc0 3 | size 51200 4 | -------------------------------------------------------------------------------- /tests/images/test.iso: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:0059b3e268f5c71979686dda92675e08ac7e3719925500874cc7cd6d32e2cc99 3 | size 53248 4 | -------------------------------------------------------------------------------- /tests/images/test.mbr: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:a2db4aeb24998c5539fd8e464d242ae9b09227170c468f8bc725249b4e79a2bd 3 | size 1024000 4 | -------------------------------------------------------------------------------- /tests/images/test.minix: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3d90f4e0605cabb810b5c12beb52e3f1bd826a6f1264fbb301de13628472e8b0 3 | size 51200 4 | -------------------------------------------------------------------------------- /tests/images/test.ntfs: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:18396d8c5350e8a0a52b2cb86de68dfbc50abac30ee60c9c77330a4533502c47 3 | size 1049088 4 | -------------------------------------------------------------------------------- /tests/images/test.sqsh: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bf58bde531ceda3b1f626b5289161166d551ffe89b8d77bb08b6272254f95927 3 | size 4096 4 | -------------------------------------------------------------------------------- /tests/images/test.zip: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:69a6a36882f751da04b50f71c0f39dcdd239cc0b80b06216f03a4eaa873b79de 3 | size 493 4 | -------------------------------------------------------------------------------- /tests/parser_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from imagemounter.exceptions import NoRootFoundError 4 | from imagemounter.parser import ImageParser 5 | from imagemounter.volume import Volume 6 | 7 | 8 | class TestReconstruction: 9 | def test_no_volumes(self): 10 | parser = ImageParser() 11 | parser.add_disk("...") 12 | with pytest.raises(NoRootFoundError): 13 | parser.reconstruct() 14 | 15 | def test_no_root(self): 16 | parser = ImageParser() 17 | disk = parser.add_disk("...") 18 | v1 = Volume(disk) 19 | v1.filesystem.mountpoint = 'xxx' 20 | v1.info['lastmountpoint'] = '/etc/x' 21 | v2 = Volume(disk) 22 | v2.filesystem.mountpoint = 'xxx' 23 | v2.info['lastmountpoint'] = '/etc' 24 | disk.volumes.volumes = [v1, v2] 25 | with pytest.raises(NoRootFoundError): 26 | parser.reconstruct() 27 | 28 | def test_simple(self, mocker): 29 | parser = ImageParser() 30 | disk = parser.add_disk("...") 31 | v1 = Volume(disk) 32 | v1.filesystem.mountpoint = 'xxx' 33 | v1.info['lastmountpoint'] = '/' 34 | v2 = Volume(disk) 35 | v2.filesystem.mountpoint = 'xxx' 36 | v2.info['lastmountpoint'] = '/etc' 37 | disk.volumes.volumes = [v1, v2] 38 | 39 | v2_bm = mocker.patch.object(v2, "bindmount") 40 | parser.reconstruct() 41 | v2_bm.assert_called_once_with("xxx/etc") 42 | 43 | def test_multiple_roots(self, mocker): 44 | parser = ImageParser() 45 | disk = parser.add_disk("...") 46 | v1 = Volume(disk) 47 | v1.index = '1' 48 | v1.filesystem.mountpoint = 'xxx' 49 | v1.info['lastmountpoint'] = '/' 50 | v2 = Volume(disk) 51 | v2.index = '2' 52 | v2.filesystem.mountpoint = 'xxx' 53 | v2.info['lastmountpoint'] = '/' 54 | v3 = Volume(disk) 55 | v3.index = '3' 56 | v3.filesystem.mountpoint = 'xxx' 57 | v3.info['lastmountpoint'] = '/etc' 58 | disk.volumes.volumes = [v1, v2, v3] 59 | 60 | v1_bm = mocker.patch.object(v1, "bindmount") 61 | v2_bm = mocker.patch.object(v2, "bindmount") 62 | v3_bm = mocker.patch.object(v3, "bindmount") 63 | parser.reconstruct() 64 | v1_bm.assert_not_called() 65 | v2_bm.assert_not_called() 66 | v3_bm.assert_called_with('xxx/etc') 67 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8~=4.0.1 2 | pytest~=7.0.1 3 | pytest-cov~=3.0.0 4 | pytest-mock~=3.6.0 5 | -------------------------------------------------------------------------------- /tests/volume_system_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from imagemounter._util import check_output_ 4 | from imagemounter.disk import Disk 5 | from imagemounter.parser import ImageParser 6 | 7 | 8 | class TestParted: 9 | def test_parted_requests_input(self, mocker): 10 | check_output = mocker.patch("imagemounter.volume_system._util.check_output_") 11 | def modified_command(cmd, *args, **kwargs): 12 | if cmd[0] == 'parted': 13 | # A command that requests user input 14 | return check_output_([sys.executable, "-c", "exec(\"try: input('>> ')\\nexcept: pass\")"], 15 | *args, **kwargs) 16 | return mocker.DEFAULT 17 | check_output.side_effect = modified_command 18 | 19 | disk = Disk(ImageParser(), path="...") 20 | 21 | list(disk.volumes.detect_volumes(method='parted')) 22 | check_output.assert_called() 23 | # TODO: kill process when test fails 24 | -------------------------------------------------------------------------------- /tests/volume_test.py: -------------------------------------------------------------------------------- 1 | import io 2 | import subprocess 3 | import sys 4 | import time 5 | 6 | import pytest 7 | 8 | from imagemounter import FILE_SYSTEM_TYPES 9 | from imagemounter.disk import Disk 10 | from imagemounter.filesystems import UnknownFileSystem 11 | from imagemounter.parser import ImageParser 12 | from imagemounter.volume import Volume 13 | 14 | 15 | def test_key_material_read(): 16 | volume = Volume(disk=Disk(ImageParser(keys={'3': 'hello'}), "..."), index='3') 17 | assert volume.key == "hello" 18 | volume = Volume(disk=Disk(ImageParser(keys={'3': 'hello', '*': 'ola'}), "..."), index='2') 19 | assert volume.key == "ola" 20 | volume = Volume(disk=Disk(ImageParser(keys={'3': 'hello'}), "..."), index='1') 21 | assert volume.key == "" 22 | 23 | 24 | class TestFsType: 25 | def test_valid_fstype(self): 26 | volume = Volume(disk=Disk(ImageParser(), "..."), fstype='ext') 27 | volume.determine_fs_type() 28 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES['ext'] 29 | 30 | def test_valid_vstype(self): 31 | volume = Volume(disk=Disk(ImageParser(), "..."), fstype="dos") 32 | volume.determine_fs_type() 33 | assert volume.volumes.vstype == "dos" 34 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES["volumesystem"] 35 | 36 | # Add names in here that are shown in the wild for output of mmls / gparted 37 | # !! Always try to add it also to test_combination 38 | @pytest.mark.parametrize("description,fstype", [ 39 | # Names assigned by imagemounter 40 | ('Logical Volume', 'unknown'), 41 | ('LUKS Volume', 'unknown'), 42 | ('BDE Volume', 'unknown'), 43 | ('RAID Volume', 'unknown'), 44 | ('VSS Store', 'unknown'), 45 | 46 | ('NTFS / exFAT', 'ntfs'), # mmls, should use fallback 47 | ('Linux (0x83)', 'unknown'), # should use unknown 48 | ('4.2BSD', 'ufs'), 49 | ('BSD/386, 386BSD, NetBSD, FreeBSD (0xa5)', 'volumesystem'), 50 | ('DOS FAT16', 'fat'), 51 | ]) 52 | def test_fsdescription(self, description, fstype, mocker): 53 | volume = Volume(disk=Disk(ImageParser(), "...")) 54 | volume._get_blkid_type = mocker.Mock(return_value=None) 55 | volume._get_magic_type = mocker.Mock(return_value=None) 56 | self.fstype = "" # prevent fallback to unknown by default 57 | volume.info['fsdescription'] = description 58 | volume.determine_fs_type() 59 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES[fstype] 60 | 61 | @pytest.mark.parametrize("description,fstype", [ 62 | ('2AE031AA-0F40-DB11-9590-000C2911D1B8', 'vmfs'), 63 | # '8053279D-AD40-DB11-BF97-000C2911D1B8': 'vmkcore-diagnostics', 64 | # '6A898CC3-1DD2-11B2-99A6-080020736631': 'zfs-member', 65 | # 'C38C896A-D21D-B211-99A6-080020736631': 'zfs-member', 66 | # '0FC63DAF-8483-4772-8E79-3D69D8477DE4': 'linux', 67 | ('E6D6D379-F507-44C2-A23C-238F2A3DF928', 'lvm'), 68 | ('79D3D6E6-07F5-C244-A23C-238F2A3DF928', 'lvm'), 69 | ('CA7D7CCB-63ED-4C53-861C-1742536059CC', 'luks'), 70 | ]) 71 | def test_guid(self, description, fstype, mocker): 72 | volume = Volume(disk=Disk(ImageParser(), "...")) 73 | volume._get_blkid_type = mocker.Mock(return_value=None) 74 | volume._get_magic_type = mocker.Mock(return_value=None) 75 | volume.info['guid'] = description 76 | volume.determine_fs_type() 77 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES[fstype] 78 | 79 | # Add values here that are shown in the wild for blkid 80 | # !! Always try to add it also to test_combination 81 | @pytest.mark.parametrize("description,fstype", [ 82 | ('cramfs', 'cramfs'), 83 | ('ext4', 'ext'), 84 | ('ext2', 'ext'), 85 | ('vfat', 'fat'), 86 | ('iso9660', 'iso'), 87 | ('minix', 'minix'), 88 | ('ntfs', 'ntfs'), 89 | ('squashfs', 'squashfs'), 90 | ('ufs', 'ufs'), 91 | 92 | ('dos', 'volumesystem'), 93 | ]) 94 | def test_blkid(self, description, fstype, mocker): 95 | volume = Volume(disk=Disk(ImageParser(), "...")) 96 | volume._get_blkid_type = mocker.Mock(return_value=description) 97 | volume._get_magic_type = mocker.Mock(return_value=None) 98 | volume.determine_fs_type() 99 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES[fstype] 100 | 101 | # Add values here that are shown in the wild for file magic output 102 | # !! Always try to add it also to test_combination 103 | @pytest.mark.parametrize("description,fstype", [ 104 | ('Linux Compressed ROM File System data', 'cramfs'), 105 | ('Linux rev 1.0 ext2 filesystem data', 'ext'), 106 | 107 | ('DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, ' 108 | 'root entries 512, sectors 100 (volumes <=32 MB) , Media descriptor 0xf8, ' 109 | 'sectors/FAT 1, sectors/track 32, heads 64, serial number 0x3cb7474b, ' 110 | 'label: "TEST ", FAT (12 bit)', "fat"), 111 | 112 | ("ISO 9660 CD-ROM filesystem data 'test'", 'iso'), 113 | ('Minix filesystem, V1, 30 char names, 12800 zones', 'minix'), 114 | ('DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS ", sectors/cluster 8, ' 115 | 'Media descriptor 0xf8, sectors/track 0, dos < 4.0 BootSector (0x80), FAT (1Y bit ' 116 | 'by descriptor); NTFS, sectors 2048, $MFT start cluster 4, $MFTMirror start ' 117 | 'cluster 128, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial ' 118 | 'number 04e8742c12a96cecd; contains Microsoft Windows XP/VISTA bootloader BOOTMGR', "ntfs"), 119 | 120 | ('Squashfs filesystem, little endian, version 4.0', 'squashfs'), 121 | ]) 122 | def test_magic(self, description, fstype, mocker): 123 | volume = Volume(disk=Disk(ImageParser(), "...")) 124 | volume._get_blkid_type = mocker.Mock(return_value=None) 125 | volume._get_magic_type = mocker.Mock(return_value=description) 126 | volume.determine_fs_type() 127 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES[fstype] 128 | 129 | # Add values here to test full combinations of specific filesystem types 130 | # The _ as key is the expected result 131 | @pytest.mark.parametrize("definition,fstype", [ 132 | ({"blkid": "cramfs", "magic": "Linux Compressed ROM File System data", "fsdescription": "???"}, "cramfs"), 133 | ({"blkid": "exfat", "fsdescription": "NTFS / exFAT", "statfstype": "exFAT"}, "exfat"), 134 | ({"blkid": "ext4", "fsdescription": "Linux (0x83)", "guid": "", "statfstype": "Ext2"}, "ext"), 135 | ({"blkid": "vfat", "magic": "FAT (12 bit)", "fsdescription": "DOS FAT12 (0x04)", "statfstype": "FAT12"}, "fat"), 136 | ({"blkid": "iso9660", "magic": ".. ISO 9660 ..", "statfstype": "ISO9660"}, "iso"), 137 | ({"blkid": "min ix", "magic": "Minix filesystem", "fsdescription": "???"}, "minix"), 138 | ({"blkid": "ntfs", "magic": ".. NTFS ..", "fsdescription": "NTFS / exFAT", "statfstype": "NTFS"}, "ntfs"), 139 | ({"blkid": "squashfs", "magic": "Squashfs filesystem", "fsdescription": "???"}, "squashfs"), 140 | ({"guid": "79D3D6E6-07F5-C244-A23C-238F2A3DF928"}, "lvm"), 141 | ({"fsdescription": "Linux (0x83)", "blkid": "linux_raid_member"}, "raid"), 142 | ({"blkid": "dos", "fsdescription": "Logical Volume"}, "volumesystem"), 143 | ({"blkid": "dos", "fsdescription": "RAID Volume"}, "volumesystem"), 144 | ({"blkid": "dos", "magic": "DOS/MBR boot sector"}, "volumesystem"), 145 | ({"fsdescription": "BSD/386, 386BSD, NetBSD, FreeBSD (0xa5)", "blkid": "ufs"}, "volumesystem"), 146 | ({"fsdescription": "4.2BSD (0x07)", "blkid": "ufs"}, "ufs"), 147 | ]) 148 | def test_combination(self, definition, fstype, mocker): 149 | volume = Volume(disk=Disk(ImageParser(), "...")) 150 | volume._get_blkid_type = mocker.Mock(return_value=definition.get("blkid")) 151 | volume._get_magic_type = mocker.Mock(return_value=definition.get("magic")) 152 | volume.info = definition 153 | volume.determine_fs_type() 154 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES[fstype] 155 | 156 | def test_no_clue_fstype(self, mocker): 157 | volume = Volume(disk=Disk(ImageParser(), "...")) 158 | volume._get_blkid_type = mocker.Mock(return_value=None) 159 | volume._get_magic_type = mocker.Mock(return_value=None) 160 | volume.determine_fs_type() 161 | assert volume.filesystem.__class__ is UnknownFileSystem 162 | 163 | def test_little_clue_fstype(self, mocker): 164 | volume = Volume(disk=Disk(ImageParser(), "...")) 165 | volume._get_blkid_type = mocker.Mock(return_value="-") 166 | volume._get_magic_type = mocker.Mock(return_value="-") 167 | volume.determine_fs_type() 168 | assert volume.filesystem.__class__ is UnknownFileSystem 169 | 170 | def test_fstype_fallback(self, mocker): 171 | volume = Volume(disk=Disk(ImageParser(), "...")) 172 | volume._get_blkid_type = mocker.Mock(return_value=None) 173 | volume._get_magic_type = mocker.Mock(return_value=None) 174 | volume._get_fstype_from_parser('?ufs') 175 | volume.determine_fs_type() 176 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES["ufs"] 177 | 178 | def test_fstype_fallback_unknown(self, mocker): 179 | volume = Volume(disk=Disk(ImageParser(), "...")) 180 | volume._get_blkid_type = mocker.Mock(return_value=None) 181 | volume._get_magic_type = mocker.Mock(return_value=None) 182 | volume.info['fsdescription'] = "Linux (0x83)" 183 | 184 | # If something more specific is set, we use that 185 | volume._get_fstype_from_parser('?ufs') 186 | volume.determine_fs_type() 187 | assert volume.filesystem.__class__ is FILE_SYSTEM_TYPES["ufs"] 188 | 189 | # Otherwise we fallback to unknown if Linux (0x83) is set 190 | volume._get_fstype_from_parser('') 191 | volume.determine_fs_type() 192 | assert volume.filesystem.__class__ is UnknownFileSystem 193 | 194 | 195 | class TestFileMagic: 196 | def test_read_bytes_crash(self, mocker): 197 | mock_open = mocker.patch("io.open") 198 | mock_open().__enter__().read.side_effect = IOError 199 | volume = Volume(disk=Disk(ImageParser(), "...")) 200 | volume.get_raw_path = mocker.Mock(return_value="...") 201 | 202 | assert volume._get_magic_type() is None 203 | 204 | 205 | class TestFsstat: 206 | def test_ext4(self, mocker): 207 | # Removed some items from this output as we don't use it in its entirety anyway 208 | result = b"""FILE SYSTEM INFORMATION 209 | -------------------------------------------- 210 | File System Type: Ext4 211 | Volume Name: Example 212 | Volume ID: 2697f5b0479b15b1b4c81994387cdba 213 | 214 | Last Written at: 2017-07-02 12:23:22 (CEST) 215 | Last Checked at: 2016-07-09 20:27:28 (CEST) 216 | 217 | Last Mounted at: 2017-07-02 12:23:23 (CEST) 218 | Unmounted properly 219 | Last mounted on: / 220 | 221 | Source OS: Linux 222 | 223 | BLOCK GROUP INFORMATION 224 | --------------------------------------------""" 225 | mock_popen = mocker.patch('subprocess.Popen') 226 | type(mock_popen()).stdout = mocker.PropertyMock(return_value=io.BytesIO(result)) 227 | 228 | volume = Volume(disk=Disk(ImageParser(), "...")) 229 | volume.get_raw_path = mocker.Mock(return_value="...") 230 | 231 | volume._load_fsstat_data() 232 | 233 | assert volume.info['statfstype'] == 'Ext4' 234 | assert volume.info['lastmountpoint'] == '/' 235 | assert volume.info['label'] == '/ (Example)' 236 | assert volume.info['version'] == 'Linux' 237 | 238 | # must be called after reading BLOCK GROUP INFORMATION 239 | mock_popen().terminate.assert_called() 240 | 241 | def test_ntfs(self, mocker): 242 | # Removed some items from this output as we don't use it in its entirety anyway 243 | result = b"""FILE SYSTEM INFORMATION 244 | -------------------------------------------- 245 | File System Type: NTFS 246 | Volume Serial Number: 4E8742C12A96CECD 247 | OEM Name: NTFS 248 | Version: Windows XP""" 249 | mock_popen = mocker.patch('subprocess.Popen') 250 | type(mock_popen()).stdout = mocker.PropertyMock(return_value=io.BytesIO(result)) 251 | 252 | volume = Volume(disk=Disk(ImageParser(), "...")) 253 | volume.get_raw_path = mocker.Mock(return_value="...") 254 | 255 | volume._load_fsstat_data() 256 | 257 | assert volume.info['statfstype'] == 'NTFS' 258 | assert "lastmountpoint" not in volume.info 259 | assert "label" not in volume.info 260 | assert volume.info['version'] == 'Windows XP' 261 | 262 | def test_utf8_label(self, mocker): 263 | # Removed some items from this output as we don't use it in its entirety anyway 264 | result = b"""FILE SYSTEM INFORMATION 265 | -------------------------------------------- 266 | File System Type: Ext4 267 | Volume Name: \xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd0\xb8 268 | Volume ID: 2697f5b0479b15b1b4c81994387cdba""" 269 | mock_popen = mocker.patch('subprocess.Popen') 270 | type(mock_popen()).stdout = mocker.PropertyMock(return_value=io.BytesIO(result)) 271 | 272 | volume = Volume(disk=Disk(ImageParser(), "...")) 273 | volume.get_raw_path = mocker.Mock(return_value="...") 274 | 275 | volume._load_fsstat_data() 276 | 277 | assert volume.info['statfstype'] == 'Ext4' 278 | assert volume.info['label'] == u'\u0420\u043e\u0441\u0441\u0438\u0438' 279 | 280 | def test_killed_after_timeout(self, mocker): 281 | def mock_side_effect(*args, **kwargs): 282 | time.sleep(0.2) 283 | return io.BytesIO(b"") 284 | 285 | mock_popen = mocker.patch('subprocess.Popen') 286 | type(mock_popen()).stdout = mocker.PropertyMock(side_effect=mock_side_effect) 287 | 288 | volume = Volume(disk=Disk(ImageParser(), "...")) 289 | volume.get_raw_path = mocker.Mock(return_value="...") 290 | 291 | volume._load_fsstat_data(timeout=0.1) 292 | mock_popen().terminate.assert_called() 293 | 294 | 295 | class TestLuks: 296 | def test_luks_key_communication(self, mocker): 297 | check_call = mocker.patch("imagemounter.volume._util.check_call_") 298 | def modified_check_call(cmd, *args, **kwargs): 299 | if cmd[0:2] == ['cryptsetup', 'isLuks']: 300 | return True 301 | if cmd[0:1] == ['losetup']: 302 | return "/dev/loop0" 303 | return mock.DEFAULT 304 | check_call.side_effect = modified_check_call 305 | 306 | check_output = mocker.patch("imagemounter.volume._util.check_output_") 307 | def modified_check_output(cmd, *args, **kwargs): 308 | if cmd[0:1] == ['losetup']: 309 | return "/dev/loop0" 310 | return mock.DEFAULT 311 | check_output.side_effect = modified_check_output 312 | 313 | original_popen = subprocess.Popen 314 | def modified_popen(cmd, *args, **kwargs): 315 | if cmd[0:3] == ['cryptsetup', '-r', 'luksOpen']: 316 | # A command that requests user input 317 | x = original_popen([sys.executable, "-c", "print(input(''))"], 318 | *args, **kwargs) 319 | return x 320 | return mock.DEFAULT 321 | popen = mocker.patch("subprocess.Popen", side_effect=modified_popen) 322 | 323 | disk = Disk(ImageParser(keys={'1': 'p:passphrase'}), "...") 324 | disk.is_mounted = True 325 | volume = Volume(disk=disk, fstype='luks', index='1', parent=disk) 326 | volume.mount() 327 | 328 | assert volume.is_mounted 329 | assert len(volume.volumes) == 1 330 | assert volume.volumes[0].info['fsdescription'] == "LUKS Volume" 331 | --------------------------------------------------------------------------------