ntfsdump 3.3.1__tar.gz → 3.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. ntfsdump-3.3.2/.github/workflows/test.yaml +52 -0
  2. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/PKG-INFO +57 -11
  3. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/README.md +56 -10
  4. ntfsdump-3.3.2/src/ntfsdump/__about__.py +2 -0
  5. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/cli.py +12 -3
  6. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/core.py +7 -3
  7. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/vhd.py +44 -18
  8. ntfsdump-3.3.2/src/ntfsdump/formats/vmdk.py +149 -0
  9. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/image.py +61 -5
  10. ntfsdump-3.3.2/src/ntfsdump/image_paths.py +22 -0
  11. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/vmware/metadata.py +16 -13
  12. ntfsdump-3.3.2/src/ntfsdump/volume.py +322 -0
  13. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/conftest.py +6 -4
  14. ntfsdump-3.3.2/tests/ntfsdump/test__init__.py +54 -0
  15. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_core.py +26 -0
  16. ntfsdump-3.3.2/tests/ntfsdump/test_image_os_volume.py +210 -0
  17. ntfsdump-3.3.2/tests/ntfsdump/test_vmdk_chain.py +306 -0
  18. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_vmware.py +72 -3
  19. ntfsdump-3.3.2/tests/ntfsdump/test_volume.py +578 -0
  20. ntfsdump-3.3.1/.github/workflows/test.yaml +0 -32
  21. ntfsdump-3.3.1/src/ntfsdump/__about__.py +0 -2
  22. ntfsdump-3.3.1/src/ntfsdump/formats/vmdk.py +0 -100
  23. ntfsdump-3.3.1/src/ntfsdump/volume.py +0 -196
  24. ntfsdump-3.3.1/tests/ntfsdump/test__init__.py +0 -37
  25. ntfsdump-3.3.1/tests/ntfsdump/test_vmdk_chain.py +0 -129
  26. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.devcontainer/Dockerfile +0 -0
  27. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.devcontainer/devcontainer.json +0 -0
  28. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.devcontainer/postCreateCommands.fish +0 -0
  29. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.github/workflows/publish-binary-pypi.yaml +0 -0
  30. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.github/workflows/publish-release.yaml +0 -0
  31. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/.gitignore +0 -0
  32. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/Dockerfile +0 -0
  33. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/LICENSE +0 -0
  34. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/docker-compose.yml +0 -0
  35. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/pyproject.toml +0 -0
  36. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/__init__.py +0 -0
  37. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/__main__.py +0 -0
  38. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/errors.py +0 -0
  39. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/__init__.py +0 -0
  40. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/base.py +0 -0
  41. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/detect.py +0 -0
  42. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/e01.py +0 -0
  43. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/formats/raw.py +0 -0
  44. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/logger.py +0 -0
  45. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/sources/__init__.py +0 -0
  46. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/sources/resolver.py +0 -0
  47. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/src/ntfsdump/vmware/__init__.py +0 -0
  48. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/cache/.gitkeep +0 -0
  49. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/SingleVM/SingleVM.vmx +0 -0
  50. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/WindowsVM/WindowsVM.vmsd +0 -0
  51. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/WindowsVM/WindowsVM.vmx +0 -0
  52. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_cli.py +0 -0
  53. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_detect.py +0 -0
  54. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_source.py +0 -0
  55. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhd_real.py +0 -0
  56. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhdx_chain.py +0 -0
  57. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhdx_real.py +0 -0
  58. {ntfsdump-3.3.1 → ntfsdump-3.3.2}/uv.lock +0 -0
@@ -0,0 +1,52 @@
1
+ name: pytest
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ - develop
8
+ pull_request:
9
+
10
+ jobs:
11
+ pytest:
12
+ name: Run tests with pytest
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ['3.13', '3.14']
17
+ steps:
18
+ - name: checkout
19
+ uses: actions/checkout@v6
20
+
21
+ - name: install python ${{ matrix.python-version }}
22
+ uses: actions/setup-python@v6
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: setup uv
27
+ run: |
28
+ pip install uv
29
+ uv sync
30
+
31
+ - name: run unit tests
32
+ run: uv run pytest --ignore=tests/ntfsdump/test__init__.py
33
+
34
+ - name: cache NTFS sample archive
35
+ id: ntfs-sample
36
+ uses: actions/cache@v4
37
+ with:
38
+ path: tests/ntfsdump/cache/ntfs.tgs
39
+ key: ntfs-sample-79b3b6f3fa173fd0d2a05d0d2693afd0
40
+
41
+ - name: download NTFS sample with retries
42
+ if: steps.ntfs-sample.outputs.cache-hit != 'true'
43
+ run: |
44
+ mkdir -p tests/ntfsdump/cache
45
+ curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 \
46
+ --connect-timeout 15 --max-time 120 \
47
+ --output tests/ntfsdump/cache/ntfs.tgs.tmp \
48
+ 'https://github.com/msuhanov/ntfs-samples/blob/master/ntfs.tgz?raw=true'
49
+ mv tests/ntfsdump/cache/ntfs.tgs.tmp tests/ntfsdump/cache/ntfs.tgs
50
+
51
+ - name: run sample image E2E tests
52
+ run: uv run pytest tests/ntfsdump/test__init__.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ntfsdump
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: A command-line tool for efficiently extracting files, directories, and alternate data streams directly from NTFS image files.
5
5
  Project-URL: Homepage, https://github.com/sumeshi/ntfsdump
6
6
  Project-URL: Repository, https://github.com/sumeshi/ntfsdump
@@ -94,15 +94,20 @@ vm.vmsd
94
94
  - `--help`, `-h`: Show help message.
95
95
  - `--version`, `-V`: Display program version.
96
96
  - `--quiet`, `-q`: Suppress normal stdout output. Errors are still written to stderr.
97
- - `--flat`: Extract all artifacts purely into a single folder without reconstructing directories.
98
- - `--volume`, `-n`: Target specific NTFS volume number (default: auto-detects main OS volume).
97
+ - `--flat`: Extract all artifacts purely into a single folder without reconstructing directories. Directory names are joined into the filename with `_` (e.g. `/Windows/System32/cmd.exe` -> `Windows_System32_cmd.exe`) so that same-name files from different directories stay apart.
98
+ - `--volume`, `-n`: Target the NTFS partition address shown in analysis logs (default: auto-detects main OS volume).
99
99
  - `--snapshot`, `-s`: Read the NTFS as it was at the given VMware snapshot. The ID is the snapshot UID shown by `--list-snapshots`. Without it, the VM's current state is read.
100
100
  - `--disk`, `-d`: Read a specific VMware virtual disk by ID (default: auto-select when unique).
101
101
  - `--list-snapshots`: List VMware snapshots found in the SOURCE and exit.
102
102
  - `--list-disks`: List VMware virtual disks found in the SOURCE and exit.
103
103
  - `--image-format`: Force the image format instead of auto-detection. Options: `raw`, `e01`, `vhd`, `vhdx`, `vmdk`.
104
104
  - `--log [FILE]`: Enable logging. Optionally specify a log file path.
105
- - `--output`, `-o`: Directory or file to save exported outputs.
105
+ - `--output`, `-o`: Output directory (default: current directory). An existing file is used as a file output.
106
+ - `--output-file`: Save one file or ADS to the path given by `-o`, including a new filename.
107
+
108
+ In directory output, conflicting filenames receive a unique suffix. File output
109
+ replaces an existing file only after extraction succeeds. Failed extraction
110
+ preserves existing files.
106
111
 
107
112
 
108
113
  #### Examples
@@ -110,10 +115,16 @@ vm.vmsd
110
115
  Dump a single file (format is auto-detected):
111
116
 
112
117
  ```bash
113
- ntfsdump -o ./dump /path/to/your/image.raw /$MFT
118
+ ntfsdump -o ./dump /path/to/your/image.raw '/$MFT'
114
119
  ntfsdump -o ./dump /path/to/your/evidence.E01 /Windows/System32/config/SYSTEM
115
120
  ```
116
121
 
122
+ Save a single file under a different name:
123
+
124
+ ```bash
125
+ ntfsdump --output-file -o ./SYSTEM.bin /path/to/your/image.raw /Windows/System32/config/SYSTEM
126
+ ```
127
+
117
128
  Dump an entire directory recursively:
118
129
 
119
130
  ```bash
@@ -123,7 +134,37 @@ ntfsdump -o ./dump /path/to/your/image.raw /Windows/System32/winevt/Logs
123
134
  Force the format for a file without a recognizable signature:
124
135
 
125
136
  ```bash
126
- ntfsdump --image-format raw /path/to/your/evidence.bin /$MFT
137
+ ntfsdump --image-format raw /path/to/your/evidence.bin '/$MFT'
138
+ ```
139
+
140
+ #### Windows Artifacts
141
+
142
+ These examples use `image.raw` and save the original directory structure under
143
+ `./dump`. Replace `Alice` with the profile directory in your image. Quote paths
144
+ to preserve `$`, spaces, and wildcard queries when running from a shell.
145
+
146
+ ```bash
147
+ # NTFS metadata: MFT, transaction log, and USN Journal
148
+ ntfsdump -o ./dump image.raw '/$MFT' '/$LogFile' '/$Extend/$UsnJrnl:$J'
149
+
150
+ # System registry hives and their transaction logs
151
+ ntfsdump -o ./dump image.raw '/Windows/System32/config'
152
+
153
+ # User registry hives and their transaction logs (prefix expansion)
154
+ ntfsdump -o ./dump image.raw '/Users/Alice/NTUSER.DAT.*' \
155
+ '/Users/Alice/AppData/Local/Microsoft/Windows/UsrClass.dat.*'
156
+
157
+ # Windows event logs
158
+ ntfsdump -o ./dump image.raw '/Windows/System32/winevt/Logs'
159
+
160
+ # Prefetch and Amcache
161
+ ntfsdump -o ./dump image.raw '/Windows/Prefetch' '/Windows/AppCompat/Programs/Amcache.hve.*'
162
+
163
+ # System Resource Usage Monitor (SRUM)
164
+ ntfsdump -o ./dump image.raw '/Windows/System32/sru'
165
+
166
+ # User shortcuts and Jump Lists
167
+ ntfsdump -o ./dump image.raw '/Users/Alice/AppData/Roaming/Microsoft/Windows/Recent'
127
168
  ```
128
169
 
129
170
  #### VMware Snapshots (`--list-snapshots`, `-s/--snapshot`)
@@ -186,9 +227,14 @@ ntfsdump ./WindowsVM -d 1 /Evidence
186
227
  Even a bare VMDK inside the VM directory is understood: parents are resolved automatically from the descriptors, so pointing at a delta file reads the whole chain (base + deltas) as one logical disk:
187
228
 
188
229
  ```bash
189
- ntfsdump ./WindowsVM/Windows-000003.vmdk /$MFT
230
+ ntfsdump ./WindowsVM/Windows-000003.vmdk '/$MFT'
190
231
  ```
191
232
 
233
+ Keep VMDK parents and data files within the initial disk's directory and its
234
+ subdirectories; symlinks and junctions are not followed. VMX/VMSD disk references
235
+ must stay within the VM directory. Update copied metadata to use local paths
236
+ when necessary.
237
+
192
238
  Using with [ntfsfind](https://github.com/sumeshi/ntfsfind) over standard input (pipe):
193
239
 
194
240
  ```bash
@@ -202,12 +248,12 @@ ntfsfind '.*\.evtx' ./image.raw | ntfsdump -o ./dump ./image.raw
202
248
  `ntfsdump` can read Hyper-V checkpoint (differencing disk) chains directly. Both a base `VHDX` and an `AVHDX` differencing disk are auto-detected, and either can be passed as `SOURCE`:
203
249
 
204
250
  ```bash
205
- ntfsdump ./HyperVM/Disk_0.avhdx /$MFT
251
+ ntfsdump ./HyperVM/Disk_0.avhdx '/$MFT'
206
252
  ntfsdump ./HyperVM/Disk_0.avhdx /Windows/System32/config/SYSTEM
207
253
  ```
208
254
 
209
255
  - The parent chain is resolved automatically from the VHDX metadata (parent locator), so pointing at any disk in the chain reads the whole chain as one logical disk.
210
- - Parent images are looked up next to the child image; absolute Windows parent paths are reduced to their basename.
256
+ - Parents can use relative or absolute paths. For copied Windows images, place parents next to the child to use the filename fallback.
211
257
  - Images are never merged or converted — nothing is written to the source directory.
212
258
  - Checkpoint listing (`--list-checkpoints`) is not yet available, and `-s` / `--snapshot` remains VMware-only.
213
259
 
@@ -254,10 +300,10 @@ Logging is disabled by default and no log file is created. To record which files
254
300
 
255
301
  ```bash
256
302
  # Auto-generated name in the current directory (e.g. ntfsdump_20260921_153000.log)
257
- ntfsdump image.raw /$MFT --log
303
+ ntfsdump image.raw '/$MFT' --log
258
304
 
259
305
  # Explicit log path
260
- ntfsdump image.raw /$MFT --log ./case.log
306
+ ntfsdump image.raw '/$MFT' --log ./case.log
261
307
  ```
262
308
 
263
309
 
@@ -78,15 +78,20 @@ vm.vmsd
78
78
  - `--help`, `-h`: Show help message.
79
79
  - `--version`, `-V`: Display program version.
80
80
  - `--quiet`, `-q`: Suppress normal stdout output. Errors are still written to stderr.
81
- - `--flat`: Extract all artifacts purely into a single folder without reconstructing directories.
82
- - `--volume`, `-n`: Target specific NTFS volume number (default: auto-detects main OS volume).
81
+ - `--flat`: Extract all artifacts purely into a single folder without reconstructing directories. Directory names are joined into the filename with `_` (e.g. `/Windows/System32/cmd.exe` -> `Windows_System32_cmd.exe`) so that same-name files from different directories stay apart.
82
+ - `--volume`, `-n`: Target the NTFS partition address shown in analysis logs (default: auto-detects main OS volume).
83
83
  - `--snapshot`, `-s`: Read the NTFS as it was at the given VMware snapshot. The ID is the snapshot UID shown by `--list-snapshots`. Without it, the VM's current state is read.
84
84
  - `--disk`, `-d`: Read a specific VMware virtual disk by ID (default: auto-select when unique).
85
85
  - `--list-snapshots`: List VMware snapshots found in the SOURCE and exit.
86
86
  - `--list-disks`: List VMware virtual disks found in the SOURCE and exit.
87
87
  - `--image-format`: Force the image format instead of auto-detection. Options: `raw`, `e01`, `vhd`, `vhdx`, `vmdk`.
88
88
  - `--log [FILE]`: Enable logging. Optionally specify a log file path.
89
- - `--output`, `-o`: Directory or file to save exported outputs.
89
+ - `--output`, `-o`: Output directory (default: current directory). An existing file is used as a file output.
90
+ - `--output-file`: Save one file or ADS to the path given by `-o`, including a new filename.
91
+
92
+ In directory output, conflicting filenames receive a unique suffix. File output
93
+ replaces an existing file only after extraction succeeds. Failed extraction
94
+ preserves existing files.
90
95
 
91
96
 
92
97
  #### Examples
@@ -94,10 +99,16 @@ vm.vmsd
94
99
  Dump a single file (format is auto-detected):
95
100
 
96
101
  ```bash
97
- ntfsdump -o ./dump /path/to/your/image.raw /$MFT
102
+ ntfsdump -o ./dump /path/to/your/image.raw '/$MFT'
98
103
  ntfsdump -o ./dump /path/to/your/evidence.E01 /Windows/System32/config/SYSTEM
99
104
  ```
100
105
 
106
+ Save a single file under a different name:
107
+
108
+ ```bash
109
+ ntfsdump --output-file -o ./SYSTEM.bin /path/to/your/image.raw /Windows/System32/config/SYSTEM
110
+ ```
111
+
101
112
  Dump an entire directory recursively:
102
113
 
103
114
  ```bash
@@ -107,7 +118,37 @@ ntfsdump -o ./dump /path/to/your/image.raw /Windows/System32/winevt/Logs
107
118
  Force the format for a file without a recognizable signature:
108
119
 
109
120
  ```bash
110
- ntfsdump --image-format raw /path/to/your/evidence.bin /$MFT
121
+ ntfsdump --image-format raw /path/to/your/evidence.bin '/$MFT'
122
+ ```
123
+
124
+ #### Windows Artifacts
125
+
126
+ These examples use `image.raw` and save the original directory structure under
127
+ `./dump`. Replace `Alice` with the profile directory in your image. Quote paths
128
+ to preserve `$`, spaces, and wildcard queries when running from a shell.
129
+
130
+ ```bash
131
+ # NTFS metadata: MFT, transaction log, and USN Journal
132
+ ntfsdump -o ./dump image.raw '/$MFT' '/$LogFile' '/$Extend/$UsnJrnl:$J'
133
+
134
+ # System registry hives and their transaction logs
135
+ ntfsdump -o ./dump image.raw '/Windows/System32/config'
136
+
137
+ # User registry hives and their transaction logs (prefix expansion)
138
+ ntfsdump -o ./dump image.raw '/Users/Alice/NTUSER.DAT.*' \
139
+ '/Users/Alice/AppData/Local/Microsoft/Windows/UsrClass.dat.*'
140
+
141
+ # Windows event logs
142
+ ntfsdump -o ./dump image.raw '/Windows/System32/winevt/Logs'
143
+
144
+ # Prefetch and Amcache
145
+ ntfsdump -o ./dump image.raw '/Windows/Prefetch' '/Windows/AppCompat/Programs/Amcache.hve.*'
146
+
147
+ # System Resource Usage Monitor (SRUM)
148
+ ntfsdump -o ./dump image.raw '/Windows/System32/sru'
149
+
150
+ # User shortcuts and Jump Lists
151
+ ntfsdump -o ./dump image.raw '/Users/Alice/AppData/Roaming/Microsoft/Windows/Recent'
111
152
  ```
112
153
 
113
154
  #### VMware Snapshots (`--list-snapshots`, `-s/--snapshot`)
@@ -170,9 +211,14 @@ ntfsdump ./WindowsVM -d 1 /Evidence
170
211
  Even a bare VMDK inside the VM directory is understood: parents are resolved automatically from the descriptors, so pointing at a delta file reads the whole chain (base + deltas) as one logical disk:
171
212
 
172
213
  ```bash
173
- ntfsdump ./WindowsVM/Windows-000003.vmdk /$MFT
214
+ ntfsdump ./WindowsVM/Windows-000003.vmdk '/$MFT'
174
215
  ```
175
216
 
217
+ Keep VMDK parents and data files within the initial disk's directory and its
218
+ subdirectories; symlinks and junctions are not followed. VMX/VMSD disk references
219
+ must stay within the VM directory. Update copied metadata to use local paths
220
+ when necessary.
221
+
176
222
  Using with [ntfsfind](https://github.com/sumeshi/ntfsfind) over standard input (pipe):
177
223
 
178
224
  ```bash
@@ -186,12 +232,12 @@ ntfsfind '.*\.evtx' ./image.raw | ntfsdump -o ./dump ./image.raw
186
232
  `ntfsdump` can read Hyper-V checkpoint (differencing disk) chains directly. Both a base `VHDX` and an `AVHDX` differencing disk are auto-detected, and either can be passed as `SOURCE`:
187
233
 
188
234
  ```bash
189
- ntfsdump ./HyperVM/Disk_0.avhdx /$MFT
235
+ ntfsdump ./HyperVM/Disk_0.avhdx '/$MFT'
190
236
  ntfsdump ./HyperVM/Disk_0.avhdx /Windows/System32/config/SYSTEM
191
237
  ```
192
238
 
193
239
  - The parent chain is resolved automatically from the VHDX metadata (parent locator), so pointing at any disk in the chain reads the whole chain as one logical disk.
194
- - Parent images are looked up next to the child image; absolute Windows parent paths are reduced to their basename.
240
+ - Parents can use relative or absolute paths. For copied Windows images, place parents next to the child to use the filename fallback.
195
241
  - Images are never merged or converted — nothing is written to the source directory.
196
242
  - Checkpoint listing (`--list-checkpoints`) is not yet available, and `-s` / `--snapshot` remains VMware-only.
197
243
 
@@ -238,10 +284,10 @@ Logging is disabled by default and no log file is created. To record which files
238
284
 
239
285
  ```bash
240
286
  # Auto-generated name in the current directory (e.g. ntfsdump_20260921_153000.log)
241
- ntfsdump image.raw /$MFT --log
287
+ ntfsdump image.raw '/$MFT' --log
242
288
 
243
289
  # Explicit log path
244
- ntfsdump image.raw /$MFT --log ./case.log
290
+ ntfsdump image.raw '/$MFT' --log ./case.log
245
291
  ```
246
292
 
247
293
 
@@ -0,0 +1,2 @@
1
+ # coding: utf-8
2
+ __version__ = "3.3.2"
@@ -63,15 +63,23 @@ def create_parser() -> argparse.ArgumentParser:
63
63
  )
64
64
  parser.add_argument(
65
65
  "--flat", action="store_true",
66
- help="save files flatly instead of reconstructing the tree (default: False)."
66
+ help=(
67
+ "save files flatly, joining the directory names into the filename "
68
+ "with '_' (e.g. Windows_System32_cmd.exe), instead of "
69
+ "reconstructing the tree (default: False)."
70
+ )
67
71
  )
68
72
  parser.add_argument(
69
73
  "--output", "-o", type=str, default=".",
70
- help="output destination directory or file path (default: current dir '.')."
74
+ help="output directory or existing file; use --output-file for a new file (default: current dir '.')."
75
+ )
76
+ parser.add_argument(
77
+ "--output-file", action="store_true",
78
+ help="treat --output/-o as a file path; requires one file or ADS query."
71
79
  )
72
80
  parser.add_argument(
73
81
  "--volume", "-n", type=int, default=None,
74
- help="target NTFS volume number (default: auto-detect main OS volume)."
82
+ help="target NTFS partition address shown in analysis logs (default: auto-detect main OS volume)."
75
83
  )
76
84
  parser.add_argument(
77
85
  "--snapshot", "-s", type=str, default=None,
@@ -242,6 +250,7 @@ def _run(args) -> None:
242
250
  disk=args.disk,
243
251
  flat=args.flat,
244
252
  quiet=args.quiet,
253
+ output_file=args.output_file,
245
254
  )
246
255
 
247
256
 
@@ -4,6 +4,7 @@ from typing import Optional, Union
4
4
 
5
5
  from ntfsdump.image import ImageFile
6
6
  from ntfsdump.logger import MetaData
7
+ from ntfsdump.errors import NtfsDumpError
7
8
 
8
9
 
9
10
  def ntfsdump(
@@ -16,6 +17,7 @@ def ntfsdump(
16
17
  disk: Optional[int] = None,
17
18
  flat: bool = False,
18
19
  quiet: bool = False,
20
+ output_file: bool = False,
19
21
  ) -> None:
20
22
  """A tool for extracting files from an NTFS volume on a disk image.
21
23
 
@@ -31,8 +33,12 @@ def ntfsdump(
31
33
  flat (bool, optional): reconstruct directory tree or not. Defaults to False.
32
34
  quiet (bool, optional): suppress progress lines on stdout.
33
35
  Errors are still written to stderr. Defaults to False.
36
+ output_file (bool, optional): treat output as a file path. Requires one file or ADS query.
34
37
  """
35
38
  MetaData.quiet = quiet
39
+ output_path = Path(output).absolute()
40
+ if (output_file or output_path.is_file()) and len(paths) != 1:
41
+ raise NtfsDumpError("File output requires a single file or ADS query.")
36
42
  img = ImageFile(
37
43
  source,
38
44
  volume=volume,
@@ -40,7 +46,5 @@ def ntfsdump(
40
46
  snapshot=snapshot,
41
47
  disk=disk,
42
48
  )
43
- output_dir = Path(output).resolve()
44
-
45
49
  for query in paths:
46
- img.main_volume.dump_files(query, output_dir, flat)
50
+ img.main_volume.dump_files(query, output_path, flat, output_file=output_file)
@@ -22,22 +22,52 @@ class VhdChain:
22
22
  handles: List["pyvhdi.file"] = field(default_factory=list)
23
23
 
24
24
 
25
- def _parent_path(child: Path, parent_filename: str) -> Path:
26
- """Resolve a parent filename relative to the child image.
27
-
28
- VHDX parent locators may contain absolute Windows paths (e.g.
29
- ``C:\\VMs\\Base\\disk.vhdx``). Those cannot be resolved on other
30
- platforms, so only the basename is used and looked up next to the child.
25
+ def _parent_candidates(child: Path, parent_filename: str) -> List[Path]:
26
+ """Candidate paths for a parent disk referenced by ``parent_filename``.
27
+
28
+ A Windows absolute path from the original host (e.g.
29
+ ``C:\\VMs\\Base\\disk.vhdx``) is tried verbatim first — but only when it
30
+ is actually usable as an absolute path on this platform, so that a POSIX
31
+ run never resolves it relative to the current working directory. Next,
32
+ the drive-stripped path is tried relative to the child directory (covers
33
+ copied trees that keep their subdirectory structure), and the basename
34
+ fallback stays last for images copied together "flat".
31
35
  """
32
36
  normalized = parent_filename.replace('\\', '/')
33
37
 
34
- if _WINDOWS_ABSOLUTE_RE.match(parent_filename) or normalized.startswith('//'):
35
- return child.parent / Path(normalized).name
36
-
37
- parent = Path(normalized)
38
- if parent.is_absolute():
39
- return parent
40
- return child.parent / parent
38
+ if _WINDOWS_ABSOLUTE_RE.match(parent_filename):
39
+ candidates: List[Path] = []
40
+ verbatim = Path(parent_filename)
41
+ if verbatim.is_absolute():
42
+ candidates.append(verbatim)
43
+ stripped = normalized.split(':', 1)[1].lstrip('/')
44
+ candidates.append(child.parent / stripped)
45
+ candidates.append(child.parent / Path(normalized).name)
46
+ elif normalized.startswith('//'):
47
+ # UNC paths from the original host are only looked up by basename.
48
+ candidates = [child.parent / Path(normalized).name]
49
+ else:
50
+ parent = Path(normalized)
51
+ if parent.is_absolute():
52
+ candidates = [parent]
53
+ else:
54
+ candidates = [child.parent / parent]
55
+
56
+ unique: List[Path] = []
57
+ for candidate in candidates:
58
+ if candidate not in unique:
59
+ unique.append(candidate)
60
+ return unique
61
+
62
+
63
+ def _resolve_parent(child: Path, parent_filename: str, parent_label: str) -> Path:
64
+ """Pick the first existing candidate parent path."""
65
+ for candidate in _parent_candidates(child, parent_filename):
66
+ if candidate.exists():
67
+ return candidate
68
+ raise NtfsDumpError(
69
+ f"Parent {parent_label} was not found: {parent_filename}"
70
+ )
41
71
 
42
72
 
43
73
  def _safe_close(handle) -> None:
@@ -78,11 +108,7 @@ def open_vhdx_chain(path: Path, _seen: Optional[List[Path]] = None) -> VhdChain:
78
108
 
79
109
  parent_filename = handle.get_parent_filename()
80
110
  if parent_filename:
81
- parent = _parent_path(path, parent_filename)
82
- if not parent.exists():
83
- raise NtfsDumpError(
84
- f"Parent {parent_label} was not found: {parent_filename}"
85
- )
111
+ parent = _resolve_parent(path, parent_filename, parent_label)
86
112
  parent_chain = open_vhdx_chain(parent, seen)
87
113
  handles.extend(parent_chain.handles)
88
114
  try:
@@ -0,0 +1,149 @@
1
+ # coding: utf-8
2
+ from dataclasses import dataclass, field
3
+ from os.path import abspath
4
+ from pathlib import Path, PureWindowsPath
5
+ from typing import BinaryIO, List, Optional
6
+
7
+ import pytsk3
8
+ import pyvmdk
9
+
10
+ from ntfsdump.errors import NtfsDumpError
11
+ from ntfsdump.formats.base import FormatHandler, BaseImgInfo
12
+ from ntfsdump.image_paths import confined_image_path
13
+
14
+
15
+ @dataclass
16
+ class VmdkChain:
17
+ """A logical VMDK disk resolved from a descriptor and its parent chain."""
18
+
19
+ handle: "pyvmdk.handle"
20
+ handles: List["pyvmdk.handle"] = field(default_factory=list)
21
+ extent_files: List[BinaryIO] = field(default_factory=list)
22
+
23
+
24
+ def _parent_candidates(child: Path, parent_filename: str, root: Path) -> List[Path]:
25
+ """Map parent hints to local paths inside the initial disk directory."""
26
+ normalized = parent_filename.replace('\\', '/')
27
+ windows_path = PureWindowsPath(normalized)
28
+ if windows_path.drive:
29
+ candidates: List[Path] = []
30
+ if len(windows_path.drive) == 2 and windows_path.root:
31
+ stripped = normalized.split(':', 1)[1].lstrip('/')
32
+ candidates.append(child.parent / stripped)
33
+ candidates.append(child.parent / windows_path.name)
34
+ elif normalized.startswith('/'):
35
+ candidates = [child.parent / Path(normalized).name]
36
+ else:
37
+ candidates = [child.parent / normalized]
38
+
39
+ unique: List[Path] = []
40
+ for candidate in candidates:
41
+ # Check lexical containment before any filesystem access.
42
+ candidate = Path(abspath(candidate))
43
+ if candidate.is_relative_to(root) and candidate not in unique:
44
+ unique.append(candidate)
45
+ return unique
46
+
47
+
48
+ def _resolve_parent(child: Path, parent_filename: str, root: Path) -> Path:
49
+ """Pick a local parent without following symlinks or Windows junctions."""
50
+ for candidate in _parent_candidates(child, parent_filename, root):
51
+ try:
52
+ candidate = confined_image_path(child.parent, str(candidate), root)
53
+ except NtfsDumpError:
54
+ continue
55
+ if candidate.is_file():
56
+ return candidate
57
+ raise NtfsDumpError(f"Parent VMDK was not found: {parent_filename}")
58
+
59
+
60
+ def _safe_close(handle) -> None:
61
+ try:
62
+ handle.close()
63
+ except Exception:
64
+ pass
65
+
66
+
67
+ def open_vmdk_chain(
68
+ path: Path,
69
+ _seen: Optional[List[Path]] = None,
70
+ _root: Optional[Path] = None,
71
+ ) -> VmdkChain:
72
+ """Open ``path`` and recursively attach its parent VMDK, without merging.
73
+
74
+ Validated extent files are passed to libvmdk as file objects so that they
75
+ are treated as a single logical disk. Snapshot/delta chains
76
+ are linked through ``set_parent()`` so that unallocated blocks fall back to
77
+ the parent image. Parents must stay within the initial disk directory;
78
+ absolute and UNC hints are mapped to local candidates. No temporary RAW
79
+ or merged VMDK is created.
80
+ """
81
+ path = Path(path)
82
+ seen = list(_seen or [])
83
+ resolved = path.resolve()
84
+ root = _root if _root is not None else resolved.parent
85
+
86
+ if resolved in seen:
87
+ chain = ' -> '.join(p.name for p in seen + [resolved])
88
+ raise NtfsDumpError(f"Circular VMDK parent reference detected: {chain}")
89
+ seen.append(resolved)
90
+
91
+ handles: List["pyvmdk.handle"] = []
92
+ extent_files: List[BinaryIO] = []
93
+ try:
94
+ handle = pyvmdk.handle()
95
+ handles.append(handle)
96
+ handle.open(str(path), mode='r')
97
+ with resolved.open('rb') as descriptor_file:
98
+ embedded_descriptor = descriptor_file.read(4) == b'KDMV'
99
+ for extent in handle.extent_descriptors:
100
+ if embedded_descriptor and handle.disk_type in (
101
+ pyvmdk.disk_types.MONOLITHIC_SPARSE, pyvmdk.disk_types.STREAM_OPTIMIZED,
102
+ ):
103
+ # Embedded extents use the source itself even after it is renamed.
104
+ extent_path = resolved
105
+ else:
106
+ extent_path = confined_image_path(resolved.parent, extent.filename, root)
107
+ extent_files.append(extent_path.open('rb'))
108
+ handle.open_extent_data_files_as_file_objects(extent_files)
109
+
110
+ parent_filename = handle.get_parent_filename()
111
+ if parent_filename:
112
+ parent = _resolve_parent(resolved, parent_filename, root)
113
+ parent_chain = open_vmdk_chain(parent, seen, root)
114
+ handles.extend(parent_chain.handles)
115
+ extent_files.extend(parent_chain.extent_files)
116
+ try:
117
+ handle.set_parent(parent_chain.handle)
118
+ except Exception as exc:
119
+ raise NtfsDumpError(
120
+ f"Unable to attach parent VMDK: {parent_filename} ({exc})"
121
+ ) from exc
122
+ except NtfsDumpError:
123
+ for item in handles + extent_files:
124
+ _safe_close(item)
125
+ raise
126
+ except Exception as exc:
127
+ for item in handles + extent_files:
128
+ _safe_close(item)
129
+ raise NtfsDumpError(f"Unable to open VMDK: {path} ({exc})") from exc
130
+
131
+ return VmdkChain(handle=handles[0], handles=handles, extent_files=extent_files)
132
+
133
+
134
+ def probe_media_size(path: Path) -> Optional[int]:
135
+ """Return the logical size of a VMDK descriptor, or None if unavailable."""
136
+ try:
137
+ handle = pyvmdk.handle()
138
+ handle.open(str(path), mode='r')
139
+ size = handle.get_media_size()
140
+ handle.close()
141
+ return int(size)
142
+ except Exception:
143
+ return None
144
+
145
+
146
+ class VMDKHandler(FormatHandler):
147
+ def get_img_info(self, path: str) -> pytsk3.Img_Info:
148
+ chain = open_vmdk_chain(Path(path))
149
+ return BaseImgInfo(chain.handle, keepalive=chain.handles + chain.extent_files)