Quick Start Guide - Python
Get from SDK download to working forensic analysis in 10 minutes.
Prerequisites
- Python 3.14+ (standard library only - no pip install needed)
- FEX Core SDK extracted to a local directory
- Windows x64 or Linux x64 (or Docker)
Step 1: Set Up Your Project
Create a working directory and copy (or symlink) the Python SDK:
my-project/
├── core/ # Copy from sdk/python/core/
│ ├── __init__.py
│ ├── constants.py
│ ├── fexcore.py
│ └── loader.py
├── my_script.py # Your code
On Windows, ensure FEX.Core.dll and its dependencies are in bin/Win64/ relative to the SDK root (already included in the SDK package). The loader searches standard locations automatically.
On Linux, place libfexcore.so in /usr/local/lib/ or set LD_LIBRARY_PATH.
Step 2: Read Image Metadata
Create my_script.py:
#!/usr/bin/env python3
"""Example: Read forensic image metadata."""
import json
import os
import sys
from core import FexCore
from core.loader import load_library
def main():
if len(sys.argv) < 2:
print("Usage: python my_script.py <image_path>")
sys.exit(1)
image_path = sys.argv[1]
# Load the FEX Core library (auto-detects Windows DLL or Linux .so)
lib, lib_path = load_library()
fex = FexCore(lib)
print(f"Loaded: {lib_path}")
# FEX2 license key from GetData — set FEX_LICENSE_KEY in your environment
# (or pull it from your secret store). See ../license-keys.md.
key = os.environ["FEX_LICENSE_KEY"]
# Known issue (Windows): InitLibrary uses GetCurrentDirectory rather than
# GetModuleFileName when locating its license context, so it returns -20
# (RESULT_INVALIDKEY) when the calling process's CWD is not the same
# directory as FEX.Core.dll — even with a valid key. Workaround: chdir
# to the DLL directory across the init_library call. The fix will move
# to the DLL itself in a future release.
prev_cwd = os.getcwd()
os.chdir(os.path.dirname(lib_path))
try:
fex.init_library(key)
finally:
os.chdir(prev_cwd)
print("Library initialized")
# Open the forensic image
image_id = fex.open_image(image_path)
if image_id < 0:
print(f"Error opening image: {image_id}")
sys.exit(1)
print(f"Image opened (ID: {image_id})")
# Get image metadata as JSON
info_str = fex.image_info_json(image_id)
info = json.loads(info_str)
print("\nImage Metadata:")
print(f" Description: {info.get('description', 'N/A')}")
print(f" Image Type: {info.get('imageType', 'N/A')}")
print(f" Image Kind: {info.get('imageKind', 'N/A')}")
print(f" Device Size: {info.get('deviceSize', 'N/A')}")
# Clean up
fex.close_image(image_id)
print("\nDone.")
if __name__ == "__main__":
main()
Run it:
python my_script.py path/to/evidence.E01
Expected output:
Loaded: C:\FEX-SDK\bin\Win64\FEX.Core.dll
Library initialized
Image opened (ID: 0)
Image Metadata:
Description: My Evidence
Image Type: E01
Image Kind: Physical
Device Size: 536870912000
Done.
Step 3: Create a CSV File Listing
This example reads the filesystem and writes a CSV with full paths, sizes, and timestamps.
#!/usr/bin/env python3
"""Example: Export file listing to CSV."""
import csv
import json
import os
import sys
from datetime import datetime, timedelta
from core import FexCore
from core.loader import load_library
from core.constants import FILESTATUS_FOLDER, FILESTATUS_DELETED
# TFileRecord date fields are Windows FILETIME: 100-nanosecond intervals
# since 1601-01-01 UTC. Zero means "not set".
FILETIME_EPOCH = datetime(1601, 1, 1)
def filetime_to_datetime(raw_int64):
"""Convert Windows FILETIME int64 to Python datetime (UTC)."""
if raw_int64 == 0:
return None
try:
return FILETIME_EPOCH + timedelta(microseconds=raw_int64 / 10)
except (ValueError, OverflowError):
return None
def format_datetime(dt):
"""Format datetime as ISO string, or empty string if None."""
if dt is None:
return ""
return dt.strftime("%Y-%m-%d %H:%M:%S")
def main():
if len(sys.argv) < 3:
print("Usage: python csv_listing.py <image_path> <output.csv>")
sys.exit(1)
image_path = sys.argv[1]
csv_path = sys.argv[2]
# Load and initialize (FEX2 key from environment — see ../license-keys.md).
# Windows: chdir to DLL directory across init_library — see the first
# example for the workaround rationale.
lib, lib_path = load_library()
fex = FexCore(lib)
prev_cwd = os.getcwd()
os.chdir(os.path.dirname(lib_path))
try:
fex.init_library(os.environ["FEX_LICENSE_KEY"])
finally:
os.chdir(prev_cwd)
# Open image
image_id = fex.open_image(image_path)
if image_id < 0:
print(f"Error opening image: {image_id}")
sys.exit(1)
# Read filesystem (returns (result_code, file_count); records are fetched separately below)
result, file_count = fex.read_file_system(image_id)
if result != 0:
print(f"Error reading filesystem: {result}")
sys.exit(1)
print(f"Filesystem read: {file_count} entries")
# Get file records (includes full paths via V2 API)
result, records = fex.get_file_system_records(image_id)
# Write CSV
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"Index", "Path", "Filename", "LogicalSize", "PhysicalSize",
"Created", "Modified", "Accessed", "IsFolder", "IsDeleted"
])
for rec in records:
is_folder = bool(rec["status"] & FILESTATUS_FOLDER)
is_deleted = bool(rec["status"] & FILESTATUS_DELETED)
writer.writerow([
rec["_index"],
rec.get("path", ""),
rec["filename"],
rec["logicalSize"],
rec["physicalSize"],
format_datetime(filetime_to_datetime(rec["createdDate"])),
format_datetime(filetime_to_datetime(rec["modifiedDate"])),
format_datetime(filetime_to_datetime(rec["accessedDate"])),
is_folder,
is_deleted,
])
print(f"Written {len(records)} records to {csv_path}")
fex.close_image(image_id)
if __name__ == "__main__":
main()
Run it:
python csv_listing.py path/to/evidence.E01 file_listing.csv
Output CSV:
Index,Path,Filename,LogicalSize,PhysicalSize,Created,Modified,Accessed,IsFolder,IsDeleted
0,,Root,0,0,,,,True,False
1,Documents,Documents,0,0,2024-01-15 10:30:00,2024-01-15 10:30:00,,True,False
2,Documents\report.pdf,report.pdf,1572864,1576960,2024-01-15 11:00:00,2024-01-15 11:05:00,2024-01-15 11:05:00,False,False
...
Step 4: Extract All Files
Use the included fex_extract.py tool for bulk extraction:
# Extract all files to a folder
python fex_extract.py path/to/evidence.E01 ./extracted_files
# Skip deleted files
python fex_extract.py path/to/evidence.E01 ./extracted_files --skip-deleted
# Dry run (show what would be extracted without writing)
python fex_extract.py path/to/evidence.E01 ./extracted_files --dry-run
# Verbose output
python fex_extract.py path/to/evidence.E01 ./extracted_files --verbose
Expected output:
FEX Extract - Bulk File Extraction
============================================================
Opening image: evidence.E01
Reading filesystem... 1,247 files found
Extracting to: ./extracted_files
[ 1/1247] Documents\report.pdf (1.5 MB)
[ 2/1247] Documents\notes.txt (2.0 KB)
[ 3/1247] Pictures\photo.jpg (100.0 KB)
...
============================================================
Extraction complete: 1,200 files extracted, 47 folders created
Total size: 2.5 GB
Errors: 0
Files are extracted preserving the original directory structure. The tool automatically:
- Preserves folder hierarchy from the forensic image
- Truncates files to logical size (strips cluster slack)
- Sanitizes Unicode filenames for cross-platform compatibility
- Handles path length limits
Step 5: Write Your Own Extraction Logic
For custom extraction (e.g., filtering by file type or size):
⚠️ Note on bulk extraction. The example below uses
fex.read_file_data(), which wraps the DLL'sReadFileData— fine for the small demo case. For extracting hundreds or thousands of files, switch to the handle-based stream API (RequestFileStream/ReadFileStream/ReleaseFileStream) to avoid unbounded memory growth. See the Python example in Common Workflows — Workflow 3.
#!/usr/bin/env python3
"""Example: Extract only PDF files larger than 1 KB."""
import os
import sys
from core import FexCore
from core.loader import load_library
from core.constants import FILESTATUS_FOLDER, RESULT_OK
READ_CHUNK = 1024 * 1024 # 1 MB chunks
def extract_file(fex, image_id, file_index, logical_size, output_path):
"""Extract a single file by reading in chunks."""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
written = 0
with open(output_path, "wb") as f:
offset = 0
while offset < logical_size:
chunk_size = min(READ_CHUNK, logical_size - offset)
result, bytes_read, buf = fex.read_file_data(
image_id, file_index, offset, chunk_size
)
if result != RESULT_OK or bytes_read == 0:
break
f.write(bytes(buf[:bytes_read]))
written += bytes_read
offset += bytes_read
# Truncate to exact logical size
if written > logical_size:
with open(output_path, "r+b") as f:
f.truncate(logical_size)
def main():
image_path = sys.argv[1]
output_dir = sys.argv[2]
# Windows: chdir to DLL directory across init_library — see the first
# example for the workaround rationale.
lib, lib_path = load_library()
fex = FexCore(lib)
prev_cwd = os.getcwd()
os.chdir(os.path.dirname(lib_path))
try:
fex.init_library(os.environ["FEX_LICENSE_KEY"])
finally:
os.chdir(prev_cwd)
image_id = fex.open_image(image_path)
result, _ = fex.read_file_system(image_id)
result, records = fex.get_file_system_records(image_id)
extracted = 0
for rec in records:
# Skip folders
if rec["status"] & FILESTATUS_FOLDER:
continue
# Filter: only PDFs larger than 1 KB
filename = rec["filename"].lower()
if not filename.endswith(".pdf") or rec["logicalSize"] < 1024:
continue
path = rec.get("path", rec["filename"])
output_path = os.path.join(output_dir, path)
print(f"Extracting: {path} ({rec['logicalSize']} bytes)")
extract_file(fex, image_id, rec["_index"], rec["logicalSize"], output_path)
extracted += 1
fex.close_image(image_id)
print(f"\nExtracted {extracted} PDF files")
if __name__ == "__main__":
main()
API Reference
The Python SDK wraps the FEX Core C API. Key methods on the FexCore class:
| Method | Description |
|---|---|
init_library(key) |
Initialize with license key |
open_image(path) |
Open forensic image, returns image_id |
close_image(image_id) |
Close an opened image |
image_info_json(image_id) |
Get image metadata as JSON string |
read_file_system(image_id) |
Read filesystem, returns (result, file_count) |
get_file_system_records(image_id) |
Get all file records as list of dicts |
read_file_data(image_id, index, offset, size) |
Read file content bytes |
size_of_file(image_id, index) |
Get logical file size |
get_file_path(image_id, index) |
Get full path for a file |
call_v2_json(func, *args) |
Call any V2 function returning JSON |
For the full C API reference, see V2 API Documentation.
Next Steps
- Docker Guide - Deploy on Linux with Docker
- V2 API Reference - Complete API documentation
- V3 API (Custom Fields) - Flexible property queries
- Error Codes - All error codes with descriptions
- Architecture Overview - API design and memory patterns