ntfsdump 3.3.0__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.
- ntfsdump-3.3.2/.github/workflows/test.yaml +52 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/PKG-INFO +57 -11
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/README.md +56 -10
- ntfsdump-3.3.2/src/ntfsdump/__about__.py +2 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/cli.py +13 -4
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/core.py +12 -3
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/vhd.py +44 -18
- ntfsdump-3.3.2/src/ntfsdump/formats/vmdk.py +149 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/image.py +61 -5
- ntfsdump-3.3.2/src/ntfsdump/image_paths.py +22 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/vmware/metadata.py +16 -13
- ntfsdump-3.3.2/src/ntfsdump/volume.py +322 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/conftest.py +6 -4
- ntfsdump-3.3.2/tests/ntfsdump/test__init__.py +54 -0
- ntfsdump-3.3.2/tests/ntfsdump/test_core.py +71 -0
- ntfsdump-3.3.2/tests/ntfsdump/test_image_os_volume.py +210 -0
- ntfsdump-3.3.2/tests/ntfsdump/test_vmdk_chain.py +306 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_vmware.py +72 -3
- ntfsdump-3.3.2/tests/ntfsdump/test_volume.py +578 -0
- ntfsdump-3.3.0/.github/workflows/test.yaml +0 -32
- ntfsdump-3.3.0/src/ntfsdump/__about__.py +0 -2
- ntfsdump-3.3.0/src/ntfsdump/formats/vmdk.py +0 -100
- ntfsdump-3.3.0/src/ntfsdump/volume.py +0 -196
- ntfsdump-3.3.0/tests/ntfsdump/test__init__.py +0 -37
- ntfsdump-3.3.0/tests/ntfsdump/test_vmdk_chain.py +0 -129
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.devcontainer/Dockerfile +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.devcontainer/devcontainer.json +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.devcontainer/postCreateCommands.fish +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.github/workflows/publish-binary-pypi.yaml +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.github/workflows/publish-release.yaml +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/.gitignore +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/Dockerfile +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/LICENSE +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/docker-compose.yml +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/pyproject.toml +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/__init__.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/__main__.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/errors.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/__init__.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/base.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/detect.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/e01.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/formats/raw.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/logger.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/sources/__init__.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/sources/resolver.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/src/ntfsdump/vmware/__init__.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/cache/.gitkeep +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/SingleVM/SingleVM.vmx +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/WindowsVM/WindowsVM.vmsd +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/fixtures/vmware/WindowsVM/WindowsVM.vmx +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_cli.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_detect.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_source.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhd_real.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhdx_chain.py +0 -0
- {ntfsdump-3.3.0 → ntfsdump-3.3.2}/tests/ntfsdump/test_vhdx_real.py +0 -0
- {ntfsdump-3.3.0 → 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.
|
|
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
|
|
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`:
|
|
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
|
-
-
|
|
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
|
|
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`:
|
|
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
|
-
-
|
|
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
|
|
|
@@ -63,15 +63,23 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
63
63
|
)
|
|
64
64
|
parser.add_argument(
|
|
65
65
|
"--flat", action="store_true",
|
|
66
|
-
help=
|
|
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
|
|
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
|
|
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,
|
|
@@ -241,6 +249,8 @@ def _run(args) -> None:
|
|
|
241
249
|
snapshot=args.snapshot,
|
|
242
250
|
disk=args.disk,
|
|
243
251
|
flat=args.flat,
|
|
252
|
+
quiet=args.quiet,
|
|
253
|
+
output_file=args.output_file,
|
|
244
254
|
)
|
|
245
255
|
|
|
246
256
|
|
|
@@ -248,7 +258,6 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
|
|
|
248
258
|
parser = create_parser()
|
|
249
259
|
args = parser.parse_args(argv)
|
|
250
260
|
|
|
251
|
-
MetaData.quiet = args.quiet
|
|
252
261
|
configure_logging(args.log)
|
|
253
262
|
|
|
254
263
|
try:
|
|
@@ -3,6 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Optional, Union
|
|
4
4
|
|
|
5
5
|
from ntfsdump.image import ImageFile
|
|
6
|
+
from ntfsdump.logger import MetaData
|
|
7
|
+
from ntfsdump.errors import NtfsDumpError
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def ntfsdump(
|
|
@@ -14,6 +16,8 @@ def ntfsdump(
|
|
|
14
16
|
snapshot=None,
|
|
15
17
|
disk: Optional[int] = None,
|
|
16
18
|
flat: bool = False,
|
|
19
|
+
quiet: bool = False,
|
|
20
|
+
output_file: bool = False,
|
|
17
21
|
) -> None:
|
|
18
22
|
"""A tool for extracting files from an NTFS volume on a disk image.
|
|
19
23
|
|
|
@@ -27,7 +31,14 @@ def ntfsdump(
|
|
|
27
31
|
snapshot (optional): VMware snapshot ID to read. Defaults to None.
|
|
28
32
|
disk (Optional[int], optional): VMware virtual disk ID to read. Defaults to None.
|
|
29
33
|
flat (bool, optional): reconstruct directory tree or not. Defaults to False.
|
|
34
|
+
quiet (bool, optional): suppress progress lines on stdout.
|
|
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.
|
|
30
37
|
"""
|
|
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.")
|
|
31
42
|
img = ImageFile(
|
|
32
43
|
source,
|
|
33
44
|
volume=volume,
|
|
@@ -35,7 +46,5 @@ def ntfsdump(
|
|
|
35
46
|
snapshot=snapshot,
|
|
36
47
|
disk=disk,
|
|
37
48
|
)
|
|
38
|
-
output_dir = Path(output).resolve()
|
|
39
|
-
|
|
40
49
|
for query in paths:
|
|
41
|
-
img.main_volume.dump_files(query,
|
|
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
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
``C:\\VMs\\Base\\disk.vhdx``)
|
|
30
|
-
|
|
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)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 =
|
|
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)
|